From 637f40600e02847dd910df0006a5cd7c99d1f75d Mon Sep 17 00:00:00 2001 From: Srirang Kalantri Date: Wed, 30 Sep 2026 22:42:19 +0530 Subject: [PATCH] fix: remove react-native-segmented-control-tab --- .../toggle-buttons-native/CHANGELOG.md | 3 +- .../toggle-buttons-native/package.json | 3 +- .../src/ToggleButtons.tsx | 13 +- .../__snapshots__/ToggleButtons.spec.tsx.snap | 173 +++---- .../src/components/SegmentedControl.tsx | 76 +++ .../__tests__/SegmentedControl.spec.tsx | 158 ++++++ .../SegmentedControl.spec.tsx.snap | 463 ++++++++++++++++++ .../toggle-buttons-native/src/ui/Styles.ts | 1 + .../ReactNativeSegmentedControlTab.d.ts | 13 - pnpm-lock.yaml | 8 - 10 files changed, 786 insertions(+), 125 deletions(-) create mode 100644 packages/pluggableWidgets/toggle-buttons-native/src/components/SegmentedControl.tsx create mode 100644 packages/pluggableWidgets/toggle-buttons-native/src/components/__tests__/SegmentedControl.spec.tsx create mode 100644 packages/pluggableWidgets/toggle-buttons-native/src/components/__tests__/__snapshots__/SegmentedControl.spec.tsx.snap delete mode 100644 packages/pluggableWidgets/toggle-buttons-native/typings/ReactNativeSegmentedControlTab.d.ts diff --git a/packages/pluggableWidgets/toggle-buttons-native/CHANGELOG.md b/packages/pluggableWidgets/toggle-buttons-native/CHANGELOG.md index 46d3a33ba..6804ec574 100644 --- a/packages/pluggableWidgets/toggle-buttons-native/CHANGELOG.md +++ b/packages/pluggableWidgets/toggle-buttons-native/CHANGELOG.md @@ -8,7 +8,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### Changed -- We updated react-native-segmented-control-tab to 3.4.1. +- Replaced external dependency `react-native-segmented-control-tab` with in-house SegmentedControl component. +- Fixed accessibility: component now uses modern React Native accessibility props (`accessibilityRole="tab"`, `accessibilityState.selected`) instead of deprecated `accessibilityTraits` and `accessibilityComponentType`. ## [2.3.0] - 2024-12-3 diff --git a/packages/pluggableWidgets/toggle-buttons-native/package.json b/packages/pluggableWidgets/toggle-buttons-native/package.json index 1a5ea06bc..08d8d3e95 100644 --- a/packages/pluggableWidgets/toggle-buttons-native/package.json +++ b/packages/pluggableWidgets/toggle-buttons-native/package.json @@ -20,8 +20,7 @@ }, "dependencies": { "@mendix/piw-native-utils-internal": "*", - "@mendix/piw-utils-internal": "*", - "react-native-segmented-control-tab": "3.4.1" + "@mendix/piw-utils-internal": "*" }, "devDependencies": { "@babel/plugin-transform-flow-strip-types": "^7.4.4", diff --git a/packages/pluggableWidgets/toggle-buttons-native/src/ToggleButtons.tsx b/packages/pluggableWidgets/toggle-buttons-native/src/ToggleButtons.tsx index ebdd8f047..e578218ba 100644 --- a/packages/pluggableWidgets/toggle-buttons-native/src/ToggleButtons.tsx +++ b/packages/pluggableWidgets/toggle-buttons-native/src/ToggleButtons.tsx @@ -1,11 +1,11 @@ import { flattenStyles } from "@mendix/piw-native-utils-internal"; import { Component, JSX } from "react"; import { Text, View } from "react-native"; -import SegmentedControlTab from "react-native-segmented-control-tab"; import { ToggleButtonsProps } from "../typings/ToggleButtonsProps"; import { defaultToggleButtonsStyle, ToggleButtonsStyle } from "./ui/Styles"; import { executeAction } from "@mendix/piw-utils-internal"; +import { SegmentedControl } from "./components/SegmentedControl"; export type Props = ToggleButtonsProps; @@ -25,16 +25,17 @@ export class ToggleButtons extends Component { return ( - {this.props.enum.validation && ( {this.props.enum.validation} diff --git a/packages/pluggableWidgets/toggle-buttons-native/src/__tests__/__snapshots__/ToggleButtons.spec.tsx.snap b/packages/pluggableWidgets/toggle-buttons-native/src/__tests__/__snapshots__/ToggleButtons.spec.tsx.snap index 76656b1fa..2a812dde8 100644 --- a/packages/pluggableWidgets/toggle-buttons-native/src/__tests__/__snapshots__/ToggleButtons.spec.tsx.snap +++ b/packages/pluggableWidgets/toggle-buttons-native/src/__tests__/__snapshots__/ToggleButtons.spec.tsx.snap @@ -10,26 +10,24 @@ exports[`ToggleButtons renders 1`] = ` testID="toggle-buttons-test" > - - - Formatted a - - + Formatted a + - - - Formatted b - - + Formatted b + diff --git a/packages/pluggableWidgets/toggle-buttons-native/src/components/SegmentedControl.tsx b/packages/pluggableWidgets/toggle-buttons-native/src/components/SegmentedControl.tsx new file mode 100644 index 000000000..4a1287383 --- /dev/null +++ b/packages/pluggableWidgets/toggle-buttons-native/src/components/SegmentedControl.tsx @@ -0,0 +1,76 @@ +import { ReactElement } from "react"; +import { Pressable, Text, View, ViewStyle, TextStyle } from "react-native"; + +export interface SegmentedControlProps { + values: string[]; + selectedIndex: number; + enabled: boolean; + onTabPress: (index: number) => void; + borderRadius: number; + buttonStyle: ViewStyle; + textStyle: TextStyle; + activeButtonStyle: ViewStyle; + activeTextStyle: TextStyle; + testID: string; +} + +export function SegmentedControl(props: SegmentedControlProps): ReactElement { + const { + values, + selectedIndex, + enabled, + onTabPress, + borderRadius, + buttonStyle, + textStyle, + activeButtonStyle, + activeTextStyle, + testID + } = props; + + const handlePress = (index: number) => { + if (!enabled || index === selectedIndex) { + return; + } + onTabPress(index); + }; + + return ( + + {values.map((value, index) => { + const isSelected = index === selectedIndex; + + return ( + handlePress(index)} + disabled={!enabled} + style={({ pressed }) => [ + { + flex: 1, + alignItems: "center", + justifyContent: "center", + paddingVertical: 5, + borderWidth: 1, + borderRadius, + backgroundColor: "transparent", + opacity: pressed ? 0.6 : 1 + }, + buttonStyle, + isSelected && activeButtonStyle + ]} + testID={`${testID}$button-${index}`} + > + + {value} + + + ); + })} + + ); +} diff --git a/packages/pluggableWidgets/toggle-buttons-native/src/components/__tests__/SegmentedControl.spec.tsx b/packages/pluggableWidgets/toggle-buttons-native/src/components/__tests__/SegmentedControl.spec.tsx new file mode 100644 index 000000000..07c4cb857 --- /dev/null +++ b/packages/pluggableWidgets/toggle-buttons-native/src/components/__tests__/SegmentedControl.spec.tsx @@ -0,0 +1,158 @@ +import { fireEvent, render } from "@testing-library/react-native"; +import { SegmentedControl, SegmentedControlProps } from "../SegmentedControl"; + +describe("SegmentedControl", () => { + let defaultProps: SegmentedControlProps; + + beforeEach(() => { + defaultProps = { + values: ["Option 1", "Option 2", "Option 3"], + selectedIndex: 0, + enabled: true, + onTabPress: jest.fn(), + borderRadius: 5, + buttonStyle: { + borderWidth: 1, + borderColor: "#CCC", + padding: 8 + }, + textStyle: { + color: "#666" + }, + activeButtonStyle: { + backgroundColor: "rgb(98, 0, 238)", + borderColor: "rgb(98, 0, 238)" + }, + activeTextStyle: { + color: "#fff" + }, + testID: "segmented-control-test" + }; + }); + + it("renders correctly", () => { + const component = render(); + expect(component.toJSON()).toMatchSnapshot(); + }); + + it("displays all values as buttons", () => { + const component = render(); + + expect(component.getByText("Option 1")).toBeTruthy(); + expect(component.getByText("Option 2")).toBeTruthy(); + expect(component.getByText("Option 3")).toBeTruthy(); + }); + + it("applies active styles to selected index", () => { + const component = render(); + + const button1 = component.getByTestId("segmented-control-test$button-1"); + expect(button1.props.accessibilityState).toEqual(expect.objectContaining({ selected: true })); + }); + + it("calls onTabPress with correct index when button pressed", () => { + const mockOnTabPress = jest.fn(); + const component = render(); + + const button2 = component.getByTestId("segmented-control-test$button-2"); + fireEvent.press(button2); + + expect(mockOnTabPress).toHaveBeenCalledWith(2); + }); + + it("does not call onTabPress when disabled", () => { + const mockOnTabPress = jest.fn(); + const component = render(); + + const button1 = component.getByTestId("segmented-control-test$button-1"); + fireEvent.press(button1); + + expect(mockOnTabPress).not.toHaveBeenCalled(); + }); + + it("does not call onTabPress when pressing already-selected button", () => { + const mockOnTabPress = jest.fn(); + const component = render(); + + const button0 = component.getByTestId("segmented-control-test$button-0"); + fireEvent.press(button0); + + expect(mockOnTabPress).not.toHaveBeenCalled(); + }); + + it("sets correct accessibility props", () => { + const component = render(); + + const button0 = component.getByTestId("segmented-control-test$button-0"); + const button1 = component.getByTestId("segmented-control-test$button-1"); + const button2 = component.getByTestId("segmented-control-test$button-2"); + + expect(button0.props.accessibilityRole).toBe("tab"); + expect(button0.props.accessibilityState).toEqual(expect.objectContaining({ selected: false })); + expect(button0.props.accessibilityLabel).toBe("Option 1"); + + expect(button1.props.accessibilityRole).toBe("tab"); + expect(button1.props.accessibilityState).toEqual(expect.objectContaining({ selected: true })); + expect(button1.props.accessibilityLabel).toBe("Option 2"); + + expect(button2.props.accessibilityRole).toBe("tab"); + expect(button2.props.accessibilityState).toEqual(expect.objectContaining({ selected: false })); + expect(button2.props.accessibilityLabel).toBe("Option 3"); + }); + + it("applies border radius to all buttons", () => { + const component = render(); + + const button0 = component.getByTestId("segmented-control-test$button-0"); + const button1 = component.getByTestId("segmented-control-test$button-1"); + const button2 = component.getByTestId("segmented-control-test$button-2"); + + // All buttons should have the same border radius applied + expect(button0.props.style).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + borderRadius: 10 + }) + ]) + ); + + expect(button1.props.style).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + borderRadius: 10 + }) + ]) + ); + + expect(button2.props.style).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + borderRadius: 10 + }) + ]) + ); + }); + + it("sets correct testIDs on buttons", () => { + const component = render(); + + expect(component.getByTestId("segmented-control-test$button-0")).toBeTruthy(); + expect(component.getByTestId("segmented-control-test$button-1")).toBeTruthy(); + expect(component.getByTestId("segmented-control-test$button-2")).toBeTruthy(); + }); + + it("renders with single value", () => { + const component = render(); + + expect(component.getByText("Single")).toBeTruthy(); + expect(component.toJSON()).toMatchSnapshot(); + }); + + it("renders with two values", () => { + const component = render(); + + expect(component.getByText("First")).toBeTruthy(); + expect(component.getByText("Second")).toBeTruthy(); + expect(component.toJSON()).toMatchSnapshot(); + }); +}); diff --git a/packages/pluggableWidgets/toggle-buttons-native/src/components/__tests__/__snapshots__/SegmentedControl.spec.tsx.snap b/packages/pluggableWidgets/toggle-buttons-native/src/components/__tests__/__snapshots__/SegmentedControl.spec.tsx.snap new file mode 100644 index 000000000..8dbc6400e --- /dev/null +++ b/packages/pluggableWidgets/toggle-buttons-native/src/components/__tests__/__snapshots__/SegmentedControl.spec.tsx.snap @@ -0,0 +1,463 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`SegmentedControl renders correctly 1`] = ` + + + + Option 1 + + + + + Option 2 + + + + + Option 3 + + + +`; + +exports[`SegmentedControl renders with single value 1`] = ` + + + + Single + + + +`; + +exports[`SegmentedControl renders with two values 1`] = ` + + + + First + + + + + Second + + + +`; diff --git a/packages/pluggableWidgets/toggle-buttons-native/src/ui/Styles.ts b/packages/pluggableWidgets/toggle-buttons-native/src/ui/Styles.ts index 46d27c2ad..3ca0943ba 100644 --- a/packages/pluggableWidgets/toggle-buttons-native/src/ui/Styles.ts +++ b/packages/pluggableWidgets/toggle-buttons-native/src/ui/Styles.ts @@ -12,6 +12,7 @@ export interface ToggleButtonsStyle extends Style { } const blue = "rgb(0, 122, 255)"; + const purple = "rgb(98, 0, 238)"; export const defaultToggleButtonsStyle: ToggleButtonsStyle = { diff --git a/packages/pluggableWidgets/toggle-buttons-native/typings/ReactNativeSegmentedControlTab.d.ts b/packages/pluggableWidgets/toggle-buttons-native/typings/ReactNativeSegmentedControlTab.d.ts deleted file mode 100644 index f8bae8319..000000000 --- a/packages/pluggableWidgets/toggle-buttons-native/typings/ReactNativeSegmentedControlTab.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -import ReactNativeSegmentedControlTab, { SegmentedControlTabProperties } from "react-native-segmented-control-tab"; - -declare module "react-native-segmented-control-tab" { - import { Props } from "react"; - - export interface SegmentedControlTabProperties extends Props { - /** - * Boolean to enable or disable the component - * @default true - */ - enabled?: boolean; - } -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0bb461204..2ccb39365 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -871,9 +871,6 @@ importers: '@mendix/piw-utils-internal': specifier: '*' version: link:../../tools/piw-utils-internal - react-native-segmented-control-tab: - specifier: 3.4.1 - version: 3.4.1 devDependencies: '@babel/plugin-transform-flow-strip-types': specifier: ^7.4.4 @@ -6665,9 +6662,6 @@ packages: react: 19.2.3 react-native: 0.84.1 - react-native-segmented-control-tab@3.4.1: - resolution: {integrity: sha512-BNPdlE9Unr0Xabewn8W+FhBMLjssXy9Ey7S7AY0hXlrKrEKFdC9z0yT+eEWd5dLam4T6T4IuGL8b7ZF4uGyWNw==} - react-native-signature-canvas@3.4.0: resolution: {integrity: sha512-c8FUCYckXErlGbqxfhMNBcPzBdhCSpBgCtTx9+rpntvveBoajE44854NLydeAvCxHuh1yV5y2EnNFij7AuXtSw==} peerDependencies: @@ -14901,8 +14895,6 @@ snapshots: react-native: 0.84.1(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(react@19.2.3) warn-once: 0.1.1 - react-native-segmented-control-tab@3.4.1: {} - react-native-signature-canvas@3.4.0(react-native-webview@13.16.1(react-native@0.84.1(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(react@19.2.3))(react@19.2.3)): dependencies: react-native-webview: 13.16.1(react-native@0.84.1(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(react@19.2.3))(react@19.2.3)