Skip to content

Commit

Permalink
fix: Makes autosuggest input use aria-expanded every time the dropdow…
Browse files Browse the repository at this point in the history
…n is shown
  • Loading branch information
pan-kot committed Dec 5, 2024
1 parent a5be1f7 commit 6b8b42a
Show file tree
Hide file tree
Showing 3 changed files with 133 additions and 87 deletions.
120 changes: 120 additions & 0 deletions src/autosuggest/__tests__/autosuggest-dropdown-states.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import * as React from 'react';
import { render } from '@testing-library/react';

import '../../__a11y__/to-validate-a11y';
import Autosuggest, { AutosuggestProps } from '../../../lib/components/autosuggest';
import createWrapper from '../../../lib/components/test-utils/dom';

import styles from '../../../lib/components/autosuggest/styles.css.js';
import statusIconStyles from '../../../lib/components/status-indicator/styles.selectors.js';

const defaultOptions: AutosuggestProps.Options = [
{ value: '1', label: 'One' },
{ value: '2', lang: 'fr' },
];
const defaultProps: AutosuggestProps = {
enteredTextLabel: () => 'Use value',
value: '',
onChange: () => {},
options: defaultOptions,
};

function renderAutosuggest(jsx: React.ReactElement) {
const { container, rerender } = render(jsx);
const wrapper = createWrapper(container).findAutosuggest()!;
return { container, wrapper, rerender };
}

(
[
['loading', true],
['error', true],
['finished', false],
] as const
).forEach(([statusType, isSticky]) => {
test(`should display ${statusType} status text as ${isSticky ? 'sticky' : 'non-sticky'} footer`, () => {
const statusText = {
[`${statusType}Text`]: `Test ${statusType} text`,
};
const { wrapper } = renderAutosuggest(<Autosuggest {...defaultProps} statusType={statusType} {...statusText} />);
wrapper.focus();
expect(wrapper.findStatusIndicator()!.getElement()).toHaveTextContent(`Test ${statusType} text`);

const dropdown = wrapper.findDropdown()!.findOpenDropdown()!;
expect(Boolean(dropdown.findByClassName(styles['list-bottom']))).toBe(!isSticky);
});

test(`check a11y for ${statusType} and ${isSticky ? 'sticky' : 'non-sticky'} footer`, async () => {
const statusText = {
[`${statusType}Text`]: `Test ${statusType} text`,
};
const { container, wrapper } = renderAutosuggest(
<Autosuggest {...defaultProps} statusType={statusType} {...statusText} ariaLabel="input" />
);
wrapper.focus();

await expect(container).toValidateA11y();
});
});

test('should display error status icon with provided aria label', () => {
const { wrapper } = renderAutosuggest(
<Autosuggest
{...defaultProps}
statusType="error"
errorText="Test error text"
errorIconAriaLabel="Test error text"
/>
);
wrapper.focus();
const statusIcon = wrapper.findStatusIndicator()!.findByClassName(statusIconStyles.icon)!.getElement();
expect(statusIcon).toHaveAttribute('aria-label', 'Test error text');
expect(statusIcon).toHaveAttribute('role', 'img');
});

test('should associate the error status footer with the dropdown', () => {
const { wrapper } = renderAutosuggest(
<Autosuggest {...defaultProps} statusType="error" errorText="Test error text" />
);
wrapper.focus();
expect(wrapper.findDropdown().find('ul')!.getElement()).toHaveAccessibleDescription('Test error text');
});

test('should associate the finished status footer with the dropdown', () => {
const { wrapper } = renderAutosuggest(
<Autosuggest {...defaultProps} statusType="finished" finishedText="Finished text" />
);
wrapper.focus();
expect(wrapper.findDropdown().find('ul')!.getElement()).toHaveAccessibleDescription('Finished text');
});

test('should associate the matches count footer with the dropdown', () => {
const { wrapper } = renderAutosuggest(
<Autosuggest {...defaultProps} value="A" statusType="finished" filteringResultsText={() => '3 items'} />
);
wrapper.focus();
expect(wrapper.findDropdown().find('ul')!.getElement()).toHaveAccessibleDescription('3 items');
});

describe('when no options is matched the dropdown is shown but aria-expanded is false', () => {
test('matched option and entered text option', () => {
const { wrapper } = renderAutosuggest(<Autosuggest {...defaultProps} statusType="finished" value="One" />);
wrapper.setInputValue('One');
expect(wrapper.findDropdown().findOpenDropdown()).not.toBe(null);
expect(wrapper.findEnteredTextOption()).not.toBe(null);
expect(wrapper.findDropdown().findOptions()).toHaveLength(1);
expect(wrapper.findNativeInput().getElement()).toHaveAttribute('aria-expanded', 'true');
});

test('only entered text option', () => {
const { wrapper } = renderAutosuggest(<Autosuggest {...defaultProps} statusType="finished" value="free-text" />);
wrapper.setInputValue('free-text');
expect(wrapper.findDropdown().findOpenDropdown()).not.toBe(null);
expect(wrapper.findEnteredTextOption()).not.toBe(null);
expect(wrapper.findDropdown().findOptions()).toHaveLength(0);
expect(wrapper.findNativeInput().getElement()).toHaveAttribute('aria-expanded', 'true');
});
});
96 changes: 11 additions & 85 deletions src/autosuggest/__tests__/autosuggest.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,7 @@ import '../../__a11y__/to-validate-a11y';
import Autosuggest, { AutosuggestProps } from '../../../lib/components/autosuggest';
import createWrapper from '../../../lib/components/test-utils/dom';

import styles from '../../../lib/components/autosuggest/styles.css.js';
import itemStyles from '../../../lib/components/internal/components/selectable-item/styles.css.js';
import statusIconStyles from '../../../lib/components/status-indicator/styles.selectors.js';

const defaultOptions: AutosuggestProps.Options = [
{ value: '1', label: 'One' },
Expand Down Expand Up @@ -140,6 +138,17 @@ test('should not close dropdown when no realted target in blur', () => {
expect(wrapper.findDropdown().findOpenDropdown()).toBe(null);
});

it('should warn if recoveryText is provided without associated handler', () => {
renderAutosuggest(
<Autosuggest {...defaultProps} statusType="error" errorText="Test error text" recoveryText="Retry" />
);
expect(warnOnce).toHaveBeenCalledTimes(1);
expect(warnOnce).toHaveBeenCalledWith(
'Autosuggest',
'`onLoadItems` must be provided for `recoveryText` to be displayed.'
);
});

describe('onSelect', () => {
test('should select normal value', () => {
const onChange = jest.fn();
Expand Down Expand Up @@ -176,89 +185,6 @@ describe('onSelect', () => {
});
});

describe('Dropdown states', () => {
(
[
['loading', true],
['error', true],
['finished', false],
] as const
).forEach(([statusType, isSticky]) => {
test(`should display ${statusType} status text as ${isSticky ? 'sticky' : 'non-sticky'} footer`, () => {
const statusText = {
[`${statusType}Text`]: `Test ${statusType} text`,
};
const { wrapper } = renderAutosuggest(<Autosuggest {...defaultProps} statusType={statusType} {...statusText} />);
wrapper.focus();
expect(wrapper.findStatusIndicator()!.getElement()).toHaveTextContent(`Test ${statusType} text`);

const dropdown = wrapper.findDropdown()!.findOpenDropdown()!;
expect(Boolean(dropdown.findByClassName(styles['list-bottom']))).toBe(!isSticky);
});

test(`check a11y for ${statusType} and ${isSticky ? 'sticky' : 'non-sticky'} footer`, async () => {
const statusText = {
[`${statusType}Text`]: `Test ${statusType} text`,
};
const { container, wrapper } = renderAutosuggest(
<Autosuggest {...defaultProps} statusType={statusType} {...statusText} ariaLabel="input" />
);
wrapper.focus();

await expect(container).toValidateA11y();
});
});

test('should display error status icon with provided aria label', () => {
const { wrapper } = renderAutosuggest(
<Autosuggest
{...defaultProps}
statusType="error"
errorText="Test error text"
errorIconAriaLabel="Test error text"
/>
);
wrapper.focus();
const statusIcon = wrapper.findStatusIndicator()!.findByClassName(statusIconStyles.icon)!.getElement();
expect(statusIcon).toHaveAttribute('aria-label', 'Test error text');
expect(statusIcon).toHaveAttribute('role', 'img');
});

test('should associate the error status footer with the dropdown', () => {
const { wrapper } = renderAutosuggest(
<Autosuggest {...defaultProps} statusType="error" errorText="Test error text" />
);
wrapper.focus();
expect(wrapper.findDropdown().find('ul')!.getElement()).toHaveAccessibleDescription('Test error text');
});

test('should associate the finished status footer with the dropdown', () => {
const { wrapper } = renderAutosuggest(
<Autosuggest {...defaultProps} statusType="finished" finishedText="Finished text" />
);
wrapper.focus();
expect(wrapper.findDropdown().find('ul')!.getElement()).toHaveAccessibleDescription('Finished text');
});

it('when no options is matched the dropdown is shown but aria-expanded is false', () => {
const { wrapper } = renderAutosuggest(<Autosuggest {...defaultProps} statusType="finished" value="free-text" />);
wrapper.setInputValue('free-text');
expect(wrapper.findNativeInput().getElement()).toHaveAttribute('aria-expanded', 'false');
expect(wrapper.findDropdown().findOpenDropdown()).not.toBe(null);
});

it('should warn if recoveryText is provided without associated handler', () => {
renderAutosuggest(
<Autosuggest {...defaultProps} statusType="error" errorText="Test error text" recoveryText="Retry" />
);
expect(warnOnce).toHaveBeenCalledTimes(1);
expect(warnOnce).toHaveBeenCalledWith(
'Autosuggest',
'`onLoadItems` must be provided for `recoveryText` to be displayed.'
);
});
});

describe('a11y props', () => {
test('adds combobox role to input', () => {
const { wrapper } = renderAutosuggest(<Autosuggest {...defaultProps} options={[]} />);
Expand Down
4 changes: 2 additions & 2 deletions src/autosuggest/internal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ const InternalAutosuggest = React.forwardRef((props: InternalAutosuggestProps, r
hasRecoveryCallback: !!onLoadItems,
});

const shouldRenderDropdownContent = !isEmpty || dropdownStatus.content;
const shouldRenderDropdownContent = !isEmpty || !!dropdownStatus.content;

return (
<AutosuggestInput
Expand All @@ -222,7 +222,7 @@ const InternalAutosuggest = React.forwardRef((props: InternalAutosuggestProps, r
expandToViewport={expandToViewport}
ariaControls={listId}
ariaActivedescendant={highlightedOptionId}
dropdownExpanded={autosuggestItemsState.items.length > 1 || dropdownStatus.content !== null}
dropdownExpanded={shouldRenderDropdownContent}
dropdownContent={
shouldRenderDropdownContent && (
<AutosuggestOptionsList
Expand Down

0 comments on commit 6b8b42a

Please sign in to comment.