@wordpress/block-editor
Version:
153 lines (133 loc) • 5.31 kB
JavaScript
import _extends from "@babel/runtime/helpers/esm/extends";
import { createElement } from "@wordpress/element";
/**
* WordPress dependencies
*/
import { NavigableMenu, Toolbar } from '@wordpress/components';
import { useState, useRef, useLayoutEffect, useEffect, useCallback } from '@wordpress/element';
import deprecated from '@wordpress/deprecated';
import { focus } from '@wordpress/dom';
import { useShortcut } from '@wordpress/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/WordPress/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 or ToolbarButton components',
link: 'https://developer.wordpress.org/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, {
bindGlobal: true,
eventName: 'keydown'
});
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) 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 createElement(Toolbar, _extends({
label: props['aria-label'],
ref: ref
}, props), children);
}
return createElement(NavigableMenu, _extends({
orientation: "horizontal",
role: "toolbar",
ref: ref
}, props), children);
}
export default NavigableToolbar;
//# sourceMappingURL=index.js.map