@vuesax-alpha/nightly
Version:
A Component Library for Vue 3
254 lines (251 loc) • 9.29 kB
JavaScript
import { defineComponent, ref, provide, watch, unref, nextTick, onMounted, onBeforeUnmount, renderSlot } from 'vue';
import { isNil } from 'lodash-unified';
import '../../../constants/index.mjs';
import '../../../hooks/index.mjs';
import '../../../utils/index.mjs';
import '../../../tokens/index.mjs';
import { focusTrapProps, focusTrapEmits } from './focus-trap.mjs';
import { useFocusReason, getEdges, createFocusOutPreventedEvent, tryFocus, focusableStack, focusFirstDescendant, obtainAllFocusableElements, isFocusCausedByUserEvent } from './utils.mjs';
import _export_sfc from '../../../_virtual/plugin-vue_export-helper.mjs';
import { useEscapeKeydown } from '../../../hooks/use-escape-keydown/index.mjs';
import { EVENT_CODE } from '../../../constants/aria.mjs';
import { focusTrapInjectionKey, onTrapFocusEvent, onReleaseFocusEvent, focusAfterTrapped, focusAfterTrappedOpts, focusAfterReleased } from '../../../tokens/focus-trap.mjs';
import { isString } from '@vue/shared';
const __default__ = defineComponent({
name: "VsFocusTrap",
inheritAttrs: false
});
const _sfc_main = defineComponent({
...__default__,
props: focusTrapProps,
emits: focusTrapEmits,
setup(__props, { emit }) {
const props = __props;
const forwardRef = ref();
let lastFocusBeforeTrapped = null;
let lastFocusAfterTrapped = null;
const { focusReason } = useFocusReason();
useEscapeKeydown((event) => {
if (props.trapped && !focusLayer.paused) {
emit("releaseRequested", 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 { key, altKey, ctrlKey, metaKey, currentTarget, shiftKey } = e;
const { loop } = props;
const isTabbing = key === EVENT_CODE.tab && !altKey && !ctrlKey && !metaKey;
const currentFocusingEl = document.activeElement;
if (isTabbing && currentFocusingEl) {
const container = currentTarget;
const [first, last] = getEdges(container);
const isTabbable = first && last;
if (!isTabbable) {
if (currentFocusingEl === container) {
const focusoutPreventedEvent = createFocusOutPreventedEvent({
focusReason: focusReason.value
});
emit("focusoutPrevented", focusoutPreventedEvent);
if (!focusoutPreventedEvent.defaultPrevented) {
e.preventDefault();
}
}
} else {
if (!shiftKey && currentFocusingEl === last) {
const focusoutPreventedEvent = createFocusOutPreventedEvent({
focusReason: focusReason.value
});
emit("focusoutPrevented", focusoutPreventedEvent);
if (!focusoutPreventedEvent.defaultPrevented) {
e.preventDefault();
if (loop)
tryFocus(first, true);
}
} else if (shiftKey && [first, container].includes(currentFocusingEl)) {
const focusoutPreventedEvent = createFocusOutPreventedEvent({
focusReason: focusReason.value
});
emit("focusoutPrevented", focusoutPreventedEvent);
if (!focusoutPreventedEvent.defaultPrevented) {
e.preventDefault();
if (loop)
tryFocus(last, true);
}
}
}
}
};
provide(focusTrapInjectionKey, {
focusTrapRef: forwardRef,
onKeydown
});
watch(
() => props.focusTrapEl,
(focusTrapEl) => {
if (focusTrapEl) {
forwardRef.value = focusTrapEl;
}
},
{ immediate: true }
);
watch(forwardRef, (forwardRef2, oldForwardRef) => {
if (forwardRef2) {
forwardRef2.addEventListener("keydown", onKeydown);
forwardRef2.addEventListener("focusin", onFocusIn);
forwardRef2.addEventListener("focusout", onFocusOut);
}
if (oldForwardRef instanceof HTMLElement) {
oldForwardRef.removeEventListener("keydown", onKeydown);
oldForwardRef.removeEventListener("focusin", onFocusIn);
oldForwardRef.removeEventListener("focusout", onFocusOut);
}
});
const trapOnFocus = (e) => {
emit(onTrapFocusEvent, e);
};
const releaseOnFocus = (e) => emit(onReleaseFocusEvent, e);
const onFocusIn = (e) => {
const trapContainer = 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 {
tryFocus(lastFocusAfterTrapped, true);
}
}
};
const onFocusOut = (e) => {
const trapContainer = unref(forwardRef);
if (focusLayer.paused || !trapContainer)
return;
if (props.trapped) {
const relatedTarget = e.relatedTarget;
if (!isNil(relatedTarget) && !trapContainer.contains(relatedTarget)) {
setTimeout(() => {
if (!focusLayer.paused && props.trapped) {
const focusoutPreventedEvent = createFocusOutPreventedEvent({
focusReason: focusReason.value
});
emit("focusoutPrevented", focusoutPreventedEvent);
if (!focusoutPreventedEvent.defaultPrevented) {
tryFocus(lastFocusAfterTrapped, true);
}
}
}, 0);
}
} else {
const target = e.target;
const isFocusedInTrap = target && trapContainer.contains(target);
if (!isFocusedInTrap)
emit("focusout", e);
}
};
const startTrap = async () => {
await nextTick();
const trapContainer = unref(forwardRef);
if (trapContainer) {
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(focusAfterTrapped, focusAfterTrappedOpts);
trapContainer.addEventListener(focusAfterTrapped, trapOnFocus);
trapContainer.dispatchEvent(focusEvent);
if (!focusEvent.defaultPrevented) {
nextTick(() => {
let focusStartEl = props.focusStartEl;
if (!isString(focusStartEl)) {
tryFocus(focusStartEl);
if (document.activeElement !== focusStartEl) {
focusStartEl = "first";
}
}
if (focusStartEl === "first") {
focusFirstDescendant(
obtainAllFocusableElements(trapContainer),
true
);
}
if (document.activeElement === prevFocusedElement || focusStartEl === "container") {
tryFocus(trapContainer);
}
});
}
}
}
};
const stopTrap = () => {
const trapContainer = unref(forwardRef);
if (trapContainer) {
trapContainer.removeEventListener(focusAfterTrapped, trapOnFocus);
const releasedEvent = new CustomEvent(focusAfterReleased, {
...focusAfterTrappedOpts,
detail: {
focusReason: focusReason.value
}
});
trapContainer.addEventListener(focusAfterReleased, releaseOnFocus);
trapContainer.dispatchEvent(releasedEvent);
if (!releasedEvent.defaultPrevented && (focusReason.value == "keyboard" || !isFocusCausedByUserEvent())) {
tryFocus(lastFocusBeforeTrapped != null ? lastFocusBeforeTrapped : document.body);
}
trapContainer.removeEventListener(focusAfterReleased, trapOnFocus);
focusableStack.remove(focusLayer);
}
};
onMounted(() => {
if (props.trapped) {
startTrap();
}
watch(
() => props.trapped,
(trapped) => {
if (trapped) {
startTrap();
} else {
stopTrap();
}
}
);
});
onBeforeUnmount(() => {
if (props.trapped) {
stopTrap();
}
});
return (_ctx, _cache) => {
return renderSlot(_ctx.$slots, "default", { handleKeydown: onKeydown });
};
}
});
var FocusTrap = /* @__PURE__ */ _export_sfc(_sfc_main, [["__file", "/home/runner/work/vuesax-alpha/vuesax-alpha/packages/components/focus-trap/src/focus-trap.vue"]]);
export { FocusTrap as default };
//# sourceMappingURL=focus-trap2.mjs.map