@wordpress/block-editor
Version:
152 lines (151 loc) • 5.62 kB
JavaScript
// packages/block-editor/src/components/writing-flow/use-tab-nav.js
import { focus, isFormElement } from "@wordpress/dom";
import { TAB } from "@wordpress/keycodes";
import { useSelect, useDispatch } from "@wordpress/data";
import { useRefEffect, useMergeRefs } from "@wordpress/compose";
import { useRef } from "@wordpress/element";
import { store as blockEditorStore } from "../../store";
import { isInSameBlock, isInsideRootBlock } from "../../utils/dom";
import { unlock } from "../../lock-unlock";
import { jsx } from "react/jsx-runtime";
function useTabNav() {
const containerRef = (
/** @type {typeof useRef<HTMLElement>} */
useRef()
);
const focusCaptureBeforeRef = useRef();
const focusCaptureAfterRef = useRef();
const {
hasMultiSelection,
getSelectedBlockClientId,
getBlockCount,
getBlockOrder,
getLastFocus,
getSectionRootClientId,
isZoomOut
} = unlock(useSelect(blockEditorStore));
const { setLastFocus } = unlock(useDispatch(blockEditorStore));
const noCaptureRef = useRef();
function onFocusCapture(event) {
const canvasElement = containerRef.current.ownerDocument === event.target.ownerDocument ? containerRef.current : containerRef.current.ownerDocument.defaultView.frameElement;
if (noCaptureRef.current) {
noCaptureRef.current = null;
} else if (hasMultiSelection()) {
containerRef.current.focus();
} else if (getSelectedBlockClientId()) {
if (getLastFocus()?.current) {
getLastFocus().current.focus();
} else {
containerRef.current.querySelector(
`[data-block="${getSelectedBlockClientId()}"]`
).focus();
}
} else if (isZoomOut()) {
const sectionRootClientId = getSectionRootClientId();
const sectionBlocks = getBlockOrder(sectionRootClientId);
if (sectionBlocks.length) {
containerRef.current.querySelector(`[data-block="${sectionBlocks[0]}"]`).focus();
} else if (sectionRootClientId) {
containerRef.current.querySelector(`[data-block="${sectionRootClientId}"]`).focus();
} else {
canvasElement.focus();
}
} else {
const isBefore = (
// eslint-disable-next-line no-bitwise
event.target.compareDocumentPosition(canvasElement) & event.target.DOCUMENT_POSITION_FOLLOWING
);
const tabbables = focus.tabbable.find(containerRef.current);
if (tabbables.length) {
const next = isBefore ? tabbables[0] : tabbables[tabbables.length - 1];
next.focus();
}
}
}
const before = /* @__PURE__ */ jsx(
"div",
{
ref: focusCaptureBeforeRef,
tabIndex: "0",
onFocus: onFocusCapture
}
);
const after = /* @__PURE__ */ jsx(
"div",
{
ref: focusCaptureAfterRef,
tabIndex: "0",
onFocus: onFocusCapture
}
);
const ref = useRefEffect((node) => {
function onKeyDown(event) {
if (event.defaultPrevented) {
return;
}
if (event.keyCode !== TAB) {
return;
}
if (
// Bails in case the focus capture elements aren’t present. They
// may be omitted to avoid silent tab stops in preview mode.
// See: https://github.com/WordPress/gutenberg/pull/59317
!focusCaptureAfterRef.current || !focusCaptureBeforeRef.current
) {
return;
}
const { target, shiftKey: isShift } = event;
const direction = isShift ? "findPrevious" : "findNext";
const nextTabbable = focus.tabbable[direction](target);
const currentBlock = target.closest("[data-block]");
const isElementPartOfSelectedBlock = currentBlock && nextTabbable && (isInSameBlock(currentBlock, nextTabbable) || isInsideRootBlock(currentBlock, nextTabbable));
if (isFormElement(nextTabbable) && isElementPartOfSelectedBlock) {
return;
}
const next = isShift ? focusCaptureBeforeRef : focusCaptureAfterRef;
noCaptureRef.current = true;
next.current.focus({ preventScroll: true });
}
function onFocusOut(event) {
setLastFocus({ ...getLastFocus(), current: event.target });
const { ownerDocument: ownerDocument2 } = node;
if (!event.relatedTarget && event.target.hasAttribute("data-block") && ownerDocument2.activeElement === ownerDocument2.body && getBlockCount() === 0) {
node.focus();
}
}
function preventScrollOnTab(event) {
if (event.keyCode !== TAB) {
return;
}
if (event.target?.getAttribute("role") === "region") {
return;
}
if (containerRef.current === event.target) {
return;
}
const isShift = event.shiftKey;
const direction = isShift ? "findPrevious" : "findNext";
const target = focus.tabbable[direction](event.target);
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([containerRef, ref]);
return [before, mergedRefs, after];
}
export {
useTabNav as default
};
//# sourceMappingURL=use-tab-nav.js.map