Skip to content

Commit

Permalink
Autocomplete: Use combobox role with ARIA attributes
Browse files Browse the repository at this point in the history
  • Loading branch information
aduth committed Oct 4, 2017
1 parent a68972a commit 081fa8b
Show file tree
Hide file tree
Showing 2 changed files with 25 additions and 8 deletions.
19 changes: 14 additions & 5 deletions components/autocomplete/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,11 @@ import { keycodes } from '@wordpress/utils';
import './style.scss';
import Button from '../button';
import Popover from '../popover';
import withInstanceId from '../higher-order/with-instance-id';

const { ENTER, ESCAPE, UP, DOWN } = keycodes;

class Autocomplete extends Component {
export class Autocomplete extends Component {
static getInitialState() {
return {
isOpen: false,
Expand Down Expand Up @@ -180,10 +181,12 @@ class Autocomplete extends Component {
}

render() {
const { children, className } = this.props;
const { children, className, instanceId } = this.props;
const { isOpen, selectedIndex } = this.state;
const classes = classnames( 'components-autocomplete__popover', className );
const filteredOptions = this.getFilteredOptions();
const listBoxId = `components-autocomplete-listbox-${ instanceId }`;
const activeId = `components-autocomplete-item-${ instanceId }-${ selectedIndex }`;

// Blur is applied to the wrapper node, since if the child is Editable,
// the event will not have `relatedTarget` assigned.
Expand All @@ -196,6 +199,10 @@ class Autocomplete extends Component {
{ cloneElement( Children.only( children ), {
onInput: this.search,
onKeyDown: this.setSelectedIndex,
role: 'combobox',
'aria-expanded': isOpen,
'aria-activedescendant': isOpen ? activeId : null,
'aria-owns': isOpen ? listBoxId : null,
} ) }
<Popover
isOpen={ isOpen && filteredOptions.length > 0 }
Expand All @@ -204,13 +211,15 @@ class Autocomplete extends Component {
className={ classes }
>
<ul
role="menu"
id={ listBoxId }
role="listbox"
className="components-autocomplete__results"
>
{ filteredOptions.map( ( option, index ) => (
<li
key={ option.value }
role="menuitem"
id={ `components-autocomplete-item-${ instanceId }-${ index }` }
role="option"
className={ classnames( 'components-autocomplete__result', {
'is-selected': index === selectedIndex,
} ) }
Expand All @@ -227,4 +236,4 @@ class Autocomplete extends Component {
}
}

export default Autocomplete;
export default withInstanceId( Autocomplete );
14 changes: 11 additions & 3 deletions components/autocomplete/test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { keycodes } from '@wordpress/utils';
/**
* Internal dependencies
*/
import Autocomplete from '../';
import { Autocomplete } from '../';

const { ENTER, ESCAPE, UP, DOWN, SPACE } = keycodes;

Expand Down Expand Up @@ -53,13 +53,17 @@ describe( 'Autocomplete', () => {
</Autocomplete>
);
const popover = wrapper.find( 'Popover' );
const clone = wrapper.find( '[data-ok]' );

expect( wrapper.state( 'isOpen' ) ).toBe( false );
expect( popover.prop( 'focusOnOpen' ) ).toBe( false );
expect( popover.hasClass( 'my-autocomplete' ) ).toBe( true );
expect( popover.hasClass( 'components-autocomplete__popover' ) ).toBe( true );
expect( wrapper.hasClass( 'components-autocomplete' ) ).toBe( true );
expect( wrapper.find( '[data-ok]' ) ).toHaveLength( 1 );
expect( clone ).toHaveLength( 1 );
expect( clone.prop( 'aria-expanded' ) ).toBe( false );
expect( clone.prop( 'aria-activedescendant' ) ).toBe( null );
expect( clone.prop( 'aria-owns' ) ).toBe( null );
} );

it( 'opens on absent trigger prefix search', () => {
Expand All @@ -68,8 +72,9 @@ describe( 'Autocomplete', () => {
<div contentEditable />
</Autocomplete>
);
const clone = wrapper.find( '[contentEditable]' );

wrapper.find( '[contentEditable]' ).simulate( 'input', {
clone.simulate( 'input', {
target: {
textContent: 'b',
},
Expand All @@ -80,6 +85,9 @@ describe( 'Autocomplete', () => {
expect( wrapper.state( 'search' ) ).toEqual( /b/i );
expect( wrapper.find( 'Popover' ).prop( 'isOpen' ) ).toBe( true );
expect( wrapper.find( '.components-autocomplete__result' ) ).toHaveLength( 1 );
expect( clone.prop( 'aria-expanded' ) ).toBe( false );
expect( clone.prop( 'aria-activedescendant' ) ).toBe( null );
expect( clone.prop( 'aria-owns' ) ).toBe( null );
} );

it( 'does not render popover as open if no results', () => {
Expand Down

0 comments on commit 081fa8b

Please sign in to comment.