@itwin/core-react
Version:
A react component library of iTwin.js UI general purpose components
196 lines • 7.57 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* 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