Skip to content

Commit 6ba065c

Browse files
committed
more
1 parent c165de0 commit 6ba065c

13 files changed

+812
-750
lines changed

packages/vaul-svelte/src/lib/components/drawer/drawer.svelte

+33-2
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@
1414
onOpenChange = noop,
1515
closeThreshold = DEFAULT_CLOSE_THRESHOLD,
1616
scrollLockTimeout = DEFAULT_SCROLL_LOCK_TIMEOUT,
17-
snapPoints,
18-
fadeFromIndex,
17+
snapPoints = [],
18+
fadeFromIndex = snapPoints && snapPoints.length - 1,
1919
backgroundColor = "black",
2020
nested = false,
2121
shouldScaleBackground = false,
@@ -204,6 +204,37 @@
204204
opacity: 1;
205205
}
206206
207+
:global([vaul-handle]) {
208+
display: block;
209+
position: relative;
210+
opacity: 0.8;
211+
margin-left: auto;
212+
margin-right: auto;
213+
height: 5px;
214+
width: 56px;
215+
border-radius: 1rem;
216+
touch-action: pan-y;
217+
cursor: grab;
218+
}
219+
220+
:global([vaul-handle]:hover, [vaul-handle]:active) {
221+
opacity: 1;
222+
}
223+
224+
:global([vaul-handle]:active) {
225+
cursor: grabbing;
226+
}
227+
228+
:global([vaul-handle-hitarea]) {
229+
position: absolute;
230+
left: 50%;
231+
top: 50%;
232+
transform: translate(-50%, -50%);
233+
width: max(100%, 2.75rem); /* 44px */
234+
height: max(100%, 2.75rem); /* 44px */
235+
touch-action: inherit;
236+
}
237+
207238
/* This will allow us to not animate via animation, but still benefit from delaying
208239
unmount via Bits */
209240
@keyframes -global-fake-animation {

packages/vaul-svelte/src/lib/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export * from "./components/index.js";
2+
export type { DrawerDirection } from "./types.js";

packages/vaul-svelte/src/lib/position-fixed.svelte.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export class PositionFixed {
3030
#preventScrollRestoration: PositionFixedProps["preventScrollRestoration"];
3131
#noBodyStyles: PositionFixedProps["noBodyStyles"];
3232
#activeUrl = $state(getActiveUrl());
33-
#scrollPos = 0;
33+
#scrollPos = $state(0);
3434

3535
constructor(props: PositionFixedProps) {
3636
this.#open = props.open;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,313 @@
1+
import type { ReadableBoxedValues, WritableBoxedValues } from "svelte-toolbelt";
2+
import { untrack } from "svelte";
3+
import type { DrawerDirection } from "./types.js";
4+
import { isVertical } from "./internal/helpers/is.js";
5+
import { setStyles } from "./internal/helpers/index.js";
6+
import { TRANSITIONS, VELOCITY_THRESHOLD } from "./internal/constants.js";
7+
8+
type SnapPointsProps = WritableBoxedValues<{
9+
activeSnapPoint: number | string | null;
10+
}> &
11+
ReadableBoxedValues<{
12+
snapPoints: (number | string)[];
13+
fadeFromIndex: number;
14+
drawerRef: HTMLElement | null;
15+
overlayRef: HTMLElement | null;
16+
direction: DrawerDirection;
17+
}> & {
18+
onSnapPointChange: (activeSnapPointIdx: number) => void;
19+
setActiveSnapPoint: (newValue: number | string | null) => void;
20+
};
21+
22+
export class SnapPoints {
23+
#activeSnapPoint: SnapPointsProps["activeSnapPoint"];
24+
#snapPoints: SnapPointsProps["snapPoints"];
25+
#fadeFromIndex: SnapPointsProps["fadeFromIndex"];
26+
#drawerRef: SnapPointsProps["drawerRef"];
27+
#overlayRef: SnapPointsProps["overlayRef"];
28+
#direction: SnapPointsProps["direction"];
29+
#onSnapPointChange: SnapPointsProps["onSnapPointChange"];
30+
#setActiveSnapPoint: SnapPointsProps["setActiveSnapPoint"];
31+
32+
constructor(props: SnapPointsProps) {
33+
this.#activeSnapPoint = props.activeSnapPoint;
34+
this.#snapPoints = props.snapPoints;
35+
this.#fadeFromIndex = props.fadeFromIndex;
36+
this.#drawerRef = props.drawerRef;
37+
this.#overlayRef = props.overlayRef;
38+
this.#direction = props.direction;
39+
this.#onSnapPointChange = props.onSnapPointChange;
40+
this.#setActiveSnapPoint = props.setActiveSnapPoint;
41+
42+
$effect(() => {
43+
const activeSnapPoint = this.#activeSnapPoint.current;
44+
const snapPoints = this.#snapPoints.current;
45+
const snapPointsOffset = this.snapPointsOffset;
46+
untrack(() => {
47+
const newIndex =
48+
snapPoints?.findIndex((snapPoint) => snapPoint === activeSnapPoint) ?? -1;
49+
if (
50+
snapPointsOffset &&
51+
newIndex !== -1 &&
52+
typeof snapPointsOffset[newIndex] === "number"
53+
) {
54+
this.snapToPoint(snapPointsOffset[newIndex]);
55+
}
56+
});
57+
});
58+
}
59+
60+
isLastSnapPoint = $derived.by(() => {
61+
const activeSnapPoint = this.#activeSnapPoint.current;
62+
const snapPoints = this.#snapPoints.current;
63+
return activeSnapPoint === snapPoints?.[snapPoints.length - 1] || null;
64+
});
65+
66+
shouldFade = $derived.by(() => {
67+
const snapPoints = this.#snapPoints.current;
68+
const fadeFromIndex = this.#fadeFromIndex.current;
69+
const activeSnapPoint = this.#activeSnapPoint.current;
70+
return (
71+
(snapPoints &&
72+
snapPoints.length > 0 &&
73+
(fadeFromIndex || fadeFromIndex === 0) &&
74+
!Number.isNaN(fadeFromIndex) &&
75+
snapPoints[fadeFromIndex] === activeSnapPoint) ||
76+
!snapPoints
77+
);
78+
});
79+
80+
activeSnapPointIndex = $derived.by(() => {
81+
const snapPoints = this.#snapPoints.current;
82+
const activeSnapPoint = this.#activeSnapPoint.current;
83+
return snapPoints?.findIndex((snapPoint) => snapPoint === activeSnapPoint) ?? null;
84+
});
85+
86+
snapPointsOffset = $derived.by(() => {
87+
const snapPoints = this.#snapPoints.current;
88+
if (!snapPoints) return [];
89+
const direction = this.#direction.current;
90+
return snapPoints.map((snapPoint) => {
91+
const hasWindow = typeof window !== "undefined";
92+
const isPx = typeof snapPoint === "string";
93+
let snapPointAsNumber = 0;
94+
95+
if (isPx) {
96+
snapPointAsNumber = Number.parseInt(snapPoint, 10);
97+
}
98+
99+
if (isVertical(direction)) {
100+
const height = isPx
101+
? snapPointAsNumber
102+
: hasWindow
103+
? snapPoint * window.innerHeight
104+
: 0;
105+
106+
if (hasWindow) {
107+
return direction === "bottom"
108+
? window.innerHeight - height
109+
: -window.innerHeight + height;
110+
}
111+
return height;
112+
}
113+
const width = isPx ? snapPointAsNumber : hasWindow ? snapPoint * window.innerWidth : 0;
114+
115+
if (hasWindow) {
116+
return direction === "right"
117+
? window.innerWidth - width
118+
: -window.innerWidth + width;
119+
}
120+
121+
return width;
122+
});
123+
});
124+
125+
activeSnapPointOffset = $derived.by(() => {
126+
const activeSnapPointIndex = this.activeSnapPointIndex;
127+
const snapPointsOffset = this.snapPointsOffset;
128+
return activeSnapPointIndex !== null ? snapPointsOffset?.[activeSnapPointIndex] : null;
129+
});
130+
131+
snapToPoint = (dimension: number) => {
132+
const snapPointsOffset = this.snapPointsOffset;
133+
const onSnapPointChange = this.#onSnapPointChange;
134+
const newSnapPointIndex =
135+
snapPointsOffset?.findIndex((snapPointDim) => snapPointDim === dimension) ?? null;
136+
onSnapPointChange(newSnapPointIndex);
137+
const drawerNode = this.#drawerRef.current;
138+
const direction = this.#direction.current;
139+
setStyles(drawerNode, {
140+
transition: `transform ${TRANSITIONS.DURATION}s cubic-bezier(${TRANSITIONS.EASE.join(",")})`,
141+
transform: isVertical(direction)
142+
? `translate3d(0, ${dimension}px, 0)`
143+
: `translate3d(${dimension}px, 0, 0)`,
144+
});
145+
146+
if (
147+
snapPointsOffset &&
148+
newSnapPointIndex !== snapPointsOffset.length - 1 &&
149+
newSnapPointIndex !== this.#fadeFromIndex.current
150+
) {
151+
setStyles(this.#overlayRef.current, {
152+
transition: `opacity ${TRANSITIONS.DURATION}s cubic-bezier(${TRANSITIONS.EASE.join(",")})`,
153+
opacity: "0",
154+
});
155+
} else {
156+
setStyles(this.#overlayRef.current, {
157+
transition: `opacity ${TRANSITIONS.DURATION}s cubic-bezier(${TRANSITIONS.EASE.join(",")})`,
158+
opacity: "1",
159+
});
160+
}
161+
162+
this.#setActiveSnapPoint(
163+
newSnapPointIndex !== null ? this.#snapPoints.current?.[newSnapPointIndex] : null
164+
);
165+
};
166+
167+
onReleaseSnapPoints = ({
168+
draggedDistance,
169+
closeDrawer,
170+
velocity,
171+
dismissible,
172+
}: {
173+
draggedDistance: number;
174+
closeDrawer: () => void;
175+
velocity: number;
176+
dismissible: boolean;
177+
}) => {
178+
if (this.#fadeFromIndex.current === undefined) return;
179+
const direction = this.#direction.current;
180+
const activeSnapPointOffset = this.activeSnapPointOffset;
181+
const activeSnapPointIndex = this.activeSnapPointIndex;
182+
const fadeFromIndex = this.#fadeFromIndex.current;
183+
184+
const currentPosition =
185+
direction === "bottom" || direction === "right"
186+
? (activeSnapPointOffset ?? 0) - draggedDistance
187+
: (activeSnapPointOffset ?? 0) + draggedDistance;
188+
const isOverlaySnapPoint = activeSnapPointIndex === fadeFromIndex - 1;
189+
const isFirst = activeSnapPointIndex === 0;
190+
const hasDraggedUp = draggedDistance > 0;
191+
192+
if (isOverlaySnapPoint) {
193+
setStyles(this.#overlayRef.current, {
194+
transition: `opacity ${TRANSITIONS.DURATION}s cubic-bezier(${TRANSITIONS.EASE.join(",")})`,
195+
});
196+
}
197+
198+
if (velocity > 2 && !hasDraggedUp) {
199+
if (dismissible) closeDrawer();
200+
else this.snapToPoint(this.snapPointsOffset[0]); // snap to initial point
201+
return;
202+
}
203+
204+
if (velocity > 2 && hasDraggedUp && this.snapPointsOffset && this.#snapPoints.current) {
205+
this.snapToPoint(this.snapPointsOffset[this.#snapPoints.current.length - 1] as number);
206+
return;
207+
}
208+
209+
// Find the closest snap point to the current position
210+
const closestSnapPoint = this.snapPointsOffset?.reduce((prev, curr) => {
211+
if (typeof prev !== "number" || typeof curr !== "number") return prev;
212+
213+
return Math.abs(curr - currentPosition) < Math.abs(prev - currentPosition)
214+
? curr
215+
: prev;
216+
});
217+
218+
const dim = isVertical(direction) ? window.innerHeight : window.innerWidth;
219+
if (velocity > VELOCITY_THRESHOLD && Math.abs(draggedDistance) < dim * 0.4) {
220+
const dragDirection = hasDraggedUp ? 1 : -1; // 1 = up, -1 = down
221+
222+
// Don't do anything if we swipe upwards while being on the last snap point
223+
if (dragDirection > 0 && this.isLastSnapPoint) {
224+
this.snapToPoint(this.snapPointsOffset[this.#snapPoints.current.length - 1]);
225+
return;
226+
}
227+
228+
if (isFirst && dragDirection < 0 && dismissible) {
229+
closeDrawer();
230+
}
231+
232+
if (activeSnapPointIndex === null) return;
233+
234+
this.snapToPoint(this.snapPointsOffset[activeSnapPointIndex + dragDirection]);
235+
return;
236+
}
237+
238+
this.snapToPoint(closestSnapPoint);
239+
};
240+
241+
onDragSnapPoints = ({ draggedDistance }: { draggedDistance: number }) => {
242+
if (this.activeSnapPointOffset === null) return;
243+
const direction = this.#direction.current;
244+
const newValue =
245+
direction === "bottom" || direction === "right"
246+
? this.activeSnapPointOffset - draggedDistance
247+
: this.activeSnapPointOffset + draggedDistance;
248+
249+
// Don't do anything if we exceed the last(biggest) snap point
250+
if (
251+
(direction === "bottom" || direction === "right") &&
252+
newValue < this.snapPointsOffset[this.snapPointsOffset.length - 1]
253+
) {
254+
return;
255+
}
256+
if (
257+
(direction === "top" || direction === "left") &&
258+
newValue > this.snapPointsOffset[this.snapPointsOffset.length - 1]
259+
) {
260+
return;
261+
}
262+
263+
setStyles(this.#drawerRef.current, {
264+
transform: isVertical(direction)
265+
? `translate3d(0, ${newValue}px, 0)`
266+
: `translate3d(${newValue}px, 0, 0)`,
267+
});
268+
};
269+
270+
getPercentageDragged = (absDraggedDistance: number, isDraggingDown: boolean) => {
271+
const snapPoints = this.#snapPoints.current;
272+
const activeSnapPointIndex = this.activeSnapPointIndex;
273+
const snapPointsOffset = this.snapPointsOffset;
274+
const fadeFromIndex = this.#fadeFromIndex.current;
275+
if (
276+
!snapPoints ||
277+
typeof activeSnapPointIndex !== "number" ||
278+
!snapPointsOffset ||
279+
fadeFromIndex === undefined
280+
)
281+
return null;
282+
283+
// If this is true we are dragging to a snap point that is supposed to have an overlay
284+
const isOverlaySnapPoint = activeSnapPointIndex === fadeFromIndex - 1;
285+
const isOverlaySnapPointOrHigher = activeSnapPointIndex >= fadeFromIndex;
286+
287+
if (isOverlaySnapPointOrHigher && isDraggingDown) {
288+
return 0;
289+
}
290+
291+
// Don't animate, but still use this one if we are dragging away from the overlaySnapPoint
292+
if (isOverlaySnapPoint && !isDraggingDown) return 1;
293+
if (!this.shouldFade && !isOverlaySnapPoint) return null;
294+
295+
// Either fadeFrom index or the one before
296+
const targetSnapPointIndex = isOverlaySnapPoint
297+
? activeSnapPointIndex + 1
298+
: activeSnapPointIndex - 1;
299+
300+
// Get the distance from overlaySnapPoint to the one before or vice-versa to calculate the opacity percentage accordingly
301+
const snapPointDistance = isOverlaySnapPoint
302+
? snapPointsOffset[targetSnapPointIndex] - snapPointsOffset[targetSnapPointIndex - 1]
303+
: snapPointsOffset[targetSnapPointIndex + 1] - snapPointsOffset[targetSnapPointIndex];
304+
305+
const percentageDragged = absDraggedDistance / Math.abs(snapPointDistance);
306+
307+
if (isOverlaySnapPoint) {
308+
return 1 - percentageDragged;
309+
} else {
310+
return percentageDragged;
311+
}
312+
};
313+
}

0 commit comments

Comments
 (0)