Skip to content

Commit

Permalink
feat: add attributes to input and wrapper component (#2570)
Browse files Browse the repository at this point in the history
Create IDs for items in dropdown. Add aria-activedescendant attribute to input to identify to assistive technologies
which item in the dropdown is being focused. Add aria-live=”assertive” to wrapper component to allow screen readers to
read out the number of options in the dropdown.
  • Loading branch information
httpsmenahassan authored Oct 11, 2023
1 parent f284985 commit acf452b
Show file tree
Hide file tree
Showing 2 changed files with 66 additions and 0 deletions.
13 changes: 13 additions & 0 deletions src/Form/FormAutosuggest.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React, {
useEffect, useState,
} from 'react';
import PropTypes from 'prop-types';
import { v4 as uuidv4 } from 'uuid';
import { useIntl } from 'react-intl';
import { KeyboardArrowUp, KeyboardArrowDown } from '../../icons';
import Icon from '../Icon';
Expand Down Expand Up @@ -38,6 +39,11 @@ function FormAutosuggest({
errorMessage: '',
dropDownItems: [],
});
const [activeMenuItemId, setActiveMenuItemId] = useState(null);

const handleMenuItemFocus = (menuItemId) => {
setActiveMenuItemId(menuItemId);
};

const handleItemClick = (e, onClick) => {
const clickedValue = e.currentTarget.getAttribute('data-value');
Expand All @@ -63,12 +69,15 @@ function FormAutosuggest({
let childrenOpt = React.Children.map(children, (child) => {
// eslint-disable-next-line no-shadow
const { children, onClick, ...rest } = child.props;
const menuItemId = uuidv4();

return React.cloneElement(child, {
...rest,
children,
'data-value': children,
onClick: (e) => handleItemClick(e, onClick),
id: menuItemId,
onFocus: () => handleMenuItemFocus(menuItemId),
});
});

Expand Down Expand Up @@ -219,6 +228,9 @@ function FormAutosuggest({

return (
<div className="pgn__form-autosuggest__wrapper" ref={parentRef}>
<div aria-live="assertive" className="sr-only" data-testid="autosuggest-screen-reader-options-count">
{`${state.dropDownItems.length} options found`}
</div>
<FormGroup isInvalid={!!state.errorMessage}>
<FormControl
aria-expanded={(state.dropDownItems.length > 0).toString()}
Expand All @@ -228,6 +240,7 @@ function FormAutosuggest({
autoComplete="off"
value={state.displayValue}
aria-invalid={state.errorMessage}
aria-activedescendant={activeMenuItemId}
onChange={handleOnChange}
onClick={handleClick}
trailingElement={iconToggle}
Expand Down
53 changes: 53 additions & 0 deletions src/Form/tests/FormAutosuggest.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,33 @@ describe('render behavior', () => {

expect(formControlFeedback).toBeInTheDocument();
});

it('renders component with options that all have IDs', () => {
const { getByTestId, getAllByTestId } = render(<FormAutosuggestTestComponent />);
const input = getByTestId('autosuggest-textbox-input');

userEvent.click(input);
const optionItemIds = getAllByTestId('autosuggest-optionitem').map(item => item.id);

expect(optionItemIds).not.toContain(null);
expect(optionItemIds).not.toContain(undefined);
});

it('confirms that the value of the aria-live attribute on the wrapper component is assertive', () => {
const { getByTestId } = render(<FormAutosuggestWrapper />);

expect(getByTestId('autosuggest-screen-reader-options-count').getAttribute('aria-live')).toEqual('assertive');
});

it('displays correct amount of options found to screen readers', () => {
const { getByText, getByTestId } = render(<FormAutosuggestTestComponent />);
const input = getByTestId('autosuggest-textbox-input');

expect(getByText('0 options found')).toBeInTheDocument();
userEvent.click(input);

expect(getByText('3 options found')).toBeInTheDocument();
});
});

describe('controlled behavior', () => {
Expand Down Expand Up @@ -136,6 +163,17 @@ describe('controlled behavior', () => {
expect(onClick).toHaveBeenCalledTimes(0);
});

it('should set the correct activedescendant', () => {
const { getByTestId, getAllByTestId } = render(<FormAutosuggestTestComponent />);
const input = getByTestId('autosuggest-textbox-input');

userEvent.click(input);
const expectedOptionId = getAllByTestId('autosuggest-optionitem')[0].id;
userEvent.keyboard('{arrowdown}');

expect(input.getAttribute('aria-activedescendant')).toEqual(expectedOptionId);
});

it('filters dropdown based on typed field value with one match', () => {
const { getByTestId, queryAllByTestId } = render(<FormAutosuggestTestComponent />);
const input = getByTestId('autosuggest-textbox-input');
Expand Down Expand Up @@ -187,4 +225,19 @@ describe('controlled behavior', () => {
const updatedList = queryAllByTestId('autosuggest-optionitem');
expect(updatedList.length).toBe(0);
});

it('updates screen reader option count based on typed field value with multiple matches', () => {
const { getByText, getByTestId } = render(<FormAutosuggestTestComponent />);
const input = getByTestId('autosuggest-textbox-input');

expect(getByText('0 options found')).toBeInTheDocument();
userEvent.click(input);

expect(getByText('3 options found')).toBeInTheDocument();

userEvent.click(input);
userEvent.type(input, '1');

expect(getByText('2 options found')).toBeInTheDocument();
});
});

0 comments on commit acf452b

Please sign in to comment.