Skip to content

Commit

Permalink
Autocomplete: duplicate list within iframe for non visual users (#47907)
Browse files Browse the repository at this point in the history
  • Loading branch information
ellatrix committed Feb 21, 2023
1 parent b9d0265 commit 44b4230
Show file tree
Hide file tree
Showing 3 changed files with 127 additions and 47 deletions.
1 change: 1 addition & 0 deletions packages/components/CHANGELOG.md
Expand Up @@ -8,6 +8,7 @@

- `ToolsPanel`: fix type inconsistencies between types, docs and normal component usage ([47944](https://github.com/WordPress/gutenberg/pull/47944)).
- `SelectControl`: Fix styling when `multiple` prop is enabled ([#47893](https://github.com/WordPress/gutenberg/pull/43213)).
- `useAutocompleteProps`, `Autocomplete`: Make accessible when rendered in an iframe ([#47907](https://github.com/WordPress/gutenberg/pull/47907)).

### Enhancements

Expand Down
106 changes: 72 additions & 34 deletions packages/components/src/autocomplete/autocompleter-ui.js
Expand Up @@ -6,15 +6,23 @@ import classnames from 'classnames';
/**
* WordPress dependencies
*/
import { useLayoutEffect, useRef, useEffect } from '@wordpress/element';
import {
useLayoutEffect,
useRef,
useEffect,
useState,
} from '@wordpress/element';
import { useAnchor } from '@wordpress/rich-text';
import { useMergeRefs, useRefEffect } from '@wordpress/compose';

/**
* Internal dependencies
*/
import getDefaultUseItems from './get-default-use-items';
import Button from '../button';
import Popover from '../popover';
import { VisuallyHidden } from '../visually-hidden';
import { createPortal } from 'react-dom';

export function getAutoCompleterUI( autocompleter ) {
const useItems = autocompleter.useItems
Expand All @@ -40,7 +48,25 @@ export function getAutoCompleterUI( autocompleter ) {
value,
} );

const [ needsA11yCompat, setNeedsA11yCompat ] = useState( false );
const popoverRef = useRef();
const popoverRefs = useMergeRefs( [
popoverRef,
useRefEffect(
( node ) => {
if ( ! contentRef.current ) return;

// If the popover is rendered in a different document than
// the content, we need to duplicate the options list in the
// content document so that it's available to the screen
// readers, which check the DOM ID based aira-* attributes.
setNeedsA11yCompat(
node.ownerDocument !== contentRef.current.ownerDocument
);
},
[ contentRef ]
),
] );

useOnClickOutside( popoverRef, reset );

Expand All @@ -55,41 +81,53 @@ export function getAutoCompleterUI( autocompleter ) {
return null;
}

return (
<Popover
focusOnMount={ false }
onClose={ onReset }
placement="top-start"
className="components-autocomplete__popover"
anchor={ popoverAnchor }
ref={ popoverRef }
const ListBox = ( { Component = 'div' } ) => (
<Component
id={ listBoxId }
role="listbox"
className="components-autocomplete__results"
>
<div
id={ listBoxId }
role="listbox"
className="components-autocomplete__results"
{ items.map( ( option, index ) => (
<Button
key={ option.key }
id={ `components-autocomplete-item-${ instanceId }-${ option.key }` }
role="option"
aria-selected={ index === selectedIndex }
disabled={ option.isDisabled }
className={ classnames(
'components-autocomplete__result',
className,
{
'is-selected': index === selectedIndex,
}
) }
onClick={ () => onSelect( option ) }
>
{ option.label }
</Button>
) ) }
</Component>
);

return (
<>
<Popover
focusOnMount={ false }
onClose={ onReset }
placement="top-start"
className="components-autocomplete__popover"
anchor={ popoverAnchor }
ref={ popoverRefs }
>
{ items.map( ( option, index ) => (
<Button
key={ option.key }
id={ `components-autocomplete-item-${ instanceId }-${ option.key }` }
role="option"
aria-selected={ index === selectedIndex }
disabled={ option.isDisabled }
className={ classnames(
'components-autocomplete__result',
className,
{
'is-selected': index === selectedIndex,
}
) }
onClick={ () => onSelect( option ) }
>
{ option.label }
</Button>
) ) }
</div>
</Popover>
<ListBox />
</Popover>
{ contentRef.current &&
needsA11yCompat &&
createPortal(
<ListBox Component={ VisuallyHidden } />,
contentRef.current.ownerDocument.body
) }
</>
);
}

Expand Down
67 changes: 54 additions & 13 deletions test/e2e/specs/editor/various/autocomplete-and-mentions.spec.js
Expand Up @@ -57,12 +57,14 @@ test.describe( 'Autocomplete (@firefox, @webkit)', () => {
} )
)
);
await requestUtils.activateTheme( 'emptytheme' );
await requestUtils.activatePlugin( 'gutenberg-test-autocompleter' );
} );

test.afterAll( async ( { requestUtils } ) => {
await requestUtils.deleteAllUsers();
await requestUtils.deactivatePlugin( 'gutenberg-test-autocompleter' );
await requestUtils.activateTheme( 'twentytwentyone' );
} );

test.beforeEach( async ( { admin } ) => {
Expand Down Expand Up @@ -98,11 +100,28 @@ test.describe( 'Autocomplete (@firefox, @webkit)', () => {
<!-- /wp:paragraph -->`;
}

await page.click( 'role=button[name="Add default block"i]' );
await editor.canvas.click(
'role=button[name="Add default block"i]'
);
await page.keyboard.type( testData.triggerString );
await expect(
page.locator( `role=option[name="${ testData.optionText }"i]` )
).toBeVisible();
const ariaOwns = await editor.canvas.evaluate( () => {
return document.activeElement.getAttribute( 'aria-owns' );
} );
const ariaActiveDescendant = await editor.canvas.evaluate( () => {
return document.activeElement.getAttribute(
'aria-activedescendant'
);
} );
// Ensure `aria-owns` is part of the same document and ensure the
// selected option is equal to the active descendant.
await expect(
await editor.canvas
.locator( `#${ ariaOwns } [aria-selected="true"]` )
.getAttribute( 'id' )
).toBe( ariaActiveDescendant );
await page.keyboard.press( 'Enter' );
await page.keyboard.type( '.' );

Expand Down Expand Up @@ -131,7 +150,9 @@ test.describe( 'Autocomplete (@firefox, @webkit)', () => {
<!-- /wp:paragraph -->`;
}

await page.click( 'role=button[name="Add default block"i]' );
await editor.canvas.click(
'role=button[name="Add default block"i]'
);
await page.keyboard.type( 'Stuck in the middle with you.' );
await pageUtils.pressKeyTimes( 'ArrowLeft', 'you.'.length );
await page.keyboard.type( testData.triggerString );
Expand Down Expand Up @@ -169,7 +190,9 @@ test.describe( 'Autocomplete (@firefox, @webkit)', () => {
<!-- /wp:paragraph -->`;
}

await page.click( 'role=button[name="Add default block"i]' );
await editor.canvas.click(
'role=button[name="Add default block"i]'
);
await page.keyboard.type( testData.firstTriggerString );
await expect(
page.locator(
Expand Down Expand Up @@ -209,7 +232,9 @@ test.describe( 'Autocomplete (@firefox, @webkit)', () => {
<!-- /wp:paragraph -->`;
}

await page.click( 'role=button[name="Add default block"i]' );
await editor.canvas.click(
'role=button[name="Add default block"i]'
);
await page.keyboard.type( testData.triggerString );
await expect(
page.locator( `role=option[name="${ testData.optionText }"i]` )
Expand Down Expand Up @@ -247,7 +272,9 @@ test.describe( 'Autocomplete (@firefox, @webkit)', () => {
<!-- /wp:paragraph -->`;
}

await page.click( 'role=button[name="Add default block"i]' );
await editor.canvas.click(
'role=button[name="Add default block"i]'
);
await page.keyboard.type( testData.triggerString );
await expect(
page.locator( `role=option[name="${ testData.optionText }"i]` )
Expand Down Expand Up @@ -282,7 +309,9 @@ test.describe( 'Autocomplete (@firefox, @webkit)', () => {
<!-- /wp:paragraph -->`;
}

await page.click( 'role=button[name="Add default block"i]' );
await editor.canvas.click(
'role=button[name="Add default block"i]'
);
await page.keyboard.type( testData.triggerString );
await expect(
page.locator( `role=option[name="${ testData.optionText }"i]` )
Expand All @@ -301,7 +330,9 @@ test.describe( 'Autocomplete (@firefox, @webkit)', () => {
page,
editor,
} ) => {
await page.click( 'role=button[name="Add default block"i]' );
await editor.canvas.click(
'role=button[name="Add default block"i]'
);
// The 'Grapes' option is disabled in our test plugin, so it should not insert the grapes emoji
await page.keyboard.type( 'Sorry, we are all out of ~g' );
await expect(
Expand Down Expand Up @@ -367,7 +398,9 @@ test.describe( 'Autocomplete (@firefox, @webkit)', () => {
<!-- /wp:paragraph -->`;
}

await page.click( 'role=button[name="Add default block"i]' );
await editor.canvas.click(
'role=button[name="Add default block"i]'
);

for ( let i = 0; i < 4; i++ ) {
await page.keyboard.type( testData.triggerString );
Expand All @@ -393,7 +426,7 @@ test.describe( 'Autocomplete (@firefox, @webkit)', () => {
page,
editor,
} ) => {
await page.click( 'role=button[name="Add default block"i]' );
await editor.canvas.click( 'role=button[name="Add default block"i]' );
await page.keyboard.type( '@fr' );
await expect(
page.locator( 'role=option', { hasText: 'Frodo Baggins' } )
Expand All @@ -412,8 +445,9 @@ test.describe( 'Autocomplete (@firefox, @webkit)', () => {

test( 'should hide UI when selection changes (by keyboard)', async ( {
page,
editor,
} ) => {
await page.click( 'role=button[name="Add default block"i]' );
await editor.canvas.click( 'role=button[name="Add default block"i]' );
await page.keyboard.type( '@fr' );
await expect(
page.locator( 'role=option', { hasText: 'Frodo Baggins' } )
Expand All @@ -426,13 +460,20 @@ test.describe( 'Autocomplete (@firefox, @webkit)', () => {

test( 'should hide UI when selection changes (by mouse)', async ( {
page,
editor,
pageUtils,
} ) => {
await page.click( 'role=button[name="Add default block"i]' );
await page.keyboard.type( '@fr' );
await editor.canvas.click( 'role=button[name="Add default block"i]' );
await page.keyboard.type( '@' );
await pageUtils.pressKeyWithModifier( 'primary', 'b' );
await page.keyboard.type( 'f' );
await pageUtils.pressKeyWithModifier( 'primary', 'b' );
await page.keyboard.type( 'r' );
await expect(
page.locator( 'role=option', { hasText: 'Frodo Baggins' } )
).toBeVisible();
await page.click( '[data-type="core/paragraph"]' );
// Use the strong tag to move the selection by mouse within the mention.
await editor.canvas.click( '[data-type="core/paragraph"] strong' );
await expect(
page.locator( 'role=option', { hasText: 'Frodo Baggins' } )
).not.toBeVisible();
Expand Down

0 comments on commit 44b4230

Please sign in to comment.