This commit is contained in:
2025-09-19 14:25:20 +08:00
parent 269893a435
commit fbf3f77229
24949 changed files with 2839404 additions and 0 deletions

View File

@@ -0,0 +1,5 @@
import ElFocusTrap from './src/focus-trap.vue';
export { ElFocusTrap };
export default ElFocusTrap;
export * from './src/tokens';
export * from './src/utils';

View File

@@ -0,0 +1,31 @@
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
var focusTrap = require('./src/focus-trap.js');
var tokens = require('./src/tokens.js');
var utils = require('./src/utils.js');
exports.ElFocusTrap = focusTrap["default"];
exports["default"] = focusTrap["default"];
exports.FOCUSOUT_PREVENTED = tokens.FOCUSOUT_PREVENTED;
exports.FOCUSOUT_PREVENTED_OPTS = tokens.FOCUSOUT_PREVENTED_OPTS;
exports.FOCUS_AFTER_RELEASED = tokens.FOCUS_AFTER_RELEASED;
exports.FOCUS_AFTER_TRAPPED = tokens.FOCUS_AFTER_TRAPPED;
exports.FOCUS_AFTER_TRAPPED_OPTS = tokens.FOCUS_AFTER_TRAPPED_OPTS;
exports.FOCUS_TRAP_INJECTION_KEY = tokens.FOCUS_TRAP_INJECTION_KEY;
exports.ON_RELEASE_FOCUS_EVT = tokens.ON_RELEASE_FOCUS_EVT;
exports.ON_TRAP_FOCUS_EVT = tokens.ON_TRAP_FOCUS_EVT;
exports.createFocusOutPreventedEvent = utils.createFocusOutPreventedEvent;
exports.focusFirstDescendant = utils.focusFirstDescendant;
exports.focusableStack = utils.focusableStack;
exports.getEdges = utils.getEdges;
exports.getVisibleElement = utils.getVisibleElement;
exports.isFocusCausedByUserEvent = utils.isFocusCausedByUserEvent;
exports.isHidden = utils.isHidden;
exports.obtainAllFocusableElements = utils.obtainAllFocusableElements;
exports.tryFocus = utils.tryFocus;
exports.useFocusReason = utils.useFocusReason;
//# sourceMappingURL=index.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}

View File

@@ -0,0 +1,264 @@
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
var vue = require('vue');
var lodashUnified = require('lodash-unified');
var utils = require('./utils.js');
var tokens = require('./tokens.js');
var pluginVue_exportHelper = require('../../../_virtual/plugin-vue_export-helper.js');
var index = require('../../../hooks/use-escape-keydown/index.js');
var aria = require('../../../constants/aria.js');
var shared = require('@vue/shared');
const _sfc_main = vue.defineComponent({
name: "ElFocusTrap",
inheritAttrs: false,
props: {
loop: Boolean,
trapped: Boolean,
focusTrapEl: Object,
focusStartEl: {
type: [Object, String],
default: "first"
}
},
emits: [
tokens.ON_TRAP_FOCUS_EVT,
tokens.ON_RELEASE_FOCUS_EVT,
"focusin",
"focusout",
"focusout-prevented",
"release-requested"
],
setup(props, { emit }) {
const forwardRef = vue.ref();
let lastFocusBeforeTrapped;
let lastFocusAfterTrapped;
const { focusReason } = utils.useFocusReason();
index.useEscapeKeydown((event) => {
if (props.trapped && !focusLayer.paused) {
emit("release-requested", event);
}
});
const focusLayer = {
paused: false,
pause() {
this.paused = true;
},
resume() {
this.paused = false;
}
};
const onKeydown = (e) => {
if (!props.loop && !props.trapped)
return;
if (focusLayer.paused)
return;
const { code, altKey, ctrlKey, metaKey, currentTarget, shiftKey } = e;
const { loop } = props;
const isTabbing = code === aria.EVENT_CODE.tab && !altKey && !ctrlKey && !metaKey;
const currentFocusingEl = document.activeElement;
if (isTabbing && currentFocusingEl) {
const container = currentTarget;
const [first, last] = utils.getEdges(container);
const isTabbable = first && last;
if (!isTabbable) {
if (currentFocusingEl === container) {
const focusoutPreventedEvent = utils.createFocusOutPreventedEvent({
focusReason: focusReason.value
});
emit("focusout-prevented", focusoutPreventedEvent);
if (!focusoutPreventedEvent.defaultPrevented) {
e.preventDefault();
}
}
} else {
if (!shiftKey && currentFocusingEl === last) {
const focusoutPreventedEvent = utils.createFocusOutPreventedEvent({
focusReason: focusReason.value
});
emit("focusout-prevented", focusoutPreventedEvent);
if (!focusoutPreventedEvent.defaultPrevented) {
e.preventDefault();
if (loop)
utils.tryFocus(first, true);
}
} else if (shiftKey && [first, container].includes(currentFocusingEl)) {
const focusoutPreventedEvent = utils.createFocusOutPreventedEvent({
focusReason: focusReason.value
});
emit("focusout-prevented", focusoutPreventedEvent);
if (!focusoutPreventedEvent.defaultPrevented) {
e.preventDefault();
if (loop)
utils.tryFocus(last, true);
}
}
}
}
};
vue.provide(tokens.FOCUS_TRAP_INJECTION_KEY, {
focusTrapRef: forwardRef,
onKeydown
});
vue.watch(() => props.focusTrapEl, (focusTrapEl) => {
if (focusTrapEl) {
forwardRef.value = focusTrapEl;
}
}, { immediate: true });
vue.watch([forwardRef], ([forwardRef2], [oldForwardRef]) => {
if (forwardRef2) {
forwardRef2.addEventListener("keydown", onKeydown);
forwardRef2.addEventListener("focusin", onFocusIn);
forwardRef2.addEventListener("focusout", onFocusOut);
}
if (oldForwardRef) {
oldForwardRef.removeEventListener("keydown", onKeydown);
oldForwardRef.removeEventListener("focusin", onFocusIn);
oldForwardRef.removeEventListener("focusout", onFocusOut);
}
});
const trapOnFocus = (e) => {
emit(tokens.ON_TRAP_FOCUS_EVT, e);
};
const releaseOnFocus = (e) => emit(tokens.ON_RELEASE_FOCUS_EVT, e);
const onFocusIn = (e) => {
const trapContainer = vue.unref(forwardRef);
if (!trapContainer)
return;
const target = e.target;
const relatedTarget = e.relatedTarget;
const isFocusedInTrap = target && trapContainer.contains(target);
if (!props.trapped) {
const isPrevFocusedInTrap = relatedTarget && trapContainer.contains(relatedTarget);
if (!isPrevFocusedInTrap) {
lastFocusBeforeTrapped = relatedTarget;
}
}
if (isFocusedInTrap)
emit("focusin", e);
if (focusLayer.paused)
return;
if (props.trapped) {
if (isFocusedInTrap) {
lastFocusAfterTrapped = target;
} else {
utils.tryFocus(lastFocusAfterTrapped, true);
}
}
};
const onFocusOut = (e) => {
const trapContainer = vue.unref(forwardRef);
if (focusLayer.paused || !trapContainer)
return;
if (props.trapped) {
const relatedTarget = e.relatedTarget;
if (!lodashUnified.isNil(relatedTarget) && !trapContainer.contains(relatedTarget)) {
setTimeout(() => {
if (!focusLayer.paused && props.trapped) {
const focusoutPreventedEvent = utils.createFocusOutPreventedEvent({
focusReason: focusReason.value
});
emit("focusout-prevented", focusoutPreventedEvent);
if (!focusoutPreventedEvent.defaultPrevented) {
utils.tryFocus(lastFocusAfterTrapped, true);
}
}
}, 0);
}
} else {
const target = e.target;
const isFocusedInTrap = target && trapContainer.contains(target);
if (!isFocusedInTrap)
emit("focusout", e);
}
};
async function startTrap() {
await vue.nextTick();
const trapContainer = vue.unref(forwardRef);
if (trapContainer) {
utils.focusableStack.push(focusLayer);
const prevFocusedElement = trapContainer.contains(document.activeElement) ? lastFocusBeforeTrapped : document.activeElement;
lastFocusBeforeTrapped = prevFocusedElement;
const isPrevFocusContained = trapContainer.contains(prevFocusedElement);
if (!isPrevFocusContained) {
const focusEvent = new Event(tokens.FOCUS_AFTER_TRAPPED, tokens.FOCUS_AFTER_TRAPPED_OPTS);
trapContainer.addEventListener(tokens.FOCUS_AFTER_TRAPPED, trapOnFocus);
trapContainer.dispatchEvent(focusEvent);
if (!focusEvent.defaultPrevented) {
vue.nextTick(() => {
let focusStartEl = props.focusStartEl;
if (!shared.isString(focusStartEl)) {
utils.tryFocus(focusStartEl);
if (document.activeElement !== focusStartEl) {
focusStartEl = "first";
}
}
if (focusStartEl === "first") {
utils.focusFirstDescendant(utils.obtainAllFocusableElements(trapContainer), true);
}
if (document.activeElement === prevFocusedElement || focusStartEl === "container") {
utils.tryFocus(trapContainer);
}
});
}
}
}
}
function stopTrap() {
const trapContainer = vue.unref(forwardRef);
if (trapContainer) {
trapContainer.removeEventListener(tokens.FOCUS_AFTER_TRAPPED, trapOnFocus);
const releasedEvent = new CustomEvent(tokens.FOCUS_AFTER_RELEASED, {
...tokens.FOCUS_AFTER_TRAPPED_OPTS,
detail: {
focusReason: focusReason.value
}
});
trapContainer.addEventListener(tokens.FOCUS_AFTER_RELEASED, releaseOnFocus);
trapContainer.dispatchEvent(releasedEvent);
if (!releasedEvent.defaultPrevented && (focusReason.value == "keyboard" || !utils.isFocusCausedByUserEvent() || trapContainer.contains(document.activeElement))) {
utils.tryFocus(lastFocusBeforeTrapped != null ? lastFocusBeforeTrapped : document.body);
}
trapContainer.removeEventListener(tokens.FOCUS_AFTER_RELEASED, releaseOnFocus);
utils.focusableStack.remove(focusLayer);
lastFocusBeforeTrapped = null;
lastFocusAfterTrapped = null;
}
}
vue.onMounted(() => {
if (props.trapped) {
startTrap();
}
vue.watch(() => props.trapped, (trapped) => {
if (trapped) {
startTrap();
} else {
stopTrap();
}
});
});
vue.onBeforeUnmount(() => {
if (props.trapped) {
stopTrap();
}
if (forwardRef.value) {
forwardRef.value.removeEventListener("keydown", onKeydown);
forwardRef.value.removeEventListener("focusin", onFocusIn);
forwardRef.value.removeEventListener("focusout", onFocusOut);
forwardRef.value = void 0;
}
});
return {
onKeydown
};
}
});
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return vue.renderSlot(_ctx.$slots, "default", { handleKeydown: _ctx.onKeydown });
}
var ElFocusTrap = /* @__PURE__ */ pluginVue_exportHelper["default"](_sfc_main, [["render", _sfc_render], ["__file", "focus-trap.vue"]]);
exports["default"] = ElFocusTrap;
//# sourceMappingURL=focus-trap.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,32 @@
import type { PropType } from 'vue';
declare const _default: import("vue").DefineComponent<{
loop: BooleanConstructor;
trapped: BooleanConstructor;
focusTrapEl: PropType<HTMLElement>;
focusStartEl: {
type: PropType<"container" | "first" | HTMLElement>;
default: string;
};
}, {
onKeydown: (e: KeyboardEvent) => void;
}, unknown, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, ("focusin" | "focusout" | "focusAfterTrapped" | "focusAfterReleased" | "focusout-prevented" | "release-requested")[], "focusin" | "focusout" | "focusAfterTrapped" | "focusAfterReleased" | "focusout-prevented" | "release-requested", import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps, Readonly<import("vue").ExtractPropTypes<{
loop: BooleanConstructor;
trapped: BooleanConstructor;
focusTrapEl: PropType<HTMLElement>;
focusStartEl: {
type: PropType<"container" | "first" | HTMLElement>;
default: string;
};
}>> & {
onFocusin?: ((...args: any[]) => any) | undefined;
onFocusout?: ((...args: any[]) => any) | undefined;
onFocusAfterTrapped?: ((...args: any[]) => any) | undefined;
onFocusAfterReleased?: ((...args: any[]) => any) | undefined;
"onFocusout-prevented"?: ((...args: any[]) => any) | undefined;
"onRelease-requested"?: ((...args: any[]) => any) | undefined;
}, {
loop: boolean;
trapped: boolean;
focusStartEl: HTMLElement | "first" | "container";
}>;
export default _default;

View File

@@ -0,0 +1,13 @@
import type { InjectionKey, Ref } from 'vue';
export declare const FOCUS_AFTER_TRAPPED = "focus-trap.focus-after-trapped";
export declare const FOCUS_AFTER_RELEASED = "focus-trap.focus-after-released";
export declare const FOCUSOUT_PREVENTED = "focus-trap.focusout-prevented";
export declare const FOCUS_AFTER_TRAPPED_OPTS: EventInit;
export declare const FOCUSOUT_PREVENTED_OPTS: EventInit;
export declare const ON_TRAP_FOCUS_EVT = "focusAfterTrapped";
export declare const ON_RELEASE_FOCUS_EVT = "focusAfterReleased";
export type FocusTrapInjectionContext = {
focusTrapRef: Ref<HTMLElement | undefined>;
onKeydown: (e: KeyboardEvent) => void;
};
export declare const FOCUS_TRAP_INJECTION_KEY: InjectionKey<FocusTrapInjectionContext>;

View File

@@ -0,0 +1,28 @@
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
const FOCUS_AFTER_TRAPPED = "focus-trap.focus-after-trapped";
const FOCUS_AFTER_RELEASED = "focus-trap.focus-after-released";
const FOCUSOUT_PREVENTED = "focus-trap.focusout-prevented";
const FOCUS_AFTER_TRAPPED_OPTS = {
cancelable: true,
bubbles: false
};
const FOCUSOUT_PREVENTED_OPTS = {
cancelable: true,
bubbles: false
};
const ON_TRAP_FOCUS_EVT = "focusAfterTrapped";
const ON_RELEASE_FOCUS_EVT = "focusAfterReleased";
const FOCUS_TRAP_INJECTION_KEY = Symbol("elFocusTrap");
exports.FOCUSOUT_PREVENTED = FOCUSOUT_PREVENTED;
exports.FOCUSOUT_PREVENTED_OPTS = FOCUSOUT_PREVENTED_OPTS;
exports.FOCUS_AFTER_RELEASED = FOCUS_AFTER_RELEASED;
exports.FOCUS_AFTER_TRAPPED = FOCUS_AFTER_TRAPPED;
exports.FOCUS_AFTER_TRAPPED_OPTS = FOCUS_AFTER_TRAPPED_OPTS;
exports.FOCUS_TRAP_INJECTION_KEY = FOCUS_TRAP_INJECTION_KEY;
exports.ON_RELEASE_FOCUS_EVT = ON_RELEASE_FOCUS_EVT;
exports.ON_TRAP_FOCUS_EVT = ON_TRAP_FOCUS_EVT;
//# sourceMappingURL=tokens.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"tokens.js","sources":["../../../../../../packages/components/focus-trap/src/tokens.ts"],"sourcesContent":["import type { InjectionKey, Ref } from 'vue'\n\nexport const FOCUS_AFTER_TRAPPED = 'focus-trap.focus-after-trapped'\nexport const FOCUS_AFTER_RELEASED = 'focus-trap.focus-after-released'\nexport const FOCUSOUT_PREVENTED = 'focus-trap.focusout-prevented'\nexport const FOCUS_AFTER_TRAPPED_OPTS: EventInit = {\n cancelable: true,\n bubbles: false,\n}\nexport const FOCUSOUT_PREVENTED_OPTS: EventInit = {\n cancelable: true,\n bubbles: false,\n}\n\nexport const ON_TRAP_FOCUS_EVT = 'focusAfterTrapped'\nexport const ON_RELEASE_FOCUS_EVT = 'focusAfterReleased'\n\nexport type FocusTrapInjectionContext = {\n focusTrapRef: Ref<HTMLElement | undefined>\n onKeydown: (e: KeyboardEvent) => void\n}\n\nexport const FOCUS_TRAP_INJECTION_KEY: InjectionKey<FocusTrapInjectionContext> =\n Symbol('elFocusTrap')\n"],"names":[],"mappings":";;;;AAAY,MAAC,mBAAmB,GAAG,iCAAiC;AACxD,MAAC,oBAAoB,GAAG,kCAAkC;AAC1D,MAAC,kBAAkB,GAAG,gCAAgC;AACtD,MAAC,wBAAwB,GAAG;AACxC,EAAE,UAAU,EAAE,IAAI;AAClB,EAAE,OAAO,EAAE,KAAK;AAChB,EAAE;AACU,MAAC,uBAAuB,GAAG;AACvC,EAAE,UAAU,EAAE,IAAI;AAClB,EAAE,OAAO,EAAE,KAAK;AAChB,EAAE;AACU,MAAC,iBAAiB,GAAG,oBAAoB;AACzC,MAAC,oBAAoB,GAAG,qBAAqB;AAC7C,MAAC,wBAAwB,GAAG,MAAM,CAAC,aAAa;;;;;;;;;;;"}

View File

@@ -0,0 +1,29 @@
declare const focusReason: import("vue").Ref<"pointer" | "keyboard" | undefined>;
declare const lastUserFocusTimestamp: import("vue").Ref<number>;
declare const lastAutomatedFocusTimestamp: import("vue").Ref<number>;
export type FocusLayer = {
paused: boolean;
pause: () => void;
resume: () => void;
};
export type FocusStack = FocusLayer[];
export declare const obtainAllFocusableElements: (element: HTMLElement) => HTMLElement[];
export declare const getVisibleElement: (elements: HTMLElement[], container: HTMLElement) => HTMLElement | undefined;
export declare const isHidden: (element: HTMLElement, container: HTMLElement) => boolean;
export declare const getEdges: (container: HTMLElement) => (HTMLElement | undefined)[];
export declare const tryFocus: (element?: HTMLElement | {
focus: () => void;
} | null, shouldSelect?: boolean) => void;
export declare const focusFirstDescendant: (elements: HTMLElement[], shouldSelect?: boolean) => void;
export declare const focusableStack: {
push: (layer: FocusLayer) => void;
remove: (layer: FocusLayer) => void;
};
export declare const isFocusCausedByUserEvent: () => boolean;
export declare const useFocusReason: () => {
focusReason: typeof focusReason;
lastUserFocusTimestamp: typeof lastUserFocusTimestamp;
lastAutomatedFocusTimestamp: typeof lastAutomatedFocusTimestamp;
};
export declare const createFocusOutPreventedEvent: (detail: CustomEventInit["detail"]) => CustomEvent<any>;
export {};

View File

@@ -0,0 +1,152 @@
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
var vue = require('vue');
var tokens = require('./tokens.js');
var aria = require('../../../utils/dom/aria.js');
const focusReason = vue.ref();
const lastUserFocusTimestamp = vue.ref(0);
const lastAutomatedFocusTimestamp = vue.ref(0);
let focusReasonUserCount = 0;
const obtainAllFocusableElements = (element) => {
const nodes = [];
const walker = document.createTreeWalker(element, NodeFilter.SHOW_ELEMENT, {
acceptNode: (node) => {
const isHiddenInput = node.tagName === "INPUT" && node.type === "hidden";
if (node.disabled || node.hidden || isHiddenInput)
return NodeFilter.FILTER_SKIP;
return node.tabIndex >= 0 || node === document.activeElement ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP;
}
});
while (walker.nextNode())
nodes.push(walker.currentNode);
return nodes;
};
const getVisibleElement = (elements, container) => {
for (const element of elements) {
if (!isHidden(element, container))
return element;
}
};
const isHidden = (element, container) => {
if (getComputedStyle(element).visibility === "hidden")
return true;
while (element) {
if (container && element === container)
return false;
if (getComputedStyle(element).display === "none")
return true;
element = element.parentElement;
}
return false;
};
const getEdges = (container) => {
const focusable = obtainAllFocusableElements(container);
const first = getVisibleElement(focusable, container);
const last = getVisibleElement(focusable.reverse(), container);
return [first, last];
};
const isSelectable = (element) => {
return element instanceof HTMLInputElement && "select" in element;
};
const tryFocus = (element, shouldSelect) => {
if (element) {
const prevFocusedElement = document.activeElement;
aria.focusElement(element, { preventScroll: true });
lastAutomatedFocusTimestamp.value = window.performance.now();
if (element !== prevFocusedElement && isSelectable(element) && shouldSelect) {
element.select();
}
}
};
function removeFromStack(list, item) {
const copy = [...list];
const idx = list.indexOf(item);
if (idx !== -1) {
copy.splice(idx, 1);
}
return copy;
}
const createFocusableStack = () => {
let stack = [];
const push = (layer) => {
const currentLayer = stack[0];
if (currentLayer && layer !== currentLayer) {
currentLayer.pause();
}
stack = removeFromStack(stack, layer);
stack.unshift(layer);
};
const remove = (layer) => {
var _a, _b;
stack = removeFromStack(stack, layer);
(_b = (_a = stack[0]) == null ? void 0 : _a.resume) == null ? void 0 : _b.call(_a);
};
return {
push,
remove
};
};
const focusFirstDescendant = (elements, shouldSelect = false) => {
const prevFocusedElement = document.activeElement;
for (const element of elements) {
tryFocus(element, shouldSelect);
if (document.activeElement !== prevFocusedElement)
return;
}
};
const focusableStack = createFocusableStack();
const isFocusCausedByUserEvent = () => {
return lastUserFocusTimestamp.value > lastAutomatedFocusTimestamp.value;
};
const notifyFocusReasonPointer = () => {
focusReason.value = "pointer";
lastUserFocusTimestamp.value = window.performance.now();
};
const notifyFocusReasonKeydown = () => {
focusReason.value = "keyboard";
lastUserFocusTimestamp.value = window.performance.now();
};
const useFocusReason = () => {
vue.onMounted(() => {
if (focusReasonUserCount === 0) {
document.addEventListener("mousedown", notifyFocusReasonPointer);
document.addEventListener("touchstart", notifyFocusReasonPointer);
document.addEventListener("keydown", notifyFocusReasonKeydown);
}
focusReasonUserCount++;
});
vue.onBeforeUnmount(() => {
focusReasonUserCount--;
if (focusReasonUserCount <= 0) {
document.removeEventListener("mousedown", notifyFocusReasonPointer);
document.removeEventListener("touchstart", notifyFocusReasonPointer);
document.removeEventListener("keydown", notifyFocusReasonKeydown);
}
});
return {
focusReason,
lastUserFocusTimestamp,
lastAutomatedFocusTimestamp
};
};
const createFocusOutPreventedEvent = (detail) => {
return new CustomEvent(tokens.FOCUSOUT_PREVENTED, {
...tokens.FOCUSOUT_PREVENTED_OPTS,
detail
});
};
exports.createFocusOutPreventedEvent = createFocusOutPreventedEvent;
exports.focusFirstDescendant = focusFirstDescendant;
exports.focusableStack = focusableStack;
exports.getEdges = getEdges;
exports.getVisibleElement = getVisibleElement;
exports.isFocusCausedByUserEvent = isFocusCausedByUserEvent;
exports.isHidden = isHidden;
exports.obtainAllFocusableElements = obtainAllFocusableElements;
exports.tryFocus = tryFocus;
exports.useFocusReason = useFocusReason;
//# sourceMappingURL=utils.js.map

File diff suppressed because one or more lines are too long