@corvu/dialog
Version:
Unstyled, accessible and customizable UI primitives for SolidJS
605 lines (594 loc) • 19.1 kB
JSX
// src/Close.tsx
import { callEventHandler } from "@corvu/utils/dom";
import {
createMemo,
splitProps
} from "solid-js";
import {
DynamicButton
} from "@corvu/utils/dynamic";
// src/context.ts
import { createContext, useContext } from "solid-js";
import {
createKeyedContext,
useKeyedContext
} from "@corvu/utils/create/keyedContext";
var DialogContext = createContext();
var createDialogContext = (contextId) => {
if (contextId === void 0) return DialogContext;
const context = createKeyedContext(`dialog-${contextId}`);
return context;
};
var useDialogContext = (contextId) => {
if (contextId === void 0) {
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 === void 0) return InternalDialogContext;
const context = createKeyedContext(
`dialog-internal-${contextId}`
);
return context;
};
var useInternalDialogContext = (contextId) => {
if (contextId === void 0) {
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 <DynamicButton
onClick={onClick}
aria-label="close"
data-corvu-dialog-close=""
{...otherProps}
/>;
};
var Close_default = DialogClose;
// src/Content.tsx
import { combineStyle } from "@corvu/utils/dom";
import {
createMemo as createMemo2,
Show,
splitProps as splitProps2
} from "solid-js";
import { Dynamic } from "@corvu/utils/dynamic";
import { mergeRefs, some } from "@corvu/utils/reactivity";
import { dataIf } from "@corvu/utils";
import Dismissible from "solid-dismissible";
var DialogContent = (props) => {
const [localProps, otherProps] = splitProps2(props, [
"forceMount",
"contextId",
"ref",
"style"
]);
const context = createMemo2(
() => useInternalDialogContext(localProps.contextId)
);
const show = () => some(context().open, () => localProps.forceMount, context().contentPresent);
const enableDismissible = createMemo2(
() => context().open() || context().contentPresent()
);
return <Dismissible
element={context().contentRef}
enabled={enableDismissible()}
dismissibleId={context().dialogId()}
onDismiss={() => context().setOpen(false)}
dismissOnEscapeKeyDown={context().closeOnEscapeKeyDown}
dismissOnOutsideFocus={context().closeOnOutsideFocus}
dismissOnOutsidePointer={context().closeOnOutsidePointer}
outsidePointerStrategy={context().closeOnOutsidePointerStrategy}
outsidePointerIgnore={() => [context().triggerRef()]}
noOutsidePointerEvents={context().noOutsidePointerEvents}
onEscapeKeyDown={context().onEscapeKeyDown}
onOutsideFocus={context().onOutsideFocus}
onOutsidePointer={context().onOutsidePointer}
>
{(props2) => <Show when={show()}>
<Dynamic
as="div"
ref={mergeRefs(context().setContentRef, localProps.ref)}
style={combineStyle(
{
"pointer-events": props2.isLastLayer ? "auto" : void 0
},
localProps.style
)}
id={context().dialogId()}
role={context().role()}
tabIndex="-1"
aria-describedby={context().descriptionId()}
aria-labelledby={context().labelId()}
aria-modal={context().modal() ? "true" : "false"}
data-closed={dataIf(!context().open())}
data-open={dataIf(context().open())}
data-corvu-dialog-content=""
{...otherProps}
/>
</Show>}
</Dismissible>;
};
var Content_default = DialogContent;
// src/Description.tsx
import {
createEffect,
createMemo as createMemo3,
onCleanup,
splitProps as splitProps3
} from "solid-js";
import { Dynamic as Dynamic2 } from "@corvu/utils/dynamic";
var DialogDescription = (props) => {
const [localProps, otherProps] = splitProps3(props, [
"contextId"
]);
const context = createMemo3(
() => useInternalDialogContext(localProps.contextId)
);
createEffect(() => {
const _context = context();
_context.registerDescriptionId();
onCleanup(() => _context.unregisterDescriptionId());
});
return <Dynamic2
as="p"
id={context().descriptionId()}
data-corvu-dialog-description=""
{...otherProps}
/>;
};
var Description_default = DialogDescription;
// src/Label.tsx
import {
createEffect as createEffect2,
createMemo as createMemo4,
onCleanup as onCleanup2,
splitProps as splitProps4
} from "solid-js";
import { Dynamic as Dynamic3 } from "@corvu/utils/dynamic";
var DialogLabel = (props) => {
const [localProps, otherProps] = splitProps4(props, [
"contextId"
]);
const context = createMemo4(
() => useInternalDialogContext(localProps.contextId)
);
createEffect2(() => {
const _context = context();
_context.registerLabelId();
onCleanup2(() => _context.unregisterLabelId());
});
return <Dynamic3
as="h2"
id={context().labelId()}
data-corvu-dialog-label=""
{...otherProps}
/>;
};
var Label_default = DialogLabel;
// src/Overlay.tsx
import { combineStyle as combineStyle2 } from "@corvu/utils/dom";
import {
createMemo as createMemo5,
Show as Show2,
splitProps as splitProps5
} from "solid-js";
import { Dynamic as Dynamic4 } from "@corvu/utils/dynamic";
import { mergeRefs as mergeRefs2, some as some2 } from "@corvu/utils/reactivity";
import { dataIf as dataIf2 } from "@corvu/utils";
var DialogOverlay = (props) => {
const [localProps, otherProps] = splitProps5(props, [
"forceMount",
"contextId",
"ref",
"style"
]);
const context = createMemo5(
() => useInternalDialogContext(localProps.contextId)
);
const show = () => some2(context().open, () => localProps.forceMount, context().overlayPresent);
return <Show2 when={show()}>
<Dynamic4
as="div"
ref={mergeRefs2(context().setOverlayRef, localProps.ref)}
style={combineStyle2(
{
"pointer-events": "auto"
},
localProps.style
)}
aria-hidden="true"
data-closed={dataIf2(!context().open())}
data-open={dataIf2(context().open())}
data-corvu-dialog-overlay=""
{...otherProps}
/>
</Show2>;
};
var Overlay_default = DialogOverlay;
// src/Portal.tsx
import { createMemo as createMemo6, Show as Show3, splitProps as splitProps6 } from "solid-js";
import { Portal } from "solid-js/web";
import { some as some3 } from "@corvu/utils/reactivity";
var DialogPortal = (props) => {
const [localProps, otherProps] = splitProps6(props, [
"forceMount",
"contextId"
]);
const context = createMemo6(
() => useInternalDialogContext(localProps.contextId)
);
const show = () => some3(
context().open,
() => localProps.forceMount,
context().contentPresent,
context().overlayPresent
);
return <Show3 when={show()}>
<Portal {...otherProps} />
</Show3>;
};
var Portal_default = DialogPortal;
// src/Root.tsx
import {
createMemo as createMemo7,
createSignal,
createUniqueId,
mergeProps,
untrack
} from "solid-js";
import { access } from "@corvu/utils/reactivity";
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";
import { isFunction } from "@corvu/utils";
var DEFAULT_MODAL = true;
var DialogRoot = (props) => {
const defaultedProps = mergeProps(
{
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 = createMemo7(() => {
const DialogContext2 = createDialogContext(defaultedProps.contextId);
const InternalDialogContext2 = createInternalDialogContext(
defaultedProps.contextId
);
return <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
}}
>
<InternalDialogContext2.Provider
value={{
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
}}
>
{untrack(() => resolveChildren())}
</InternalDialogContext2.Provider>
</DialogContext2.Provider>;
});
return memoizedDialogRoot;
};
var Root_default = DialogRoot;
// src/Trigger.tsx
import { callEventHandler as callEventHandler2 } from "@corvu/utils/dom";
import {
createMemo as createMemo8,
splitProps as splitProps7
} from "solid-js";
import {
DynamicButton as DynamicButton2
} from "@corvu/utils/dynamic";
import { dataIf as dataIf3 } from "@corvu/utils";
import { mergeRefs as mergeRefs3 } from "@corvu/utils/reactivity";
var DialogTrigger = (props) => {
const [localProps, otherProps] = splitProps7(props, [
"contextId",
"ref",
"onClick"
]);
const context = createMemo8(
() => useInternalDialogContext(localProps.contextId)
);
const onClick = (e) => {
!callEventHandler2(localProps.onClick, e) && context().setOpen((open) => !open);
};
return <DynamicButton2
ref={mergeRefs3(context().setTriggerRef, localProps.ref)}
onClick={onClick}
aria-controls={context().dialogId()}
aria-expanded={context().open() ? "true" : "false"}
aria-haspopup="dialog"
data-closed={dataIf3(!context().open())}
data-open={dataIf3(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
};