Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions apps/www/src/components/examples/responsive.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Drawer.Root direction={isMobile ? "bottom" : "center"}>
<Drawer.Trigger
render={(props) => <Button {...props}>Open Drawer</Button>}
/>
<Drawer.Portal>
<Drawer.Overlay className="fixed inset-0 bg-black/80" />
<Drawer.Content className="bg-background text-foreground fixed inset-x-0 bottom-0 h-auto w-full border group rounded-lg

data-[vaul-drawer-direction=center]:max-w-xl
data-[vaul-drawer-direction=bottom]:max-w-full data-[vaul-drawer-direction=bottom]:rounded-b-none
data-[vaul-drawer-direction=top]:max-w-full data-[vaul-drawer-direction=top]:rounded-t-none
data-[vaul-drawer-direction=left]:max-w-64 data-[vaul-drawer-direction=left]:w-2/3 data-[vaul-drawer-direction=left]:rounded-l-none data-[vaul-drawer-direction=left]:h-full
data-[vaul-drawer-direction=right]:max-w-64 data-[vaul-drawer-direction=right]:w-2/3 data-[vaul-drawer-direction=right]:rounded-r-none data-[vaul-drawer-direction=right]:h-full
">
<div className="hidden group-data-[vaul-drawer-direction=bottom]:block">
<Drawer.Handle className="top-3" />
</div>

<div className="flex flex-col space-y-4 p-6">
<h4 className="font-semibold">Welcome to the Drawer</h4>
<p>
This drawer adapts its direction based on your screen size.
</p>
<p>
We just change the direction prop dynamically using a custom hook. The transition between directions is
handeled smoothly by the drawer component.
</p>


<pre className="rounded bg-muted p-4 text-sm overflow-x-auto">
<code>{`<Drawer.Root direction={isMobile ? "bottom" : "center"}>
...
</Drawer.Root>`}
</code>
</pre>
</div>

<div className="hidden group-data-[vaul-drawer-direction=top]:block">
<Drawer.Handle className="-top-3" />
</div>
</Drawer.Content>
</Drawer.Portal>
</Drawer.Root>
)
}

export default ResponsiveDrawer
6 changes: 6 additions & 0 deletions apps/www/src/constants/examples.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -23,6 +24,11 @@ export const EXAMPLES = {
name: "Directions",
description: "Drawers can be opened from different sides of the screen.",
render: () => <DirectionsDrawer />,
},
responsive: {
name: "Responsive",
description: "Changing drawer direction based on screen size.",
render: () => <ResponsiveDrawer />,
},
"scaled-background": {
name: "Scaled Background",
Expand Down
21 changes: 21 additions & 0 deletions apps/www/src/hooks/use-mobile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import * as React from 'react';

const MOBILE_BREAKPOINT = 768;

export function useIsMobile() {
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(
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;
}
105 changes: 77 additions & 28 deletions packages/core/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { useSnapPoints } from "@/hooks/use-snap-points"
import { isIOS, isMobileFirefox } from "@/utils/browser"
import {
dampenValue,
getTransform,
getTranslate,
isVertical,
reset,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -287,6 +288,7 @@ export function Root({
}

function onPress(event: React.PointerEvent<HTMLDivElement>) {
if (direction === "center") return
if (!dismissible && !snapPoints) return
if (drawerRef.current && !drawerRef.current.contains(event.target as Node))
return
Expand Down Expand Up @@ -332,7 +334,11 @@ export function Root({
return false
}

if (direction === "right" || direction === "left") {
if (
direction === "right" ||
direction === "left" ||
direction === "top"
) {
return true
}

Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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),
})
}
}
Expand Down Expand Up @@ -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(",")})`,
})

Expand Down Expand Up @@ -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
) {
Expand Down Expand Up @@ -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)
}
Expand All @@ -816,6 +842,7 @@ export function Root({
_event: React.PointerEvent<HTMLDivElement>,
percentageDragged: number
) {
if (direction === "center") return
if (percentageDragged < 0) return

const initialScale =
Expand All @@ -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",
})
}
Expand All @@ -836,16 +861,15 @@ export function Root({
_event: React.PointerEvent<HTMLDivElement>,
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

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})`,
})
}
}
Expand All @@ -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 (
<Dialog.Root
defaultOpen={defaultOpen}
Expand Down Expand Up @@ -984,6 +1015,21 @@ export const Content = React.forwardRef<HTMLDivElement, ContentProps>(function (
React.useRef<React.PointerEvent<HTMLDivElement> | 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 = (
Expand Down Expand Up @@ -1032,8 +1078,11 @@ export const Content = React.forwardRef<HTMLDivElement, ContentProps>(function (
<Dialog.Popup
data-vaul-drawer-direction={direction}
data-vaul-drawer=""
data-vaul-delayed-snap-points={delayedSnapPoints ? "true" : "false"}
data-vaul-snap-points={isOpen && hasSnapPoints ? "true" : "false"}
data-vaul-drawer-morphing={isMorphing.current ? "true" : "false"}
data-vaul-delayed-snap-points={
isOpen && delayedSnapPoints ? "true" : "false"
}
data-vaul-snap-points={hasSnapPoints ? "true" : "false"}
data-vaul-custom-container={container ? "true" : "false"}
data-vaul-animate={shouldAnimate?.current ? "true" : "false"}
{...rest}
Expand Down
Loading