feat(scanner): complete localized artifact quality checkpoint
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
import { useEffect, useId, useRef, type RefObject } from "react";
|
||||
|
||||
const focusableSelector = [
|
||||
"button:not([disabled])",
|
||||
"a[href]",
|
||||
"input:not([disabled])",
|
||||
"select:not([disabled])",
|
||||
"textarea:not([disabled])",
|
||||
"summary",
|
||||
"[tabindex]:not([tabindex='-1'])",
|
||||
].join(",");
|
||||
|
||||
function isVisibleFocusable(element: HTMLElement) {
|
||||
if (element.matches(":disabled") || element.closest("[hidden], [aria-hidden='true']")) return false;
|
||||
const closedDetails = element.closest("details:not([open])");
|
||||
if (closedDetails) {
|
||||
const summary = closedDetails.querySelector(":scope > summary");
|
||||
if (!summary?.contains(element)) return false;
|
||||
}
|
||||
const style = window.getComputedStyle(element);
|
||||
return style.display !== "none" && style.visibility !== "hidden" && element.getClientRects().length > 0;
|
||||
}
|
||||
|
||||
interface DialogFocusResult {
|
||||
dialogRef: RefObject<HTMLDivElement>;
|
||||
titleId: string;
|
||||
}
|
||||
|
||||
export function useDialogFocus(open: boolean, onClose: () => void): DialogFocusResult {
|
||||
const dialogRef = useRef<HTMLDivElement>(null);
|
||||
const titleId = useId();
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const dialog = dialogRef.current;
|
||||
if (!dialog) return;
|
||||
const previousFocus = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
||||
const focusable = () => [...dialog.querySelectorAll<HTMLElement>(focusableSelector)].filter(isVisibleFocusable);
|
||||
const first = focusable()[0] ?? dialog;
|
||||
const focusFrame = window.requestAnimationFrame(() => first.focus());
|
||||
const inertedBranches: Array<{ element: HTMLElement; wasInert: boolean }> = [];
|
||||
let activeBranch: HTMLElement = dialog;
|
||||
|
||||
while (activeBranch.parentElement) {
|
||||
const parent = activeBranch.parentElement;
|
||||
for (const sibling of parent.children) {
|
||||
if (!(sibling instanceof HTMLElement) || sibling === activeBranch) continue;
|
||||
inertedBranches.push({ element: sibling, wasInert: sibling.inert });
|
||||
sibling.inert = true;
|
||||
}
|
||||
activeBranch = parent;
|
||||
if (parent === document.body) break;
|
||||
}
|
||||
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === "Escape") {
|
||||
event.preventDefault();
|
||||
onClose();
|
||||
return;
|
||||
}
|
||||
if (event.key !== "Tab") return;
|
||||
const items = focusable();
|
||||
if (items.length === 0) {
|
||||
event.preventDefault();
|
||||
dialog.focus();
|
||||
return;
|
||||
}
|
||||
const firstItem = items[0];
|
||||
const lastItem = items[items.length - 1];
|
||||
if (event.shiftKey && document.activeElement === firstItem) {
|
||||
event.preventDefault();
|
||||
lastItem.focus();
|
||||
} else if (!event.shiftKey && document.activeElement === lastItem) {
|
||||
event.preventDefault();
|
||||
firstItem.focus();
|
||||
}
|
||||
};
|
||||
|
||||
const onFocusIn = (event: FocusEvent) => {
|
||||
if (event.target instanceof Node && dialog.contains(event.target)) return;
|
||||
window.requestAnimationFrame(() => (focusable()[0] ?? dialog).focus());
|
||||
};
|
||||
|
||||
document.addEventListener("keydown", onKeyDown);
|
||||
document.addEventListener("focusin", onFocusIn);
|
||||
return () => {
|
||||
window.cancelAnimationFrame(focusFrame);
|
||||
document.removeEventListener("keydown", onKeyDown);
|
||||
document.removeEventListener("focusin", onFocusIn);
|
||||
for (const { element, wasInert } of inertedBranches.reverse()) element.inert = wasInert;
|
||||
window.requestAnimationFrame(() => previousFocus?.focus());
|
||||
};
|
||||
}, [onClose, open]);
|
||||
|
||||
return { dialogRef, titleId };
|
||||
}
|
||||
Reference in New Issue
Block a user