@corvu/dialog
Version:
Unstyled, accessible and customizable UI primitives for SolidJS
545 lines (540 loc) • 19.2 kB
JavaScript
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 };