diff --git a/CHANGELOG.md b/CHANGELOG.md index d1ec5cec9c..d721450c19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,9 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ### BREAKING CHANGES - Rename `inputFocusBorderBottomColor` to `inputFocusBorderColor` in `InputVariables` @layershifter ([#1247](https://github.com/stardust-ui/react/pull/1247)) +### Fixes +- Fix a11y message cleanup for add and remove items in `Dropdown` @silviuavram ([#1237](https://github.com/stardust-ui/react/pull/1237)) + ### Features - Move `Input` styles to Base theme @layershifter ([#1247](https://github.com/stardust-ui/react/pull/1247)) diff --git a/packages/react/src/components/Dropdown/Dropdown.tsx b/packages/react/src/components/Dropdown/Dropdown.tsx index 33a6ee81f4..38e2306d4a 100644 --- a/packages/react/src/components/Dropdown/Dropdown.tsx +++ b/packages/react/src/components/Dropdown/Dropdown.tsx @@ -226,6 +226,8 @@ class Dropdown extends AutoControlledComponent, Dropdo static className = 'ui-dropdown' + static a11yStatusCleanupTime = 500 + static slotClassNames: DropdownSlotClassNames static propTypes = { @@ -315,6 +317,12 @@ class Dropdown extends AutoControlledComponent, Dropdo } } + a11yStatusTimeout: any + + componentWillUnmount() { + clearTimeout(this.a11yStatusTimeout) + } + public renderComponent({ ElementType, classes, @@ -950,6 +958,7 @@ class Dropdown extends AutoControlledComponent, Dropdo if (getA11ySelectionMessage && getA11ySelectionMessage.onAdd) { this.setState({ a11ySelectionStatus: getA11ySelectionMessage.onAdd(item) }) + this.setA11ySelectionMessage() } if (multiple) { @@ -1035,6 +1044,7 @@ class Dropdown extends AutoControlledComponent, Dropdo if (getA11ySelectionMessage && getA11ySelectionMessage.onRemove) { this.setState({ a11ySelectionStatus: getA11ySelectionMessage.onRemove(poppedItem) }) + this.setA11ySelectionMessage() } this.trySetStateAndInvokeHandler('onSelectedChange', null, { value }) @@ -1130,6 +1140,17 @@ class Dropdown extends AutoControlledComponent, Dropdo // otherwise, highlight no item. return null } + + /** + * Function that sets and cleans the selection message after it has been set, + * so it is not read anymore via virtual cursor. + */ + private setA11ySelectionMessage = (): void => { + clearTimeout(this.a11yStatusTimeout) + this.a11yStatusTimeout = setTimeout(() => { + this.setState({ a11ySelectionStatus: '' }) + }, Dropdown.a11yStatusCleanupTime) + } } Dropdown.slotClassNames = { diff --git a/packages/react/test/specs/components/Dropdown/Dropdown-test.tsx b/packages/react/test/specs/components/Dropdown/Dropdown-test.tsx index 9984d1c3f4..c47b725dba 100644 --- a/packages/react/test/specs/components/Dropdown/Dropdown-test.tsx +++ b/packages/react/test/specs/components/Dropdown/Dropdown-test.tsx @@ -4,10 +4,12 @@ import * as _ from 'lodash' import Dropdown from 'src/components/Dropdown/Dropdown' import DropdownSearchInput from 'src/components/Dropdown/DropdownSearchInput' +import DropdownSelectedItem from 'src/components/Dropdown/DropdownSelectedItem' import { isConformant } from 'test/specs/commonTests' import { mountWithProvider } from 'test/utils' jest.dontMock('keyboard-key') +jest.useFakeTimers() describe('Dropdown', () => { const items = ['item1', 'item2', 'item3', 'item4', 'item5'] @@ -780,6 +782,10 @@ describe('Dropdown', () => { }) describe('getA11ySelectionMessage', () => { + afterEach(() => { + jest.runAllTimers() + }) + it('creates message container element', () => { mountWithProvider() expect( @@ -788,6 +794,53 @@ describe('Dropdown', () => { ), ).toBeTruthy() }) + + it('has the onAdd message inserted and cleared after an item has been added to selection', () => { + const wrapper = mountWithProvider( + 'bla bla added' }} + />, + ) + const dropdown = wrapper.find(Dropdown) + const triggerButton = wrapper.find(`button.${Dropdown.slotClassNames.triggerButton}`) + + triggerButton.simulate('click') + const firstItem = wrapper.find(`li.${Dropdown.slotClassNames.item}`).at(0) + firstItem.simulate('click') + + expect(dropdown.state('a11ySelectionStatus')).toBe('bla bla added') + + jest.runAllTimers() + + expect(dropdown.state('a11ySelectionStatus')).toBe('') + }) + + it('has the onRemove message inserted and cleared after an item has been removed from selection', () => { + const wrapper = mountWithProvider( + 'bla bla removed' }} + />, + ) + const dropdown = wrapper.find(Dropdown) + const triggerButton = wrapper.find(`button.${Dropdown.slotClassNames.triggerButton}`) + + triggerButton.simulate('click') + const firstItem = wrapper.find(`li.${Dropdown.slotClassNames.item}`).at(0) + firstItem.simulate('click') + jest.runAllTimers() + const removeIcon = wrapper.find(`span.${DropdownSelectedItem.slotClassNames.icon}`) + removeIcon.simulate('click') + + expect(dropdown.state('a11ySelectionStatus')).toBe('bla bla removed') + + jest.runAllTimers() + + expect(dropdown.state('a11ySelectionStatus')).toBe('') + }) }) describe('searchQuery', () => {