@wordpress/block-editor
Version:
218 lines (212 loc) • 8.21 kB
JavaScript
;
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = NavigableToolbar;
var _components = require("@wordpress/components");
var _element = require("@wordpress/element");
var _data = require("@wordpress/data");
var _deprecated = _interopRequireDefault(require("@wordpress/deprecated"));
var _dom = require("@wordpress/dom");
var _keyboardShortcuts = require("@wordpress/keyboard-shortcuts");
var _keycodes = require("@wordpress/keycodes");
var _store = require("../../store");
var _lockUnlock = require("../../lock-unlock");
var _jsxRuntime = require("react/jsx-runtime");
/**
* WordPress dependencies
*/
/**
* Internal dependencies
*/
function hasOnlyToolbarItem(elements) {
const dataProp = 'toolbarItem';
return !elements.some(element => !(dataProp in element.dataset));
}
function getAllFocusableToolbarItemsIn(container) {
return Array.from(container.querySelectorAll('[data-toolbar-item]:not([disabled])'));
}
function hasFocusWithin(container) {
return container.contains(container.ownerDocument.activeElement);
}
function focusFirstTabbableIn(container) {
const [firstTabbable] = _dom.focus.tabbable.find(container);
if (firstTabbable) {
firstTabbable.focus({
// When focusing newly mounted toolbars,
// the position of the popover is often not right on the first render
// This prevents the layout shifts when focusing the dialogs.
preventScroll: true
});
}
}
function useIsAccessibleToolbar(toolbarRef) {
/*
* 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] = (0, _element.useState)(initialAccessibleToolbarState);
const determineIsAccessibleToolbar = (0, _element.useCallback)(() => {
const tabbables = _dom.focus.tabbable.find(toolbarRef.current);
const onlyToolbarItem = hasOnlyToolbarItem(tabbables);
if (!onlyToolbarItem) {
(0, _deprecated.default)('Using custom components as toolbar controls', {
since: '5.6',
alternative: 'ToolbarItem, ToolbarButton or ToolbarDropdownMenu components',
link: 'https://developer.wordpress.org/block-editor/components/toolbar-button/#inside-blockcontrols'
});
}
setIsAccessibleToolbar(onlyToolbarItem);
}, [toolbarRef]);
(0, _element.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(toolbarRef.current, {
childList: true,
subtree: true
});
return () => observer.disconnect();
}, [determineIsAccessibleToolbar, isAccessibleToolbar, toolbarRef]);
return isAccessibleToolbar;
}
function useToolbarFocus({
toolbarRef,
focusOnMount,
isAccessibleToolbar,
defaultIndex,
onIndexChange,
shouldUseKeyboardFocusShortcut,
focusEditorOnEscape
}) {
// Make sure we don't use modified versions of this prop.
const [initialFocusOnMount] = (0, _element.useState)(focusOnMount);
const [initialIndex] = (0, _element.useState)(defaultIndex);
const focusToolbar = (0, _element.useCallback)(() => {
focusFirstTabbableIn(toolbarRef.current);
}, [toolbarRef]);
const focusToolbarViaShortcut = () => {
if (shouldUseKeyboardFocusShortcut) {
focusToolbar();
}
};
// Focus on toolbar when pressing alt+F10 when the toolbar is visible.
(0, _keyboardShortcuts.useShortcut)('core/block-editor/focus-toolbar', focusToolbarViaShortcut);
(0, _element.useEffect)(() => {
if (initialFocusOnMount) {
focusToolbar();
}
}, [isAccessibleToolbar, initialFocusOnMount, focusToolbar]);
(0, _element.useEffect)(() => {
// Store ref so we have access on useEffect cleanup: https://legacy.reactjs.org/blog/2020/08/10/react-v17-rc.html#effect-cleanup-timing
const navigableToolbarRef = toolbarRef.current;
// 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 the toolbar already had focus before the render, we don't want to move it.
// https://github.com/WordPress/gutenberg/issues/58511
if (!initialFocusOnMount && !hasFocusWithin(navigableToolbarRef)) {
raf = window.requestAnimationFrame(() => {
const items = getAllFocusableToolbarItemsIn(navigableToolbarRef);
const index = initialIndex || 0;
if (items[index] && hasFocusWithin(navigableToolbarRef)) {
items[index].focus({
// When focusing newly mounted toolbars,
// the position of the popover is often not right on the first render
// This prevents the layout shifts when focusing the dialogs.
preventScroll: true
});
}
});
}
return () => {
window.cancelAnimationFrame(raf);
if (!onIndexChange || !navigableToolbarRef) {
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 = getAllFocusableToolbarItemsIn(navigableToolbarRef);
const index = items.findIndex(item => item.tabIndex === 0);
onIndexChange(index);
};
}, [initialIndex, initialFocusOnMount, onIndexChange, toolbarRef]);
const {
getLastFocus
} = (0, _lockUnlock.unlock)((0, _data.useSelect)(_store.store));
/**
* Handles returning focus to the block editor canvas when pressing escape.
*/
(0, _element.useEffect)(() => {
const navigableToolbarRef = toolbarRef.current;
if (focusEditorOnEscape) {
const handleKeyDown = event => {
const lastFocus = getLastFocus();
if (event.keyCode === _keycodes.ESCAPE && lastFocus?.current) {
// Focus the last focused element when pressing escape.
event.preventDefault();
lastFocus.current.focus();
}
};
navigableToolbarRef.addEventListener('keydown', handleKeyDown);
return () => {
navigableToolbarRef.removeEventListener('keydown', handleKeyDown);
};
}
}, [focusEditorOnEscape, getLastFocus, toolbarRef]);
}
function NavigableToolbar({
children,
focusOnMount,
focusEditorOnEscape = false,
shouldUseKeyboardFocusShortcut = true,
__experimentalInitialIndex: initialIndex,
__experimentalOnIndexChange: onIndexChange,
orientation = 'horizontal',
...props
}) {
const toolbarRef = (0, _element.useRef)();
const isAccessibleToolbar = useIsAccessibleToolbar(toolbarRef);
useToolbarFocus({
toolbarRef,
focusOnMount,
defaultIndex: initialIndex,
onIndexChange,
isAccessibleToolbar,
shouldUseKeyboardFocusShortcut,
focusEditorOnEscape
});
if (isAccessibleToolbar) {
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_components.Toolbar, {
label: props['aria-label'],
ref: toolbarRef,
orientation: orientation,
...props,
children: children
});
}
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_components.NavigableMenu, {
orientation: orientation,
role: "toolbar",
ref: toolbarRef,
...props,
children: children
});
}
//# sourceMappingURL=index.js.map