UNPKG

@corvu/drawer

Version:

Unstyled, accessible and customizable UI primitives for SolidJS

785 lines (772 loc) 24.6 kB
// 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 };