Skip to content

Commit

Permalink
Focus first tabbable on mount of the popover wrapper
Browse files Browse the repository at this point in the history
  • Loading branch information
talldan committed Oct 7, 2020
1 parent b615337 commit 5a1e62a
Show file tree
Hide file tree
Showing 2 changed files with 69 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,17 @@ function BlockContextualToolbar( { focusOnMount, ...props } ) {
}
}
return (
<div className="block-editor-block-contextual-toolbar-wrapper">
<div
className={ classnames(
'block-editor-block-contextual-toolbar-wrapper',
{
'is-editing-in-toolbar': isEditingInToolbar,
}
) }
>
<NavigableToolbar
focusOnMount={ focusOnMount }
className={ classnames(
'block-editor-block-contextual-toolbar',
{
'is-editing-in-toolbar': isEditingInToolbar,
}
) }
className="block-editor-block-contextual-toolbar"
/* translators: accessibility text for the block toolbar */
aria-label={ __( 'Block tools' ) }
{ ...props }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import {
withFocusReturn,
withFocusOutside,
} from '@wordpress/components';
import { Component } from '@wordpress/element';
import { focus } from '@wordpress/dom';
import { Component, useEffect, useRef } from '@wordpress/element';
import { ESCAPE, UP, RIGHT, DOWN, LEFT } from '@wordpress/keycodes';

const ARROW_KEYCODES = [ UP, RIGHT, DOWN, LEFT ];
Expand All @@ -31,7 +32,59 @@ const FocusManaged = withConstrainedTabbing(
withFocusReturn( ( { children } ) => children )
);

export default function PopoverWrapper( { onClose, children, className } ) {
/**
* Hook used to focus the first tabbable element on mount.
*
* @param {boolean|string} focusOnMount Focus on mount mode.
* @param {Object} contentRef Reference to the popover content element.
*/
function useFocusContentOnMount( focusOnMount, contentRef ) {
// Focus handling
useEffect( () => {
/*
* Without the setTimeout, the dom node is not being focused. Related:
* https://stackoverflow.com/questions/35522220/react-ref-with-focus-doesnt-work-without-settimeout-my-example
*
* TODO: Treat the cause, not the symptom.
*/
const focusTimeout = setTimeout( () => {
if ( ! focusOnMount || ! contentRef.current ) {
return;
}

if ( focusOnMount === 'firstElement' ) {
// Find first tabbable node within content and shift focus, falling
// back to the popover panel itself.
const firstTabbable = focus.tabbable.find(
contentRef.current
)[ 0 ];

if ( firstTabbable ) {
firstTabbable.focus();
} else {
contentRef.current.focus();
}

return;
}

if ( focusOnMount === 'container' ) {
// Focus the popover panel itself so items in the popover are easily
// accessed via keyboard navigation.
contentRef.current.focus();
}
}, 0 );

return () => clearTimeout( focusTimeout );
}, [] );
}

export default function PopoverWrapper( {
onClose,
focusOnMount = 'firstElement',
children,
className,
} ) {
// Event handlers
const onKeyDown = ( event ) => {
if ( ARROW_KEYCODES.includes( event.keyCode ) ) {
Expand All @@ -46,11 +99,16 @@ export default function PopoverWrapper( { onClose, children, className } ) {
}
};

const contentRef = useRef();

useFocusContentOnMount( focusOnMount, contentRef );

// Disable reason: this stops certain events from propagating outside of the component.
// - onMouseDown is disabled as this can cause interactions with other DOM elements
/* eslint-disable jsx-a11y/no-static-element-interactions */
return (
<div
ref={ contentRef }
className={ className }
onKeyDown={ onKeyDown }
onMouseDown={ stopPropagation }
Expand Down

0 comments on commit 5a1e62a

Please sign in to comment.