From 8d5fe421071e4433cfc214739969d1f433714e1f Mon Sep 17 00:00:00 2001 From: bhumika biyani <73336200+bhumikabiyani@users.noreply.github.com> Date: Wed, 4 Nov 2026 22:53:28 +0530 Subject: [PATCH 1/2] fix(chip): forward accessibility state props --- src/components/Chip/Chip.tsx | 16 +++- src/components/__tests__/Chip.test.tsx | 120 +++++++++++++++++++++++++ 2 files changed, 135 insertions(+), 1 deletion(-) diff --git a/src/components/Chip/Chip.tsx b/src/components/Chip/Chip.tsx index 4507561a48..c4c736ff12 100644 --- a/src/components/Chip/Chip.tsx +++ b/src/components/Chip/Chip.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import { Platform, StyleSheet, Pressable, View } from 'react-native'; import type { + AccessibilityState, ColorValue, GestureResponderEvent, PressableAndroidRippleConfig, @@ -83,6 +84,15 @@ export type Props = Omit & { * Accessibility label for the chip. This is read by the screen reader when the user taps the chip. */ 'aria-label'?: string; + /** + * Accessibility state for the chip. This is read by the screen reader when the user taps the chip. + */ + accessibilityState?: AccessibilityState; + /** + * Indicates whether the chip is checked. Accepts `true`, `false`, + * or `'mixed'` for an indeterminate state. + */ + 'aria-checked'?: boolean | 'mixed'; /** * Accessibility label for the close icon. This is read by the screen reader when the user taps the close icon. */ @@ -186,6 +196,8 @@ const Chip = ({ avatar, selected = false, disabled = false, + accessibilityState, + 'aria-checked': ariaChecked, background, 'aria-label': ariaLabel, role = 'button', @@ -298,10 +310,12 @@ const Chip = ({ onPressOut={hasPassedTouchHandler ? handlePressOut : undefined} delayLongPress={delayLongPress} disabled={disabled} + accessibilityState={accessibilityState} aria-label={ariaLabel} role={role} - aria-selected={selected} + aria-selected={selected || Boolean(accessibilityState?.selected)} aria-disabled={disabled} + aria-checked={ariaChecked ?? accessibilityState?.checked} testID={testID} theme={theme} hitSlop={hitSlop} diff --git a/src/components/__tests__/Chip.test.tsx b/src/components/__tests__/Chip.test.tsx index a7d522e4f0..826cc08741 100644 --- a/src/components/__tests__/Chip.test.tsx +++ b/src/components/__tests__/Chip.test.tsx @@ -309,3 +309,123 @@ describe('getChipColor - border color', () => { }); }); }); + +describe('accessibility', () => { + it('forwards accessibilityState to the underlying touchable element', async () => { + await render( + {}} + accessibilityState={{ checked: true, busy: true, expanded: false }} + > + Accessibility Chip + + ); + + expect(screen.getByRole('button')).toHaveProp( + 'accessibilityState', + expect.objectContaining({ checked: true, busy: true, expanded: false }) + ); + }); + + it('forwards aria-checked to the underlying touchable element', async () => { + await render( + {}} aria-checked={true}> + Checkable Chip + + ); + + expect(screen.getByRole('button')).toHaveProp( + 'accessibilityState', + expect.objectContaining({ checked: true }) + ); + }); + + it('prioritizes aria-checked over accessibilityState.checked', async () => { + await render( + {}} + accessibilityState={{ checked: false }} + aria-checked={true} + > + Chip + + ); + + expect(screen.getByRole('button')).toHaveProp( + 'accessibilityState', + expect.objectContaining({ checked: true }) + ); + }); + + it('handles both selected and checked state simultaneously', async () => { + await render( + {}} selected aria-checked={true}> + Selected Checked Chip + + ); + + expect(screen.getByRole('button')).toHaveProp( + 'accessibilityState', + expect.objectContaining({ checked: true, selected: true }) + ); + }); + + it('handles accessibilityState.selected when selected prop is omitted', async () => { + await render( + {}} accessibilityState={{ selected: true }}> + Selected Chip + + ); + + expect(screen.getByRole('button')).toHaveProp( + 'accessibilityState', + expect.objectContaining({ selected: true }) + ); + }); + + it('forwards aria-checked when role is checkbox', async () => { + await render( + {}} role="checkbox" aria-checked={true}> + Checkbox Chip + + ); + + expect(screen.getByRole('checkbox')).toHaveProp( + 'accessibilityState', + expect.objectContaining({ checked: true }) + ); + }); + + it('does not pass accessibilityState or aria-checked to the outer surface', async () => { + await render( + {}} + testID="chip-touchable" + accessibilityState={{ checked: true }} + aria-checked={true} + > + Chip + + ); + + const touchable = screen.getByTestId('chip-touchable'); + const surface = touchable.parent; + expect(surface).not.toHaveProp('accessibilityState'); + expect(surface).not.toHaveProp('aria-checked'); + }); + + it('has default accessibilityState when accessibilityState and aria-checked are omitted', async () => { + await render( {}}>Default Chip); + + const button = screen.getByRole('button'); + expect(button).toHaveProp( + 'accessibilityState', + expect.not.objectContaining({ checked: expect.anything() }) + ); + expect(button).toHaveProp( + 'accessibilityState', + expect.objectContaining({ selected: false, disabled: false }) + ); + expect(button).not.toHaveProp('aria-checked'); + }); +}); From dcfd4f801bc5fc0955118b9491c4235d816a9936 Mon Sep 17 00:00:00 2001 From: bhumika biyani <73336200+bhumikabiyani@users.noreply.github.com> Date: Sun, 8 Nov 2026 13:35:33 +0530 Subject: [PATCH 2/2] fix: align Chip accessibility state with 6.x --- src/components/Chip/Chip.tsx | 24 ++-- src/components/__tests__/Chip.test.tsx | 180 ++++++++++++++----------- 2 files changed, 109 insertions(+), 95 deletions(-) diff --git a/src/components/Chip/Chip.tsx b/src/components/Chip/Chip.tsx index c4c736ff12..82874c6ad9 100644 --- a/src/components/Chip/Chip.tsx +++ b/src/components/Chip/Chip.tsx @@ -1,7 +1,6 @@ import * as React from 'react'; import { Platform, StyleSheet, Pressable, View } from 'react-native'; import type { - AccessibilityState, ColorValue, GestureResponderEvent, PressableAndroidRippleConfig, @@ -84,15 +83,6 @@ export type Props = Omit & { * Accessibility label for the chip. This is read by the screen reader when the user taps the chip. */ 'aria-label'?: string; - /** - * Accessibility state for the chip. This is read by the screen reader when the user taps the chip. - */ - accessibilityState?: AccessibilityState; - /** - * Indicates whether the chip is checked. Accepts `true`, `false`, - * or `'mixed'` for an indeterminate state. - */ - 'aria-checked'?: boolean | 'mixed'; /** * Accessibility label for the close icon. This is read by the screen reader when the user taps the close icon. */ @@ -194,10 +184,10 @@ const Chip = ({ children, icon, avatar, - selected = false, + selected, disabled = false, - accessibilityState, - 'aria-checked': ariaChecked, + accessibilityState: _accessibilityState, + 'aria-checked': _ariaChecked, background, 'aria-label': ariaLabel, role = 'button', @@ -310,12 +300,14 @@ const Chip = ({ onPressOut={hasPassedTouchHandler ? handlePressOut : undefined} delayLongPress={delayLongPress} disabled={disabled} - accessibilityState={accessibilityState} aria-label={ariaLabel} role={role} - aria-selected={selected || Boolean(accessibilityState?.selected)} + {...(Platform.OS === 'web' + ? selected !== undefined + ? { 'aria-pressed': selected } + : undefined + : { 'aria-selected': selected ?? false })} aria-disabled={disabled} - aria-checked={ariaChecked ?? accessibilityState?.checked} testID={testID} theme={theme} hitSlop={hitSlop} diff --git a/src/components/__tests__/Chip.test.tsx b/src/components/__tests__/Chip.test.tsx index 826cc08741..061922809e 100644 --- a/src/components/__tests__/Chip.test.tsx +++ b/src/components/__tests__/Chip.test.tsx @@ -1,4 +1,6 @@ -import { describe, expect, it } from '@jest/globals'; +import { Platform } from 'react-native'; + +import { afterEach, beforeEach, describe, expect, it } from '@jest/globals'; import color from 'color'; import { render, screen } from '../../test-utils'; @@ -311,97 +313,111 @@ describe('getChipColor - border color', () => { }); describe('accessibility', () => { - it('forwards accessibilityState to the underlying touchable element', async () => { - await render( - {}} - accessibilityState={{ checked: true, busy: true, expanded: false }} - > - Accessibility Chip - - ); + const originalPlatform = Platform.OS; - expect(screen.getByRole('button')).toHaveProp( - 'accessibilityState', - expect.objectContaining({ checked: true, busy: true, expanded: false }) - ); + afterEach(() => { + Platform.OS = originalPlatform; }); - it('forwards aria-checked to the underlying touchable element', async () => { - await render( - {}} aria-checked={true}> - Checkable Chip - - ); + describe('web', () => { + beforeEach(() => { + Platform.OS = 'web'; + }); - expect(screen.getByRole('button')).toHaveProp( - 'accessibilityState', - expect.objectContaining({ checked: true }) - ); - }); + it('does not expose aria-pressed for an action Chip when selected is omitted', async () => { + await render( {}}>Action Chip); - it('prioritizes aria-checked over accessibilityState.checked', async () => { - await render( - {}} - accessibilityState={{ checked: false }} - aria-checked={true} - > - Chip - - ); + const button = screen.getByRole('button'); + expect(button).not.toHaveProp('aria-pressed'); + }); - expect(screen.getByRole('button')).toHaveProp( - 'accessibilityState', - expect.objectContaining({ checked: true }) - ); - }); + it('exposes aria-pressed as true when selected is true', async () => { + await render( + {}} selected={true}> + Selected Chip + + ); - it('handles both selected and checked state simultaneously', async () => { - await render( - {}} selected aria-checked={true}> - Selected Checked Chip - - ); + const button = screen.getByRole('button'); + expect(button).toHaveProp('aria-pressed', true); + }); - expect(screen.getByRole('button')).toHaveProp( - 'accessibilityState', - expect.objectContaining({ checked: true, selected: true }) - ); - }); + it('exposes aria-pressed as false when selected is explicitly false', async () => { + await render( + {}} selected={false}> + Unselected Chip + + ); - it('handles accessibilityState.selected when selected prop is omitted', async () => { - await render( - {}} accessibilityState={{ selected: true }}> - Selected Chip - - ); + const button = screen.getByRole('button'); + expect(button).toHaveProp('aria-pressed', false); + }); - expect(screen.getByRole('button')).toHaveProp( - 'accessibilityState', - expect.objectContaining({ selected: true }) - ); + it('removes aria-selected from the interactive button', async () => { + await render( + {}} selected={true}> + Toggle Chip + + ); + + const button = screen.getByRole('button'); + expect(button).not.toHaveProp('aria-selected'); + }); + + it('removes aria-selected when selected is omitted', async () => { + await render( {}}>Action Chip); + + const button = screen.getByRole('button'); + expect(button).not.toHaveProp('aria-selected'); + }); }); - it('forwards aria-checked when role is checkbox', async () => { - await render( - {}} role="checkbox" aria-checked={true}> - Checkbox Chip - - ); + describe('native', () => { + it('preserves selected state when selected is true', async () => { + await render( + {}} selected={true}> + Selected Chip + + ); + + const button = screen.getByRole('button'); + expect(button).toHaveProp( + 'accessibilityState', + expect.objectContaining({ selected: true }) + ); + }); - expect(screen.getByRole('checkbox')).toHaveProp( - 'accessibilityState', - expect.objectContaining({ checked: true }) - ); + it('produces native selected state false when selected is explicitly false', async () => { + await render( + {}} selected={false}> + Unselected Chip + + ); + + const button = screen.getByRole('button'); + expect(button).toHaveProp( + 'accessibilityState', + expect.objectContaining({ selected: false }) + ); + }); + + it('produces native selected state false when selected is omitted', async () => { + await render( {}}>Default Chip); + + const button = screen.getByRole('button'); + expect(button).toHaveProp( + 'accessibilityState', + expect.objectContaining({ selected: false }) + ); + }); }); - it('does not pass accessibilityState or aria-checked to the outer surface', async () => { + it('does not forward accessibilityState or aria-checked to the outer Surface or interactive TouchableRipple', async () => { await render( {}} testID="chip-touchable" - accessibilityState={{ checked: true }} + accessibilityState={{ checked: true, busy: true }} aria-checked={true} > Chip @@ -410,22 +426,28 @@ describe('accessibility', () => { const touchable = screen.getByTestId('chip-touchable'); const surface = touchable.parent; + expect(surface).not.toHaveProp('accessibilityState'); expect(surface).not.toHaveProp('aria-checked'); + + expect(touchable).not.toHaveProp('aria-checked'); + expect(touchable).toHaveProp( + 'accessibilityState', + expect.not.objectContaining({ + checked: expect.anything(), + busy: expect.anything(), + }) + ); }); - it('has default accessibilityState when accessibilityState and aria-checked are omitted', async () => { + it('does not forward accessibilityState or aria-checked to the interactive button by default', async () => { await render( {}}>Default Chip); const button = screen.getByRole('button'); + expect(button).not.toHaveProp('aria-checked'); expect(button).toHaveProp( 'accessibilityState', expect.not.objectContaining({ checked: expect.anything() }) ); - expect(button).toHaveProp( - 'accessibilityState', - expect.objectContaining({ selected: false, disabled: false }) - ); - expect(button).not.toHaveProp('aria-checked'); }); });