diff --git a/src/components/Chip/Chip.tsx b/src/components/Chip/Chip.tsx index 4507561a48..82874c6ad9 100644 --- a/src/components/Chip/Chip.tsx +++ b/src/components/Chip/Chip.tsx @@ -184,8 +184,10 @@ const Chip = ({ children, icon, avatar, - selected = false, + selected, disabled = false, + accessibilityState: _accessibilityState, + 'aria-checked': _ariaChecked, background, 'aria-label': ariaLabel, role = 'button', @@ -300,7 +302,11 @@ const Chip = ({ disabled={disabled} aria-label={ariaLabel} role={role} - aria-selected={selected} + {...(Platform.OS === 'web' + ? selected !== undefined + ? { 'aria-pressed': selected } + : undefined + : { 'aria-selected': selected ?? false })} aria-disabled={disabled} testID={testID} theme={theme} diff --git a/src/components/__tests__/Chip.test.tsx b/src/components/__tests__/Chip.test.tsx index a7d522e4f0..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'; @@ -309,3 +311,143 @@ describe('getChipColor - border color', () => { }); }); }); + +describe('accessibility', () => { + const originalPlatform = Platform.OS; + + afterEach(() => { + Platform.OS = originalPlatform; + }); + + describe('web', () => { + beforeEach(() => { + Platform.OS = 'web'; + }); + + it('does not expose aria-pressed for an action Chip when selected is omitted', async () => { + await render( {}}>Action Chip); + + const button = screen.getByRole('button'); + expect(button).not.toHaveProp('aria-pressed'); + }); + + it('exposes aria-pressed as true when selected is true', async () => { + await render( + {}} selected={true}> + Selected Chip + + ); + + const button = screen.getByRole('button'); + expect(button).toHaveProp('aria-pressed', true); + }); + + it('exposes aria-pressed as false when selected is explicitly false', async () => { + await render( + {}} selected={false}> + Unselected Chip + + ); + + const button = screen.getByRole('button'); + expect(button).toHaveProp('aria-pressed', false); + }); + + 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'); + }); + }); + + 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 }) + ); + }); + + 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 forward accessibilityState or aria-checked to the outer Surface or interactive TouchableRipple', async () => { + await render( + {}} + testID="chip-touchable" + accessibilityState={{ checked: true, busy: 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'); + + expect(touchable).not.toHaveProp('aria-checked'); + expect(touchable).toHaveProp( + 'accessibilityState', + expect.not.objectContaining({ + checked: expect.anything(), + busy: expect.anything(), + }) + ); + }); + + 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() }) + ); + }); +});