@abhivarde/svelte-drawer 0.0.2 → 0.0.4

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
@@ -1,125 +1,161 @@
1
- # Svelte Drawer
2
-
3
- A drawer component for Svelte 5, inspired by [Vaul](https://github.com/emilkowalski/vaul).
4
-
5
- ## Features
6
-
7
- - ✅ Smooth animations using Svelte 5's Spring motion
8
- - ✅ Multiple directions (bottom, top, left, right)
9
- - ✅ Nested drawers support
10
- - ✅ Scrollable content
11
- - ✅ Fully accessible with keyboard navigation
12
- - ✅ TypeScript support
13
- - ✅ Customizable styling with Tailwind CSS
14
-
15
- ## Installation
16
-
17
- ```bash
18
- npm install @abhivarde/svelte-drawer
19
- ```
20
-
21
- ## Usage
22
-
23
- ### Basic Example
24
-
25
- ```svelte
26
- <script>
27
- import { Drawer, DrawerOverlay, DrawerContent } from '@abhivarde/svelte-drawer';
28
-
29
- let open = $state(false);
30
- </script>
31
-
32
- <button onclick={() => open = true}>
33
- Open Drawer
34
- </button>
35
-
36
- <Drawer bind:open>
37
- <DrawerOverlay class="fixed inset-0 bg-black/40" />
38
- <DrawerContent class="fixed bottom-0 left-0 right-0 bg-white rounded-t-lg p-4">
39
- <h2>Drawer Content</h2>
40
- <p>This is a drawer component.</p>
41
- <button onclick={() => open = false}>Close</button>
42
- </DrawerContent>
43
- </Drawer>
44
- ```
45
-
46
- ### Side Drawer
47
-
48
- ```svelte
49
- <script>
50
- import { Drawer, DrawerOverlay, DrawerContent } from '@abhivarde/svelte-drawer';
51
-
52
- let open = $state(false);
53
- </script>
54
-
55
- <Drawer bind:open direction="right">
56
- <DrawerOverlay class="fixed inset-0 bg-black/40" />
57
- <DrawerContent class="fixed right-0 top-0 bottom-0 w-80 bg-white p-4">
58
- <h2>Side Drawer</h2>
59
- <button onclick={() => open = false}>Close</button>
60
- </DrawerContent>
61
- </Drawer>
62
- ```
63
-
64
- ### Controlled Drawer
65
-
66
- ```svelte
67
- <script>
68
- import { Drawer, DrawerOverlay, DrawerContent } from '@abhivarde/svelte-drawer';
69
-
70
- let open = $state(false);
71
-
72
- function handleOpenChange(isOpen) {
73
- console.log('Drawer is now:', isOpen ? 'open' : 'closed');
74
- open = isOpen;
75
- }
76
- </script>
77
-
78
- <Drawer bind:open onOpenChange={handleOpenChange}>
79
- <DrawerOverlay class="fixed inset-0 bg-black/40" />
80
- <DrawerContent class="fixed bottom-0 left-0 right-0 bg-white rounded-t-lg p-4">
81
- <h2>Controlled Drawer</h2>
82
- </DrawerContent>
83
- </Drawer>
84
- ```
85
-
86
- ## API Reference
87
-
88
- ### Drawer
89
-
90
- Main wrapper component that manages drawer state and animations.
91
-
92
- **Props:**
93
-
94
- - `open` (boolean, bindable) - Controls the open/closed state
95
- - `onOpenChange` (function, optional) - Callback when open state changes
96
- - `direction` ('bottom' | 'top' | 'left' | 'right', default: 'bottom') - Direction from which drawer slides
97
-
98
- ### DrawerOverlay
99
-
100
- Overlay component that appears behind the drawer.
101
-
102
- **Props:**
103
-
104
- - `class` (string, optional) - CSS classes for styling
105
-
106
- ### DrawerContent
107
-
108
- Content container for the drawer.
109
-
110
- **Props:**
111
-
112
- - `class` (string, optional) - CSS classes for styling
113
-
114
- ## Demo
115
-
116
- Visit [drawer.abhivarde.in](https://drawer.abhivarde.in) to see live examples.
117
-
118
- ## License
119
-
120
- This project is licensed under the MIT License.
121
- See the [LICENSE](./LICENSE) file for details.
122
-
123
- ## Credits
124
-
125
- Inspired by [Vaul](https://github.com/emilkowalski/vaul) by Emil Kowalski.
1
+ # Svelte Drawer
2
+
3
+ A drawer component for Svelte 5, inspired by [Vaul](https://github.com/emilkowalski/vaul).
4
+
5
+ ## Features
6
+
7
+ - ✅ Smooth animations using Svelte 5's Spring motion
8
+ - ✅ Multiple directions (bottom, top, left, right)
9
+ - ✅ Nested drawers support
10
+ - ✅ Scrollable content
11
+ - ✅ Keyboard shortcuts (Escape to close, Tab navigation)
12
+ - ✅ Focus management (auto-focus, focus trap, focus restoration)
13
+ - ✅ Fully accessible with keyboard navigation
14
+ - ✅ TypeScript support
15
+ - ✅ Customizable styling with Tailwind CSS
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ npm install @abhivarde/svelte-drawer
21
+ ```
22
+
23
+ ## Usage
24
+
25
+ ### Basic Example
26
+
27
+ ```svelte
28
+ <script>
29
+ import { Drawer, DrawerOverlay, DrawerContent } from '@abhivarde/svelte-drawer';
30
+
31
+ let open = $state(false);
32
+ </script>
33
+
34
+ <button onclick={() => open = true}>
35
+ Open Drawer
36
+ </button>
37
+
38
+ <Drawer bind:open>
39
+ <DrawerOverlay class="fixed inset-0 bg-black/40" />
40
+ <DrawerContent class="fixed bottom-0 left-0 right-0 bg-white rounded-t-lg p-4">
41
+ <h2>Drawer Content</h2>
42
+ <p>This is a drawer component.</p>
43
+ <button onclick={() => open = false}>Close</button>
44
+ </DrawerContent>
45
+ </Drawer>
46
+ ```
47
+
48
+ ### Side Drawer
49
+
50
+ ```svelte
51
+ <script>
52
+ import { Drawer, DrawerOverlay, DrawerContent } from '@abhivarde/svelte-drawer';
53
+
54
+ let open = $state(false);
55
+ </script>
56
+
57
+ <Drawer bind:open direction="right">
58
+ <DrawerOverlay class="fixed inset-0 bg-black/40" />
59
+ <DrawerContent class="fixed right-0 top-0 bottom-0 w-80 bg-white p-4">
60
+ <h2>Side Drawer</h2>
61
+ <button onclick={() => open = false}>Close</button>
62
+ </DrawerContent>
63
+ </Drawer>
64
+ ```
65
+
66
+ ### Controlled Drawer
67
+
68
+ ```svelte
69
+ <script>
70
+ import { Drawer, DrawerOverlay, DrawerContent } from '@abhivarde/svelte-drawer';
71
+
72
+ let open = $state(false);
73
+
74
+ function handleOpenChange(isOpen) {
75
+ console.log('Drawer is now:', isOpen ? 'open' : 'closed');
76
+ open = isOpen;
77
+ }
78
+ </script>
79
+
80
+ <Drawer bind:open onOpenChange={handleOpenChange}>
81
+ <DrawerOverlay class="fixed inset-0 bg-black/40" />
82
+ <DrawerContent class="fixed bottom-0 left-0 right-0 bg-white rounded-t-lg p-4">
83
+ <h2>Controlled Drawer</h2>
84
+ </DrawerContent>
85
+ </Drawer>
86
+ ```
87
+
88
+ ### Disable Keyboard Features
89
+
90
+ ```svelte
91
+ <script>
92
+ import { Drawer, DrawerOverlay, DrawerContent } from '@abhivarde/svelte-drawer';
93
+
94
+ let open = $state(false);
95
+ </script>
96
+
97
+ <!-- Disable Escape key -->
98
+ <Drawer bind:open closeOnEscape={false}>
99
+ <DrawerOverlay class="fixed inset-0 bg-black/40" />
100
+ <DrawerContent class="fixed bottom-0 left-0 right-0 bg-white rounded-t-lg p-4">
101
+ <h2>Cannot close with Escape</h2>
102
+ </DrawerContent>
103
+ </Drawer>
104
+
105
+ <!-- Disable focus trap -->
106
+ <Drawer bind:open>
107
+ <DrawerOverlay class="fixed inset-0 bg-black/40" />
108
+ <DrawerContent trapFocus={false} class="fixed bottom-0 left-0 right-0 bg-white rounded-t-lg p-4">
109
+ <h2>Tab navigation not restricted</h2>
110
+ </DrawerContent>
111
+ </Drawer>
112
+ ```
113
+
114
+ ## Keyboard Shortcuts
115
+
116
+ - **Escape** - Close the drawer (can be disabled with `closeOnEscape={false}`)
117
+ - **Tab / Shift+Tab** - Navigate between focusable elements inside the drawer
118
+ - **Enter / Space** (on overlay) - Close the drawer
119
+
120
+ ## API Reference
121
+
122
+ ### Drawer
123
+
124
+ Main wrapper component that manages drawer state and animations.
125
+
126
+ **Props:**
127
+
128
+ - `open` (boolean, bindable) - Controls the open/closed state
129
+ - `onOpenChange` (function, optional) - Callback when open state changes
130
+ - `direction` ('bottom' | 'top' | 'left' | 'right', default: 'bottom') - Direction from which drawer slides
131
+ - `closeOnEscape` (boolean, optional, default: true) - Whether Escape key closes the drawer
132
+
133
+ ### DrawerOverlay
134
+
135
+ Overlay component that appears behind the drawer.
136
+
137
+ **Props:**
138
+
139
+ - `class` (string, optional) - CSS classes for styling
140
+
141
+ ### DrawerContent
142
+
143
+ Content container for the drawer.
144
+
145
+ **Props:**
146
+
147
+ - `class` (string, optional) - CSS classes for styling
148
+ - `trapFocus` (boolean, optional, default: true) - Whether to trap focus inside drawer
149
+
150
+ ## Demo
151
+
152
+ Visit [drawer.abhivarde.in](https://drawer.abhivarde.in) to see live examples.
153
+
154
+ ## License
155
+
156
+ This project is licensed under the MIT License.
157
+ See the [LICENSE](./LICENSE) file for details.
158
+
159
+ ## Credits
160
+
161
+ Inspired by [Vaul](https://github.com/emilkowalski/vaul) by Emil Kowalski.
@@ -1,25 +1,34 @@
1
1
  <script lang="ts">
2
2
  import { Tween } from "svelte/motion";
3
3
  import { cubicOut } from "svelte/easing";
4
- import { setContext } from "svelte";
4
+ import { setContext, onMount } from "svelte";
5
5
 
6
6
  let {
7
7
  open = $bindable(false),
8
8
  onOpenChange = undefined,
9
9
  direction = "bottom",
10
+ closeOnEscape = true,
10
11
  children,
11
12
  } = $props();
12
13
 
13
14
  let overlayOpacity = new Tween(0, { duration: 300, easing: cubicOut });
14
15
  let drawerPosition = new Tween(100, { duration: 300, easing: cubicOut });
16
+ let previouslyFocusedElement: HTMLElement | null = null;
15
17
 
16
18
  $effect(() => {
17
19
  if (open) {
20
+ previouslyFocusedElement = document.activeElement as HTMLElement;
21
+
18
22
  overlayOpacity.set(1);
19
23
  drawerPosition.set(0);
20
24
  } else {
21
25
  overlayOpacity.set(0);
22
26
  drawerPosition.set(100);
27
+
28
+ if (previouslyFocusedElement) {
29
+ previouslyFocusedElement.focus();
30
+ previouslyFocusedElement = null;
31
+ }
23
32
  }
24
33
  });
25
34
 
@@ -28,6 +37,20 @@
28
37
  if (onOpenChange) onOpenChange(false);
29
38
  }
30
39
 
40
+ function handleKeydown(e: KeyboardEvent) {
41
+ if (open && closeOnEscape && e.key === "Escape") {
42
+ e.preventDefault();
43
+ closeDrawer();
44
+ }
45
+ }
46
+
47
+ onMount(() => {
48
+ window.addEventListener("keydown", handleKeydown);
49
+ return () => {
50
+ window.removeEventListener("keydown", handleKeydown);
51
+ };
52
+ });
53
+
31
54
  setContext("drawer", {
32
55
  get open() {
33
56
  return open;
@@ -2,6 +2,7 @@ declare const Drawer: import("svelte").Component<{
2
2
  open?: boolean;
3
3
  onOpenChange?: any;
4
4
  direction?: string;
5
+ closeOnEscape?: boolean;
5
6
  children: any;
6
7
  }, {}, "open">;
7
8
  type Drawer = ReturnType<typeof Drawer>;
@@ -1,7 +1,6 @@
1
1
  <script lang="ts">
2
- import { getContext } from "svelte";
2
+ import { getContext, onMount, tick } from "svelte";
3
3
 
4
- // Minimal inline type just to fix TypeScript
5
4
  type DrawerContext = {
6
5
  open: boolean;
7
6
  overlayOpacity: { current: number; set: (v: number) => void };
@@ -10,11 +9,16 @@
10
9
  closeDrawer: () => void;
11
10
  };
12
11
 
13
- let { class: className = "", children, ...restProps } = $props();
12
+ let {
13
+ class: className = "",
14
+ trapFocus = true,
15
+ children,
16
+ ...restProps
17
+ } = $props();
14
18
 
15
- // Tell TypeScript that context has this shape
16
19
  const drawer = getContext<DrawerContext>("drawer");
17
20
 
21
+ let contentElement = $state<HTMLDivElement | null>(null);
18
22
  let startPos = 0;
19
23
  let dragging = false;
20
24
 
@@ -36,6 +40,8 @@
36
40
 
37
41
  function onPointerDown(e: PointerEvent | TouchEvent) {
38
42
  dragging = true;
43
+ document.body.style.cursor = "grabbing";
44
+
39
45
  startPos =
40
46
  drawer.direction === "bottom" || drawer.direction === "top"
41
47
  ? "clientY" in e
@@ -51,6 +57,7 @@
51
57
 
52
58
  function onPointerMove(e: PointerEvent | TouchEvent) {
53
59
  if (!dragging) return;
60
+
54
61
  const current =
55
62
  drawer.direction === "bottom" || drawer.direction === "top"
56
63
  ? "clientY" in e
@@ -80,19 +87,91 @@
80
87
 
81
88
  function onPointerUp() {
82
89
  dragging = false;
83
- if (drawer.drawerPosition.current > 30) drawer.closeDrawer();
84
- else drawer.drawerPosition.set(0);
90
+ document.body.style.cursor = "default";
91
+
92
+ const pos = drawer.drawerPosition.current;
93
+ const deltaThreshold = 30;
94
+
95
+ if (pos > deltaThreshold) {
96
+ drawer.closeDrawer();
97
+ } else {
98
+ drawer.drawerPosition.set(0);
99
+ }
85
100
 
86
101
  window.removeEventListener("pointermove", onPointerMove);
87
102
  window.removeEventListener("pointerup", onPointerUp);
88
103
  }
104
+
105
+ function getFocusableElements(): HTMLElement[] {
106
+ if (!contentElement) return [];
107
+
108
+ const focusableSelectors = [
109
+ "a[href]",
110
+ "button:not([disabled])",
111
+ "textarea:not([disabled])",
112
+ "input:not([disabled])",
113
+ "select:not([disabled])",
114
+ '[tabindex]:not([tabindex="-1"])',
115
+ ];
116
+
117
+ return Array.from(
118
+ contentElement.querySelectorAll(focusableSelectors.join(","))
119
+ ) as HTMLElement[];
120
+ }
121
+
122
+ function handleFocusTrap(e: KeyboardEvent) {
123
+ if (!trapFocus || !drawer.open) return;
124
+ if (e.key !== "Tab") return;
125
+
126
+ const focusableElements = getFocusableElements();
127
+ if (focusableElements.length === 0) return;
128
+
129
+ const firstElement = focusableElements[0];
130
+ const lastElement = focusableElements[focusableElements.length - 1];
131
+
132
+ if (e.shiftKey) {
133
+ if (document.activeElement === firstElement) {
134
+ e.preventDefault();
135
+ lastElement.focus();
136
+ }
137
+ } else {
138
+ if (document.activeElement === lastElement) {
139
+ e.preventDefault();
140
+ firstElement.focus();
141
+ }
142
+ }
143
+ }
144
+
145
+ $effect(() => {
146
+ if (drawer.open && trapFocus && contentElement) {
147
+ tick().then(() => {
148
+ const focusableElements = getFocusableElements();
149
+ if (focusableElements.length > 0) {
150
+ focusableElements[0].focus();
151
+ } else {
152
+ contentElement?.focus();
153
+ }
154
+ });
155
+ }
156
+ });
157
+
158
+ onMount(() => {
159
+ window.addEventListener("keydown", handleFocusTrap);
160
+ return () => {
161
+ window.removeEventListener("keydown", handleFocusTrap);
162
+ };
163
+ });
89
164
  </script>
90
165
 
91
166
  {#if drawer.open}
92
167
  <div
168
+ bind:this={contentElement}
93
169
  class={className}
94
- style="transform: {getTransform()}; z-index: 50;"
170
+ style="transform: {getTransform()}; z-index: 50; cursor: grab;"
95
171
  onpointerdown={onPointerDown}
172
+ tabindex="-1"
173
+ role="dialog"
174
+ aria-modal="true"
96
175
  {...restProps}
97
176
  >
98
177
  {@render children()}
@@ -1,5 +1,6 @@
1
1
  declare const DrawerContent: import("svelte").Component<{
2
2
  class?: string;
3
+ trapFocus?: boolean;
3
4
  children: any;
4
5
  } & Record<string, any>, {}, "">;
5
6
  type DrawerContent = ReturnType<typeof DrawerContent>;
package/dist/types.d.ts CHANGED
@@ -2,9 +2,11 @@ export interface DrawerProps {
2
2
  open?: boolean;
3
3
  onOpenChange?: (open: boolean) => void;
4
4
  direction?: "bottom" | "top" | "left" | "right";
5
+ closeOnEscape?: boolean;
5
6
  }
6
7
  export interface DrawerContentProps {
7
8
  class?: string;
9
+ trapFocus?: boolean;
8
10
  }
9
11
  export interface DrawerOverlayProps {
10
12
  class?: string;
package/package.json CHANGED
@@ -1,65 +1,65 @@
1
- {
2
- "name": "@abhivarde/svelte-drawer",
3
- "version": "0.0.2",
4
- "description": "A drawer component for Svelte 5, inspired by Vaul",
5
- "author": "Abhi Varde",
6
- "license": "MIT",
7
- "type": "module",
8
- "scripts": {
9
- "dev": "vite dev",
10
- "build": "vite build && npm run package",
11
- "preview": "vite preview",
12
- "package": "svelte-kit sync && svelte-package && publint",
13
- "prepublishOnly": "npm run package",
14
- "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
15
- "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
16
- "test": "npm run test:integration && npm run test:unit",
17
- "lint": "prettier --check . && eslint .",
18
- "format": "prettier --write ."
19
- },
20
- "exports": {
21
- ".": {
22
- "types": "./dist/index.d.ts",
23
- "svelte": "./dist/index.js"
24
- }
25
- },
26
- "files": [
27
- "dist",
28
- "!dist/**/*.test.*",
29
- "!dist/**/*.spec.*"
30
- ],
31
- "peerDependencies": {
32
- "svelte": "^5.0.0"
33
- },
34
- "devDependencies": {
35
- "@sveltejs/adapter-auto": "^7.0.0",
36
- "@sveltejs/kit": "^2.49.1",
37
- "@sveltejs/package": "^2.5.7",
38
- "@sveltejs/vite-plugin-svelte": "^6.2.1",
39
- "publint": "^0.2.12",
40
- "svelte": "^5.45.6",
41
- "svelte-check": "^4.3.4",
42
- "typescript": "^5.9.3",
43
- "vite": "^7.2.6"
44
- },
45
- "svelte": "./dist/index.js",
46
- "types": "./dist/index.d.ts",
47
- "keywords": [
48
- "svelte",
49
- "svelte5",
50
- "drawer",
51
- "dialog",
52
- "modal",
53
- "sheet",
54
- "component",
55
- "ui"
56
- ],
57
- "repository": {
58
- "type": "git",
59
- "url": "https://github.com/AbhiVarde/svelte-drawer"
60
- },
61
- "bugs": {
62
- "url": "https://github.com/AbhiVarde/svelte-drawer/issues"
63
- },
64
- "homepage": "https://drawer.abhivarde.in"
65
- }
1
+ {
2
+ "name": "@abhivarde/svelte-drawer",
3
+ "version": "0.0.4",
4
+ "description": "A drawer component for Svelte 5, inspired by Vaul",
5
+ "author": "Abhi Varde",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "scripts": {
9
+ "dev": "vite dev",
10
+ "build": "vite build && npm run package",
11
+ "preview": "vite preview",
12
+ "package": "svelte-kit sync && svelte-package && publint",
13
+ "prepublishOnly": "npm run package",
14
+ "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
15
+ "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
16
+ "test": "npm run test:integration && npm run test:unit",
17
+ "lint": "prettier --check . && eslint .",
18
+ "format": "prettier --write ."
19
+ },
20
+ "exports": {
21
+ ".": {
22
+ "types": "./dist/index.d.ts",
23
+ "svelte": "./dist/index.js"
24
+ }
25
+ },
26
+ "files": [
27
+ "dist",
28
+ "!dist/**/*.test.*",
29
+ "!dist/**/*.spec.*"
30
+ ],
31
+ "peerDependencies": {
32
+ "svelte": "^5.0.0"
33
+ },
34
+ "devDependencies": {
35
+ "@sveltejs/adapter-auto": "^7.0.0",
36
+ "@sveltejs/kit": "^2.49.1",
37
+ "@sveltejs/package": "^2.5.7",
38
+ "@sveltejs/vite-plugin-svelte": "^6.2.1",
39
+ "publint": "^0.2.12",
40
+ "svelte": "^5.45.6",
41
+ "svelte-check": "^4.3.4",
42
+ "typescript": "^5.9.3",
43
+ "vite": "^7.2.6"
44
+ },
45
+ "svelte": "./dist/index.js",
46
+ "types": "./dist/index.d.ts",
47
+ "keywords": [
48
+ "svelte",
49
+ "svelte5",
50
+ "drawer",
51
+ "dialog",
52
+ "modal",
53
+ "sheet",
54
+ "component",
55
+ "ui"
56
+ ],
57
+ "repository": {
58
+ "type": "git",
59
+ "url": "https://github.com/AbhiVarde/svelte-drawer"
60
+ },
61
+ "bugs": {
62
+ "url": "https://github.com/AbhiVarde/svelte-drawer/issues"
63
+ },
64
+ "homepage": "https://drawer.abhivarde.in"
65
+ }