Skip to content

Commit 6b44f71

Browse files
johannes-weberJohannes Weber
and
Johannes Weber
authored
fix: Re-announce dropdown footer message on dropdown toggle (#3408)
Co-authored-by: Johannes Weber <[email protected]>
1 parent c072970 commit 6b44f71

File tree

7 files changed

+143
-11
lines changed

7 files changed

+143
-11
lines changed

src/autosuggest/__tests__/autosuggest-dropdown-states.test.tsx

+40-6
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
import * as React from 'react';
55
import { render } from '@testing-library/react';
66

7+
import { KeyCode } from '@cloudscape-design/test-utils-core/utils';
8+
79
import '../../__a11y__/to-validate-a11y';
810
import Autosuggest, { AutosuggestProps } from '../../../lib/components/autosuggest';
911
import createWrapper from '../../../lib/components/test-utils/dom';
@@ -38,9 +40,9 @@ function focusInput() {
3840
createWrapper().findAutosuggest()!.focus();
3941
}
4042

41-
function expectDropdown() {
43+
function expectDropdown(expandToViewport = false) {
4244
const wrapper = createWrapper().findAutosuggest()!;
43-
expect(wrapper.findDropdown().findOpenDropdown()).not.toBe(null);
45+
expect(wrapper.findDropdown({ expandToViewport }).findOpenDropdown()).not.toBe(null);
4446
expect(wrapper.findNativeInput().getElement()).toHaveAttribute('aria-expanded', 'true');
4547
}
4648

@@ -53,11 +55,16 @@ function expectFooterSticky(isSticky: boolean) {
5355
expect(Boolean(dropdown.findByClassName(styles['list-bottom']))).toBe(!isSticky);
5456
}
5557

56-
function expectFooterContent(expectedText: string) {
58+
function expectFooterContent(expectedText: string, expandToViewport = false) {
5759
const wrapper = createWrapper().findAutosuggest()!;
58-
expect(wrapper.findDropdown().findFooterRegion()!).not.toBe(null);
59-
expect(wrapper.findDropdown().findFooterRegion()!.getElement()).toHaveTextContent(expectedText);
60-
expect(wrapper.findDropdown().find('ul')!.getElement()).toHaveAccessibleDescription(expectedText);
60+
expect(wrapper.findDropdown({ expandToViewport }).findFooterRegion()!).not.toBe(null);
61+
expect(wrapper.findDropdown({ expandToViewport }).findFooterRegion()!.getElement()).toHaveTextContent(expectedText);
62+
expect(wrapper.findDropdown({ expandToViewport }).find('ul')!.getElement()).toHaveAccessibleDescription(expectedText);
63+
}
64+
65+
function expectLiveRegionText(expectedText: string) {
66+
const liveRegion = createWrapper().findLiveRegion()!.getElement();
67+
expect(liveRegion).toHaveTextContent(expectedText);
6168
}
6269

6370
function expectFooterImage(expectedText: string) {
@@ -127,6 +134,33 @@ describe('footer types', () => {
127134
});
128135
});
129136

137+
describe.each([true, false])('footer live announcements [expandToViewport=%s]', (expandToViewport: boolean) => {
138+
test('live announces error text on initial dropdown render', () => {
139+
renderAutosuggest({ statusType: 'error', expandToViewport });
140+
focusInput();
141+
expectDropdown(expandToViewport);
142+
expectFooterContent('error!', expandToViewport);
143+
expectLiveRegionText('error!');
144+
});
145+
146+
test('live announces error text on dropdown toggle', () => {
147+
const { wrapper } = renderAutosuggest({ statusType: 'error', expandToViewport });
148+
focusInput();
149+
expectDropdown(expandToViewport);
150+
expectFooterContent('error!', expandToViewport);
151+
expectLiveRegionText('error!');
152+
153+
wrapper.findNativeInput().keydown(KeyCode.enter);
154+
expect(wrapper.findDropdown()!.findOpenDropdown()).toBe(null);
155+
expect(createWrapper().findLiveRegion()).toBeNull();
156+
157+
wrapper.findNativeInput().keydown(KeyCode.down);
158+
expectDropdown(expandToViewport);
159+
expectFooterContent('error!', expandToViewport);
160+
expectLiveRegionText('error!');
161+
});
162+
});
163+
130164
describe('filtering results', () => {
131165
describe('with empty state', () => {
132166
test('displays empty state footer when value is empty', () => {

src/date-range-picker/__tests__/date-range-picker.test.tsx

+3-3
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,7 @@ describe('Date range picker', () => {
291291

292292
wrapper.findDropdown()!.findApplyButton().click();
293293
expect(wrapper.findDropdown()!.findValidationError()?.getElement()).toHaveTextContent('10 is not allowed.');
294-
expect(createWrapper().findAll('[aria-live]')[1]!.getElement()).toHaveTextContent('10 is not allowed.');
294+
expect(createWrapper().findLiveRegion()!.getElement()).toHaveTextContent('10 is not allowed.');
295295
});
296296

297297
test('after rendering the error once, displays subsequent errors in real time', () => {
@@ -360,7 +360,7 @@ describe('Date range picker', () => {
360360
wrapper.findDropdown()?.findDateAt('left', 1, 1).click();
361361
wrapper.findDropdown()!.findApplyButton().click();
362362

363-
const liveRegion = document.querySelectorAll('[aria-live=polite]')![3];
363+
const liveRegion = document.querySelectorAll('[aria-live=polite]')![2];
364364

365365
// announces first validation error
366366
await waitFor(() => expect(liveRegion).toHaveTextContent('You must provide an end date'));
@@ -372,7 +372,7 @@ describe('Date range picker', () => {
372372

373373
wrapper.findDropdown()!.findApplyButton().click();
374374

375-
// reannounces second validation error
375+
// re-announces second validation error, indicated by the trailing dot.
376376
await waitFor(() => expect(liveRegion).toHaveTextContent('The range cannot start before 2020.'));
377377

378378
Mockdate.reset();

src/internal/components/autosuggest-input/index.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,7 @@ const AutosuggestInput = React.forwardRef(
286286
footer={
287287
dropdownFooterRef && (
288288
<div ref={dropdownFooterRef} className={styles['dropdown-footer']}>
289-
{dropdownFooter}
289+
{open && dropdownFooter ? dropdownFooter : null}
290290
</div>
291291
)
292292
}

src/internal/components/dropdown-footer/__tests__/dropdown-footer.test.tsx

+10
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import React from 'react';
44
import { render } from '@testing-library/react';
55

66
import DropdownFooter from '../../../../../lib/components/internal/components/dropdown-footer';
7+
import createWrapper from '../../../../../lib/components/test-utils/dom';
78
import DropdownWrapper from '../../../../../lib/components/test-utils/dom/internal/dropdown';
89

910
import dropdownFooterStyles from '../../../../../lib/components/internal/components/dropdown-footer/styles.css.js';
@@ -21,6 +22,7 @@ describe('Dropdown footer', () => {
2122
expect(element).toHaveTextContent('hello world');
2223
expect(element).toHaveClass(dropdownFooterStyles.root);
2324
expect(element).not.toHaveClass(dropdownFooterStyles.hidden);
25+
expect(createWrapper().findLiveRegion()!.getElement()).toHaveTextContent('hello world');
2426
});
2527

2628
test('adds hidden class when given content is null', () => {
@@ -29,4 +31,12 @@ describe('Dropdown footer', () => {
2931
expect(element).toHaveClass(dropdownFooterStyles.root);
3032
expect(element).toHaveClass(dropdownFooterStyles.hidden);
3133
});
34+
35+
test('does not render live region when given content is null', () => {
36+
const { wrapper } = renderComponent(<DropdownFooter content={null} />);
37+
const element = wrapper.find('div')!.getElement();
38+
expect(element).toHaveClass(dropdownFooterStyles.root);
39+
expect(element).toHaveClass(dropdownFooterStyles.hidden);
40+
expect(createWrapper().findLiveRegion()).toBeNull();
41+
});
3242
});

src/internal/components/dropdown-footer/index.tsx

+5-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,11 @@ interface DropdownFooter {
1616

1717
const DropdownFooter: React.FC<DropdownFooter> = ({ content, id, hasItems = true }: DropdownFooter) => (
1818
<div className={clsx(styles.root, { [styles.hidden]: content === null, [styles['no-items']]: !hasItems })}>
19-
<InternalLiveRegion id={id}>{content && <DropdownStatus>{content}</DropdownStatus>}</InternalLiveRegion>
19+
{content && (
20+
<InternalLiveRegion id={id}>
21+
<DropdownStatus>{content}</DropdownStatus>
22+
</InternalLiveRegion>
23+
)}
2024
</div>
2125
);
2226

src/multiselect/__tests__/multiselect.test.tsx

+49
Original file line numberDiff line numberDiff line change
@@ -459,6 +459,55 @@ describe('Dropdown states', () => {
459459
});
460460
});
461461

462+
describe.each([true, false])('footer live announcements [expandToViewport=%s]', (expandToViewport: boolean) => {
463+
test('live announces footer text on initial dropdown render', () => {
464+
const { wrapper } = renderMultiselect(
465+
<Multiselect
466+
selectedOptions={[]}
467+
options={defaultOptions}
468+
statusType="error"
469+
errorText="Test error text"
470+
errorIconAriaLabel="Test error text"
471+
recoveryText="Retry"
472+
expandToViewport={expandToViewport}
473+
keepOpen={false}
474+
/>
475+
);
476+
expect(createWrapper().findLiveRegion()).toBeNull();
477+
478+
wrapper.openDropdown();
479+
expect(wrapper.findDropdown({ expandToViewport }).findFooterRegion()!.getElement()).toHaveTextContent(
480+
'Test error text'
481+
);
482+
expect(createWrapper().findLiveRegion()!.getElement()).toHaveTextContent('Test error text');
483+
});
484+
485+
test('live announce footer text on dropdown toggle', () => {
486+
const { wrapper } = renderMultiselect(
487+
<Multiselect
488+
selectedOptions={[]}
489+
options={defaultOptions}
490+
statusType="error"
491+
errorText="Test error text"
492+
errorIconAriaLabel="Test error text"
493+
recoveryText="Retry"
494+
expandToViewport={expandToViewport}
495+
keepOpen={false}
496+
/>
497+
);
498+
expect(createWrapper().findLiveRegion()).toBeNull();
499+
500+
wrapper.openDropdown();
501+
expect(createWrapper().findLiveRegion()!.getElement()).toHaveTextContent('Test error text');
502+
503+
wrapper.closeDropdown({ expandToViewport });
504+
expect(createWrapper().findLiveRegion()).toBeNull();
505+
506+
wrapper.openDropdown();
507+
expect(createWrapper().findLiveRegion()!.getElement()).toHaveTextContent('Test error text');
508+
});
509+
});
510+
462511
test('fires a change event when user selects a group option from the dropdown', () => {
463512
const onChange = jest.fn();
464513
const { wrapper } = renderMultiselect(

src/select/__tests__/select-a11y.test.tsx

+35
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ const defaultOptions: SelectProps.Options = [
2626
},
2727
];
2828

29+
function expectLiveRegionText(expectedText: string) {
30+
const liveRegion = createWrapper().findLiveRegion()!.getElement();
31+
expect(liveRegion).toHaveTextContent(expectedText);
32+
}
33+
2934
describe.each([false, true])('expandToViewport=%s', expandToViewport => {
3035
const defaultProps = {
3136
options: defaultOptions,
@@ -78,4 +83,34 @@ describe.each([false, true])('expandToViewport=%s', expandToViewport => {
7883
.join(' ');
7984
expect(label).toBe('select First');
8085
});
86+
87+
test('live announces footer text on initial dropdown render', () => {
88+
const { wrapper } = renderSelect({
89+
selectedOption: { label: 'First', value: '1' },
90+
statusType: 'error',
91+
errorText: 'Test error text',
92+
});
93+
expect(createWrapper().findLiveRegion()).toBeNull();
94+
95+
wrapper.openDropdown();
96+
expectLiveRegionText('Test error text');
97+
});
98+
99+
test('live announce footer text on dropdown toggle', () => {
100+
const { wrapper } = renderSelect({
101+
selectedOption: { label: 'First', value: '1' },
102+
statusType: 'error',
103+
errorText: 'Test error text',
104+
});
105+
expect(createWrapper().findLiveRegion()).toBeNull();
106+
107+
wrapper.openDropdown();
108+
expectLiveRegionText('Test error text');
109+
110+
wrapper.closeDropdown({ expandToViewport });
111+
expect(createWrapper().findLiveRegion()).toBeNull();
112+
113+
wrapper.openDropdown();
114+
expectLiveRegionText('Test error text');
115+
});
81116
});

0 commit comments

Comments
 (0)