diff --git a/apps/www/src/components/examples/responsive.tsx b/apps/www/src/components/examples/responsive.tsx new file mode 100644 index 0000000..001aec2 --- /dev/null +++ b/apps/www/src/components/examples/responsive.tsx @@ -0,0 +1,55 @@ +import { Drawer } from "vaul-base" + +import { Button } from "@/components/button" +import { useIsMobile } from "@/hooks/use-mobile" + +const ResponsiveDrawer = () => { + const isMobile = useIsMobile() + return ( + + } + /> + + + +
+ +
+ +
+

Welcome to the Drawer

+

+ This drawer adapts its direction based on your screen size. +

+

+ We just change the direction prop dynamically using a custom hook. The transition between directions is + handeled smoothly by the drawer component. +

+ + +
+              {`
+  ...
+`}
+              
+            
+
+ +
+ +
+
+
+
+ ) +} + +export default ResponsiveDrawer diff --git a/apps/www/src/constants/examples.tsx b/apps/www/src/constants/examples.tsx index 6cc6e24..674c19d 100644 --- a/apps/www/src/constants/examples.tsx +++ b/apps/www/src/constants/examples.tsx @@ -2,6 +2,7 @@ import BasicDrawer from "@/components/examples/basic" import DirectionsDrawer from "@/components/examples/directions" import NestedDrawer from "@/components/examples/nested" import NonDismissableDrawer from "@/components/examples/non-dismissable" +import ResponsiveDrawer from "@/components/examples/responsive" import ScaledBackgroundDrawer from "@/components/examples/scaled-background" import { ScrollableDrawer } from "@/components/examples/scrollable" import SnapPointsDrawer from "@/components/examples/snap-points" @@ -23,6 +24,11 @@ export const EXAMPLES = { name: "Directions", description: "Drawers can be opened from different sides of the screen.", render: () => , + }, + responsive: { + name: "Responsive", + description: "Changing drawer direction based on screen size.", + render: () => , }, "scaled-background": { name: "Scaled Background", diff --git a/apps/www/src/hooks/use-mobile.ts b/apps/www/src/hooks/use-mobile.ts new file mode 100644 index 0000000..6be5f6c --- /dev/null +++ b/apps/www/src/hooks/use-mobile.ts @@ -0,0 +1,21 @@ +import * as React from 'react'; + +const MOBILE_BREAKPOINT = 768; + +export function useIsMobile() { + const [isMobile, setIsMobile] = React.useState( + undefined + ); + + React.useEffect(() => { + const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`); + const onChange = () => { + setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); + }; + mql.addEventListener('change', onChange); + setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); + return () => mql.removeEventListener('change', onChange); + }, []); + + return !!isMobile; +} diff --git a/packages/core/src/index.tsx b/packages/core/src/index.tsx index fce2049..eaec2d1 100644 --- a/packages/core/src/index.tsx +++ b/packages/core/src/index.tsx @@ -13,6 +13,7 @@ import { useSnapPoints } from "@/hooks/use-snap-points" import { isIOS, isMobileFirefox } from "@/utils/browser" import { dampenValue, + getTransform, getTranslate, isVertical, reset, @@ -115,7 +116,7 @@ export type DialogProps = { * Direction of the drawer. Can be `top` or `bottom`, `left`, `right`. * @default 'bottom' */ - direction?: "top" | "bottom" | "left" | "right" + direction?: "top" | "bottom" | "left" | "right" | "center" /** * Opened by default, skips initial enter animation. Still reacts to `open` state changes * @default false @@ -287,6 +288,7 @@ export function Root({ } function onPress(event: React.PointerEvent) { + if (direction === "center") return if (!dismissible && !snapPoints) return if (drawerRef.current && !drawerRef.current.contains(event.target as Node)) return @@ -332,7 +334,11 @@ export function Root({ return false } - if (direction === "right" || direction === "left") { + if ( + direction === "right" || + direction === "left" || + direction === "top" + ) { return true } @@ -466,10 +472,17 @@ export function Root({ const translateValue = Math.min(dampenedDraggedDistance * -1, 0) * directionMultiplier + + const transformValue = + direction === "left" || direction === "top" + ? `calc(100% + ${translateValue}px)` + : `${translateValue}px` + + const x = isVertical(direction) ? "50%" : transformValue + const y = isVertical(direction) ? transformValue : "50%" + set(drawerRef.current, { - transform: isVertical(direction) - ? `translate3d(0, ${translateValue}px, 0)` - : `translate3d(${translateValue}px, 0, 0)`, + transform: getTransform(direction, translateValue), }) return } @@ -518,10 +531,16 @@ export function Root({ if (!snapPoints) { const translateValue = absDraggedDistance * directionMultiplier + const transformValue = + direction === "left" || direction === "top" + ? `calc(100% + ${translateValue}px)` + : `${translateValue}px` + + const x = isVertical(direction) ? "50%" : transformValue + const y = isVertical(direction) ? transformValue : "50%" + set(drawerRef.current, { - transform: isVertical(direction) - ? `translate3d(0, ${translateValue}px, 0)` - : `translate3d(${translateValue}px, 0, 0)`, + transform: getTransform(direction, translateValue), }) } } @@ -626,8 +645,14 @@ export function Root({ const wrapper = document.querySelector("[data-vaul-drawer-wrapper]") const currentSwipeAmount = getTranslate(drawerRef.current, direction) + const transformValue = + direction === "left" || direction === "top" ? "100%" : "0px" + + const x = isVertical(direction) ? "50%" : transformValue + const y = isVertical(direction) ? transformValue : "50%" + set(drawerRef.current, { - transform: "translate3d(0, 0, 0)", + transform: `translate3d(${x}, ${y}, 0)`, transition: `transform ${TRANSITIONS.DURATION}s cubic-bezier(${TRANSITIONS.EASE.join(",")})`, }) @@ -749,8 +774,14 @@ export function Root({ ) const isHorizontalSwipe = direction === "left" || direction === "right" + const swipeToClose = + direction === "left" || direction === "top" + ? (isHorizontalSwipe ? visibleDrawerWidth : visibleDrawerHeight) - + swipeAmount + : swipeAmount + if ( - Math.abs(swipeAmount) >= + Math.abs(swipeToClose) >= (isHorizontalSwipe ? visibleDrawerWidth : visibleDrawerHeight) * closeThreshold ) { @@ -791,22 +822,17 @@ export function Root({ set(drawerRef.current, { transition: `transform ${TRANSITIONS.DURATION}s cubic-bezier(${TRANSITIONS.EASE.join(",")})`, - transform: isVertical(direction) - ? `scale(${scale}) translate3d(0, ${initialTranslate}px, 0)` - : `scale(${scale}) translate3d(${initialTranslate}px, 0, 0)`, + transform: `${getTransform(direction, initialTranslate)} scale(${scale})`, }) if (!o && drawerRef.current) { nestedOpenChangeTimer.current = setTimeout(() => { - const translateValue = getTranslate( - drawerRef.current as HTMLElement, - direction - ) + const translateValue = + getTranslate(drawerRef.current as HTMLElement, direction) || 0 + set(drawerRef.current, { transition: "none", - transform: isVertical(direction) - ? `translate3d(0, ${translateValue}px, 0)` - : `translate3d(${translateValue}px, 0, 0)`, + transform: getTransform(direction, translateValue), }) }, 500) } @@ -816,6 +842,7 @@ export function Root({ _event: React.PointerEvent, percentageDragged: number ) { + if (direction === "center") return if (percentageDragged < 0) return const initialScale = @@ -825,9 +852,7 @@ export function Root({ -NESTED_DISPLACEMENT + percentageDragged * NESTED_DISPLACEMENT set(drawerRef.current, { - transform: isVertical(direction) - ? `scale(${newScale}) translate3d(0, ${newTranslate}px, 0)` - : `scale(${newScale}) translate3d(${newTranslate}px, 0, 0)`, + transform: `${getTransform(direction, newTranslate)} scale(${newScale})`, transition: "none", }) } @@ -836,6 +861,7 @@ export function Root({ _event: React.PointerEvent, o: boolean ) { + if (direction === "center") return const dim = isVertical(direction) ? window.innerHeight : window.innerWidth const scale = o ? (dim - NESTED_DISPLACEMENT) / dim : 1 const translate = o ? -NESTED_DISPLACEMENT : 0 @@ -843,9 +869,7 @@ export function Root({ if (o) { set(drawerRef.current, { transition: `transform ${TRANSITIONS.DURATION}s cubic-bezier(${TRANSITIONS.EASE.join(",")})`, - transform: isVertical(direction) - ? `scale(${scale}) translate3d(0, ${translate}px, 0)` - : `scale(${scale}) translate3d(${translate}px, 0, 0)`, + transform: `${getTransform(direction, translate)} scale(${scale})`, }) } } @@ -859,6 +883,13 @@ export function Root({ } }, [modal]) + React.useLayoutEffect(() => { + if (drawerRef.current) { + drawerRef.current.style.removeProperty("transform") + drawerRef.current.style.removeProperty("transition") + } + }, [direction]) + return ( (function ( React.useRef | null>(null) const wasBeyondThePointRef = React.useRef(false) const hasSnapPoints = snapPoints && snapPoints.length > 0 + const prevDirectionRef = React.useRef(direction) + const wasOpenRef = React.useRef(isOpen) + const isMorphing = React.useRef(false) + + if (isOpen && wasOpenRef.current && prevDirectionRef.current !== direction) { + isMorphing.current = true + } else if (!isOpen) { + isMorphing.current = false + } + + React.useEffect(() => { + prevDirectionRef.current = direction + wasOpenRef.current = isOpen + }) + useScaleBackground() const isDeltaInDirection = ( @@ -1032,8 +1078,11 @@ export const Content = React.forwardRef(function ( { case "left": case "right": return false + case "center": + return true default: return direction satisfies never } } +export function getTransform(direction: DrawerDirection, translateValue: number) { + let x = isVertical(direction) ? "50%" : `${translateValue}px` + let y = isVertical(direction) ? `${translateValue}px` : "50%" + + if (direction === "left") { + x = `calc(100% + ${translateValue}px)` + } else if (direction === "top") { + y = `calc(100% + ${translateValue}px)` + } + + return `translate3d(${x}, ${y}, 0)` +} + export function getTranslate( element: HTMLElement, direction: DrawerDirection