@corvu/drawer
Version:
Unstyled, accessible and customizable UI primitives for SolidJS
785 lines (772 loc) • 24.6 kB
JSX
// src/Close.tsx
import Dialog from "@corvu/dialog";
var DrawerClose = (props) => {
return <Dialog.Close
data-corvu-drawer-close=""
data-corvu-dialog-close={null}
{...props}
/>;
};
var Close_default = DrawerClose;
// src/Content.tsx
import { dataIf } from "@corvu/utils";
import {
batch,
createEffect,
createMemo,
onCleanup,
splitProps
} from "solid-js";
// src/lib.ts
var resolveSnapPoint = (snapPoint, drawerSize, index, breakPoints) => {
if (index === void 0 || breakPoints === void 0) {
return {
value: snapPoint,
offset: resolvePoint(snapPoint, drawerSize)
};
}
const upperBreakPoint = breakPoints[index - 1] !== void 0 && breakPoints[index - 1] !== null ? resolvePoint(breakPoints[index - 1], drawerSize) : void 0;
const lowerBreakPoint = breakPoints[index] !== void 0 && breakPoints[index] !== null ? resolvePoint(breakPoints[index], drawerSize) : void 0;
return {
value: snapPoint,
offset: resolvePoint(snapPoint, drawerSize),
lowerBreakPoint,
upperBreakPoint
};
};
var resolvePoint = (point, drawerSize) => {
if (typeof point === "number") return drawerSize - point * drawerSize;
if (!point.endsWith("px")) {
throw new Error(
`[corvu] Snap and break points must be a number or a string ending with 'px'. Got ${point}`
);
}
return drawerSize - parseInt(point, 10);
};
var findClosestSnapPoint = (snapPoints, offset, offsetWithVelocity, allowSkippingSnapPoints) => {
const upperSnapPoint = findNearbySnapPoint(
"upper",
snapPoints,
allowSkippingSnapPoints ? offsetWithVelocity : offset
);
const lowerSnapPoint = findNearbySnapPoint(
"lower",
snapPoints,
allowSkippingSnapPoints ? offsetWithVelocity : offset
);
if (!upperSnapPoint) return lowerSnapPoint;
if (!lowerSnapPoint) return upperSnapPoint;
if (lowerSnapPoint.upperBreakPoint === void 0 || upperSnapPoint.lowerBreakPoint === void 0) {
return Math.abs(lowerSnapPoint.offset - offsetWithVelocity) < Math.abs(upperSnapPoint.offset - offsetWithVelocity) ? lowerSnapPoint : upperSnapPoint;
}
return offsetWithVelocity < upperSnapPoint.lowerBreakPoint ? lowerSnapPoint : upperSnapPoint;
};
var findNearbySnapPoint = (side, snapPoints, offset) => {
return snapPoints.reduce(
(previousSnapPoint, currentSnapPoint) => {
if (side == "upper" && currentSnapPoint.offset >= offset && (!previousSnapPoint || currentSnapPoint.offset < previousSnapPoint.offset)) {
return currentSnapPoint;
} else if (side == "lower" && currentSnapPoint.offset <= offset && (!previousSnapPoint || currentSnapPoint.offset > previousSnapPoint.offset)) {
return currentSnapPoint;
}
return previousSnapPoint;
},
void 0
);
};
var locationIsDraggable = (location, stopAt, pointerType) => {
let currentElement = location;
let stopReached = false;
do {
if (currentElement.hasAttribute("data-corvu-no-drag") || currentElement.type === "range" || currentElement.tagName === "SELECT" && pointerType === "mouse")
return false;
if (currentElement === stopAt) {
stopReached = true;
} else {
currentElement = currentElement.parentElement;
}
} while (currentElement && !stopReached);
return true;
};
// src/Content.tsx
import { combineStyle } from "@corvu/utils/dom";
import Dialog2 from "@corvu/dialog";
import { getScrollAtLocation } from "@corvu/utils/scroll";
// src/context.ts
import { createContext, useContext } from "solid-js";
import {
createKeyedContext,
useKeyedContext
} from "@corvu/utils/create/keyedContext";
var DrawerContext = createContext();
var createDrawerContext = (contextId) => {
if (contextId === void 0) return DrawerContext;
const context = createKeyedContext(`drawer-${contextId}`);
return context;
};
var useDrawerContext = (contextId) => {
if (contextId === void 0) {
const context2 = useContext(DrawerContext);
if (!context2) {
throw new Error(
"[corvu]: Drawer context not found. Make sure to wrap Drawer components in <Drawer.Root>"
);
}
return context2;
}
const context = useKeyedContext(`drawer-${contextId}`);
if (!context) {
throw new Error(
`[corvu]: Drawer context with id "${contextId}" not found. Make sure to wrap Drawer components in <Drawer.Root contextId="${contextId}">`
);
}
return context;
};
var InternalDrawerContext = createContext();
var createInternalDrawerContext = (contextId) => {
if (contextId === void 0) return InternalDrawerContext;
const context = createKeyedContext(
`drawer-internal-${contextId}`
);
return context;
};
var useInternalDrawerContext = (contextId) => {
if (contextId === void 0) {
const context2 = useContext(InternalDrawerContext);
if (!context2) {
throw new Error(
"[corvu]: Drawer context not found. Make sure to wrap Drawer components in <Drawer.Root>"
);
}
return context2;
}
const context = useKeyedContext(
`drawer-internal-${contextId}`
);
if (!context) {
throw new Error(
`[corvu]: Drawer context with id "${contextId}" not found. Make sure to wrap Drawer components in <Drawer.Root contextId="${contextId}">`
);
}
return context;
};
// src/Content.tsx
var DrawerContent = (props) => {
const [localProps, otherProps] = splitProps(props, [
"contextId",
"style"
]);
let pointerDown = false;
let dragStartPos = null;
let currentPointerStart = [0, 0];
let cachedMoveTimestamp;
let cachedTranslate = 0;
const drawerContext = createMemo(
() => useInternalDrawerContext(localProps.contextId)
);
const dialogContext = createMemo(
() => Dialog2.useContext(localProps.contextId)
);
const snapPoints = createMemo(
() => drawerContext().snapPoints().map(
(snapPoint, index) => resolveSnapPoint(
snapPoint,
drawerContext().drawerSize(),
index,
drawerContext().breakPoints()
)
)
);
const transformValue = createMemo(() => {
switch (drawerContext().side()) {
case "top":
return `translate3d(0, ${-drawerContext().translate()}px, 0)`;
case "bottom":
return `translate3d(0, ${drawerContext().translate()}px, 0)`;
case "right":
return `translate3d(${drawerContext().translate()}px, 0, 0)`;
case "left":
return `translate3d(${-drawerContext().translate()}px, 0, 0)`;
}
});
const transitionHeight = createMemo(() => {
const transitionSize = drawerContext().transitionSize();
if (transitionSize === null) return void 0;
switch (drawerContext().side()) {
case "top":
case "bottom":
return `${transitionSize}px`;
}
return void 0;
});
const transitionWidth = createMemo(() => {
const transitionSize = drawerContext().transitionSize();
if (transitionSize === null) return void 0;
switch (drawerContext().side()) {
case "left":
case "right":
return `${transitionSize}px`;
}
return void 0;
});
createEffect(() => {
if (!dialogContext().open()) return;
document.addEventListener("pointermove", onPointerMove);
document.addEventListener("touchmove", onTouchMove);
document.addEventListener("pointerup", onPointerUp);
document.addEventListener("touchend", onTouchEnd);
document.addEventListener("contextmenu", onUp);
onCleanup(() => {
document.removeEventListener("pointermove", onPointerMove);
document.removeEventListener("touchmove", onTouchMove);
document.removeEventListener("pointerup", onPointerUp);
document.removeEventListener("touchend", onTouchEnd);
document.removeEventListener("contextmenu", onUp);
});
});
createEffect(() => {
if (drawerContext().transitionState() === "closing") {
drawerContext().setIsDragging(false);
}
});
const onPointerDown = (event) => {
if (event.button !== 0) return;
if (!locationIsDraggable(
event.target,
dialogContext().contentRef(),
event.pointerType
))
return;
if (drawerContext().transitionState() === "closing") return;
pointerDown = true;
if (drawerContext().handleScrollableElements()) {
currentPointerStart = [event.clientX, event.clientY];
}
};
const onPointerMove = (event) => {
onMove(event.target, event.clientX, event.clientY);
};
const onTouchMove = (event) => {
if (!event.touches[0]) return;
onMove(
event.target,
event.touches[0].clientX,
event.touches[0].clientY
);
};
const onMove = (target, x, y) => {
if (!pointerDown) return;
if (!drawerContext().isDragging() || dragStartPos === null) {
const selection = window.getSelection();
if (selection && selection.toString().length > 0) {
onUp();
return;
}
if (drawerContext().handleScrollableElements()) {
const delta2 = [x, y].map(
(pointer, i) => currentPointerStart[i] - pointer
);
const axis = Math.abs(delta2[0]) > Math.abs(delta2[1]) ? "x" : "y";
const axisDelta = axis === "x" ? delta2[0] : delta2[1];
if (Math.abs(axisDelta) < 0.3) return;
const wrapper = dialogContext().contentRef();
const [availableScroll, availableScrollTop] = getScrollAtLocation(
target,
axis,
wrapper
);
if (axisDelta > 0 && Math.abs(availableScroll) > 1 || axisDelta < 0 && Math.abs(availableScrollTop) > 0) {
onUp();
return;
}
}
switch (drawerContext().side()) {
case "top":
case "bottom":
dragStartPos = y;
break;
case "right":
case "left":
dragStartPos = x;
}
cachedMoveTimestamp = /* @__PURE__ */ new Date();
cachedTranslate = drawerContext().translate();
batch(() => {
drawerContext().setIsDragging(true);
drawerContext().setTransitionState(null);
});
}
let delta;
switch (drawerContext().side()) {
case "top":
delta = -(dragStartPos - y);
break;
case "bottom":
delta = dragStartPos - y;
break;
case "right":
delta = dragStartPos - x;
break;
case "left":
delta = -(dragStartPos - x);
break;
}
delta -= drawerContext().resolvedActiveSnapPoint().offset;
if (delta > 0) delta = drawerContext().dampFunction(delta);
const now = /* @__PURE__ */ new Date();
if (now.getTime() - cachedMoveTimestamp.getTime() > drawerContext().velocityCacheReset()) {
cachedMoveTimestamp = now;
cachedTranslate = drawerContext().translate();
}
drawerContext().setTranslate(-delta);
};
const onPointerUp = (event) => {
if (event.pointerType !== "touch") onUp();
};
const onTouchEnd = (event) => {
if (event.touches.length === 0) onUp();
};
const onUp = () => {
pointerDown = false;
if (!drawerContext().isDragging()) return;
const now = /* @__PURE__ */ new Date();
const velocity = drawerContext().velocityFunction(
-(cachedTranslate - drawerContext().translate()),
now.getTime() - cachedMoveTimestamp.getTime() || 1
);
const translateWithVelocity = drawerContext().translate() * velocity;
const closestSnapPoint = findClosestSnapPoint(
snapPoints(),
drawerContext().translate(),
translateWithVelocity,
drawerContext().allowSkippingSnapPoints()
);
batch(() => {
drawerContext().setTransitionState("snapping");
drawerContext().setIsDragging(false);
});
batch(() => {
drawerContext().setActiveSnapPoint(closestSnapPoint.value);
if (closestSnapPoint.offset === drawerContext().drawerSize()) {
dialogContext().setOpen(false);
} else {
drawerContext().setTranslate(closestSnapPoint.offset);
const transitionDuration = parseFloat(
drawerContext().drawerStyles().transitionDuration
);
if (transitionDuration === 0) {
drawerContext().setTransitionState(null);
}
}
});
};
return <Dialog2.Content
contextId={localProps.contextId}
style={combineStyle(
{
transform: transformValue(),
"transition-duration": drawerContext().isDragging() ? "0ms" : void 0,
height: transitionHeight(),
width: transitionWidth()
},
localProps.style
)}
onPointerDown={onPointerDown}
onTouchStart={(e) => {
if (e.touches.length !== 1) return;
dragStartPos = null;
}}
onTransitionEnd={(e) => {
if (e.target !== dialogContext().contentRef()) return;
batch(() => {
if (drawerContext().transitionState() === "closing") {
drawerContext().closeDrawer();
}
if (drawerContext().transitionState() !== "resizing") {
drawerContext().setTransitionState(null);
}
});
}}
data-closing={dataIf(drawerContext().transitionState() === "closing")}
data-opening={dataIf(drawerContext().transitionState() === "opening")}
data-resizing={dataIf(drawerContext().transitionState() === "resizing")}
data-snapping={dataIf(drawerContext().transitionState() === "snapping")}
data-transitioning={dataIf(drawerContext().isTransitioning())}
data-corvu-drawer-content=""
data-corvu-dialog-content={null}
{...otherProps}
/>;
};
var Content_default = DrawerContent;
// src/Description.tsx
import Dialog3 from "@corvu/dialog";
var DrawerDescription = (props) => {
return <Dialog3.Description
data-corvu-drawer-description=""
data-corvu-dialog-description={null}
{...props}
/>;
};
var Description_default = DrawerDescription;
// src/index.ts
import {
Portal,
useContext as useDialogContext
} from "@corvu/dialog";
// src/Label.tsx
import Dialog4 from "@corvu/dialog";
var DrawerLabel = (props) => {
return <Dialog4.Label
data-corvu-drawer-label=""
data-corvu-dialog-label={null}
{...props}
/>;
};
var Label_default = DrawerLabel;
// src/Overlay.tsx
import {
createMemo as createMemo2,
splitProps as splitProps2
} from "solid-js";
import Dialog5 from "@corvu/dialog";
import { dataIf as dataIf2 } from "@corvu/utils";
var DrawerOverlay = (props) => {
const [localProps, otherProps] = splitProps2(props, [
"contextId"
]);
const drawerContext = createMemo2(
() => useInternalDrawerContext(localProps.contextId)
);
return <Dialog5.Overlay
contextId={localProps.contextId}
data-closing={dataIf2(drawerContext().transitionState() === "closing")}
data-opening={dataIf2(drawerContext().transitionState() === "opening")}
data-resizing={dataIf2(drawerContext().transitionState() === "resizing")}
data-snapping={dataIf2(drawerContext().transitionState() === "snapping")}
data-transitioning={dataIf2(drawerContext().isTransitioning())}
data-corvu-drawer-overlay=""
data-corvu-dialog-overlay={null}
{...otherProps}
/>;
};
var Overlay_default = DrawerOverlay;
// src/Root.tsx
import {
batch as batch2,
createEffect as createEffect2,
createMemo as createMemo3,
createSignal,
mergeProps,
splitProps as splitProps3,
untrack
} from "solid-js";
import { isFunction } from "@corvu/utils";
import { afterPaint } from "@corvu/utils/dom";
import createControllableSignal from "@corvu/utils/create/controllableSignal";
import createOnce from "@corvu/utils/create/once";
import createSize from "@corvu/utils/create/size";
import createTransitionSize from "solid-transition-size";
import { createWritableMemo } from "@solid-primitives/memo";
import Dialog6 from "@corvu/dialog";
var DrawerRoot = (props) => {
const defaultedProps = mergeProps(
{
initialOpen: false,
snapPoints: [0, 1],
breakPoints: [null],
defaultSnapPoint: 1,
side: "bottom",
dampFunction: (distance) => 6 * Math.log(distance + 1),
velocityFunction: (distance, time) => {
const velocity = distance / time;
return velocity < 1 && velocity > -1 ? 1 : velocity;
},
velocityCacheReset: 200,
allowSkippingSnapPoints: true,
handleScrollableElements: true,
transitionResize: false,
closeOnOutsidePointer: true,
allowPinchZoom: false
},
props
);
const [localProps, otherProps] = splitProps3(defaultedProps, [
"snapPoints",
"breakPoints",
"defaultSnapPoint",
"activeSnapPoint",
"onActiveSnapPointChange",
"side",
"dampFunction",
"velocityFunction",
"velocityCacheReset",
"allowSkippingSnapPoints",
"handleScrollableElements",
"transitionResize",
"open",
"initialOpen",
"onOpenChange",
"closeOnOutsidePointer",
"contextId",
"children"
]);
const [open, setOpen] = createControllableSignal({
value: () => localProps.open,
initialValue: localProps.initialOpen,
onChange: localProps.onOpenChange
});
const [activeSnapPoint, setActiveSnapPoint] = createControllableSignal({
value: () => localProps.activeSnapPoint,
initialValue: 0,
onChange: localProps.onActiveSnapPointChange
});
const [dialogContext, setDialogContext] = createSignal();
const { transitioning: sizeTransitioning, transitionSize } = createTransitionSize({
element: () => dialogContext()?.contentRef() ?? null,
enabled: () => open() && localProps.transitionResize,
dimension: () => {
switch (localProps.side) {
case "top":
case "bottom":
return "height";
case "left":
case "right":
return "width";
}
}
});
const [isDragging, setIsDragging] = createSignal(false);
const [transitionState, setTransitionState] = createWritableMemo(() => {
if (sizeTransitioning()) return "resizing";
return null;
});
const drawerStyles = createMemo3(() => {
const contentRef = dialogContext()?.contentRef();
if (!contentRef) return void 0;
return getComputedStyle(contentRef);
});
const [transitionAwareOpen, setTransitionAwareOpen] = createSignal(false);
createEffect2(() => {
const _open = open();
untrack(() => {
if (transitionAwareOpen() === _open) {
return;
}
if (_open) {
setTransitionAwareOpen(true);
afterPaint(() => {
batch2(() => {
setTransitionState("opening");
setActiveSnapPoint(localProps.defaultSnapPoint);
});
const transitionDuration = parseFloat(
drawerStyles().transitionDuration
);
if (transitionDuration === 0) {
setTransitionState(null);
}
});
} else {
batch2(() => {
setTransitionState("closing");
setActiveSnapPoint(0);
});
afterPaint(() => {
const transitionDuration = parseFloat(
drawerStyles().transitionDuration
);
if (transitionDuration === 0) {
closeDrawer();
}
});
}
});
});
const closeDrawer = () => {
batch2(() => {
setTransitionAwareOpen(false);
setTransitionState(null);
});
};
const drawerSize = createSize({
element: () => dialogContext()?.contentRef() ?? null,
dimension: () => {
switch (localProps.side) {
case "top":
case "bottom":
return "height";
case "left":
case "right":
return "width";
}
}
});
const resolvedActiveSnapPoint = createMemo3(
() => resolveSnapPoint(activeSnapPoint(), drawerSize())
);
const [translate, setTranslate] = createWritableMemo(
() => resolvedActiveSnapPoint().offset
);
const openPercentage = createMemo3(() => {
if (!drawerSize()) return 0;
return (drawerSize() - translate()) / drawerSize();
});
const childrenProps = {
get snapPoints() {
return localProps.snapPoints;
},
get breakPoints() {
return localProps.breakPoints;
},
get defaultSnapPoint() {
return localProps.defaultSnapPoint;
},
get activeSnapPoint() {
return activeSnapPoint();
},
setActiveSnapPoint,
get side() {
return localProps.side;
},
get isDragging() {
return isDragging();
},
get isTransitioning() {
return transitionState() !== null;
},
get transitionState() {
return transitionState();
},
get openPercentage() {
return openPercentage();
},
get translate() {
return translate();
},
get velocityCacheReset() {
return localProps.velocityCacheReset;
},
get allowSkippingSnapPoints() {
return localProps.allowSkippingSnapPoints;
},
get handleScrollableElements() {
return localProps.handleScrollableElements;
},
get transitionResize() {
return localProps.transitionResize;
}
};
const memoizedChildren = createOnce(() => localProps.children);
const resolveChildren = (dialogChildrenProps) => {
setDialogContext(Dialog6.useContext(localProps.contextId));
const children = memoizedChildren()();
if (isFunction(children)) {
const mergedProps = mergeProps(dialogChildrenProps, childrenProps);
return children(mergedProps);
}
return children;
};
const memoizedDrawerRoot = createMemo3(() => {
const DrawerContext2 = createDrawerContext(localProps.contextId);
const InternalDrawerContext2 = createInternalDrawerContext(
localProps.contextId
);
return untrack(() => <DrawerContext2.Provider
value={{
snapPoints: () => localProps.snapPoints,
breakPoints: () => localProps.breakPoints,
defaultSnapPoint: () => localProps.defaultSnapPoint,
activeSnapPoint,
setActiveSnapPoint,
side: () => localProps.side,
isDragging,
isTransitioning: () => transitionState() !== null,
transitionState,
openPercentage,
translate,
velocityCacheReset: () => localProps.velocityCacheReset,
allowSkippingSnapPoints: () => localProps.allowSkippingSnapPoints,
handleScrollableElements: () => localProps.handleScrollableElements,
transitionResize: () => localProps.transitionResize
}}
>
<InternalDrawerContext2.Provider
value={{
snapPoints: () => localProps.snapPoints,
breakPoints: () => localProps.breakPoints,
defaultSnapPoint: () => localProps.defaultSnapPoint,
activeSnapPoint,
setActiveSnapPoint,
side: () => localProps.side,
isDragging,
isTransitioning: () => transitionState() !== null,
transitionState,
openPercentage,
translate,
velocityCacheReset: () => localProps.velocityCacheReset,
allowSkippingSnapPoints: () => localProps.allowSkippingSnapPoints,
handleScrollableElements: () => localProps.handleScrollableElements,
transitionResize: () => localProps.transitionResize,
dampFunction: localProps.dampFunction,
velocityFunction: localProps.velocityFunction,
setIsDragging,
setTranslate,
drawerSize,
resolvedActiveSnapPoint,
drawerStyles,
setTransitionState,
transitionSize,
closeDrawer
}}
>
<Dialog6
open={transitionAwareOpen()}
onOpenChange={setOpen}
contextId={localProps.contextId}
closeOnOutsidePointer={!isDragging() && localProps.closeOnOutsidePointer}
{...otherProps}
>
{(dialogChildrenProps) => resolveChildren(dialogChildrenProps)}
</Dialog6>
</InternalDrawerContext2.Provider>
</DrawerContext2.Provider>);
});
return memoizedDrawerRoot;
};
var Root_default = DrawerRoot;
// src/Trigger.tsx
import Dialog7 from "@corvu/dialog";
var DrawerTrigger = (props) => {
return <Dialog7.Trigger
data-corvu-drawer-trigger=""
data-corvu-dialog-trigger={null}
{...props}
/>;
};
var Trigger_default = DrawerTrigger;
// src/index.ts
var Drawer = Object.assign(Root_default, {
Trigger: Trigger_default,
Portal,
Overlay: Overlay_default,
Content: Content_default,
Label: Label_default,
Description: Description_default,
Close: Close_default,
useContext: useDrawerContext,
useDialogContext
});
var index_default = Drawer;
export {
Close_default as Close,
Content_default as Content,
Description_default as Description,
Label_default as Label,
Overlay_default as Overlay,
Portal,
Root_default as Root,
Trigger_default as Trigger,
index_default as default,
useDrawerContext as useContext,
useDialogContext
};