@gechiui/block-editor
Version:
177 lines (157 loc) • 5.13 kB
JavaScript
/**
* GeChiUI dependencies
*/
import { NavigableMenu, Toolbar } from '@gechiui/components';
import {
useState,
useRef,
useLayoutEffect,
useEffect,
useCallback,
} from '@gechiui/element';
import deprecated from '@gechiui/deprecated';
import { focus } from '@gechiui/dom';
import { useShortcut } from '@gechiui/keyboard-shortcuts';
function hasOnlyToolbarItem( elements ) {
const dataProp = 'toolbarItem';
return ! elements.some( ( element ) => ! ( dataProp in element.dataset ) );
}
function getAllToolbarItemsIn( container ) {
return Array.from( container.querySelectorAll( '[data-toolbar-item]' ) );
}
function hasFocusWithin( container ) {
return container.contains( container.ownerDocument.activeElement );
}
function focusFirstTabbableIn( container ) {
const [ firstTabbable ] = focus.tabbable.find( container );
if ( firstTabbable ) {
firstTabbable.focus();
}
}
function useIsAccessibleToolbar( ref ) {
/*
* By default, we'll assume the starting accessible state of the Toolbar
* is true, as it seems to be the most common case.
*
* Transitioning from an (initial) false to true state causes the
* <Toolbar /> component to mount twice, which is causing undesired
* side-effects. These side-effects appear to only affect certain
* E2E tests.
*
* This was initial discovered in this pull-request:
* https://github.com/GeChiUI/gutenberg/pull/23425
*/
const initialAccessibleToolbarState = true;
// By default, it's gonna render NavigableMenu. If all the tabbable elements
// inside the toolbar are ToolbarItem components (or derived components like
// ToolbarButton), then we can wrap them with the accessible Toolbar
// component.
const [ isAccessibleToolbar, setIsAccessibleToolbar ] = useState(
initialAccessibleToolbarState
);
const determineIsAccessibleToolbar = useCallback( () => {
const tabbables = focus.tabbable.find( ref.current );
const onlyToolbarItem = hasOnlyToolbarItem( tabbables );
if ( ! onlyToolbarItem ) {
deprecated( 'Using custom components as toolbar controls', {
since: '5.6',
alternative:
'ToolbarItem, ToolbarButton or ToolbarDropdownMenu components',
link:
'https://developer.gechiui.com/block-editor/components/toolbar-button/#inside-blockcontrols',
} );
}
setIsAccessibleToolbar( onlyToolbarItem );
}, [] );
useLayoutEffect( () => {
// Toolbar buttons may be rendered asynchronously, so we use
// MutationObserver to check if the toolbar subtree has been modified
const observer = new window.MutationObserver(
determineIsAccessibleToolbar
);
observer.observe( ref.current, { childList: true, subtree: true } );
return () => observer.disconnect();
}, [ isAccessibleToolbar ] );
return isAccessibleToolbar;
}
function useToolbarFocus(
ref,
focusOnMount,
isAccessibleToolbar,
defaultIndex,
onIndexChange
) {
// Make sure we don't use modified versions of this prop
const [ initialFocusOnMount ] = useState( focusOnMount );
const [ initialIndex ] = useState( defaultIndex );
const focusToolbar = useCallback( () => {
focusFirstTabbableIn( ref.current );
}, [] );
// Focus on toolbar when pressing alt+F10 when the toolbar is visible
useShortcut( 'core/block-editor/focus-toolbar', focusToolbar );
useEffect( () => {
if ( initialFocusOnMount ) {
focusToolbar();
}
}, [ isAccessibleToolbar, initialFocusOnMount, focusToolbar ] );
useEffect( () => {
// If initialIndex is passed, we focus on that toolbar item when the
// toolbar gets mounted and initial focus is not forced.
// We have to wait for the next browser paint because block controls aren't
// rendered right away when the toolbar gets mounted.
let raf = 0;
if ( initialIndex && ! initialFocusOnMount ) {
raf = window.requestAnimationFrame( () => {
const items = getAllToolbarItemsIn( ref.current );
const index = initialIndex || 0;
if ( items[ index ] && hasFocusWithin( ref.current ) ) {
items[ index ].focus();
}
} );
}
return () => {
window.cancelAnimationFrame( raf );
if ( ! onIndexChange || ! ref.current ) return;
// When the toolbar element is unmounted and onIndexChange is passed, we
// pass the focused toolbar item index so it can be hydrated later.
const items = getAllToolbarItemsIn( ref.current );
const index = items.findIndex( ( item ) => item.tabIndex === 0 );
onIndexChange( index );
};
}, [ initialIndex, initialFocusOnMount ] );
}
function NavigableToolbar( {
children,
focusOnMount,
__experimentalInitialIndex: initialIndex,
__experimentalOnIndexChange: onIndexChange,
...props
} ) {
const ref = useRef();
const isAccessibleToolbar = useIsAccessibleToolbar( ref );
useToolbarFocus(
ref,
focusOnMount,
isAccessibleToolbar,
initialIndex,
onIndexChange
);
if ( isAccessibleToolbar ) {
return (
<Toolbar label={ props[ 'aria-label' ] } ref={ ref } { ...props }>
{ children }
</Toolbar>
);
}
return (
<NavigableMenu
orientation="horizontal"
role="toolbar"
ref={ ref }
{ ...props }
>
{ children }
</NavigableMenu>
);
}
export default NavigableToolbar;