@wix/design-system
Version:
@wix/design-system
161 lines • 6.92 kB
JavaScript
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