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: Fix Voiceover not announcing suggestions #54902

Merged
merged 19 commits into from
Oct 19, 2023
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/components/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

### Bug Fix

- `Autocomplete`: Add `aria-live` announcements for Mac and IOS Voiceover to fix lack of support for `aria-owns` ([#54902](https://github.com/WordPress/gutenberg/pull/54902)).

## 25.10.0 (2023-10-18)

### Enhancements
Expand Down
62 changes: 54 additions & 8 deletions packages/components/src/autocomplete/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import {
isCollapsed,
getTextContent,
} from '@wordpress/rich-text';
import { speak } from '@wordpress/a11y';
import { isAppleOS } from '@wordpress/keycodes';

/**
* Internal dependencies
Expand All @@ -39,6 +41,35 @@ import type {
WPCompleter,
} from './types';

const getNodeText = ( node: React.ReactNode ): string => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function is only used once, but it would be useful for it to be its own exported function so we can have some unit tests for it. Given the release is coming up so quickly, I think an extra day of manual testing it "in the wild" is more important than delaying this for unit tests.

if ( node === null ) {
return '';
}

switch ( typeof node ) {
case 'string':
case 'number':
return node.toString();
break;
case 'boolean':
return '';
break;
case 'object': {
if ( node instanceof Array ) {
return node.map( getNodeText ).join( '' );
}
if ( 'props' in node ) {
return getNodeText( node.props.children );
}
break;
}
default:
return '';
}

return '';
Comment on lines +49 to +70
Copy link
Contributor

@jeryj jeryj Oct 19, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can remove the breaks that are preceded by a return since that code won't ever be run. The final return can be removed as well since the default will get hit first, I believe. Thanks for @ajlende for catching that!

We can do that after this is merged though. I'll open an issue about it.

};
Comment on lines +44 to +71
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could simplify a bit.

Suggested change
const getNodeText = ( node: React.ReactNode ): string => {
if ( node === null ) {
return '';
}
switch ( typeof node ) {
case 'string':
case 'number':
return node.toString();
break;
case 'boolean':
return '';
break;
case 'object': {
if ( node instanceof Array ) {
return node.map( getNodeText ).join( '' );
}
if ( 'props' in node ) {
return getNodeText( node.props.children );
}
break;
}
default:
return '';
}
return '';
};
const getNodeText = ( node: React.ReactNode ): string => {
if ( node === null ) {
return '';
}
switch ( typeof node ) {
case 'string':
case 'number':
return node.toString();
case 'object': {
if ( node instanceof Array ) {
return node.map( getNodeText ).join( '' );
}
if ( 'props' in node ) {
return getNodeText( node.props.children );
}
return '';
}
default:
return '';
}
};


const EMPTY_FILTERED_OPTIONS: KeyedOption[] = [];

export function useAutocomplete( {
Expand Down Expand Up @@ -163,20 +194,35 @@ export function useAutocomplete( {
) {
return;
}

switch ( event.key ) {
case 'ArrowUp':
setSelectedIndex(
case 'ArrowUp': {
const newIndex =
( selectedIndex === 0
? filteredOptions.length
: selectedIndex ) - 1
);
: selectedIndex ) - 1;
setSelectedIndex( newIndex );
// See the related PR as to why this is necessary: https://github.com/WordPress/gutenberg/pull/54902.
if ( isAppleOS() ) {
speak(
getNodeText( filteredOptions[ newIndex ].label ),
'assertive'
);
}
break;
}

case 'ArrowDown':
setSelectedIndex(
( selectedIndex + 1 ) % filteredOptions.length
);
case 'ArrowDown': {
const newIndex = ( selectedIndex + 1 ) % filteredOptions.length;
setSelectedIndex( newIndex );
if ( isAppleOS() ) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be helpful to have a link to this PR and a brief comment explaining why we need to use speak on appleOS. Let's do it in a follow-up though, so we don't need to retrigger all the tests since they're currently passing.

speak(
getNodeText( filteredOptions[ newIndex ].label ),
'assertive'
);
}
break;
}

case 'Escape':
setAutocompleter( null );
Expand Down
Loading