first commit

This commit is contained in:
Stefan Hacker
2026-04-03 09:38:48 +02:00
commit 37ad745546
47450 changed files with 3120798 additions and 0 deletions
@@ -0,0 +1,10 @@
import * as React from 'react';
import type { IFocusTrapZoneProps } from './FocusTrapZone.types';
export declare const FocusTrapZone: React.FunctionComponent<IFocusTrapZoneProps> & {
/**
* Stack of active FocusTrapZone identifiers, exposed for testing purposes only.
* (This is always set, just marked as optional to avoid a cast in the component definition.)
* @internal
*/
focusStack?: string[];
};
@@ -0,0 +1,250 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.FocusTrapZone = void 0;
var tslib_1 = require("tslib");
var React = require("react");
var Utilities_1 = require("../../Utilities");
var react_hooks_1 = require("@fluentui/react-hooks");
var WindowProvider_1 = require("../../WindowProvider");
var dom_1 = require("../../utilities/dom");
var COMPONENT_NAME = 'FocusTrapZone';
var DEFAULT_PROPS = {
disabled: false,
disableFirstFocus: false,
forceFocusInsideTrap: true,
isClickableOutsideFocusTrap: false,
// Hardcoding completely uncontrolled flag for proper interop with FluentUI V9.
'data-tabster': '{"uncontrolled": {"completely": true}}',
};
var useComponentRef = function (componentRef, previouslyFocusedElement, focusFTZ) {
React.useImperativeHandle(componentRef, function () { return ({
get previouslyFocusedElement() {
return previouslyFocusedElement;
},
focus: focusFTZ,
}); }, [focusFTZ, previouslyFocusedElement]);
};
exports.FocusTrapZone = React.forwardRef(function (propsWithoutDefaults, ref) {
var _a;
var root = React.useRef(null);
var firstBumper = React.useRef(null);
var lastBumper = React.useRef(null);
var mergedRootRef = (0, react_hooks_1.useMergedRefs)(root, ref);
var doc = (0, WindowProvider_1.useDocument)();
var win = (0, dom_1.useWindowEx)();
var inShadow = (0, Utilities_1.useHasMergeStylesShadowRootContext)();
var isFirstRender = (_a = (0, react_hooks_1.usePrevious)(false)) !== null && _a !== void 0 ? _a : true;
var props = (0, Utilities_1.getPropsWithDefaults)(DEFAULT_PROPS, propsWithoutDefaults);
var internalState = (0, react_hooks_1.useConst)({
hasFocus: false,
focusStackId: (0, react_hooks_1.useId)('ftz-', props.id),
});
var children = props.children, componentRef = props.componentRef, disabled = props.disabled, disableFirstFocus = props.disableFirstFocus, forceFocusInsideTrap = props.forceFocusInsideTrap, focusPreviouslyFocusedInnerElement = props.focusPreviouslyFocusedInnerElement,
// eslint-disable-next-line @typescript-eslint/no-deprecated
firstFocusableSelector = props.firstFocusableSelector, firstFocusableTarget = props.firstFocusableTarget,
// eslint-disable-next-line @typescript-eslint/no-deprecated
_b = props.disableRestoreFocus,
// eslint-disable-next-line @typescript-eslint/no-deprecated
disableRestoreFocus = _b === void 0 ? props.ignoreExternalFocusing : _b, isClickableOutsideFocusTrap = props.isClickableOutsideFocusTrap, enableAriaHiddenSiblings = props.enableAriaHiddenSiblings;
var bumperProps = {
'aria-hidden': true,
style: {
pointerEvents: 'none',
position: 'fixed', // 'fixed' prevents browsers from scrolling to bumpers when viewport does not contain them
},
tabIndex: disabled ? -1 : 0, // make bumpers tabbable only when enabled
'data-is-visible': true,
'data-is-focus-trap-zone-bumper': true,
};
var focusElementAsync = React.useCallback(function (element) {
if (element !== firstBumper.current && element !== lastBumper.current) {
(0, Utilities_1.focusAsync)(element);
}
}, []);
/**
* Callback to force focus into FTZ (named to avoid overlap with global focus() callback).
* useEventCallback always returns the same callback reference but updates the implementation
* every render to avoid stale captured values.
*/
var focusFTZ = (0, react_hooks_1.useEventCallback)(function () {
if (!root.current) {
return; // not done mounting
}
var previouslyFocusedElementInTrapZone = internalState.previouslyFocusedElementInTrapZone;
if (focusPreviouslyFocusedInnerElement &&
previouslyFocusedElementInTrapZone &&
(0, Utilities_1.elementContains)(root.current, previouslyFocusedElementInTrapZone)) {
// focus on the last item that had focus in the zone before we left the zone
focusElementAsync(previouslyFocusedElementInTrapZone);
return;
}
var firstFocusableChild = null;
if (typeof firstFocusableTarget === 'string') {
firstFocusableChild = root.current.querySelector(firstFocusableTarget);
}
else if (firstFocusableTarget) {
firstFocusableChild = firstFocusableTarget(root.current);
}
else if (firstFocusableSelector) {
var focusSelector = typeof firstFocusableSelector === 'string' ? firstFocusableSelector : firstFocusableSelector();
firstFocusableChild = root.current.querySelector('.' + focusSelector);
}
// Fall back to first element if query selector did not match any elements.
if (!firstFocusableChild) {
firstFocusableChild = (0, Utilities_1.getNextElement)(root.current, root.current.firstChild, false, false, false, true, undefined, undefined, undefined, inShadow);
}
if (firstFocusableChild) {
focusElementAsync(firstFocusableChild);
}
});
/** Used in root div focus/blur handlers */
var focusBumper = function (isFirstBumper) {
if (disabled || !root.current) {
return;
}
var nextFocusable = isFirstBumper === internalState.hasFocus
? (0, Utilities_1.getLastTabbable)(root.current, lastBumper.current, true, false, inShadow)
: (0, Utilities_1.getFirstTabbable)(root.current, firstBumper.current, true, false, inShadow);
if (nextFocusable) {
if (nextFocusable === firstBumper.current || nextFocusable === lastBumper.current) {
// This can happen when FTZ contains no tabbable elements.
// focusFTZ() will take care of finding a focusable element in FTZ.
focusFTZ();
}
else {
nextFocusable.focus();
}
}
};
/** Root div blur handler (doesn't need useCallback since it's for a native element) */
var onRootBlurCapture = function (ev) {
var _a;
(_a = props.onBlurCapture) === null || _a === void 0 ? void 0 : _a.call(props, ev);
var relatedTarget = ev.relatedTarget;
if (ev.relatedTarget === null) {
// In IE11, due to lack of support, event.relatedTarget is always
// null making every onBlur call to be "outside" of the root
// even when it's not. Using document.activeElement is another way
// for us to be able to get what the relatedTarget without relying
// on the event
relatedTarget = (0, Utilities_1.getActiveElement)(doc);
}
if (!(0, Utilities_1.elementContains)(root.current, relatedTarget)) {
internalState.hasFocus = false;
}
};
/** Root div focus handler (doesn't need useCallback since it's for a native element) */
var onRootFocusCapture = function (ev) {
var _a;
(_a = props.onFocusCapture) === null || _a === void 0 ? void 0 : _a.call(props, ev);
if (ev.target === firstBumper.current) {
focusBumper(true);
}
else if (ev.target === lastBumper.current) {
focusBumper(false);
}
internalState.hasFocus = true;
if (ev.target !== ev.currentTarget && !(ev.target === firstBumper.current || ev.target === lastBumper.current)) {
// every time focus changes within the trap zone, remember the focused element so that
// it can be restored if focus leaves the pane and returns via keystroke (i.e. via a call to this.focus(true))
internalState.previouslyFocusedElementInTrapZone = (0, Utilities_1.getEventTarget)(ev.nativeEvent);
}
};
/** Called to restore focus on unmount or props change. (useEventCallback ensures latest prop values are used.) */
var returnFocusToInitiator = (0, react_hooks_1.useEventCallback)(function (elementToFocusOnDismiss) {
exports.FocusTrapZone.focusStack = exports.FocusTrapZone.focusStack.filter(function (value) { return internalState.focusStackId !== value; });
if (!doc) {
return;
}
// Do not use getActiveElement() here.
// When the FTZ is in shadow DOM focus returns to the
// shadow host rather than body so we need to be
// able to inspect that
var activeElement = doc.activeElement;
if (!disableRestoreFocus &&
typeof (elementToFocusOnDismiss === null || elementToFocusOnDismiss === void 0 ? void 0 : elementToFocusOnDismiss.focus) === 'function' &&
// only restore focus if the current focused element is within the FTZ, or if nothing is focused
((0, Utilities_1.elementContains)(root.current, activeElement) || activeElement === doc.body || activeElement.shadowRoot)) {
focusElementAsync(elementToFocusOnDismiss);
}
});
/** Called in window event handlers. (useEventCallback ensures latest prop values are used.) */
var forceFocusOrClickInTrap = (0, react_hooks_1.useEventCallback)(function (ev) {
// be sure to use the latest values here
if (disabled) {
return;
}
if (internalState.focusStackId === exports.FocusTrapZone.focusStack.slice(-1)[0]) {
var targetElement = (0, Utilities_1.getEventTarget)(ev);
if (targetElement && !(0, Utilities_1.elementContains)(root.current, targetElement)) {
if (doc && (0, Utilities_1.getActiveElement)(doc) === doc.body) {
setTimeout(function () {
if (doc && (0, Utilities_1.getActiveElement)(doc) === doc.body) {
focusFTZ();
internalState.hasFocus = true; // set focus here since we stop event propagation
}
}, 0);
}
else {
focusFTZ();
internalState.hasFocus = true; // set focus here since we stop event propagation
}
ev.preventDefault();
ev.stopPropagation();
}
}
});
// Update window event handlers when relevant props change
React.useEffect(function () {
var disposables = [];
if (forceFocusInsideTrap) {
disposables.push((0, Utilities_1.on)(win, 'focus', forceFocusOrClickInTrap, true));
}
if (!isClickableOutsideFocusTrap) {
disposables.push((0, Utilities_1.on)(win, 'click', forceFocusOrClickInTrap, true));
}
return function () {
disposables.forEach(function (dispose) { return dispose(); });
};
// eslint-disable-next-line react-hooks/exhaustive-deps -- should only run when these two props change
}, [forceFocusInsideTrap, isClickableOutsideFocusTrap, win]);
// On prop change or first render, focus the FTZ and update focusStack if appropriate
React.useEffect(function () {
// Do nothing if disabled, or if it's a re-render and forceFocusInsideTrap is false
// (to match existing behavior, the FTZ handles first focus even if forceFocusInsideTrap
// is false, though it's debatable whether it should do this)
if (disabled || (!isFirstRender && !forceFocusInsideTrap) || !root.current) {
return;
}
// Transition from forceFocusInsideTrap / FTZ disabled to enabled (or initial mount)
exports.FocusTrapZone.focusStack.push(internalState.focusStackId);
var elementToFocusOnDismiss = props.elementToFocusOnDismiss || (0, Utilities_1.getActiveElement)(doc);
if (!disableFirstFocus && !(0, Utilities_1.elementContains)(root.current, elementToFocusOnDismiss)) {
focusFTZ();
}
// To match existing behavior, always return focus on cleanup (even if we didn't handle
// initial focus), but it's debatable whether that's correct
return function () { return returnFocusToInitiator(elementToFocusOnDismiss); };
// eslint-disable-next-line react-hooks/exhaustive-deps -- should only run when these two props change
}, [forceFocusInsideTrap, disabled]);
// Handle modalization separately from first focus
React.useEffect(function () {
if (!disabled && enableAriaHiddenSiblings) {
var unmodalize = (0, Utilities_1.modalize)(root.current);
return unmodalize;
}
}, [disabled, enableAriaHiddenSiblings, root]);
// Cleanup lifecyle method for internalState.
(0, react_hooks_1.useUnmount)(function () {
// Dispose of element references so the DOM Nodes can be garbage-collected
delete internalState.previouslyFocusedElementInTrapZone;
});
useComponentRef(componentRef, internalState.previouslyFocusedElementInTrapZone, focusFTZ);
return (React.createElement("div", tslib_1.__assign({ "aria-labelledby": props.ariaLabelledBy }, (0, Utilities_1.getNativeProps)(props, Utilities_1.divProperties), { ref: mergedRootRef, onFocusCapture: onRootFocusCapture, onBlurCapture: onRootBlurCapture }),
React.createElement("div", tslib_1.__assign({}, bumperProps, { ref: firstBumper })),
children,
React.createElement("div", tslib_1.__assign({}, bumperProps, { ref: lastBumper }))));
});
exports.FocusTrapZone.displayName = COMPONENT_NAME;
exports.FocusTrapZone.focusStack = [];
//# sourceMappingURL=FocusTrapZone.js.map
File diff suppressed because one or more lines are too long
@@ -0,0 +1,86 @@
import * as React from 'react';
import type { IRefObject } from '../../Utilities';
/**
* {@docCategory FocusTrapZone}
*/
export interface IFocusTrapZone {
/**
* Sets focus to a descendant in the Trap Zone.
* See firstFocusableSelector and focusPreviouslyFocusedInnerElement for details.
*/
focus: () => void;
}
/**
* {@docCategory FocusTrapZone}
*/
export interface IFocusTrapZoneProps extends React.HTMLAttributes<HTMLDivElement>, React.RefAttributes<HTMLDivElement> {
/**
* Optional callback to access the IFocusTrapZone interface. Use this instead of ref for accessing
* the public methods and properties of the component.
*/
componentRef?: IRefObject<IFocusTrapZone>;
/**
* Whether to disable the FocusTrapZone's focus trapping behavior.
* @defaultvalue false
*/
disabled?: boolean;
/**
* Sets the element to focus on when exiting the FocusTrapZone.
* @defaultvalue The `element.target` that triggered the FTZ.
*/
elementToFocusOnDismiss?: HTMLElement;
/**
* Sets the aria-labelledby attribute.
*/
ariaLabelledBy?: string;
/**
* Whether clicks are allowed outside this FocusTrapZone.
* @defaultvalue false
*/
isClickableOutsideFocusTrap?: boolean;
/**
* If false (the default), the trap zone will restore focus to the element which activated it
* once the trap zone is unmounted or disabled. Set to true to disable this behavior.
* @defaultvalue false
*/
disableRestoreFocus?: boolean;
/**
* @deprecated Use `disableRestoreFocus` (it has the same behavior and a clearer name).
*/
ignoreExternalFocusing?: boolean;
/**
* Whether the focus trap zone should force focus to stay inside of it.
* @defaultvalue true
*/
forceFocusInsideTrap?: boolean;
/**
* Class name (not actual selector) for first focusable item. Do not append a dot.
* Only applies if `focusPreviouslyFocusedInnerElement` is false.
* @deprecated Use `firstFocusableTarget`, since it is more generic. `firstFocusableTarget` takes precedence if
* supplied.
*/
firstFocusableSelector?: string | (() => string);
/**
* Either a full query selector for the first focusable element, or a function to select the focusable element
* within the area directly.
*/
firstFocusableTarget?: string | ((element: HTMLElement) => HTMLElement | null);
/**
* Do not put focus onto the first element inside the focus trap zone.
* @defaultvalue false
*/
disableFirstFocus?: boolean;
/**
* Specifies which descendant element to focus when `focus()` is called.
* If false, use the first focusable descendant, filtered by the `firstFocusableSelector` property if present.
* If true, use the element that was focused when the trap zone last had a focused descendant
* (or fall back to the first focusable descendant if the trap zone has never been focused).
* @defaultvalue false
*/
focusPreviouslyFocusedInnerElement?: boolean;
/**
* Puts aria-hidden=true on all non-ancestors of the current element, for screen readers.
* In future versions of the library, this will be the default behavior.
*/
enableAriaHiddenSiblings?: boolean;
}
@@ -0,0 +1,3 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
//# sourceMappingURL=FocusTrapZone.types.js.map
@@ -0,0 +1 @@
{"version":3,"file":"FocusTrapZone.types.js","sourceRoot":"../src/","sources":["components/FocusTrapZone/FocusTrapZone.types.ts"],"names":[],"mappings":"","sourcesContent":["import * as React from 'react';\nimport type { IRefObject } from '../../Utilities';\n\n/**\n * {@docCategory FocusTrapZone}\n */\nexport interface IFocusTrapZone {\n /**\n * Sets focus to a descendant in the Trap Zone.\n * See firstFocusableSelector and focusPreviouslyFocusedInnerElement for details.\n */\n focus: () => void;\n}\n\n/**\n * {@docCategory FocusTrapZone}\n */\nexport interface IFocusTrapZoneProps extends React.HTMLAttributes<HTMLDivElement>, React.RefAttributes<HTMLDivElement> {\n /**\n * Optional callback to access the IFocusTrapZone interface. Use this instead of ref for accessing\n * the public methods and properties of the component.\n */\n componentRef?: IRefObject<IFocusTrapZone>;\n\n /**\n * Whether to disable the FocusTrapZone's focus trapping behavior.\n * @defaultvalue false\n */\n disabled?: boolean;\n\n /**\n * Sets the element to focus on when exiting the FocusTrapZone.\n * @defaultvalue The `element.target` that triggered the FTZ.\n */\n elementToFocusOnDismiss?: HTMLElement;\n\n /**\n * Sets the aria-labelledby attribute.\n */\n ariaLabelledBy?: string;\n\n /**\n * Whether clicks are allowed outside this FocusTrapZone.\n * @defaultvalue false\n */\n isClickableOutsideFocusTrap?: boolean;\n\n /**\n * If false (the default), the trap zone will restore focus to the element which activated it\n * once the trap zone is unmounted or disabled. Set to true to disable this behavior.\n * @defaultvalue false\n */\n disableRestoreFocus?: boolean;\n\n /**\n * @deprecated Use `disableRestoreFocus` (it has the same behavior and a clearer name).\n */\n ignoreExternalFocusing?: boolean;\n\n /**\n * Whether the focus trap zone should force focus to stay inside of it.\n * @defaultvalue true\n */\n forceFocusInsideTrap?: boolean;\n\n /**\n * Class name (not actual selector) for first focusable item. Do not append a dot.\n * Only applies if `focusPreviouslyFocusedInnerElement` is false.\n * @deprecated Use `firstFocusableTarget`, since it is more generic. `firstFocusableTarget` takes precedence if\n * supplied.\n */\n firstFocusableSelector?: string | (() => string);\n\n /**\n * Either a full query selector for the first focusable element, or a function to select the focusable element\n * within the area directly.\n */\n firstFocusableTarget?: string | ((element: HTMLElement) => HTMLElement | null);\n\n /**\n * Do not put focus onto the first element inside the focus trap zone.\n * @defaultvalue false\n */\n disableFirstFocus?: boolean;\n\n /**\n * Specifies which descendant element to focus when `focus()` is called.\n * If false, use the first focusable descendant, filtered by the `firstFocusableSelector` property if present.\n * If true, use the element that was focused when the trap zone last had a focused descendant\n * (or fall back to the first focusable descendant if the trap zone has never been focused).\n * @defaultvalue false\n */\n focusPreviouslyFocusedInnerElement?: boolean;\n\n /**\n * Puts aria-hidden=true on all non-ancestors of the current element, for screen readers.\n * In future versions of the library, this will be the default behavior.\n */\n enableAriaHiddenSiblings?: boolean;\n}\n"]}
@@ -0,0 +1,2 @@
export * from './FocusTrapZone';
export * from './FocusTrapZone.types';
@@ -0,0 +1,6 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var tslib_1 = require("tslib");
tslib_1.__exportStar(require("./FocusTrapZone"), exports);
tslib_1.__exportStar(require("./FocusTrapZone.types"), exports);
//# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"../src/","sources":["components/FocusTrapZone/index.ts"],"names":[],"mappings":";;;AAAA,0DAAgC;AAChC,gEAAsC","sourcesContent":["export * from './FocusTrapZone';\nexport * from './FocusTrapZone.types';\n"]}