@abhivarde/svelte-drawer 0.0.14 → 0.0.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -7,18 +7,19 @@ A drawer component for Svelte 5, inspired by [Vaul](https://github.com/emilkowal
|
|
|
7
7
|
|
|
8
8
|
## Features
|
|
9
9
|
|
|
10
|
-
- ✅ Smooth animations
|
|
10
|
+
- ✅ Smooth animations with **gesture-driven dragging** (mouse & touch)
|
|
11
11
|
- ✅ Mobile-optimized drag handling with **scroll prevention**
|
|
12
|
-
- ✅
|
|
13
|
-
- ✅ Prebuilt variants (default, sheet, dialog, minimal, sidebar)
|
|
12
|
+
- ✅ Support for multiple directions (**bottom, top, left, right**)
|
|
13
|
+
- ✅ Prebuilt variants (**default, sheet, dialog, minimal, sidebar**)
|
|
14
14
|
- ✅ **Drag handle component** with auto-adaptive orientation
|
|
15
|
-
- ✅
|
|
16
|
-
- ✅
|
|
17
|
-
- ✅
|
|
18
|
-
- ✅
|
|
15
|
+
- ✅ **Snap points** for iOS-like multi-height drawers
|
|
16
|
+
- ✅ Nested drawer support
|
|
17
|
+
- ✅ Scrollable content areas
|
|
18
|
+
- ✅ Keyboard shortcuts (**Escape to close**, Tab navigation)
|
|
19
|
+
- ✅ Focus management (**auto-focus, focus trap, focus restoration**)
|
|
19
20
|
- ✅ Fully accessible with keyboard navigation
|
|
20
|
-
- ✅ TypeScript support
|
|
21
|
-
- ✅ Customizable styling with Tailwind CSS
|
|
21
|
+
- ✅ Full **TypeScript** support
|
|
22
|
+
- ✅ Customizable styling with **Tailwind CSS**
|
|
22
23
|
|
|
23
24
|
## Installation
|
|
24
25
|
|
|
@@ -192,6 +193,44 @@ npm install @abhivarde/svelte-drawer
|
|
|
192
193
|
</Drawer>
|
|
193
194
|
```
|
|
194
195
|
|
|
196
|
+
### Snap Points
|
|
197
|
+
|
|
198
|
+
Snap points allow the drawer to rest at predefined heights, creating an iOS-like sheet experience.
|
|
199
|
+
|
|
200
|
+
```svelte
|
|
201
|
+
<script>
|
|
202
|
+
import { Drawer, DrawerOverlay, DrawerContent, DrawerHandle } from '@abhivarde/svelte-drawer';
|
|
203
|
+
|
|
204
|
+
let open = $state(false);
|
|
205
|
+
let activeSnapPoint = $state(undefined);
|
|
206
|
+
</script>
|
|
207
|
+
|
|
208
|
+
<Drawer
|
|
209
|
+
bind:open
|
|
210
|
+
snapPoints={[0.25, 0.5, 0.9]}
|
|
211
|
+
bind:activeSnapPoint
|
|
212
|
+
onSnapPointChange={(point) => console.log('Snapped to:', point)}
|
|
213
|
+
>
|
|
214
|
+
<DrawerOverlay class="fixed inset-0 bg-black/40" />
|
|
215
|
+
<DrawerContent class="fixed bottom-0 left-0 right-0 bg-white rounded-t-lg p-4">
|
|
216
|
+
<DrawerHandle class="mb-8" />
|
|
217
|
+
<h2>Drawer with Snap Points</h2>
|
|
218
|
+
<p>Drag to see snapping behavior at 25%, 50%, and 90%</p>
|
|
219
|
+
|
|
220
|
+
<!-- Programmatically change snap point -->
|
|
221
|
+
<button onclick={() => activeSnapPoint = 0.5}>Jump to 50%</button>
|
|
222
|
+
</DrawerContent>
|
|
223
|
+
</Drawer>
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
**How it works:**
|
|
227
|
+
|
|
228
|
+
- Snap point values range from 0 to 1 (e.g., `0.5` = 50% of screen height)
|
|
229
|
+
- The drawer automatically snaps to the nearest point when released
|
|
230
|
+
- Dragging beyond the lowest snap point dismisses the drawer
|
|
231
|
+
- Use `bind:activeSnapPoint` to programmatically control the current position
|
|
232
|
+
- Use `onSnapPointChange` callback to react to snap changes
|
|
233
|
+
|
|
195
234
|
## Variants
|
|
196
235
|
|
|
197
236
|
Available variants for `DrawerVariants` component:
|
|
@@ -220,6 +259,9 @@ Main wrapper component that manages drawer state and animations.
|
|
|
220
259
|
- `onOpenChange` (function, optional) - Callback when open state changes
|
|
221
260
|
- `direction` ('bottom' | 'top' | 'left' | 'right', default: 'bottom') - Direction from which drawer slides
|
|
222
261
|
- `closeOnEscape` (boolean, optional, default: true) - Whether Escape key closes the drawer
|
|
262
|
+
- `snapPoints` (number[], optional) - Array of snap positions between 0-1, where 1 is fully open (e.g., `[0.25, 0.5, 0.9]`)
|
|
263
|
+
- `activeSnapPoint` (number, bindable, optional) - Current active snap point value
|
|
264
|
+
- `onSnapPointChange` (function, optional) - Callback fired when the drawer snaps to a different point
|
|
223
265
|
|
|
224
266
|
### DrawerOverlay
|
|
225
267
|
|
|
@@ -8,6 +8,9 @@
|
|
|
8
8
|
onOpenChange = undefined,
|
|
9
9
|
direction = "bottom",
|
|
10
10
|
closeOnEscape = true,
|
|
11
|
+
snapPoints = undefined,
|
|
12
|
+
activeSnapPoint = $bindable(undefined),
|
|
13
|
+
onSnapPointChange = undefined,
|
|
11
14
|
children,
|
|
12
15
|
} = $props();
|
|
13
16
|
|
|
@@ -33,6 +36,14 @@
|
|
|
33
36
|
|
|
34
37
|
overlayOpacity.set(1);
|
|
35
38
|
drawerPosition.set(0);
|
|
39
|
+
|
|
40
|
+
if (
|
|
41
|
+
snapPoints &&
|
|
42
|
+
snapPoints.length > 0 &&
|
|
43
|
+
activeSnapPoint === undefined
|
|
44
|
+
) {
|
|
45
|
+
activeSnapPoint = snapPoints[snapPoints.length - 1];
|
|
46
|
+
}
|
|
36
47
|
} else if (visible) {
|
|
37
48
|
overlayOpacity.set(0, { duration: 120 });
|
|
38
49
|
drawerPosition.set(100, { duration: 180 });
|
|
@@ -86,6 +97,16 @@
|
|
|
86
97
|
get direction() {
|
|
87
98
|
return direction;
|
|
88
99
|
},
|
|
100
|
+
get snapPoints() {
|
|
101
|
+
return snapPoints;
|
|
102
|
+
},
|
|
103
|
+
get activeSnapPoint() {
|
|
104
|
+
return activeSnapPoint;
|
|
105
|
+
},
|
|
106
|
+
setActiveSnapPoint(point: number) {
|
|
107
|
+
activeSnapPoint = point;
|
|
108
|
+
onSnapPointChange?.(point);
|
|
109
|
+
},
|
|
89
110
|
closeDrawer,
|
|
90
111
|
});
|
|
91
112
|
</script>
|
|
@@ -3,7 +3,10 @@ declare const Drawer: import("svelte").Component<{
|
|
|
3
3
|
onOpenChange?: any;
|
|
4
4
|
direction?: string;
|
|
5
5
|
closeOnEscape?: boolean;
|
|
6
|
+
snapPoints?: any;
|
|
7
|
+
activeSnapPoint?: any;
|
|
8
|
+
onSnapPointChange?: any;
|
|
6
9
|
children: any;
|
|
7
|
-
}, {}, "open">;
|
|
10
|
+
}, {}, "open" | "activeSnapPoint">;
|
|
8
11
|
type Drawer = ReturnType<typeof Drawer>;
|
|
9
12
|
export default Drawer;
|
|
@@ -8,6 +8,9 @@
|
|
|
8
8
|
drawerPosition: { current: number; set: (v: number, opts?: any) => void };
|
|
9
9
|
direction: "bottom" | "top" | "left" | "right";
|
|
10
10
|
closeDrawer: () => void;
|
|
11
|
+
snapPoints?: number[];
|
|
12
|
+
activeSnapPoint?: number;
|
|
13
|
+
setActiveSnapPoint?: (point: number) => void;
|
|
11
14
|
};
|
|
12
15
|
|
|
13
16
|
let {
|
|
@@ -24,8 +27,53 @@
|
|
|
24
27
|
let startDragPos = 0;
|
|
25
28
|
let dragging = false;
|
|
26
29
|
|
|
30
|
+
function snapPointToPosition(snapPoint: number): number {
|
|
31
|
+
// snapPoint is 0-1 where 1 = fully open (0% position)
|
|
32
|
+
// Convert: 1 -> 0%, 0.5 -> 50%, 0 -> 100%
|
|
33
|
+
return (1 - snapPoint) * 100;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function findNearestSnapPoint(currentPos: number): number {
|
|
37
|
+
if (!drawer.snapPoints || drawer.snapPoints.length === 0) {
|
|
38
|
+
return currentPos;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const currentSnapValue = 1 - currentPos / 100;
|
|
42
|
+
let nearest = drawer.snapPoints[0];
|
|
43
|
+
let minDiff = Math.abs(currentSnapValue - nearest);
|
|
44
|
+
|
|
45
|
+
for (const snapPoint of drawer.snapPoints) {
|
|
46
|
+
const diff = Math.abs(currentSnapValue - snapPoint);
|
|
47
|
+
if (diff < minDiff) {
|
|
48
|
+
minDiff = diff;
|
|
49
|
+
nearest = snapPoint;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return nearest;
|
|
54
|
+
}
|
|
55
|
+
|
|
27
56
|
function getTransform(): string {
|
|
28
57
|
const pos = drawer.drawerPosition.current;
|
|
58
|
+
|
|
59
|
+
if (
|
|
60
|
+
drawer.snapPoints &&
|
|
61
|
+
drawer.activeSnapPoint !== undefined &&
|
|
62
|
+
!dragging
|
|
63
|
+
) {
|
|
64
|
+
const snapPos = snapPointToPosition(drawer.activeSnapPoint);
|
|
65
|
+
switch (drawer.direction) {
|
|
66
|
+
case "bottom":
|
|
67
|
+
return `translateY(${snapPos}%)`;
|
|
68
|
+
case "top":
|
|
69
|
+
return `translateY(-${snapPos}%)`;
|
|
70
|
+
case "left":
|
|
71
|
+
return `translateX(-${snapPos}%)`;
|
|
72
|
+
case "right":
|
|
73
|
+
return `translateX(${snapPos}%)`;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
29
77
|
switch (drawer.direction) {
|
|
30
78
|
case "bottom":
|
|
31
79
|
return `translateY(${pos}%)`;
|
|
@@ -40,10 +88,10 @@
|
|
|
40
88
|
|
|
41
89
|
function onPointerDown(e: PointerEvent | TouchEvent) {
|
|
42
90
|
const target = e.target as HTMLElement;
|
|
43
|
-
|
|
91
|
+
|
|
44
92
|
if (
|
|
45
|
-
target.closest(
|
|
46
|
-
!target.closest(
|
|
93
|
+
target.closest("button, a, input, textarea, select") &&
|
|
94
|
+
!target.closest("[data-drawer-drag]")
|
|
47
95
|
) {
|
|
48
96
|
return;
|
|
49
97
|
}
|
|
@@ -101,15 +149,30 @@
|
|
|
101
149
|
|
|
102
150
|
function onPointerUp() {
|
|
103
151
|
if (!dragging) return;
|
|
104
|
-
|
|
152
|
+
|
|
105
153
|
dragging = false;
|
|
106
154
|
|
|
107
155
|
const pos = drawer.drawerPosition.current;
|
|
108
156
|
|
|
109
|
-
if (
|
|
110
|
-
|
|
157
|
+
if (drawer.snapPoints && drawer.snapPoints.length > 0) {
|
|
158
|
+
const nearestSnapPoint = findNearestSnapPoint(pos);
|
|
159
|
+
const snapPos = snapPointToPosition(nearestSnapPoint);
|
|
160
|
+
|
|
161
|
+
const lowestSnapPoint = Math.min(...drawer.snapPoints);
|
|
162
|
+
const lowestSnapPos = snapPointToPosition(lowestSnapPoint);
|
|
163
|
+
|
|
164
|
+
if (pos > lowestSnapPos + 30) {
|
|
165
|
+
drawer.closeDrawer();
|
|
166
|
+
} else {
|
|
167
|
+
drawer.drawerPosition.set(snapPos);
|
|
168
|
+
drawer.setActiveSnapPoint?.(nearestSnapPoint);
|
|
169
|
+
}
|
|
111
170
|
} else {
|
|
112
|
-
|
|
171
|
+
if (pos > 30) {
|
|
172
|
+
drawer.closeDrawer();
|
|
173
|
+
} else {
|
|
174
|
+
drawer.drawerPosition.set(0);
|
|
175
|
+
}
|
|
113
176
|
}
|
|
114
177
|
|
|
115
178
|
window.removeEventListener("pointermove", onPointerMove);
|
|
@@ -180,4 +243,4 @@
|
|
|
180
243
|
>
|
|
181
244
|
{@render children()}
|
|
182
245
|
</div>
|
|
183
|
-
{/if}
|
|
246
|
+
{/if}
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
|
|
16
16
|
const defaultClasses = $derived(
|
|
17
17
|
isVertical
|
|
18
|
-
? "mx-auto w-12 h-1.5 flex-shrink-0 rounded-full bg-gray-300"
|
|
19
|
-
: "my-auto w-1.5 h-12 flex-shrink-0 rounded-full bg-gray-300"
|
|
18
|
+
? "mx-auto w-12 h-1.5 flex-shrink-0 rounded-full bg-gray-300 cursor-grab active:cursor-grabbing"
|
|
19
|
+
: "my-auto w-1.5 h-12 flex-shrink-0 rounded-full bg-gray-300 cursor-grab active:cursor-grabbing"
|
|
20
20
|
);
|
|
21
21
|
|
|
22
22
|
const combinedClass = $derived(
|
package/dist/types.d.ts
CHANGED
|
@@ -3,6 +3,9 @@ export interface DrawerProps {
|
|
|
3
3
|
onOpenChange?: (open: boolean) => void;
|
|
4
4
|
direction?: "bottom" | "top" | "left" | "right";
|
|
5
5
|
closeOnEscape?: boolean;
|
|
6
|
+
snapPoints?: number[];
|
|
7
|
+
activeSnapPoint?: number;
|
|
8
|
+
onSnapPointChange?: (snapPoint: number) => void;
|
|
6
9
|
}
|
|
7
10
|
export interface DrawerContentProps {
|
|
8
11
|
class?: string;
|