UNPKG

@wix/design-system

Version:

@wix/design-system

161 lines 6.92 kB
import React, { useCallback, useEffect, useRef } from 'react'; import * as Dialog from '@radix-ui/react-dialog'; import { classes, st } from './Drawer.st.css.js'; import { DATA_HOOKS } from './Drawer.constants'; import { ZIndex } from '../common/ZIndex'; const Drawer = ({ open, backdrop = true, renderBackdrop = true, resizable = true, margin = true, onClose, role = 'dialog', ariaLabelledBy, snapPoints = [1], children, dismissible = true, dataHook, zIndex = ZIndex.popover, scrollable = true, repositionInputs = true, }) => { const contentRef = useRef(null); const restingY = useRef(0); const dragState = useRef({ isDragging: false, startY: 0, startTranslateY: 0, currentDelta: 0, }); useEffect(() => { if (!open || !repositionInputs || typeof window === 'undefined' || !window.visualViewport) { return; } const viewport = window.visualViewport; const el = contentRef.current; const onViewportResize = () => { if (!el) return; const keyboardHeight = Math.max(0, window.innerHeight - viewport.height - viewport.offsetTop); el.style.bottom = keyboardHeight > 0 ? `${keyboardHeight}px` : ''; }; viewport.addEventListener('resize', onViewportResize); return () => { viewport.removeEventListener('resize', onViewportResize); if (el) { el.style.bottom = ''; } }; }, [open, repositionInputs]); const resolveSnapPoint = (snap) => { const value = typeof snap === 'string' ? parseFloat(snap.replace('px', '')) : snap; if (value <= 1) { return value * (contentRef.current?.clientHeight ?? window.innerHeight); } return value; }; const animateToY = (fromY, toY, onDone) => { const el = contentRef.current; if (!el) return; el.style.animation = 'none'; // Check needed for JSDOM environment, since animation api isn't implemented if (typeof el.animate !== 'function') { el.style.transform = toY === 0 ? '' : `translateY(${toY}px)`; el.style.animation = ''; restingY.current = toY; onDone?.(); return; } const anim = el.animate([ { transform: `translateY(${fromY}px)` }, { transform: `translateY(${toY}px)` }, ], { duration: 300, easing: 'ease' }); anim.onfinish = () => { el.style.transform = toY === 0 ? '' : `translateY(${toY}px)`; if (onDone) { onDone(); } else { el.style.animation = ''; restingY.current = toY; } }; }; const onHandlePointerDown = (e) => { e.currentTarget.setPointerCapture(e.pointerId); dragState.current = { isDragging: true, startY: e.clientY, startTranslateY: restingY.current, currentDelta: 0, }; if (contentRef.current) { contentRef.current.style.animation = 'none'; contentRef.current.style.transition = 'none'; } }; const onHandlePointerMove = (e) => { if (!dragState.current.isDragging) return; const delta = e.clientY - dragState.current.startY; dragState.current.currentDelta = delta; const newY = Math.max(0, dragState.current.startTranslateY + delta); if (contentRef.current) { contentRef.current.style.transform = `translateY(${newY}px)`; } }; const onHandlePointerUp = () => { if (!dragState.current.isDragging) return; dragState.current.isDragging = false; const el = contentRef.current; if (!el) return; const contentHeight = el.offsetHeight; const currentY = Math.max(0, dragState.current.startTranslateY + dragState.current.currentDelta); const resolvedSnaps = snapPoints .map(resolveSnapPoint) .sort((a, b) => a - b); const visibleHeight = contentHeight - currentY; const lowestSnap = resolvedSnaps[0]; if (dismissible && visibleHeight < lowestSnap * 0.5) { animateToY(currentY, contentHeight, () => onClose(false)); return; } let nearest = resolvedSnaps[0]; let minDist = Infinity; for (const snap of resolvedSnaps) { const dist = Math.abs(visibleHeight - snap); if (dist < minDist) { minDist = dist; nearest = snap; } } animateToY(currentY, Math.max(0, contentHeight - nearest)); }; const handleDismiss = (e) => { e.preventDefault(); if (!dismissible) return; const el = contentRef.current; if (!el) { onClose(false); return; } animateToY(restingY.current, el.offsetHeight, () => onClose(false)); }; const lastAnimatedElRef = useRef(null); const handleContentRef = useCallback((el) => { if (el && el !== lastAnimatedElRef.current) { contentRef.current = el; lastAnimatedElRef.current = el; if (!contentRef.current) return; const snapPx = resolveSnapPoint(snapPoints[0]); const targetY = Math.max(0, el.offsetHeight - snapPx); animateToY(contentRef.current.clientHeight, targetY); } }, [snapPoints]); return (React.createElement("div", { "data-hook": dataHook }, React.createElement(Dialog.Root, { open: open, onOpenChange: onClose }, React.createElement(Dialog.Portal, null, renderBackdrop && (React.createElement(Dialog.Overlay, { "data-hook": DATA_HOOKS.OVERLAY, className: st(classes.overlay, { backdrop }), style: { zIndex } })), React.createElement(Dialog.Content, { ref: handleContentRef, "data-hook": DATA_HOOKS.CONTENT, "aria-modal": "true", "aria-labelledby": ariaLabelledBy, role: role, className: st(classes.content, { margin }), onInteractOutside: handleDismiss, onEscapeKeyDown: handleDismiss, style: { zIndex, } }, React.createElement(Dialog.Title, { className: classes.title }), resizable && (React.createElement("div", { className: classes.handle, "data-hook": DATA_HOOKS.HANDLE, onPointerDown: onHandlePointerDown, onPointerMove: onHandlePointerMove, onPointerUp: onHandlePointerUp })), scrollable ? (React.createElement("div", { className: st(classes.contentWrapper, { margin }) }, children)) : (children)))))); }; Drawer.displayName = 'Drawer'; export default Drawer; //# sourceMappingURL=Drawer.js.map