New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

@zag-js/focus-trap

Package Overview
Dependencies
Maintainers
0
Versions
29
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@zag-js/focus-trap - npm Package Compare versions

Comparing version 0.79.1 to 0.79.2

233

dist/index.d.ts

@@ -1,9 +0,234 @@

import { Options } from 'focus-trap';
export { FocusTrap, createFocusTrap } from 'focus-trap';
type FocusableElement = HTMLElement | SVGElement;
type FocusTargetValue = FocusableElement | string;
type FocusTargetValueOrFalse = FocusTargetValue | false;
/**
* A DOM node, a selector string (which will be passed to
* `document.querySelector()` to find the DOM node), or a function that
* returns a DOM node.
*/
type FocusTarget = FocusTargetValue | (() => FocusTargetValue);
/**
* A DOM node, a selector string (which will be passed to
* `document.querySelector()` to find the DOM node), `false` to explicitly indicate
* an opt-out, or a function that returns a DOM node or `false`.
*/
type FocusTargetOrFalse = FocusTargetValueOrFalse | (() => FocusTargetValueOrFalse);
type MouseEventToBoolean = (event: MouseEvent | TouchEvent) => boolean;
type KeyboardEventToBoolean = (event: KeyboardEvent) => boolean;
interface FocusTrapOptions {
/**
* A function that will be called **before** sending focus to the
* target element upon activation.
*/
onActivate?: VoidFunction;
/**
* A function that will be called **after** focus has been sent to the
* target element upon activation.
*/
onPostActivate?: VoidFunction;
/**
* A function that will be called immediately after the trap's state is updated to be paused.
*/
onPause?: VoidFunction;
/**
* A function that will be called after the trap has been completely paused and is no longer
* managing/trapping focus.
*/
onPostPause?: VoidFunction;
/**
* A function that will be called immediately after the trap's state is updated to be active
* again, but prior to updating its knowledge of what nodes are tabbable within its containers,
* and prior to actively managing/trapping focus.
*/
onUnpause?: VoidFunction;
/**
* A function that will be called after the trap has been completely unpaused and is once
* again managing/trapping focus.
*/
onPostUnpause?: VoidFunction;
/**
* A function for determining if it is safe to send focus to the focus trap
* or not.
*
* It should return a promise that only resolves once all the listed `containers`
* are able to receive focus.
*
* The purpose of this is to prevent early focus-trap activation on animated
* dialogs that fade in and out. When a dialog fades in, there is a brief delay
* between the activation of the trap and the trap element being focusable.
*/
checkCanFocusTrap?: (containers: Array<HTMLElement | SVGElement>) => Promise<void>;
/**
* A function that will be called **before** sending focus to the
* trigger element upon deactivation.
*/
onDeactivate?: VoidFunction;
/**
* A function that will be called after the trap is deactivated, after `onDeactivate`.
* If `returnFocus` was set, it will be called **after** focus has been sent to the trigger
* element upon deactivation; otherwise, it will be called after deactivation completes.
*/
onPostDeactivate?: VoidFunction;
/**
* A function for determining if it is safe to send focus back to the `trigger` element.
*
* It should return a promise that only resolves once `trigger` is focusable.
*
* The purpose of this is to prevent the focus being sent to an animated trigger element too early.
* If a trigger element fades in upon trap deactivation, there is a brief delay between the deactivation
* of the trap and when the trigger element is focusable.
*
* `trigger` will be either the node that had focus prior to the trap being activated,
* or the result of the `setReturnFocus` option, if configured.
*
* This handler is **not** called if the `returnFocusOnDeactivate` configuration option
* (or the `returnFocus` deactivation option) is falsy.
*/
checkCanReturnFocus?: (trigger: HTMLElement | SVGElement) => Promise<void>;
/**
* By default, when a focus trap is activated the first element in the
* focus trap's tab order will receive focus. With this option you can
* specify a different element to receive that initial focus, or use `false`
* for no initially focused element at all.
*
* NOTE: Setting this option to `false` (or a function that returns `false`)
* will prevent the `fallbackFocus` option from being used.
*
* Setting this option to `undefined` (or a function that returns `undefined`)
* will result in the default behavior.
*/
initialFocus?: FocusTargetOrFalse | undefined | VoidFunction;
/**
* By default, an error will be thrown if the focus trap contains no
* elements in its tab order. With this option you can specify a
* fallback element to programmatically receive focus if no other
* tabbable elements are found. For example, you may want a popover's
* `<div>` to receive focus if the popover's content includes no
* tabbable elements. *Make sure the fallback element has a negative
* `tabindex` so it can be programmatically focused.
*
* NOTE: If `initialFocus` is `false` (or a function that returns `false`),
* this function will not be called when the trap is activated, and no element
* will be initially focused. This function may still be called while the trap
* is active if things change such that there are no longer any tabbable nodes
* in the trap.
*/
fallbackFocus?: FocusTarget;
/**
* Default: `true`. If `false`, when the trap is deactivated,
* focus will *not* return to the element that had focus before activation.
*/
returnFocusOnDeactivate?: boolean;
/**
* By default, focus trap on deactivation will return to the element
* that was focused before activation.
*/
setReturnFocus?: FocusTargetValueOrFalse | ((nodeFocusedBeforeActivation: HTMLElement | SVGElement) => FocusTargetValueOrFalse);
/**
* Default: `true`. If `false` or returns `false`, the `Escape` key will not trigger
* deactivation of the focus trap. This can be useful if you want
* to force the user to make a decision instead of allowing an easy
* way out. Note that if a function is given, it's only called if the ESC key
* was pressed.
*/
escapeDeactivates?: boolean | KeyboardEventToBoolean;
/**
* If `true` or returns `true`, a click outside the focus trap will
* deactivate the focus trap and allow the click event to do its thing (i.e.
* to pass-through to the element that was clicked). This option **takes
* precedence** over `allowOutsideClick` when it's set to `true`, causing
* that option to be ignored. Default: `false`.
*/
clickOutsideDeactivates?: boolean | MouseEventToBoolean;
/**
* If set and is or returns `true`, a click outside the focus trap will not
* be prevented, even when `clickOutsideDeactivates` is `false`. When
* `clickOutsideDeactivates` is `true`, this option is **ignored** (i.e.
* if it's a function, it will not be called). Use this option to control
* if (and even which) clicks are allowed outside the trap in conjunction
* with `clickOutsideDeactivates: false`. Default: `false`.
*/
allowOutsideClick?: boolean | MouseEventToBoolean;
/**
* By default, focus() will scroll to the element if not in viewport.
* It can produce unintended effects like scrolling back to the top of a modal.
* If set to `true`, no scroll will happen.
*/
preventScroll?: boolean;
/**
* Default: `true`. Delays the autofocus when the focus trap is activated.
* This prevents elements within the focusable element from capturing
* the event that triggered the focus trap activation.
*/
delayInitialFocus?: boolean;
/**
* Default: `window.document`. Document where the focus trap will be active.
* This allows to use FocusTrap in an iFrame context.
*/
document?: Document;
/**
* Determines if the given keyboard event is a "tab forward" event that will move
* the focus to the next trapped element in tab order. Defaults to the `TAB` key.
* Use this to override the trap's behavior if you want to use arrow keys to control
* keyboard navigation within the trap, for example. Also see `isKeyBackward()` option.
*/
isKeyForward?: KeyboardEventToBoolean;
/**
* Determines if the given keyboard event is a "tab backward" event that will move
* the focus to the previous trapped element in tab order. Defaults to the `SHIFT+TAB` key.
* Use this to override the trap's behavior if you want to use arrow keys to control
* keyboard navigation within the trap, for example. Also see `isKeyForward()` option.
*/
isKeyBackward?: KeyboardEventToBoolean;
/**
* Default: `[]`. An array of FocusTrap instances that will be managed by this FocusTrap.
*/
trapStack?: any[];
}
interface DeactivateOptions extends Pick<FocusTrapOptions, "onDeactivate" | "onPostDeactivate" | "checkCanReturnFocus"> {
returnFocus?: boolean | undefined;
}
type ActivateOptions = Pick<FocusTrapOptions, "onActivate" | "onPostActivate" | "checkCanFocusTrap">;
type PauseOptions = Pick<FocusTrapOptions, "onPause" | "onPostPause">;
type UnpauseOptions = Pick<FocusTrapOptions, "onUnpause" | "onPostUnpause">;
declare class FocusTrap {
private trapStack;
private config;
private doc;
private state;
get active(): boolean;
get paused(): boolean;
constructor(elements: HTMLElement | HTMLElement[], options: FocusTrapOptions);
private findContainerIndex;
private updateTabbableNodes;
private listenerCleanups;
private addListeners;
private removeListeners;
private handleFocus;
private handlePointerDown;
private handleClick;
private handleTabKey;
private handleEscapeKey;
private _mutationObserver?;
private setupMutationObserver;
private updateObservedNodes;
private getInitialFocusNode;
private tryFocus;
activate(activateOptions?: ActivateOptions): this;
deactivate: (deactivateOptions?: DeactivateOptions) => this;
pause: (pauseOptions?: PauseOptions) => this;
unpause: (unpauseOptions?: UnpauseOptions) => this;
updateContainerElements: (containerElements: HTMLElement | HTMLElement[]) => this;
private getReturnFocusNode;
private getOption;
private getNodeForOption;
private findNextNavNode;
}
type ElementOrGetter = HTMLElement | null | (() => HTMLElement | null);
interface TrapFocusOptions extends Omit<Options, "document"> {
interface TrapFocusOptions extends Omit<FocusTrapOptions, "document"> {
}
declare function trapFocus(el: ElementOrGetter, options?: TrapFocusOptions): () => void;
export { type TrapFocusOptions, trapFocus };
export { FocusTrap, type FocusTrapOptions, type TrapFocusOptions, trapFocus };
'use strict';
var domQuery = require('@zag-js/dom-query');
var focusTrap = require('focus-trap');
var __defProp = Object.defineProperty;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
var activeFocusTraps = {
activateTrap(trapStack, trap) {
if (trapStack.length > 0) {
const activeTrap = trapStack[trapStack.length - 1];
if (activeTrap !== trap) {
activeTrap.pause();
}
}
const trapIndex = trapStack.indexOf(trap);
if (trapIndex === -1) {
trapStack.push(trap);
} else {
trapStack.splice(trapIndex, 1);
trapStack.push(trap);
}
},
deactivateTrap(trapStack, trap) {
const trapIndex = trapStack.indexOf(trap);
if (trapIndex !== -1) {
trapStack.splice(trapIndex, 1);
}
if (trapStack.length > 0) {
trapStack[trapStack.length - 1].unpause();
}
}
};
var sharedTrapStack = [];
var FocusTrap = class {
constructor(elements, options) {
__publicField(this, "trapStack");
__publicField(this, "config");
__publicField(this, "doc");
__publicField(this, "state", {
containers: [],
containerGroups: [],
tabbableGroups: [],
nodeFocusedBeforeActivation: null,
mostRecentlyFocusedNode: null,
active: false,
paused: false,
delayInitialFocusTimer: void 0,
recentNavEvent: void 0
});
__publicField(this, "listenerCleanups", []);
__publicField(this, "handleFocus", (event) => {
const target = domQuery.getEventTarget(event);
const targetContained = this.findContainerIndex(target, event) >= 0;
if (targetContained || domQuery.isDocument(target)) {
if (targetContained) {
this.state.mostRecentlyFocusedNode = target;
}
} else {
event.stopImmediatePropagation();
let nextNode;
let navAcrossContainers = true;
if (this.state.mostRecentlyFocusedNode) {
if (domQuery.getTabIndex(this.state.mostRecentlyFocusedNode) > 0) {
const mruContainerIdx = this.findContainerIndex(this.state.mostRecentlyFocusedNode);
const { tabbableNodes } = this.state.containerGroups[mruContainerIdx];
if (tabbableNodes.length > 0) {
const mruTabIdx = tabbableNodes.findIndex((node) => node === this.state.mostRecentlyFocusedNode);
if (mruTabIdx >= 0) {
if (this.config.isKeyForward(this.state.recentNavEvent)) {
if (mruTabIdx + 1 < tabbableNodes.length) {
nextNode = tabbableNodes[mruTabIdx + 1];
navAcrossContainers = false;
}
} else {
if (mruTabIdx - 1 >= 0) {
nextNode = tabbableNodes[mruTabIdx - 1];
navAcrossContainers = false;
}
}
}
}
} else {
if (!this.state.containerGroups.some((g) => g.tabbableNodes.some((n) => domQuery.getTabIndex(n) > 0))) {
navAcrossContainers = false;
}
}
} else {
navAcrossContainers = false;
}
if (navAcrossContainers) {
nextNode = this.findNextNavNode({
// move FROM the MRU node, not event-related node (which will be the node that is
// outside the trap causing the focus escape we're trying to fix)
target: this.state.mostRecentlyFocusedNode,
isBackward: this.config.isKeyBackward(this.state.recentNavEvent)
});
}
if (nextNode) {
this.tryFocus(nextNode);
} else {
this.tryFocus(this.state.mostRecentlyFocusedNode || this.getInitialFocusNode());
}
}
this.state.recentNavEvent = void 0;
});
__publicField(this, "handlePointerDown", (event) => {
const target = domQuery.getEventTarget(event);
if (this.findContainerIndex(target, event) >= 0) {
return;
}
if (valueOrHandler(this.config.clickOutsideDeactivates, event)) {
this.deactivate({ returnFocus: this.config.returnFocusOnDeactivate });
return;
}
if (valueOrHandler(this.config.allowOutsideClick, event)) {
return;
}
event.preventDefault();
});
__publicField(this, "handleClick", (event) => {
const target = domQuery.getEventTarget(event);
if (this.findContainerIndex(target, event) >= 0) {
return;
}
if (valueOrHandler(this.config.clickOutsideDeactivates, event)) {
return;
}
if (valueOrHandler(this.config.allowOutsideClick, event)) {
return;
}
event.preventDefault();
event.stopImmediatePropagation();
});
__publicField(this, "handleTabKey", (event) => {
if (this.config.isKeyForward(event) || this.config.isKeyBackward(event)) {
this.state.recentNavEvent = event;
const isBackward = this.config.isKeyBackward(event);
const destinationNode = this.findNextNavNode({ event, isBackward });
if (!destinationNode) return;
if (isTabEvent(event)) {
event.preventDefault();
}
this.tryFocus(destinationNode);
}
});
__publicField(this, "handleEscapeKey", (event) => {
if (isEscapeEvent(event) && valueOrHandler(this.config.escapeDeactivates, event) !== false) {
event.preventDefault();
this.deactivate();
}
});
__publicField(this, "_mutationObserver");
__publicField(this, "setupMutationObserver", () => {
const win = this.doc.defaultView || window;
this._mutationObserver = new win.MutationObserver((mutations) => {
const isFocusedNodeRemoved = mutations.some((mutation) => {
const removedNodes = Array.from(mutation.removedNodes);
return removedNodes.some((node) => node === this.state.mostRecentlyFocusedNode);
});
if (isFocusedNodeRemoved) {
this.tryFocus(this.getInitialFocusNode());
}
});
});
__publicField(this, "updateObservedNodes", () => {
this._mutationObserver?.disconnect();
if (this.state.active && !this.state.paused) {
this.state.containers.map((container) => {
this._mutationObserver?.observe(container, { subtree: true, childList: true });
});
}
});
__publicField(this, "getInitialFocusNode", () => {
let node = this.getNodeForOption("initialFocus", { hasFallback: true });
if (node === false) {
return false;
}
if (node === void 0 || node && !domQuery.isFocusable(node)) {
if (this.findContainerIndex(this.doc.activeElement) >= 0) {
node = this.doc.activeElement;
} else {
const firstTabbableGroup = this.state.tabbableGroups[0];
const firstTabbableNode = firstTabbableGroup && firstTabbableGroup.firstTabbableNode;
node = firstTabbableNode || this.getNodeForOption("fallbackFocus");
}
} else if (node === null) {
node = this.getNodeForOption("fallbackFocus");
}
if (!node) {
throw new Error("Your focus-trap needs to have at least one focusable element");
}
if (!node.isConnected) {
node = this.getNodeForOption("fallbackFocus");
}
return node;
});
__publicField(this, "tryFocus", (node) => {
if (node === false) return;
if (node === domQuery.getActiveElement(this.doc)) return;
if (!node || !node.focus) {
this.tryFocus(this.getInitialFocusNode());
return;
}
node.focus({ preventScroll: !!this.config.preventScroll });
this.state.mostRecentlyFocusedNode = node;
if (isSelectableInput(node)) {
node.select();
}
});
__publicField(this, "deactivate", (deactivateOptions) => {
if (!this.state.active) return this;
const options = {
onDeactivate: this.config.onDeactivate,
onPostDeactivate: this.config.onPostDeactivate,
checkCanReturnFocus: this.config.checkCanReturnFocus,
...deactivateOptions
};
clearTimeout(this.state.delayInitialFocusTimer);
this.state.delayInitialFocusTimer = void 0;
this.removeListeners();
this.state.active = false;
this.state.paused = false;
this.updateObservedNodes();
activeFocusTraps.deactivateTrap(this.trapStack, this);
const onDeactivate = this.getOption(options, "onDeactivate");
const onPostDeactivate = this.getOption(options, "onPostDeactivate");
const checkCanReturnFocus = this.getOption(options, "checkCanReturnFocus");
const returnFocus = this.getOption(options, "returnFocus", "returnFocusOnDeactivate");
onDeactivate?.();
const finishDeactivation = () => {
delay(() => {
if (returnFocus) {
const returnFocusNode = this.getReturnFocusNode(this.state.nodeFocusedBeforeActivation);
this.tryFocus(returnFocusNode);
}
onPostDeactivate?.();
});
};
if (returnFocus && checkCanReturnFocus) {
const returnFocusNode = this.getReturnFocusNode(this.state.nodeFocusedBeforeActivation);
checkCanReturnFocus(returnFocusNode).then(finishDeactivation, finishDeactivation);
return this;
}
finishDeactivation();
return this;
});
__publicField(this, "pause", (pauseOptions) => {
if (this.state.paused || !this.state.active) {
return this;
}
const onPause = this.getOption(pauseOptions, "onPause");
const onPostPause = this.getOption(pauseOptions, "onPostPause");
this.state.paused = true;
onPause?.();
this.removeListeners();
this.updateObservedNodes();
onPostPause?.();
return this;
});
__publicField(this, "unpause", (unpauseOptions) => {
if (!this.state.paused || !this.state.active) {
return this;
}
const onUnpause = this.getOption(unpauseOptions, "onUnpause");
const onPostUnpause = this.getOption(unpauseOptions, "onPostUnpause");
this.state.paused = false;
onUnpause?.();
this.updateTabbableNodes();
this.addListeners();
this.updateObservedNodes();
onPostUnpause?.();
return this;
});
__publicField(this, "updateContainerElements", (containerElements) => {
this.state.containers = Array.isArray(containerElements) ? containerElements.filter(Boolean) : [containerElements].filter(Boolean);
if (this.state.active) {
this.updateTabbableNodes();
}
this.updateObservedNodes();
return this;
});
__publicField(this, "getReturnFocusNode", (previousActiveElement) => {
const node = this.getNodeForOption("setReturnFocus", {
params: [previousActiveElement]
});
return node ? node : node === false ? false : previousActiveElement;
});
__publicField(this, "getOption", (configOverrideOptions, optionName, configOptionName) => {
return configOverrideOptions && configOverrideOptions[optionName] !== void 0 ? configOverrideOptions[optionName] : (
// @ts-expect-error
this.config[configOptionName || optionName]
);
});
__publicField(this, "getNodeForOption", (optionName, { hasFallback = false, params = [] } = {}) => {
let optionValue = this.config[optionName];
if (typeof optionValue === "function") optionValue = optionValue(...params);
if (optionValue === true) optionValue = void 0;
if (!optionValue) {
if (optionValue === void 0 || optionValue === false) {
return optionValue;
}
throw new Error(`\`${optionName}\` was specified but was not a node, or did not return a node`);
}
let node = optionValue;
if (typeof optionValue === "string") {
try {
node = this.doc.querySelector(optionValue);
} catch (err) {
throw new Error(`\`${optionName}\` appears to be an invalid selector; error="${err.message}"`);
}
if (!node) {
if (!hasFallback) {
throw new Error(`\`${optionName}\` as selector refers to no known node`);
}
}
}
return node;
});
__publicField(this, "findNextNavNode", (opts) => {
const { event, isBackward = false } = opts;
const target = opts.target || domQuery.getEventTarget(event);
this.updateTabbableNodes();
let destinationNode = null;
if (this.state.tabbableGroups.length > 0) {
const containerIndex = this.findContainerIndex(target, event);
const containerGroup = containerIndex >= 0 ? this.state.containerGroups[containerIndex] : void 0;
if (containerIndex < 0) {
if (isBackward) {
destinationNode = this.state.tabbableGroups[this.state.tabbableGroups.length - 1].lastTabbableNode;
} else {
destinationNode = this.state.tabbableGroups[0].firstTabbableNode;
}
} else if (isBackward) {
let startOfGroupIndex = this.state.tabbableGroups.findIndex(
({ firstTabbableNode }) => target === firstTabbableNode
);
if (startOfGroupIndex < 0 && (containerGroup?.container === target || domQuery.isFocusable(target) && !domQuery.isTabbable(target) && !containerGroup?.nextTabbableNode(target, false))) {
startOfGroupIndex = containerIndex;
}
if (startOfGroupIndex >= 0) {
const destinationGroupIndex = startOfGroupIndex === 0 ? this.state.tabbableGroups.length - 1 : startOfGroupIndex - 1;
const destinationGroup = this.state.tabbableGroups[destinationGroupIndex];
destinationNode = domQuery.getTabIndex(target) >= 0 ? destinationGroup.lastTabbableNode : destinationGroup.lastDomTabbableNode;
} else if (!isTabEvent(event)) {
destinationNode = containerGroup?.nextTabbableNode(target, false);
}
} else {
let lastOfGroupIndex = this.state.tabbableGroups.findIndex(
({ lastTabbableNode }) => target === lastTabbableNode
);
if (lastOfGroupIndex < 0 && (containerGroup?.container === target || domQuery.isFocusable(target) && !domQuery.isTabbable(target) && !containerGroup?.nextTabbableNode(target))) {
lastOfGroupIndex = containerIndex;
}
if (lastOfGroupIndex >= 0) {
const destinationGroupIndex = lastOfGroupIndex === this.state.tabbableGroups.length - 1 ? 0 : lastOfGroupIndex + 1;
const destinationGroup = this.state.tabbableGroups[destinationGroupIndex];
destinationNode = domQuery.getTabIndex(target) >= 0 ? destinationGroup.firstTabbableNode : destinationGroup.firstDomTabbableNode;
} else if (!isTabEvent(event)) {
destinationNode = containerGroup?.nextTabbableNode(target);
}
}
} else {
destinationNode = this.getNodeForOption("fallbackFocus");
}
return destinationNode;
});
this.trapStack = options.trapStack || sharedTrapStack;
const config = {
returnFocusOnDeactivate: true,
escapeDeactivates: true,
delayInitialFocus: true,
isKeyForward(e) {
return isTabEvent(e) && !e.shiftKey;
},
isKeyBackward(e) {
return isTabEvent(e) && e.shiftKey;
},
...options
};
this.doc = config.document || domQuery.getDocument(Array.isArray(elements) ? elements[0] : elements);
this.config = config;
this.updateContainerElements(elements);
this.setupMutationObserver();
}
get active() {
return this.state.active;
}
get paused() {
return this.state.paused;
}
findContainerIndex(element, event) {
const composedPath = typeof event?.composedPath === "function" ? event.composedPath() : void 0;
return this.state.containerGroups.findIndex(
({ container, tabbableNodes }) => container.contains(element) || composedPath?.includes(container) || tabbableNodes.find((node) => node === element)
);
}
updateTabbableNodes() {
this.state.containerGroups = this.state.containers.map((container) => {
const tabbableNodes = domQuery.getTabbables(container);
const focusableNodes = domQuery.getFocusables(container);
const firstTabbableNode = tabbableNodes.length > 0 ? tabbableNodes[0] : void 0;
const lastTabbableNode = tabbableNodes.length > 0 ? tabbableNodes[tabbableNodes.length - 1] : void 0;
const firstDomTabbableNode = focusableNodes.find((node) => domQuery.isTabbable(node));
const lastDomTabbableNode = focusableNodes.slice().reverse().find((node) => domQuery.isTabbable(node));
const posTabIndexesFound = !!tabbableNodes.find((node) => domQuery.getTabIndex(node) > 0);
function nextTabbableNode(node, forward = true) {
const nodeIdx = tabbableNodes.indexOf(node);
if (nodeIdx < 0) {
if (forward) {
return focusableNodes.slice(focusableNodes.indexOf(node) + 1).find((el) => domQuery.isTabbable(el));
}
return focusableNodes.slice(0, focusableNodes.indexOf(node)).reverse().find((el) => domQuery.isTabbable(el));
}
return tabbableNodes[nodeIdx + (forward ? 1 : -1)];
}
return {
container,
tabbableNodes,
focusableNodes,
posTabIndexesFound,
firstTabbableNode,
lastTabbableNode,
firstDomTabbableNode,
lastDomTabbableNode,
nextTabbableNode
};
});
this.state.tabbableGroups = this.state.containerGroups.filter((group) => group.tabbableNodes.length > 0);
if (this.state.tabbableGroups.length <= 0 && !this.getNodeForOption("fallbackFocus")) {
throw new Error(
"Your focus-trap must have at least one container with at least one tabbable node in it at all times"
);
}
if (this.state.containerGroups.find((g) => g.posTabIndexesFound) && this.state.containerGroups.length > 1) {
throw new Error(
"At least one node with a positive tabindex was found in one of your focus-trap's multiple containers. Positive tabindexes are only supported in single-container focus-traps."
);
}
}
addListeners() {
if (!this.state.active) return;
activeFocusTraps.activateTrap(this.trapStack, this);
this.state.delayInitialFocusTimer = this.config.delayInitialFocus ? delay(() => {
this.tryFocus(this.getInitialFocusNode());
}) : this.tryFocus(this.getInitialFocusNode());
this.listenerCleanups.push(
domQuery.addDomEvent(this.doc, "focusin", this.handleFocus, true),
domQuery.addDomEvent(this.doc, "mousedown", this.handlePointerDown, { capture: true, passive: false }),
domQuery.addDomEvent(this.doc, "touchstart", this.handlePointerDown, { capture: true, passive: false }),
domQuery.addDomEvent(this.doc, "click", this.handleClick, { capture: true, passive: false }),
domQuery.addDomEvent(this.doc, "keydown", this.handleTabKey, { capture: true, passive: false }),
domQuery.addDomEvent(this.doc, "keydown", this.handleEscapeKey)
);
return this;
}
removeListeners() {
if (!this.state.active) return;
this.listenerCleanups.forEach((cleanup) => cleanup());
this.listenerCleanups = [];
return this;
}
activate(activateOptions) {
if (this.state.active) {
return this;
}
const onActivate = this.getOption(activateOptions, "onActivate");
const onPostActivate = this.getOption(activateOptions, "onPostActivate");
const checkCanFocusTrap = this.getOption(activateOptions, "checkCanFocusTrap");
if (!checkCanFocusTrap) {
this.updateTabbableNodes();
}
this.state.active = true;
this.state.paused = false;
this.state.nodeFocusedBeforeActivation = this.doc.activeElement || null;
onActivate?.();
const finishActivation = () => {
if (checkCanFocusTrap) {
this.updateTabbableNodes();
}
this.addListeners();
this.updateObservedNodes();
onPostActivate?.();
};
if (checkCanFocusTrap) {
checkCanFocusTrap(this.state.containers.concat()).then(finishActivation, finishActivation);
return this;
}
finishActivation();
return this;
}
};
var isTabEvent = (event) => event.key === "Tab";
var valueOrHandler = (value, ...params) => typeof value === "function" ? value(...params) : value;
var isEscapeEvent = (event) => !event.isComposing && event.key === "Escape";
var delay = (fn) => setTimeout(fn, 0);
var isSelectableInput = (node) => node.localName === "input" && "select" in node && typeof node.select === "function";
// src/index.ts
function trapFocus(el, options = {}) {
let trap;
domQuery.raf(() => {
const cleanup = domQuery.raf(() => {
const contentEl = typeof el === "function" ? el() : el;
if (!contentEl) return;
trap = focusTrap.createFocusTrap(contentEl, {
trap = new FocusTrap(contentEl, {
escapeDeactivates: false,

@@ -29,9 +522,7 @@ allowOutsideClick: true,

trap?.deactivate();
cleanup();
};
}
Object.defineProperty(exports, "createFocusTrap", {
enumerable: true,
get: function () { return focusTrap.createFocusTrap; }
});
exports.FocusTrap = FocusTrap;
exports.trapFocus = trapFocus;

5

package.json
{
"name": "@zag-js/focus-trap",
"version": "0.79.1",
"version": "0.79.2",
"description": "Focus trap utility",

@@ -27,4 +27,3 @@ "keywords": [

"dependencies": {
"focus-trap": "7.6.2",
"@zag-js/dom-query": "0.79.1"
"@zag-js/dom-query": "0.79.2"
},

@@ -31,0 +30,0 @@ "devDependencies": {

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc