@grail-ui/svelte
Version:
[](https://www.npmjs.com/package/@grail-ui/svelte) [](https://bundlephobia.com/package/@grail-ui/svelte) [ • 3.43 kB
JavaScript
import { derived, get, readable, writable } from 'svelte/store';
import { tick } from 'svelte';
import { uniqueId } from '../util/id.js';
import { createFocusTrap } from '../focusTrap/focusTrap.js';
import { usePortal } from '../portal/portal.js';
import { ESCAPE } from '../util/keyboard.js';
import { useClickOutside } from '../clickOutside/clickOutside.js';
import { writableEffect } from '../util/store.js';
import { addEventListener } from '../eventListener/eventListener.js';
export const createModal = ({ open = true, portal = 'body', onOpenChange, dismissible = false, keyboardDismissible = true, onInteractOutside, onKeyboardDismiss, initialFocus, } = {}) => {
const id = uniqueId('modal');
const titleId = uniqueId('modal-title');
const open$ = writableEffect(open, onOpenChange);
const dismissible$ = writable(dismissible);
const keyboardDismissible$ = writable(keyboardDismissible);
const modalAttrs = readable({
id,
role: 'dialog',
tabindex: '-1',
'aria-modal': 'true',
'aria-labelledby': titleId,
});
const useModal = (node) => {
const contentEl = node.id === id ? node : node.querySelector(`#${id}`);
const removeEvent = addEventListener(contentEl, `keydown`, (e) => {
if (e.key === ESCAPE && get(keyboardDismissible$)) {
const shouldClose = onKeyboardDismiss?.(e);
if (shouldClose !== false && !e.defaultPrevented) {
e.preventDefault();
open$.set(false);
}
}
});
const portalAction = portal ? usePortal(node, { target: portal }) : undefined;
const clickOutsideAction = useClickOutside(contentEl, {
enabled: derived([open$, dismissible$], ([$open, $dismissible]) => $open && $dismissible),
handler: (e) => {
const shouldClose = onInteractOutside?.(e);
if (shouldClose !== false && !e.defaultPrevented) {
open$.set(false);
}
},
});
const { activate, deactivate, useFocusTrap } = createFocusTrap({
immediate: false,
escapeDeactivates: false,
fallbackFocus: contentEl,
...(initialFocus ? { initialFocus } : null),
});
const focusTrapAction = useFocusTrap(contentEl);
const unsubscribe = open$.subscribe(async (value) => {
if (value) {
// Delay activating trap, for DOM structure to settle
await tick();
setTimeout(async () => activate(), 50);
}
else {
deactivate();
}
});
return {
destroy() {
focusTrapAction?.destroy?.();
portalAction?.destroy?.();
clickOutsideAction?.destroy?.();
removeEvent();
unsubscribe();
},
};
};
const titleAttrs = readable({
id: titleId,
});
const triggerAttrs = derived(open$, (open) => ({
'aria-haspopup': 'dialog',
'aria-controls': id,
'aria-expanded': `${open}`,
}));
return {
useModal,
modalAttrs,
titleAttrs,
triggerAttrs,
open: open$,
dismissible: dismissible$,
keyboardDismissible: keyboardDismissible$,
};
};