diff --git a/components/popover/index.js b/components/popover/index.js index 236a861da520a8..330b246edd780f 100644 --- a/components/popover/index.js +++ b/components/popover/index.js @@ -3,12 +3,12 @@ */ import classnames from 'classnames'; import { isEqual, noop } from 'lodash'; +import queryFirstTabbable from 'ally.js/esm/query/first-tabbable'; /** * WordPress dependencies */ import { createPortal, Component } from '@wordpress/element'; -import { focus } from '@wordpress/utils'; /** * Internal dependencies @@ -87,9 +87,10 @@ export class Popover extends Component { } focus() { - const firstFocusable = focus.findFocusable( this.nodes.content ); - if ( firstFocusable ) { - firstFocusable.focus(); + const context = this.nodes.content; + const firstTabbable = queryFirstTabbable( { context } ); + if ( firstTabbable ) { + firstTabbable.focus(); } } diff --git a/package.json b/package.json index c9d6de94448399..5a0c87f9865872 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ ], "dependencies": { "@wordpress/url": "0.1.0-beta.1", + "ally.js": "^1.4.1", "classnames": "^2.2.5", "clipboard": "^1.7.1", "dom-react": "^2.2.0", diff --git a/utils/focus.js b/utils/focus.js deleted file mode 100644 index f63a99aca8b75f..00000000000000 --- a/utils/focus.js +++ /dev/null @@ -1,96 +0,0 @@ -/** - * External dependencies - */ -import { includes } from 'lodash'; - -/** - * Node names for elements which can receive focus - * - * @type {Array} - */ -const FOCUSABLE_NODE_NAMES = [ - 'INPUT', - 'SELECT', - 'TEXTAREA', - 'BUTTON', - 'OBJECT', -]; - -/** - * Returns true if the specified node can receive focus, or false otherwise. - * - * @param {HTMLElement} node Node to check - * @return {Boolean} Whether the node can receive focus - */ -export function isFocusable( node ) { - if ( ! node ) { - return false; - } - - if ( node.tabIndex < 0 ) { - return false; - } else if ( includes( FOCUSABLE_NODE_NAMES, node.nodeName ) ) { - return ! node.disabled; - } else if ( 'A' === node.nodeName || 'AREA' === node.nodeName ) { - return node.hasAttribute( 'href' ); - } - - return node.tabIndex >= 0; -} - -/** - * Returns the first focusable element within the specified node, or undefined - * if there are no focusable elements. - * - * @param {HTMLElement} node Context in which to find focusable - * @return {HTMLElement} First focusable element - */ -export function findFocusable( node ) { - if ( ! node ) { - return; - } - - return findFirstFocusable( node.firstChild, node ); -} - -/** - * Returns the first focusable element starting from specified node: - * - If node is focusable, returns node - * - If node contains a descendant which is focusable, returns descendant - * - If node has a focusable sibling, returns sibling - * - Otherwise, continues through parents following same guidelines until - * reaching context. If context is reached, the function returns undefined. - * - * @param {HTMLElement} node Starting node - * @param {?HTMLElement} context Context in which to find focusable - * @return {HTMLElement} First focusable element - */ -export function findFirstFocusable( node, context = document.body ) { - if ( ! node || node === context ) { - return; - } - - // Starting node is focusable? - if ( isFocusable( node ) ) { - return node; - } - - // Traverse into children - if ( node.firstChild ) { - return findFirstFocusable( node.firstChild, context ); - } - - // Find next sibling or parent - let nextSibling; - while ( ! ( nextSibling = node.nextSibling ) ) { - node = node.parentNode; - - // Terminate if reached context - if ( node === context ) { - return; - } - } - - // Find in sibling or parent's sibling - return findFirstFocusable( nextSibling, context ); -} diff --git a/utils/index.js b/utils/index.js index f654fe09c73e4d..2487d3d1224529 100644 --- a/utils/index.js +++ b/utils/index.js @@ -1,8 +1,6 @@ -import * as focus from './focus'; import * as keycodes from './keycodes'; import * as nodetypes from './nodetypes'; -export { focus }; export { keycodes }; export { nodetypes }; diff --git a/utils/test/focus.js b/utils/test/focus.js deleted file mode 100644 index d1f4758e6d2d2a..00000000000000 --- a/utils/test/focus.js +++ /dev/null @@ -1,162 +0,0 @@ -/** - * Internal dependencies - */ -import { isFocusable, findFocusable, findFirstFocusable } from '../focus'; - -describe( 'focus', () => { - beforeEach( () => { - document.body.innerHTML = ''; - } ); - - describe( 'isFocusable()', () => { - it( 'returns false if passed falsey value', () => { - expect( isFocusable() ).toBe( false ); - } ); - - it( 'returns false if node has negative tabIndex', () => { - const node = document.createElement( 'input' ); - node.tabIndex = -1; - - expect( isFocusable( node ) ).toBe( false ); - } ); - - it( 'returns false if node is focusable input type and disabled', () => { - const node = document.createElement( 'input' ); - node.disabled = true; - - expect( isFocusable( node ) ).toBe( false ); - } ); - - it( 'returns true if node is focusable input type and not disabled', () => { - const node = document.createElement( 'input' ); - - expect( isFocusable( node ) ).toBe( true ); - } ); - - it( 'returns false if anchor without href', () => { - const node = document.createElement( 'a' ); - - expect( isFocusable( node ) ).toBe( false ); - } ); - - it( 'returns true if anchor with href', () => { - const node = document.createElement( 'a' ); - node.href = 'https://wordpress.org'; - - expect( isFocusable( node ) ).toBe( true ); - } ); - - it( 'returns true if tabindex 0', () => { - const node = document.createElement( 'div' ); - node.tabIndex = 0; - - expect( isFocusable( node ) ).toBe( true ); - } ); - - it( 'returns true if positive tabindex', () => { - const node = document.createElement( 'div' ); - node.tabIndex = 1; - - expect( isFocusable( node ) ).toBe( true ); - } ); - } ); - - describe( 'findFocusable()', () => { - it( 'returns undefined if falsey argument passed', () => { - expect( findFocusable() ).toBe( undefined ); - } ); - - it( 'returns undefined if no children', () => { - const node = document.createElement( 'div' ); - - expect( findFocusable( node ) ).toBe( undefined ); - } ); - - it( 'returns undefined if no focusable children', () => { - const node = document.createElement( 'div' ); - node.appendChild( document.createElement( 'div' ) ); - - expect( findFocusable( node ) ).toBe( undefined ); - } ); - - it( 'finds first focusable child', () => { - const node = document.createElement( 'div' ); - node.appendChild( document.createElement( 'input' ) ); - - expect( findFocusable( node ).nodeName ).toBe( 'INPUT' ); - } ); - - it( 'finds nested first focusable child', () => { - const node = document.createElement( 'div' ); - node.appendChild( document.createElement( 'div' ) ); - node.firstChild.appendChild( document.createElement( 'input' ) ); - - expect( findFocusable( node ).nodeName ).toBe( 'INPUT' ); - } ); - - it( 'does not traverse up to body', () => { - const node = document.createElement( 'div' ); - document.body.appendChild( node ); - document.body.appendChild( document.createElement( 'input' ) ); - - expect( findFocusable( node ) ).toBe( undefined ); - } ); - - it( 'does not return context even if focusable', () => { - const node = document.createElement( 'div' ); - node.tabIndex = 0; - - expect( findFocusable( node ) ).toBe( undefined ); - } ); - } ); - - describe( 'findFirstFocusable()', () => { - it( 'returns undefined if falsey node', () => { - expect( findFirstFocusable() ).toBe( undefined ); - } ); - - it( 'returns node if focusable', () => { - const node = document.createElement( 'input' ); - - expect( findFirstFocusable( node ) ).toBe( node ); - } ); - - it( 'traverses into children to find focusable', () => { - const node = document.createElement( 'div' ); - node.appendChild( document.createElement( 'div' ) ); - node.firstChild.appendChild( document.createElement( 'input' ) ); - - expect( findFirstFocusable( node ).nodeName ).toBe( 'INPUT' ); - } ); - - it( 'traverses through siblings to find focusable', () => { - const node = document.createElement( 'div' ); - node.appendChild( document.createElement( 'div' ) ); - node.appendChild( document.createElement( 'input' ) ); - - expect( findFirstFocusable( node ).nodeName ).toBe( 'INPUT' ); - } ); - - it( 'traverses through parent siblings to find focusable', () => { - const node = document.createElement( 'div' ); - node.appendChild( document.createElement( 'div' ) ); - document.body.appendChild( node ); - document.body.appendChild( document.createElement( 'input' ) ); - - expect( findFirstFocusable( node ).nodeName ).toBe( 'INPUT' ); - } ); - - it( 'returns undefined if nothing focusable', () => { - expect( findFirstFocusable( document.body ) ).toBe( undefined ); - } ); - - it( 'limits found focusables to specific context', () => { - const node = document.createElement( 'div' ); - node.appendChild( document.createElement( 'div' ) ); - document.body.appendChild( node ); - document.body.appendChild( document.createElement( 'input' ) ); - - expect( findFirstFocusable( node, node ) ).toBe( undefined ); - } ); - } ); -} );