@kobalte/core
Version:
Unstyled components and primitives for building accessible web apps and design systems with SolidJS.
207 lines (204 loc) • 6.7 kB
JavaScript
import { DATA_TOP_LAYER_ATTR } from './ZKYDDHM6.js';
import { getActiveElement, contains, focusWithoutScrolling, access, removeItemFromArray, getDocument, visuallyHiddenStyles, isFocusable, getAllTabbableIn } from '@kobalte/utils';
import { createSignal, createEffect, onCleanup } from 'solid-js';
import { isServer } from 'solid-js/web';
var AUTOFOCUS_ON_MOUNT_EVENT = "focusScope.autoFocusOnMount";
var AUTOFOCUS_ON_UNMOUNT_EVENT = "focusScope.autoFocusOnUnmount";
var EVENT_OPTIONS = {
bubbles: false,
cancelable: true
};
var focusScopeStack = {
/** A stack of focus scopes, with the active one at the top */
stack: [],
active() {
return this.stack[0];
},
add(scope) {
if (scope !== this.active()) {
this.active()?.pause();
}
this.stack = removeItemFromArray(this.stack, scope);
this.stack.unshift(scope);
},
remove(scope) {
this.stack = removeItemFromArray(this.stack, scope);
this.active()?.resume();
}
};
function createFocusScope(props, ref) {
const [isPaused, setIsPaused] = createSignal(false);
const focusScope = {
pause() {
setIsPaused(true);
},
resume() {
setIsPaused(false);
}
};
let lastFocusedElement = null;
const onMountAutoFocus = (e) => props.onMountAutoFocus?.(e);
const onUnmountAutoFocus = (e) => props.onUnmountAutoFocus?.(e);
const ownerDocument = () => getDocument(ref());
const createSentinel = () => {
const element = ownerDocument().createElement("span");
element.setAttribute("data-focus-trap", "");
element.tabIndex = 0;
Object.assign(element.style, visuallyHiddenStyles);
return element;
};
const tabbables = () => {
const container = ref();
if (!container) {
return [];
}
return getAllTabbableIn(container, true).filter((el) => !el.hasAttribute("data-focus-trap"));
};
const firstTabbable = () => {
const items = tabbables();
return items.length > 0 ? items[0] : null;
};
const lastTabbable = () => {
const items = tabbables();
return items.length > 0 ? items[items.length - 1] : null;
};
const shouldPreventUnmountAutoFocus = () => {
const container = ref();
if (!container) {
return false;
}
const activeElement = getActiveElement(container);
if (!activeElement) {
return false;
}
if (contains(container, activeElement)) {
return false;
}
return isFocusable(activeElement);
};
createEffect(() => {
if (isServer) {
return;
}
const container = ref();
if (!container) {
return;
}
focusScopeStack.add(focusScope);
const previouslyFocusedElement = getActiveElement(container);
const hasFocusedCandidate = contains(container, previouslyFocusedElement);
if (!hasFocusedCandidate) {
const mountEvent = new CustomEvent(AUTOFOCUS_ON_MOUNT_EVENT, EVENT_OPTIONS);
container.addEventListener(AUTOFOCUS_ON_MOUNT_EVENT, onMountAutoFocus);
container.dispatchEvent(mountEvent);
if (!mountEvent.defaultPrevented) {
setTimeout(() => {
focusWithoutScrolling(firstTabbable());
if (getActiveElement(container) === previouslyFocusedElement) {
focusWithoutScrolling(container);
}
}, 0);
}
}
onCleanup(() => {
container.removeEventListener(AUTOFOCUS_ON_MOUNT_EVENT, onMountAutoFocus);
setTimeout(() => {
const unmountEvent = new CustomEvent(AUTOFOCUS_ON_UNMOUNT_EVENT, EVENT_OPTIONS);
if (shouldPreventUnmountAutoFocus()) {
unmountEvent.preventDefault();
}
container.addEventListener(AUTOFOCUS_ON_UNMOUNT_EVENT, onUnmountAutoFocus);
container.dispatchEvent(unmountEvent);
if (!unmountEvent.defaultPrevented) {
focusWithoutScrolling(previouslyFocusedElement ?? ownerDocument().body);
}
container.removeEventListener(AUTOFOCUS_ON_UNMOUNT_EVENT, onUnmountAutoFocus);
focusScopeStack.remove(focusScope);
}, 0);
});
});
createEffect(() => {
if (isServer) {
return;
}
const container = ref();
if (!container || !access(props.trapFocus) || isPaused()) {
return;
}
const onFocusIn = (event) => {
const target = event.target;
if (target?.closest(`[${DATA_TOP_LAYER_ATTR}]`)) {
return;
}
if (contains(container, target)) {
lastFocusedElement = target;
} else {
focusWithoutScrolling(lastFocusedElement);
}
};
const onFocusOut = (event) => {
const relatedTarget = event.relatedTarget;
const target = relatedTarget ?? getActiveElement(container);
if (target?.closest(`[${DATA_TOP_LAYER_ATTR}]`)) {
return;
}
if (!contains(container, target)) {
focusWithoutScrolling(lastFocusedElement);
}
};
ownerDocument().addEventListener("focusin", onFocusIn);
ownerDocument().addEventListener("focusout", onFocusOut);
onCleanup(() => {
ownerDocument().removeEventListener("focusin", onFocusIn);
ownerDocument().removeEventListener("focusout", onFocusOut);
});
});
createEffect(() => {
if (isServer) {
return;
}
const container = ref();
if (!container || !access(props.trapFocus) || isPaused()) {
return;
}
const startSentinel = createSentinel();
container.insertAdjacentElement("afterbegin", startSentinel);
const endSentinel = createSentinel();
container.insertAdjacentElement("beforeend", endSentinel);
function onFocus(event) {
const first = firstTabbable();
const last = lastTabbable();
if (event.relatedTarget === first) {
focusWithoutScrolling(last);
} else {
focusWithoutScrolling(first);
}
}
startSentinel.addEventListener("focusin", onFocus);
endSentinel.addEventListener("focusin", onFocus);
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.previousSibling === endSentinel) {
endSentinel.remove();
container.insertAdjacentElement("beforeend", endSentinel);
}
if (mutation.nextSibling === startSentinel) {
startSentinel.remove();
container.insertAdjacentElement("afterbegin", startSentinel);
}
}
});
observer.observe(container, {
childList: true,
subtree: false
});
onCleanup(() => {
startSentinel.removeEventListener("focusin", onFocus);
endSentinel.removeEventListener("focusin", onFocus);
startSentinel.remove();
endSentinel.remove();
observer.disconnect();
});
});
}
export { createFocusScope };