diff --git a/src/components/uis/Button/Button.test.tsx b/src/components/uis/Button/Button.test.tsx index 7370079..45621d7 100644 --- a/src/components/uis/Button/Button.test.tsx +++ b/src/components/uis/Button/Button.test.tsx @@ -545,6 +545,80 @@ describe('[Button]', () => { }); }); + describe('[Button] accessibility', () => { + it('should have accessibilityRole="button"', () => { + testingLib = render( + Component({ + props: { + testID: 'a11y-button', + text: 'Click me', + }, + }), + ); + + const button = testingLib.getByTestId('a11y-button'); + expect(button.props.accessibilityRole).toBe('button'); + }); + + it('should use text as default accessibilityLabel', () => { + testingLib = render( + Component({ + props: { + testID: 'a11y-button', + text: 'Submit', + }, + }), + ); + + const button = testingLib.getByTestId('a11y-button'); + expect(button.props.accessibilityLabel).toBe('Submit'); + }); + + it('should use custom accessibilityLabel when provided', () => { + testingLib = render( + Component({ + props: { + testID: 'a11y-button', + text: 'Submit', + accessibilityLabel: 'Submit form', + }, + }), + ); + + const button = testingLib.getByTestId('a11y-button'); + expect(button.props.accessibilityLabel).toBe('Submit form'); + }); + + it('should have undefined accessibilityLabel when text is ReactElement', () => { + testingLib = render( + Component({ + props: { + testID: 'a11y-button', + text: Custom, + }, + }), + ); + + const button = testingLib.getByTestId('a11y-button'); + expect(button.props.accessibilityLabel).toBeUndefined(); + }); + + it('should allow custom accessibilityLabel with ReactElement text', () => { + testingLib = render( + Component({ + props: { + testID: 'a11y-button', + text: Custom, + accessibilityLabel: 'Custom button', + }, + }), + ); + + const button = testingLib.getByTestId('a11y-button'); + expect(button.props.accessibilityLabel).toBe('Custom button'); + }); + }); + describe('[Button] custom theme', () => { it('should render with custom primary color from theme', () => { const customTheme = { diff --git a/src/components/uis/Button/Button.tsx b/src/components/uis/Button/Button.tsx index 4ed5366..b94595a 100644 --- a/src/components/uis/Button/Button.tsx +++ b/src/components/uis/Button/Button.tsx @@ -51,6 +51,7 @@ export type Props = { >; hitSlop?: null | Insets | number | undefined; hapticFeedback?: Haptics.ImpactFeedbackStyle; + accessibilityLabel?: string; }; type Styles = { @@ -241,6 +242,7 @@ export function Button({ loadingColor, hitSlop = {top: 8, bottom: 8, left: 8, right: 8}, hapticFeedback, + accessibilityLabel, }: Props): ReactElement { const ref = useRef>(null); const hovered = useHover(ref); @@ -410,6 +412,8 @@ export function Button({ return ( { expect(checkbox).toBeTruthy(); }); }); + + describe('accessibility', () => { + it('should have accessibilityRole="checkbox"', () => { + testingLib = render( + Component({ + text: 'Accept terms', + }), + ); + + const json = testingLib.toJSON(); + expect(json).toBeTruthy(); + // The root element should have accessibilityRole + expect((json as any).props.accessibilityRole).toBe('checkbox'); + }); + + it('should use text as default accessibilityLabel', () => { + testingLib = render( + Component({ + text: 'Accept terms', + }), + ); + + const json = testingLib.toJSON(); + expect((json as any).props.accessibilityLabel).toBe('Accept terms'); + }); + + it('should use custom accessibilityLabel when provided', () => { + testingLib = render( + Component({ + text: 'Accept', + accessibilityLabel: 'Accept terms and conditions', + }), + ); + + const json = testingLib.toJSON(); + expect((json as any).props.accessibilityLabel).toBe('Accept terms and conditions'); + }); + + it('should have accessibilityState with checked=true when checked', () => { + testingLib = render( + Component({ + text: 'Accept terms', + checked: true, + }), + ); + + const json = testingLib.toJSON(); + expect((json as any).props.accessibilityState).toEqual({ + checked: true, + disabled: false, + }); + }); + + it('should have accessibilityState with checked=false when unchecked', () => { + testingLib = render( + Component({ + text: 'Accept terms', + checked: false, + }), + ); + + const json = testingLib.toJSON(); + expect((json as any).props.accessibilityState).toEqual({ + checked: false, + disabled: false, + }); + }); + + it('should have accessibilityState with disabled=true when disabled', () => { + testingLib = render( + Component({ + text: 'Accept terms', + disabled: true, + }), + ); + + const json = testingLib.toJSON(); + expect((json as any).props.accessibilityState).toEqual({ + checked: false, + disabled: true, + }); + }); + + it('should have undefined accessibilityLabel when text is ReactElement', () => { + testingLib = render( + Component({ + text: Custom text, + }), + ); + + const json = testingLib.toJSON(); + expect((json as any).props.accessibilityLabel).toBeUndefined(); + }); + }); }); diff --git a/src/components/uis/Checkbox/Checkbox.tsx b/src/components/uis/Checkbox/Checkbox.tsx index 203b0e3..e317017 100644 --- a/src/components/uis/Checkbox/Checkbox.tsx +++ b/src/components/uis/Checkbox/Checkbox.tsx @@ -38,6 +38,7 @@ export interface CheckboxProps { checked?: boolean; text?: string | ReactElement; direction?: 'left' | 'right'; + accessibilityLabel?: string; } const Container = styled(TouchableOpacity)` @@ -77,6 +78,7 @@ export function Checkbox({ disabled = false, checked = false, onPress, + accessibilityLabel, }: CheckboxProps): ReactElement { const checkboxSize = typeof size === 'number' ? size @@ -188,6 +190,9 @@ export function Checkbox({ return ( ( decoration = 'underline', colors = {}, required = false, + accessibilityLabel, }: EditTextProps, ref, ): ReactElement => { @@ -410,6 +412,7 @@ export const EditText = forwardRef( }) : startElement} { expect(json).toBeTruthy(); }); + + describe('accessibility', () => { + it('should have accessibilityRole="button"', () => { + testingLib = render( + Component({ + testID: 'a11y-icon-button', + icon: 'Plus', + }), + ); + + const button = testingLib.getByTestId('a11y-icon-button'); + expect(button.props.accessibilityRole).toBe('button'); + }); + + it('should use icon name as default accessibilityLabel', () => { + testingLib = render( + Component({ + testID: 'a11y-icon-button', + icon: 'Plus', + }), + ); + + const button = testingLib.getByTestId('a11y-icon-button'); + expect(button.props.accessibilityLabel).toBe('Plus'); + }); + + it('should use custom accessibilityLabel when provided', () => { + testingLib = render( + Component({ + testID: 'a11y-icon-button', + icon: 'Plus', + accessibilityLabel: 'Add new item', + }), + ); + + const button = testingLib.getByTestId('a11y-icon-button'); + expect(button.props.accessibilityLabel).toBe('Add new item'); + }); + }); }); diff --git a/src/components/uis/IconButton/IconButton.tsx b/src/components/uis/IconButton/IconButton.tsx index 1122198..71a35db 100644 --- a/src/components/uis/IconButton/IconButton.tsx +++ b/src/components/uis/IconButton/IconButton.tsx @@ -99,6 +99,7 @@ export type IconButtonProps = { activeOpacity?: number; touchableHighlightProps?: Omit; hapticFeedback?: Haptics.ImpactFeedbackStyle; + accessibilityLabel?: string; }; export function IconButton({ @@ -117,6 +118,7 @@ export function IconButton({ activeOpacity = 0.95, touchableHighlightProps, hapticFeedback, + accessibilityLabel, }: IconButtonProps): ReactElement { const ref = useRef>(null); const hovered = useHover(ref); @@ -245,6 +247,8 @@ export function IconButton({ return ( (); const fadeAnim = useRef(new Animated.Value(selected ? 1 : 0)).current; @@ -131,6 +133,9 @@ export default function RadioButton({ return ( { expect(baseElement).toBeTruthy(); }); }); + + describe('accessibility', () => { + it('should have accessibilityRole="radio" on RadioButton', async () => { + component = createComponent( + , + ); + + testingLib = render(component); + + const radioButton = testingLib.getByTestId('radio-a11y'); + expect(radioButton.props.accessibilityRole).toBe('radio'); + }); + + it('should use label as default accessibilityLabel', async () => { + component = createComponent( + , + ); + + testingLib = render(component); + + const radioButton = testingLib.getByTestId('radio-a11y'); + expect(radioButton.props.accessibilityLabel).toBe('Option 1'); + }); + + it('should use custom accessibilityLabel when provided', async () => { + component = createComponent( + , + ); + + testingLib = render(component); + + const radioButton = testingLib.getByTestId('radio-a11y'); + expect(radioButton.props.accessibilityLabel).toBe('Select option one'); + }); + + it('should have accessibilityState with selected=true when selected', async () => { + component = createComponent( + , + ); + + testingLib = render(component); + + const radioButton = testingLib.getByTestId('radio-a11y'); + expect(radioButton.props.accessibilityState).toEqual({ + selected: true, + disabled: false, + }); + }); + + it('should have accessibilityState with disabled=true when disabled', async () => { + component = createComponent( + , + ); + + testingLib = render(component); + + const radioButton = testingLib.getByTestId('radio-a11y'); + expect(radioButton.props.accessibilityState).toEqual({ + selected: undefined, + disabled: true, + }); + }); + }); }); diff --git a/src/components/uis/SegmentedControl/SegmentedControl.tsx b/src/components/uis/SegmentedControl/SegmentedControl.tsx index 821741b..2c373b3 100644 --- a/src/components/uis/SegmentedControl/SegmentedControl.tsx +++ b/src/components/uis/SegmentedControl/SegmentedControl.tsx @@ -169,6 +169,9 @@ function SegmentedControlContainer({ handlePress(item.value)} diff --git a/src/components/uis/SwitchToggle/SwitchToggle.test.tsx b/src/components/uis/SwitchToggle/SwitchToggle.test.tsx index 5f855ce..02f0085 100644 --- a/src/components/uis/SwitchToggle/SwitchToggle.test.tsx +++ b/src/components/uis/SwitchToggle/SwitchToggle.test.tsx @@ -102,4 +102,48 @@ describe('[SwitchToggle]', () => { expect(baseElement).toBeTruthy(); }); }); + + describe('accessibility', () => { + it('should have accessibilityRole="switch"', () => { + testingLib = render( + createComponent(), + ); + + const switchElement = testingLib.getByRole('switch'); + expect(switchElement).toBeTruthy(); + }); + + it('should have accessibilityState with checked=true when isOn', () => { + testingLib = render( + createComponent(), + ); + + const json = testingLib.toJSON(); + expect((json as any).props.accessibilityState).toEqual({checked: true}); + }); + + it('should have accessibilityState with checked=false when not isOn', () => { + testingLib = render( + createComponent(), + ); + + const json = testingLib.toJSON(); + expect((json as any).props.accessibilityState).toEqual({checked: false}); + }); + + it('should use custom accessibilityLabel when provided', () => { + testingLib = render( + createComponent( + , + ), + ); + + const json = testingLib.toJSON(); + expect((json as any).props.accessibilityLabel).toBe('Toggle dark mode'); + }); + }); }); diff --git a/src/components/uis/SwitchToggle/SwitchToggle.tsx b/src/components/uis/SwitchToggle/SwitchToggle.tsx index eec7dcc..2615d66 100644 --- a/src/components/uis/SwitchToggle/SwitchToggle.tsx +++ b/src/components/uis/SwitchToggle/SwitchToggle.tsx @@ -31,6 +31,7 @@ type Props = { onElement?: any; offElement?: any; onPress?: () => void; + accessibilityLabel?: string; }; // Using AnimatedView created above - functionally identical to styled(Animated.View) @@ -88,6 +89,7 @@ export function SwitchToggle({ size = 'medium', offElement, onPress, + accessibilityLabel, }: Props): ReactElement { const {theme} = useTheme(); @@ -287,7 +289,9 @@ export function SwitchToggle({ return (