Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Autocomplete: Announce results to screen readers when first becoming visible #51018

Merged
merged 11 commits into from
Jun 23, 2023
1 change: 1 addition & 0 deletions packages/components/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

- `Popover`: Allow legitimate 0 positions to update popover position ([#51320](https://github.com/WordPress/gutenberg/pull/51320)).
- `Button`: Remove unnecessary margin from dashicon ([#51395](https://github.com/WordPress/gutenberg/pull/51395)).
- `Autocomplete`: Announce how many results are available to screen readers when suggestions list first renders ([#51018](https://github.com/WordPress/gutenberg/pull/51018)).

### Internal

Expand Down
46 changes: 44 additions & 2 deletions packages/components/src/autocomplete/autocompleter-ui.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ import {
useState,
} from '@wordpress/element';
import { useAnchor } from '@wordpress/rich-text';
import { useMergeRefs, useRefEffect } from '@wordpress/compose';
import { useDebounce, useMergeRefs, useRefEffect } from '@wordpress/compose';
import { speak } from '@wordpress/a11y';
import { __, _n, sprintf } from '@wordpress/i18n';

/**
* Internal dependencies
Expand All @@ -23,7 +25,7 @@ import Button from '../button';
import Popover from '../popover';
import { VisuallyHidden } from '../visually-hidden';
import { createPortal } from 'react-dom';
import type { AutocompleterUIProps, WPCompleter } from './types';
import type { AutocompleterUIProps, KeyedOption, WPCompleter } from './types';

export function getAutoCompleterUI( autocompleter: WPCompleter ) {
const useItems = autocompleter.useItems
Expand Down Expand Up @@ -69,8 +71,48 @@ export function getAutoCompleterUI( autocompleter: WPCompleter ) {

useOnClickOutside( popoverRef, reset );

const debouncedSpeak = useDebounce( speak, 500 );

function announce( options: Array< KeyedOption > ) {
if ( ! debouncedSpeak ) {
return;
}
if ( !! options.length ) {
if ( filterValue ) {
debouncedSpeak(
sprintf(
/* translators: %d: number of results. */
_n(
'%d result found, use up and down arrow keys to navigate.',
'%d results found, use up and down arrow keys to navigate.',
options.length
),
options.length
),
'assertive'
);
} else {
debouncedSpeak(
sprintf(
/* translators: %d: number of results. */
_n(
'Initial %d result loaded. Type to filter all available results. Use up and down arrow keys to navigate.',
'Initial %d results loaded. Type to filter all available results. Use up and down arrow keys to navigate.',
options.length
),
options.length
),
'assertive'
);
}
} else {
debouncedSpeak( __( 'No results.' ), 'assertive' );
}
}

useLayoutEffect( () => {
onChangeOptions( items );
announce( items );
// Temporarily disabling exhaustive-deps to avoid introducing unexpected side effecst.
// See https://github.com/WordPress/gutenberg/pull/41820
// eslint-disable-next-line react-hooks/exhaustive-deps
Expand Down
34 changes: 2 additions & 32 deletions packages/components/src/autocomplete/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,15 @@ import {
useRef,
useMemo,
} from '@wordpress/element';
import { __, _n, sprintf } from '@wordpress/i18n';
import {
useInstanceId,
useDebounce,
useMergeRefs,
useRefEffect,
} from '@wordpress/compose';
import { __, _n } from '@wordpress/i18n';
import { useInstanceId, useMergeRefs, useRefEffect } from '@wordpress/compose';
import {
create,
slice,
insert,
isCollapsed,
getTextContent,
} from '@wordpress/rich-text';
import { speak } from '@wordpress/a11y';

/**
* Internal dependencies
Expand All @@ -54,7 +48,6 @@ export function useAutocomplete( {
completers,
contentRef,
}: UseAutocompleteProps ) {
const debouncedSpeak = useDebounce( speak, 500 );
const instanceId = useInstanceId( useAutocomplete );
const [ selectedIndex, setSelectedIndex ] = useState( 0 );

Expand Down Expand Up @@ -137,28 +130,6 @@ export function useAutocomplete( {
setAutocompleterUI( null );
}

function announce( options: Array< KeyedOption > ) {
if ( ! debouncedSpeak ) {
return;
}
if ( !! options.length ) {
debouncedSpeak(
sprintf(
/* translators: %d: number of results. */
_n(
'%d result found, use up and down arrow keys to navigate.',
'%d results found, use up and down arrow keys to navigate.',
options.length
),
options.length
),
'assertive'
);
} else {
debouncedSpeak( __( 'No results.' ), 'assertive' );
}
}

/**
* Load options for an autocompleter.
*
Expand All @@ -169,7 +140,6 @@ export function useAutocomplete( {
options.length === filteredOptions.length ? selectedIndex : 0
);
setFilteredOptions( options );
announce( options );
}

function handleKeyDown( event: KeyboardEvent ) {
Expand Down
28 changes: 28 additions & 0 deletions test/e2e/specs/editor/various/autocomplete-and-mentions.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -475,4 +475,32 @@ test.describe( 'Autocomplete (@firefox, @webkit)', () => {
page.locator( 'role=option', { hasText: 'Frodo Baggins' } )
).not.toBeVisible();
} );

test( 'should allow speaking number of initial results', async ( {
page,
editor,
} ) => {
await editor.canvas.click( 'role=button[name="Add default block"i]' );
await page.keyboard.type( '/' );
await expect(
page.locator( `role=option[name="Image"i]` )
).toBeVisible();
// Get the assertive live region screen reader announcement.
await expect(
page.getByText(
'Initial 9 results loaded. Type to filter all available results. Use up and down arrow keys to navigate.'
)
).toBeVisible();

await page.keyboard.type( 'heading' );
await expect(
page.locator( `role=option[name="Heading"i]` )
).toBeVisible();
// Get the assertive live region screen reader announcement.
await expect(
page.getByText(
'2 results found, use up and down arrow keys to navigate.'
)
).toBeVisible();
} );
} );