UNPKG

@corvu/dialog

Version:

Unstyled, accessible and customizable UI primitives for SolidJS

545 lines (540 loc) 19.2 kB
import { createComponent, mergeProps, Portal } from 'solid-js/web'; import { combineStyle, callEventHandler } from '@corvu/utils/dom'; import { createContext, useContext, splitProps, createMemo, Show, createEffect, onCleanup, mergeProps as mergeProps$1, createUniqueId, createSignal, untrack } from 'solid-js'; import { DynamicButton, Dynamic } from '@corvu/utils/dynamic'; import { useKeyedContext, createKeyedContext } from '@corvu/utils/create/keyedContext'; import { mergeRefs, access, some } from '@corvu/utils/reactivity'; import { dataIf, isFunction } from '@corvu/utils'; import Dismissible from 'solid-dismissible'; import createControllableSignal from '@corvu/utils/create/controllableSignal'; import createFocusTrap from 'solid-focus-trap'; import createOnce from '@corvu/utils/create/once'; import createPresence from 'solid-presence'; import createPreventScroll from 'solid-prevent-scroll'; import createRegister from '@corvu/utils/create/register'; // src/Close.tsx var DialogContext = createContext(); var createDialogContext = (contextId) => { if (contextId === undefined) return DialogContext; const context = createKeyedContext(`dialog-${contextId}`); return context; }; var useDialogContext = (contextId) => { if (contextId === undefined) { const context2 = useContext(DialogContext); if (!context2) { throw new Error( "[corvu]: Dialog context not found. Make sure to wrap Dialog components in <Dialog.Root>" ); } return context2; } const context = useKeyedContext(`dialog-${contextId}`); if (!context) { throw new Error( `[corvu]: Dialog context with id "${contextId}" not found. Make sure to wrap Dialog components in <Dialog.Root contextId="${contextId}">` ); } return context; }; var InternalDialogContext = createContext(); var createInternalDialogContext = (contextId) => { if (contextId === undefined) return InternalDialogContext; const context = createKeyedContext( `dialog-internal-${contextId}` ); return context; }; var useInternalDialogContext = (contextId) => { if (contextId === undefined) { const context2 = useContext(InternalDialogContext); if (!context2) { throw new Error( "[corvu]: Dialog context not found. Make sure to wrap Dialog components in <Dialog.Root>" ); } return context2; } const context = useKeyedContext( `dialog-internal-${contextId}` ); if (!context) { throw new Error( `[corvu]: Dialog context with id "${contextId}" not found. Make sure to wrap Dialog components in <Dialog.Root contextId="${contextId}">` ); } return context; }; // src/Close.tsx var DialogClose = (props) => { const [localProps, otherProps] = splitProps(props, ["contextId", "onClick"]); const context = createMemo(() => useInternalDialogContext(localProps.contextId)); const onClick = (event) => { !callEventHandler(localProps.onClick, event) && context().setOpen(false); }; return createComponent(DynamicButton, mergeProps({ onClick, "aria-label": "close", "data-corvu-dialog-close": "" }, otherProps)); }; var Close_default = DialogClose; var DialogContent = (props) => { const [localProps, otherProps] = splitProps(props, ["forceMount", "contextId", "ref", "style"]); const context = createMemo(() => useInternalDialogContext(localProps.contextId)); const show = () => some(context().open, () => localProps.forceMount, context().contentPresent); const enableDismissible = createMemo(() => context().open() || context().contentPresent()); return createComponent(Dismissible, { get element() { return context().contentRef; }, get enabled() { return enableDismissible(); }, get dismissibleId() { return context().dialogId(); }, onDismiss: () => context().setOpen(false), get dismissOnEscapeKeyDown() { return context().closeOnEscapeKeyDown; }, get dismissOnOutsideFocus() { return context().closeOnOutsideFocus; }, get dismissOnOutsidePointer() { return context().closeOnOutsidePointer; }, get outsidePointerStrategy() { return context().closeOnOutsidePointerStrategy; }, outsidePointerIgnore: () => [context().triggerRef()], get noOutsidePointerEvents() { return context().noOutsidePointerEvents; }, get onEscapeKeyDown() { return context().onEscapeKeyDown; }, get onOutsideFocus() { return context().onOutsideFocus; }, get onOutsidePointer() { return context().onOutsidePointer; }, children: (props2) => createComponent(Show, { get when() { return show(); }, get children() { return createComponent(Dynamic, mergeProps({ as: "div", ref(r$) { var _ref$ = mergeRefs(context().setContentRef, localProps.ref); typeof _ref$ === "function" && _ref$(r$); }, get style() { return combineStyle({ "pointer-events": props2.isLastLayer ? "auto" : undefined }, localProps.style); }, get id() { return context().dialogId(); }, get role() { return context().role(); }, tabIndex: "-1", get ["aria-describedby"]() { return context().descriptionId(); }, get ["aria-labelledby"]() { return context().labelId(); }, get ["aria-modal"]() { return context().modal() ? "true" : "false"; }, get ["data-closed"]() { return dataIf(!context().open()); }, get ["data-open"]() { return dataIf(context().open()); }, "data-corvu-dialog-content": "" }, otherProps)); } }) }); }; var Content_default = DialogContent; var DialogDescription = (props) => { const [localProps, otherProps] = splitProps(props, ["contextId"]); const context = createMemo(() => useInternalDialogContext(localProps.contextId)); createEffect(() => { const _context = context(); _context.registerDescriptionId(); onCleanup(() => _context.unregisterDescriptionId()); }); return createComponent(Dynamic, mergeProps({ as: "p", get id() { return context().descriptionId(); }, "data-corvu-dialog-description": "" }, otherProps)); }; var Description_default = DialogDescription; var DialogLabel = (props) => { const [localProps, otherProps] = splitProps(props, ["contextId"]); const context = createMemo(() => useInternalDialogContext(localProps.contextId)); createEffect(() => { const _context = context(); _context.registerLabelId(); onCleanup(() => _context.unregisterLabelId()); }); return createComponent(Dynamic, mergeProps({ as: "h2", get id() { return context().labelId(); }, "data-corvu-dialog-label": "" }, otherProps)); }; var Label_default = DialogLabel; var DialogOverlay = (props) => { const [localProps, otherProps] = splitProps(props, ["forceMount", "contextId", "ref", "style"]); const context = createMemo(() => useInternalDialogContext(localProps.contextId)); const show = () => some(context().open, () => localProps.forceMount, context().overlayPresent); return createComponent(Show, { get when() { return show(); }, get children() { return createComponent(Dynamic, mergeProps({ as: "div", ref(r$) { var _ref$ = mergeRefs(context().setOverlayRef, localProps.ref); typeof _ref$ === "function" && _ref$(r$); }, get style() { return combineStyle({ "pointer-events": "auto" }, localProps.style); }, "aria-hidden": "true", get ["data-closed"]() { return dataIf(!context().open()); }, get ["data-open"]() { return dataIf(context().open()); }, "data-corvu-dialog-overlay": "" }, otherProps)); } }); }; var Overlay_default = DialogOverlay; var DialogPortal = (props) => { const [localProps, otherProps] = splitProps(props, ["forceMount", "contextId"]); const context = createMemo(() => useInternalDialogContext(localProps.contextId)); const show = () => some(context().open, () => localProps.forceMount, context().contentPresent, context().overlayPresent); return createComponent(Show, { get when() { return show(); }, get children() { return createComponent(Portal, otherProps); } }); }; var Portal_default = DialogPortal; var DEFAULT_MODAL = true; var DialogRoot = (props) => { const defaultedProps = mergeProps$1({ role: "dialog", initialOpen: false, modal: true, closeOnEscapeKeyDown: true, closeOnOutsideFocus: () => props.trapFocus ?? true, closeOnOutsidePointer: () => props.modal ?? DEFAULT_MODAL, closeOnOutsidePointerStrategy: "pointerup", noOutsidePointerEvents: () => props.modal ?? DEFAULT_MODAL, preventScroll: () => props.modal ?? DEFAULT_MODAL, hideScrollbar: true, preventScrollbarShift: true, preventScrollbarShiftMode: "padding", restoreScrollPosition: true, allowPinchZoom: true, trapFocus: true, restoreFocus: true, dialogId: createUniqueId() }, props); const [open, setOpen] = createControllableSignal({ value: () => defaultedProps.open, initialValue: defaultedProps.initialOpen, onChange: defaultedProps.onOpenChange }); const [labelId, registerLabelId, unregisterLabelId] = createRegister({ value: () => defaultedProps.labelId ?? createUniqueId() }); const [descriptionId, registerDescriptionId, unregisterDescriptionId] = createRegister({ value: () => defaultedProps.descriptionId ?? createUniqueId() }); const [triggerRef, setTriggerRef] = createSignal(null); const [contentRef, setContentRef] = createSignal(null); const [overlayRef, setOverlayRef] = createSignal(null); const { present: contentPresent } = createPresence({ show: open, element: contentRef, onStateChange: (state) => { if (state === "present") { defaultedProps.onContentPresentChange?.(true); } else if (state === "hidden") { defaultedProps.onContentPresentChange?.(false); } } }); const { present: overlayPresent } = createPresence({ show: open, element: overlayRef, onStateChange: (state) => { if (state === "present") { defaultedProps.onOverlayPresentChange?.(true); } else if (state === "hidden") { defaultedProps.onOverlayPresentChange?.(false); } } }); createFocusTrap({ element: contentRef, enabled: () => open() && defaultedProps.trapFocus, initialFocusElement: () => defaultedProps.initialFocusEl ?? null, restoreFocus: () => defaultedProps.restoreFocus, finalFocusElement: () => defaultedProps.finalFocusEl ?? null, onFinalFocus: defaultedProps.onFinalFocus, onInitialFocus: defaultedProps.onInitialFocus }); createPreventScroll({ element: contentRef, enabled: () => contentPresent() && access(defaultedProps.preventScroll), hideScrollbar: () => defaultedProps.hideScrollbar, preventScrollbarShift: () => defaultedProps.preventScrollbarShift, preventScrollbarShiftMode: () => defaultedProps.preventScrollbarShiftMode, restoreScrollPosition: () => defaultedProps.restoreScrollPosition, allowPinchZoom: () => defaultedProps.allowPinchZoom }); const childrenProps = { get role() { return defaultedProps.role; }, get open() { return open(); }, setOpen, get modal() { return defaultedProps.modal; }, get closeOnEscapeKeyDown() { return defaultedProps.closeOnEscapeKeyDown; }, get closeOnOutsideFocus() { return access(defaultedProps.closeOnOutsideFocus); }, get closeOnOutsidePointer() { return access(defaultedProps.closeOnOutsidePointer); }, get closeOnOutsidePointerStrategy() { return defaultedProps.closeOnOutsidePointerStrategy; }, get noOutsidePointerEvents() { return access(defaultedProps.noOutsidePointerEvents); }, get preventScroll() { return access(defaultedProps.preventScroll); }, get hideScrollbar() { return defaultedProps.hideScrollbar; }, get preventScrollbarShift() { return access(defaultedProps.preventScrollbarShift); }, get preventScrollbarShiftMode() { return defaultedProps.preventScrollbarShiftMode; }, get restoreScrollPosition() { return defaultedProps.restoreScrollPosition; }, get allowPinchZoom() { return defaultedProps.allowPinchZoom; }, get trapFocus() { return defaultedProps.trapFocus; }, get restoreFocus() { return defaultedProps.restoreFocus; }, get initialFocusEl() { return defaultedProps.initialFocusEl; }, get finalFocusEl() { return defaultedProps.finalFocusEl; }, get contentPresent() { return contentPresent(); }, get contentRef() { return contentRef(); }, get overlayPresent() { return overlayPresent(); }, get overlayRef() { return overlayRef(); }, get dialogId() { return defaultedProps.dialogId; }, get labelId() { return labelId(); }, get descriptionId() { return descriptionId(); } }; const memoizedChildren = createOnce(() => defaultedProps.children); const resolveChildren = () => { const children = memoizedChildren()(); if (isFunction(children)) { return children(childrenProps); } return children; }; const memoizedDialogRoot = createMemo(() => { const DialogContext2 = createDialogContext(defaultedProps.contextId); const InternalDialogContext2 = createInternalDialogContext(defaultedProps.contextId); return createComponent(DialogContext2.Provider, { value: { role: () => defaultedProps.role, open, setOpen, modal: () => defaultedProps.modal, closeOnEscapeKeyDown: () => defaultedProps.closeOnEscapeKeyDown, closeOnOutsideFocus: () => access(defaultedProps.closeOnOutsideFocus), closeOnOutsidePointer: () => access(defaultedProps.closeOnOutsidePointer), closeOnOutsidePointerStrategy: () => defaultedProps.closeOnOutsidePointerStrategy, noOutsidePointerEvents: () => access(defaultedProps.noOutsidePointerEvents), preventScroll: () => access(defaultedProps.preventScroll), hideScrollbar: () => defaultedProps.hideScrollbar, preventScrollbarShift: () => access(defaultedProps.preventScrollbarShift), preventScrollbarShiftMode: () => defaultedProps.preventScrollbarShiftMode, restoreScrollPosition: () => defaultedProps.restoreScrollPosition, allowPinchZoom: () => defaultedProps.allowPinchZoom, trapFocus: () => defaultedProps.trapFocus, restoreFocus: () => defaultedProps.restoreFocus, initialFocusEl: () => defaultedProps.initialFocusEl, finalFocusEl: () => defaultedProps.finalFocusEl, contentPresent, contentRef, overlayPresent, overlayRef, dialogId: () => defaultedProps.dialogId, labelId, descriptionId }, get children() { return createComponent(InternalDialogContext2.Provider, { get value() { return { role: () => defaultedProps.role, open, setOpen, modal: () => defaultedProps.modal, closeOnEscapeKeyDown: () => defaultedProps.closeOnEscapeKeyDown, onEscapeKeyDown: defaultedProps.onEscapeKeyDown, closeOnOutsideFocus: () => access(defaultedProps.closeOnOutsideFocus), closeOnOutsidePointer: () => access(defaultedProps.closeOnOutsidePointer), closeOnOutsidePointerStrategy: () => defaultedProps.closeOnOutsidePointerStrategy, onOutsideFocus: defaultedProps.onOutsideFocus, onOutsidePointer: defaultedProps.onOutsidePointer, noOutsidePointerEvents: () => access(defaultedProps.noOutsidePointerEvents), preventScroll: () => access(defaultedProps.preventScroll), hideScrollbar: () => defaultedProps.hideScrollbar, preventScrollbarShift: () => access(defaultedProps.preventScrollbarShift), preventScrollbarShiftMode: () => defaultedProps.preventScrollbarShiftMode, restoreScrollPosition: () => defaultedProps.restoreScrollPosition, allowPinchZoom: () => defaultedProps.allowPinchZoom, trapFocus: () => defaultedProps.trapFocus, restoreFocus: () => defaultedProps.restoreFocus, initialFocusEl: () => defaultedProps.initialFocusEl, finalFocusEl: () => defaultedProps.finalFocusEl, contentPresent, contentRef, overlayPresent, overlayRef, dialogId: () => defaultedProps.dialogId, labelId, registerLabelId, unregisterLabelId, descriptionId, registerDescriptionId, unregisterDescriptionId, setContentRef, setOverlayRef, triggerRef, setTriggerRef }; }, get children() { return untrack(() => resolveChildren()); } }); } }); }); return memoizedDialogRoot; }; var Root_default = DialogRoot; var DialogTrigger = (props) => { const [localProps, otherProps] = splitProps(props, ["contextId", "ref", "onClick"]); const context = createMemo(() => useInternalDialogContext(localProps.contextId)); const onClick = (e) => { !callEventHandler(localProps.onClick, e) && context().setOpen((open) => !open); }; return createComponent(DynamicButton, mergeProps({ ref(r$) { var _ref$ = mergeRefs(context().setTriggerRef, localProps.ref); typeof _ref$ === "function" && _ref$(r$); }, onClick, get ["aria-controls"]() { return context().dialogId(); }, get ["aria-expanded"]() { return context().open() ? "true" : "false"; }, "aria-haspopup": "dialog", get ["data-closed"]() { return dataIf(!context().open()); }, get ["data-open"]() { return dataIf(context().open()); }, "data-corvu-dialog-trigger": "" }, otherProps)); }; var Trigger_default = DialogTrigger; // src/index.ts var Dialog = Object.assign(Root_default, { Trigger: Trigger_default, Portal: Portal_default, Overlay: Overlay_default, Content: Content_default, Label: Label_default, Description: Description_default, Close: Close_default, useContext: useDialogContext }); var index_default = Dialog; export { Close_default as Close, Content_default as Content, Description_default as Description, Label_default as Label, Overlay_default as Overlay, Portal_default as Portal, Root_default as Root, Trigger_default as Trigger, index_default as default, useDialogContext as useContext };