@wordpress/block-editor
Version:
190 lines (164 loc) • 7.22 kB
JavaScript
import { createElement } from "@wordpress/element";
/**
* WordPress dependencies
*/
import { focus, isFormElement } from '@wordpress/dom';
import { TAB, ESCAPE } from '@wordpress/keycodes';
import { useSelect, useDispatch } from '@wordpress/data';
import { useRefEffect, useMergeRefs } from '@wordpress/compose';
import { useRef } from '@wordpress/element';
/**
* Internal dependencies
*/
import { store as blockEditorStore } from '../../store';
export default function useTabNav() {
const container = useRef();
const focusCaptureBeforeRef = useRef();
const focusCaptureAfterRef = useRef();
const lastFocus = useRef();
const {
hasMultiSelection,
getSelectedBlockClientId,
getBlockCount
} = useSelect(blockEditorStore);
const {
setNavigationMode
} = useDispatch(blockEditorStore);
const isNavigationMode = useSelect(select => select(blockEditorStore).isNavigationMode(), []); // Don't allow tabbing to this element in Navigation mode.
const focusCaptureTabIndex = !isNavigationMode ? '0' : undefined; // Reference that holds the a flag for enabling or disabling
// capturing on the focus capture elements.
const noCapture = useRef();
function onFocusCapture(event) {
// Do not capture incoming focus if set by us in WritingFlow.
if (noCapture.current) {
noCapture.current = null;
} else if (hasMultiSelection()) {
container.current.focus();
} else if (getSelectedBlockClientId()) {
lastFocus.current.focus();
} else {
setNavigationMode(true);
const canvasElement = container.current.ownerDocument === event.target.ownerDocument ? container.current : container.current.ownerDocument.defaultView.frameElement;
const isBefore = // eslint-disable-next-line no-bitwise
event.target.compareDocumentPosition(canvasElement) & event.target.DOCUMENT_POSITION_FOLLOWING;
const tabbables = focus.tabbable.find(container.current);
if (tabbables.length) {
const next = isBefore ? tabbables[0] : tabbables[tabbables.length - 1];
next.focus();
}
}
}
const before = createElement("div", {
ref: focusCaptureBeforeRef,
tabIndex: focusCaptureTabIndex,
onFocus: onFocusCapture
});
const after = createElement("div", {
ref: focusCaptureAfterRef,
tabIndex: focusCaptureTabIndex,
onFocus: onFocusCapture
});
const ref = useRefEffect(node => {
function onKeyDown(event) {
if (event.defaultPrevented) {
return;
}
if (event.keyCode === ESCAPE) {
event.preventDefault();
setNavigationMode(true);
return;
} // In Edit mode, Tab should focus the first tabbable element after
// the content, which is normally the sidebar (with block controls)
// and Shift+Tab should focus the first tabbable element before the
// content, which is normally the block toolbar.
// Arrow keys can be used, and Tab and arrow keys can be used in
// Navigation mode (press Esc), to navigate through blocks.
if (event.keyCode !== TAB) {
return;
}
const isShift = event.shiftKey;
const direction = isShift ? 'findPrevious' : 'findNext';
if (!hasMultiSelection() && !getSelectedBlockClientId()) {
// Preserve the behaviour of entering navigation mode when
// tabbing into the content without a block selection.
// `onFocusCapture` already did this previously, but we need to
// do it again here because after clearing block selection,
// focus land on the writing flow container and pressing Tab
// will no longer send focus through the focus capture element.
if (event.target === node) setNavigationMode(true);
return;
} // Allow tabbing from the block wrapper to a form element,
// and between form elements rendered in a block,
// such as inside a placeholder. Form elements are generally
// meant to be UI rather than part of the content. Ideally
// these are not rendered in the content and perhaps in the
// future they can be rendered in an iframe or shadow DOM.
if ((isFormElement(event.target) || event.target.getAttribute('data-block') === getSelectedBlockClientId()) && isFormElement(focus.tabbable[direction](event.target))) {
return;
}
const next = isShift ? focusCaptureBeforeRef : focusCaptureAfterRef; // Disable focus capturing on the focus capture element, so it
// doesn't refocus this block and so it allows default behaviour
// (moving focus to the next tabbable element).
noCapture.current = true; // Focusing the focus capture element, which is located above and
// below the editor, should not scroll the page all the way up or
// down.
next.current.focus({
preventScroll: true
});
}
function onFocusOut(event) {
lastFocus.current = event.target;
const {
ownerDocument
} = node; // If focus disappears due to there being no blocks, move focus to
// the writing flow wrapper.
if (!event.relatedTarget && ownerDocument.activeElement === ownerDocument.body && getBlockCount() === 0) {
node.focus();
}
} // When tabbing back to an element in block list, this event handler prevents scrolling if the
// focus capture divs (before/after) are outside of the viewport. (For example shift+tab back to a paragraph
// when focus is on a sidebar element. This prevents the scrollable writing area from jumping either to the
// top or bottom of the document.
//
// Note that it isn't possible to disable scrolling in the onFocus event. We need to intercept this
// earlier in the keypress handler, and call focus( { preventScroll: true } ) instead.
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLOrForeignElement/focus#parameters
function preventScrollOnTab(event) {
if (event.keyCode !== TAB) {
return;
}
if (event.target?.getAttribute('role') === 'region') {
return;
}
if (container.current === event.target) {
return;
}
const isShift = event.shiftKey;
const direction = isShift ? 'findPrevious' : 'findNext';
const target = focus.tabbable[direction](event.target); // Only do something when the next tabbable is a focus capture div (before/after)
if (target === focusCaptureBeforeRef.current || target === focusCaptureAfterRef.current) {
event.preventDefault();
target.focus({
preventScroll: true
});
}
}
const {
ownerDocument
} = node;
const {
defaultView
} = ownerDocument;
defaultView.addEventListener('keydown', preventScrollOnTab);
node.addEventListener('keydown', onKeyDown);
node.addEventListener('focusout', onFocusOut);
return () => {
defaultView.removeEventListener('keydown', preventScrollOnTab);
node.removeEventListener('keydown', onKeyDown);
node.removeEventListener('focusout', onFocusOut);
};
}, []);
const mergedRefs = useMergeRefs([container, ref]);
return [before, mergedRefs, after];
}
//# sourceMappingURL=use-tab-nav.js.map