UNPKG

@itwin/core-react

Version:

A react component library of iTwin.js UI general purpose components

196 lines 7.57 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Bentley Systems, Incorporated. All rights reserved. * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ /** @packageDocumentation * @module Popup */ import * as React from "react"; import { Logger } from "@itwin/core-bentley"; import { UiCore } from "../UiCore.js"; /* eslint-disable @typescript-eslint/no-deprecated */ function isFocusable(element) { if (!element || element.tabIndex < 0) return false; if (element.classList && element.classList.contains("core-focus-trap-ignore-initial")) return false; if (element.getAttribute && typeof element.getAttribute === "function" && element.getAttribute("disabled") !== null) return false; if (element.tabIndex > 0 || (element.tabIndex === 0 && element.getAttribute("tabIndex") !== null)) return true; switch (element.nodeName) { case "A": const anchorElement = element; return !!anchorElement.href && anchorElement.rel !== "ignore"; case "INPUT": const inputElement = element; return inputElement.type !== "hidden" && inputElement.type !== "file"; case "BUTTON": case "SELECT": case "TEXTAREA": return true; default: return false; } } function processFindFocusableDescendant(element) { if (!element) return null; for (const child of element.childNodes) { if (isFocusable(child)) return child; const focusable = processFindFocusableDescendant(child); if (focusable) return focusable; } return null; } function findFirstFocusableDescendant(focusContainer) { return processFindFocusableDescendant(focusContainer); } function processFindLastFocusableDescendant(element) { for (let i = element.childNodes.length - 1; i >= 0; i--) { const child = element.childNodes[i]; const focusable = processFindLastFocusableDescendant(child); if (focusable) return focusable; if (isFocusable(child)) return child; } return null; } function findLastFocusableDescendant(focusContainer) { return processFindLastFocusableDescendant(focusContainer); } function getInitialFocusElement(focusContainer, initialFocusSpec) { if (!focusContainer) return null; if (initialFocusSpec) { if (typeof initialFocusSpec === "string") { const node = focusContainer.querySelector(initialFocusSpec); if (node) { return node; } else { Logger.logError(`${UiCore.packageName}.FocusTrap`, `Unable to locate element via selector ${initialFocusSpec}`); } } else { return initialFocusSpec.current; } } return findFirstFocusableDescendant(focusContainer); } function attemptFocus(element, preventScroll) { if (!isFocusable(element)) return false; try { if (document.activeElement !== element) element.focus({ preventScroll: preventScroll ? true : false, }); } catch { return false; } return document.activeElement === element; } // end attemptFocus /** Focus into first focusable element of a container. * @internal */ export function focusIntoContainer(focusContainer, initialFocusElement) { let result = false; const focusElement = getInitialFocusElement(focusContainer, initialFocusElement); if (focusElement) { // delay setting focus immediately because in some browsers other focus events happen when popup is initially opened. setTimeout(() => { attemptFocus(focusElement, true); }, 60); result = true; } return result; } /** Trap Focus in container while trap is active. * @internal */ export function FocusTrap(props) { const restoreFocusElement = React.useRef(null); const initialFocusElement = React.useRef(null); const focusContainer = React.useRef(null); const isInitialMount = React.useRef(true); const timeoutRef = React.useRef(undefined); // Run on initial mount and when dependencies change. which could happen often. React.useEffect(() => { if (isInitialMount.current) { isInitialMount.current = false; if (props.active) { if (props.returnFocusOnDeactivate) { restoreFocusElement.current = document.activeElement; } initialFocusElement.current = getInitialFocusElement(focusContainer.current, props.initialFocusElement); if (initialFocusElement.current) { // delay setting focus immediately because in some browsers other focus events happen when popup is initially opened. timeoutRef.current = window.setTimeout(() => { attemptFocus(initialFocusElement.current, true); }, 60); } } } }, [ props.children, props.initialFocusElement, props.active, props.returnFocusOnDeactivate, ]); // Return function to run only when FocusTrap is unmounted to restore focus React.useEffect(() => { return () => { window.clearTimeout(timeoutRef.current); if (restoreFocusElement.current) restoreFocusElement.current.focus({ preventScroll: true, }); }; }, []); // this is hit if Shift tab is used. const cycleFocusToEnd = React.useCallback((event) => { if (!props.active) return; if (focusContainer.current && event.target === focusContainer.current) { event.stopPropagation(); event.preventDefault(); const focusable = findLastFocusableDescendant(focusContainer.current); if (focusable) { focusable.focus(); } else { if (initialFocusElement.current && initialFocusElement.current !== document.activeElement) attemptFocus(initialFocusElement.current, true); } } }, [props.active]); // this is hit if tab is used on last focusable item in child container. const cycleFocusToStart = React.useCallback((event) => { if (!props.active) return; event.stopPropagation(); if (initialFocusElement.current && initialFocusElement.current !== document.activeElement) initialFocusElement.current.focus(); }, [props.active]); if (!props.children) return null; return (React.createElement(React.Fragment, null, React.createElement("div", { "data-testid": "focus-trap-div", onFocus: cycleFocusToEnd, ref: focusContainer, /* eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex */ tabIndex: 0, style: { outline: "none", WebkitTapHighlightColor: "rgba(0,0,0,0)" } }, props.children), React.createElement("div", { "data-testid": "focus-trap-limit-div", onFocus: cycleFocusToStart, /* eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex */ tabIndex: 0 }))); } //# sourceMappingURL=FocusTrap.js.map