@hashrytech/quick-components-kit 0.17.2 → 0.17.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/CHANGELOG.md +12 -0
- package/dist/actions/portal.d.ts +2 -2
- package/dist/actions/portal.js +5 -5
- package/dist/actions/trap-focus.d.ts +25 -0
- package/dist/actions/trap-focus.js +91 -0
- package/dist/components/button/Button.svelte +1 -1
- package/dist/components/drawer/Drawer.svelte +56 -47
- package/dist/components/drawer/Drawer.svelte.d.ts +15 -21
- package/dist/components/modal/Modal.svelte +33 -11
- package/dist/components/modal/Modal.svelte.d.ts +2 -1
- package/dist/components/overlay/Overlay.svelte +2 -2
- package/dist/components/overlay/Overlay.svelte.d.ts +1 -1
- package/dist/components/overlay/index.d.ts +1 -1
- package/dist/components/overlay/index.js +1 -1
- package/dist/components/portal/Portal.svelte +17 -17
- package/dist/components/portal/Portal.svelte.d.ts +3 -3
- package/dist/components/table/Table.svelte +30 -7
- package/dist/components/table/Table.svelte.d.ts +1 -1
- package/dist/components/table/TableTh.svelte +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @hashrytech/quick-components-kit
|
|
2
2
|
|
|
3
|
+
## 0.17.4
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- patch: adding higher z-index for modal and drawer
|
|
8
|
+
|
|
9
|
+
## 0.17.3
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- refactor: Drawers, Modals, Tables, trap focus etc
|
|
14
|
+
|
|
3
15
|
## 0.17.2
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/dist/actions/portal.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export type PortalOptions = {
|
|
2
2
|
target?: HTMLElement;
|
|
3
|
-
|
|
3
|
+
append?: boolean;
|
|
4
4
|
};
|
|
5
|
-
export declare function portalAction(node: HTMLElement, { target,
|
|
5
|
+
export declare function portalAction(node: HTMLElement, { target, append }?: PortalOptions): {
|
|
6
6
|
destroy(): void;
|
|
7
7
|
};
|
package/dist/actions/portal.js
CHANGED
|
@@ -21,15 +21,15 @@
|
|
|
21
21
|
* @param node - The HTML element to be portaled.
|
|
22
22
|
* @param options - Optional config:
|
|
23
23
|
* - `target`: The DOM node to portal into (defaults to `document.body`)
|
|
24
|
-
* - `
|
|
24
|
+
* - `append`: Whether to insert at the end instead of appending to the begining (default: true)
|
|
25
25
|
*/
|
|
26
26
|
import { browser } from '$app/environment';
|
|
27
|
-
export function portalAction(node, { target = browser ? document.body : undefined,
|
|
28
|
-
if (
|
|
29
|
-
target?.
|
|
27
|
+
export function portalAction(node, { target = browser ? document.body : undefined, append = true } = {}) {
|
|
28
|
+
if (append) {
|
|
29
|
+
target?.appendChild(node);
|
|
30
30
|
}
|
|
31
31
|
else {
|
|
32
|
-
target?.
|
|
32
|
+
target?.prepend(node);
|
|
33
33
|
}
|
|
34
34
|
return {
|
|
35
35
|
destroy() {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export declare const FOCUSABLE_ELEMENTS: string[];
|
|
2
|
+
/**
|
|
3
|
+
* @action trapFocus
|
|
4
|
+
*
|
|
5
|
+
* A Svelte action to trap keyboard focus within a DOM node.
|
|
6
|
+
* Useful for modals, dialogs, and overlays to ensure accessibility and usability.
|
|
7
|
+
*
|
|
8
|
+
* Automatically focuses the first focusable element inside the node when mounted.
|
|
9
|
+
* Prevents `Tab` and `Shift+Tab` from leaving the focusable area.
|
|
10
|
+
* Restores focus to the previously focused element when destroyed.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```svelte
|
|
14
|
+
* <div use:trapFocus>
|
|
15
|
+
* <button>First</button>
|
|
16
|
+
* <button>Second</button>
|
|
17
|
+
* </div>
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* @param node HTMLElement to trap focus within
|
|
21
|
+
* @returns An object with a `destroy` method to clean up listeners and restore previous focus
|
|
22
|
+
*/
|
|
23
|
+
export declare function trapFocus(node: HTMLElement): {
|
|
24
|
+
destroy(): void;
|
|
25
|
+
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
export const FOCUSABLE_ELEMENTS = [
|
|
2
|
+
'a[href]',
|
|
3
|
+
'area[href]',
|
|
4
|
+
'input:not([disabled]):not([type="hidden"]):not([aria-hidden])',
|
|
5
|
+
'select:not([disabled]):not([aria-hidden])',
|
|
6
|
+
'textarea:not([disabled]):not([aria-hidden])',
|
|
7
|
+
'button:not([disabled]):not([aria-hidden])',
|
|
8
|
+
'iframe',
|
|
9
|
+
'object',
|
|
10
|
+
'embed',
|
|
11
|
+
'[contenteditable]',
|
|
12
|
+
'[tabindex]:not([tabindex^="-"])',
|
|
13
|
+
];
|
|
14
|
+
/**
|
|
15
|
+
* @action trapFocus
|
|
16
|
+
*
|
|
17
|
+
* A Svelte action to trap keyboard focus within a DOM node.
|
|
18
|
+
* Useful for modals, dialogs, and overlays to ensure accessibility and usability.
|
|
19
|
+
*
|
|
20
|
+
* Automatically focuses the first focusable element inside the node when mounted.
|
|
21
|
+
* Prevents `Tab` and `Shift+Tab` from leaving the focusable area.
|
|
22
|
+
* Restores focus to the previously focused element when destroyed.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```svelte
|
|
26
|
+
* <div use:trapFocus>
|
|
27
|
+
* <button>First</button>
|
|
28
|
+
* <button>Second</button>
|
|
29
|
+
* </div>
|
|
30
|
+
* ```
|
|
31
|
+
*
|
|
32
|
+
* @param node HTMLElement to trap focus within
|
|
33
|
+
* @returns An object with a `destroy` method to clean up listeners and restore previous focus
|
|
34
|
+
*/
|
|
35
|
+
export function trapFocus(node) {
|
|
36
|
+
const previous = document.activeElement;
|
|
37
|
+
function focusable() {
|
|
38
|
+
return Array.from(node.querySelectorAll('button, [href], input, select, textarea, iframe, object, embed, [contenteditable], [tabindex]:not([tabindex="-1"])')).filter(el => {
|
|
39
|
+
// Skip if disabled
|
|
40
|
+
if (el.hasAttribute('disabled'))
|
|
41
|
+
return false;
|
|
42
|
+
// Skip if hidden attribute is present
|
|
43
|
+
if (el.hidden)
|
|
44
|
+
return false;
|
|
45
|
+
// Skip if any parent is aria-hidden
|
|
46
|
+
let current = el;
|
|
47
|
+
while (current) {
|
|
48
|
+
if (current.getAttribute('aria-hidden') === 'true')
|
|
49
|
+
return false;
|
|
50
|
+
current = current.parentElement;
|
|
51
|
+
}
|
|
52
|
+
// Skip if not visible via CSS
|
|
53
|
+
const style = getComputedStyle(el);
|
|
54
|
+
if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0')
|
|
55
|
+
return false;
|
|
56
|
+
return true;
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
function handleKeydown(event) {
|
|
60
|
+
if (event.key !== 'Tab')
|
|
61
|
+
return;
|
|
62
|
+
const elements = focusable();
|
|
63
|
+
if (elements.length === 0)
|
|
64
|
+
return;
|
|
65
|
+
const first = elements[0];
|
|
66
|
+
const last = elements[elements.length - 1];
|
|
67
|
+
const active = document.activeElement;
|
|
68
|
+
if (event.shiftKey) {
|
|
69
|
+
if (active === first || !elements.includes(active)) {
|
|
70
|
+
last.focus();
|
|
71
|
+
event.preventDefault();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
if (active === last || !elements.includes(active)) {
|
|
76
|
+
first.focus();
|
|
77
|
+
event.preventDefault();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// Focus the first element and add keydown listener
|
|
82
|
+
const elements = focusable();
|
|
83
|
+
elements[0]?.focus();
|
|
84
|
+
node.addEventListener('keydown', handleKeydown);
|
|
85
|
+
return {
|
|
86
|
+
destroy() {
|
|
87
|
+
node.removeEventListener('keydown', handleKeydown);
|
|
88
|
+
previous?.focus();
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
}
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
|
|
21
21
|
</script>
|
|
22
22
|
|
|
23
|
-
<button {disabled} class={twMerge("flex flex-row items-center gap-2 px-4 py-2 bg-primary-button hover:bg-primary-button-hover rounded-primary w-fit cursor-pointer text-white focus:
|
|
23
|
+
<button {disabled} class={twMerge("flex flex-row items-center gap-2 px-4 py-2 focus:outline-primary-focus bg-primary-button hover:bg-primary-button-hover rounded-primary w-fit cursor-pointer text-white focus:ring-primary-focus focus:ring",
|
|
24
24
|
"disabled:bg-primary-button/60 disabled:cursor-default", props.class)}
|
|
25
25
|
{onclick}>
|
|
26
26
|
{#if icon}<span class="w-10">{@render icon()}</span>{/if}
|
|
@@ -1,65 +1,60 @@
|
|
|
1
1
|
<!--
|
|
2
2
|
@component Drawer
|
|
3
3
|
|
|
4
|
-
A flexible slide-in drawer component
|
|
4
|
+
A flexible, accessible slide-in drawer component for Svelte 5 using Tailwind CSS and transitions. Includes focus trapping, overlay support, inert background, and escape key handling.
|
|
5
5
|
|
|
6
6
|
## Props
|
|
7
7
|
|
|
8
8
|
- `open?`: `boolean` — Whether the drawer is visible. Bindable.
|
|
9
|
-
- `escapeKeyClose?`: `boolean` — If true
|
|
10
|
-
- `disableBodyScroll?`: `boolean` — Prevents body
|
|
11
|
-
- `
|
|
12
|
-
- `
|
|
13
|
-
- `
|
|
14
|
-
- `
|
|
15
|
-
- `
|
|
16
|
-
- `
|
|
17
|
-
- `
|
|
18
|
-
|
|
19
|
-
## Slots
|
|
20
|
-
|
|
21
|
-
This component uses `children` as a rendered snippet via `{@render}`.
|
|
22
|
-
|
|
23
|
-
## Overlay
|
|
24
|
-
|
|
25
|
-
The `<Overlay>` component is used to darken the background and optionally block interaction. It is clickable and will trigger the drawer to close unless stopped by event propagation. You can customize its styling using the `overlayClasses` prop.
|
|
9
|
+
- `escapeKeyClose?`: `boolean` — If `true`, allows closing the drawer via the Escape key. Default: `true`.
|
|
10
|
+
- `disableBodyScroll?`: `boolean` — Prevents body scroll while drawer is open. Default: `true`.
|
|
11
|
+
- `inertId?`: `string` — DOM element ID to set `inert` while the drawer is open (for accessibility).
|
|
12
|
+
- `ariaLabel?`: `string` — ARIA label for screen readers. Default: `"Drawer"`.
|
|
13
|
+
- `position?`: `"left" | "right" | "top" | "bottom"` — Drawer slide direction. Default: `"left"`.
|
|
14
|
+
- `transitionDuration?`: `number` — Drawer animation duration in milliseconds.
|
|
15
|
+
- `transitionDistance?`: `number` — Distance the drawer flies in from (px).
|
|
16
|
+
- `overlayClasses?`: `string` — Custom Tailwind classes for the backdrop overlay.
|
|
17
|
+
- `class?`: `ClassNameValue` — Classes passed to the drawer container.
|
|
18
|
+
- `children?`: `Snippet` — Svelte content rendered inside the drawer.
|
|
26
19
|
|
|
27
20
|
## Usage
|
|
28
21
|
|
|
29
22
|
```svelte
|
|
30
23
|
<script>
|
|
31
|
-
import Drawer from '@
|
|
32
|
-
let
|
|
24
|
+
import { Drawer } from '@your-lib';
|
|
25
|
+
let open = false;
|
|
33
26
|
</script>
|
|
34
27
|
|
|
35
|
-
<Drawer bind:open={
|
|
28
|
+
<Drawer bind:open={open} position="right" inertId="main-content">
|
|
36
29
|
<p class="p-4">Drawer content goes here</p>
|
|
37
30
|
</Drawer>
|
|
38
31
|
```
|
|
39
32
|
-->
|
|
40
33
|
|
|
41
34
|
<script lang="ts" module>
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
35
|
+
import { type Snippet } from 'svelte';
|
|
36
|
+
import type { ClassNameValue } from 'tailwind-merge';
|
|
37
|
+
import { fly } from 'svelte/transition';
|
|
38
|
+
import { twMerge } from 'tailwind-merge';
|
|
39
|
+
import { Overlay } from '../overlay/index.js';
|
|
40
|
+
import { onKeydown } from '../../actions/on-keydown.js';
|
|
41
|
+
import { config } from '../../configs/config.js';
|
|
42
|
+
import { Portal } from "../portal/index.js";
|
|
43
|
+
import { trapFocus } from '../../actions/trap-focus.js';
|
|
50
44
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
45
|
+
export type DrawerProps = {
|
|
46
|
+
open?: boolean;
|
|
47
|
+
escapeKeyClose?: boolean;
|
|
48
|
+
disableBodyScroll?: boolean;
|
|
49
|
+
inertId?: string;
|
|
50
|
+
ariaLabel?: string;
|
|
51
|
+
transitionDuration?: number;
|
|
52
|
+
transitionDistance?: number;
|
|
53
|
+
position?: "left" | "right" | "top" | "bottom";
|
|
54
|
+
overlayClasses?: string;
|
|
55
|
+
children?: Snippet;
|
|
56
|
+
class?: ClassNameValue;
|
|
57
|
+
};
|
|
63
58
|
|
|
64
59
|
</script>
|
|
65
60
|
|
|
@@ -68,6 +63,7 @@ The `<Overlay>` component is used to darken the background and optionally block
|
|
|
68
63
|
open=$bindable(false),
|
|
69
64
|
escapeKeyClose=true,
|
|
70
65
|
disableBodyScroll=true,
|
|
66
|
+
inertId,
|
|
71
67
|
ariaLabel="Drawer",
|
|
72
68
|
position="left",
|
|
73
69
|
transitionDuration=config.transitionDuration,
|
|
@@ -77,6 +73,8 @@ The `<Overlay>` component is used to darken the background and optionally block
|
|
|
77
73
|
...props
|
|
78
74
|
}: DrawerProps = $props();
|
|
79
75
|
|
|
76
|
+
let drawerElement: HTMLDivElement | undefined = $state();
|
|
77
|
+
|
|
80
78
|
const transitionProperties = {
|
|
81
79
|
x: position == "left" ? -transitionDistance : position == "right" ? transitionDistance : 0,
|
|
82
80
|
y: position == "top" ? -transitionDistance : position == "bottom" ? transitionDistance : 0,
|
|
@@ -90,6 +88,15 @@ The `<Overlay>` component is used to darken the background and optionally block
|
|
|
90
88
|
bottom: "bottom-0 left-0 right-0 h-60",
|
|
91
89
|
};
|
|
92
90
|
|
|
91
|
+
$effect(() => {
|
|
92
|
+
if (open && drawerElement) {
|
|
93
|
+
if(inertId)
|
|
94
|
+
document.getElementById(inertId)?.setAttribute("inert", "true");
|
|
95
|
+
drawerElement?.focus();
|
|
96
|
+
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
93
100
|
function handleKeydown() {
|
|
94
101
|
if(open && escapeKeyClose) {
|
|
95
102
|
closeDrawer();
|
|
@@ -97,21 +104,23 @@ The `<Overlay>` component is used to darken the background and optionally block
|
|
|
97
104
|
};
|
|
98
105
|
|
|
99
106
|
export function closeDrawer() {
|
|
100
|
-
|
|
107
|
+
if(inertId)
|
|
108
|
+
document.getElementById(inertId)?.removeAttribute("inert");
|
|
109
|
+
open = false;
|
|
101
110
|
};
|
|
102
111
|
|
|
103
112
|
</script>
|
|
104
113
|
|
|
105
114
|
{#if open}
|
|
106
|
-
<
|
|
107
|
-
<Overlay {transitionDuration} {disableBodyScroll} class={overlayClasses} onclick={
|
|
108
|
-
<div role="dialog" aria-modal="true" aria-label={ariaLabel} tabindex=
|
|
109
|
-
class={twMerge("fixed
|
|
115
|
+
<Portal class="fixed z-50">
|
|
116
|
+
<Overlay {transitionDuration} {disableBodyScroll} class={overlayClasses} onclick={closeDrawer} />
|
|
117
|
+
<div bind:this={drawerElement} use:trapFocus role="dialog" aria-modal="true" aria-label={ariaLabel} tabindex={open ? 0 : -1} aria-hidden={!open}
|
|
118
|
+
class={twMerge("fixed bg-white overflow-y-auto focus:outline-none", postionClasses[position], props.class)}
|
|
110
119
|
in:fly={transitionProperties}
|
|
111
120
|
out:fly={transitionProperties}
|
|
112
121
|
use:onKeydown={{key: "Escape", callback: handleKeydown}}>
|
|
113
122
|
{@render children?.()}
|
|
114
123
|
</div>
|
|
115
|
-
</
|
|
124
|
+
</Portal>
|
|
116
125
|
{/if}
|
|
117
126
|
|
|
@@ -4,6 +4,7 @@ export type DrawerProps = {
|
|
|
4
4
|
open?: boolean;
|
|
5
5
|
escapeKeyClose?: boolean;
|
|
6
6
|
disableBodyScroll?: boolean;
|
|
7
|
+
inertId?: string;
|
|
7
8
|
ariaLabel?: string;
|
|
8
9
|
transitionDuration?: number;
|
|
9
10
|
transitionDistance?: number;
|
|
@@ -15,38 +16,31 @@ export type DrawerProps = {
|
|
|
15
16
|
/**
|
|
16
17
|
* Drawer
|
|
17
18
|
*
|
|
18
|
-
* A flexible slide-in drawer component
|
|
19
|
+
* A flexible, accessible slide-in drawer component for Svelte 5 using Tailwind CSS and transitions. Includes focus trapping, overlay support, inert background, and escape key handling.
|
|
19
20
|
*
|
|
20
21
|
* ## Props
|
|
21
22
|
*
|
|
22
23
|
* - `open?`: `boolean` — Whether the drawer is visible. Bindable.
|
|
23
|
-
* - `escapeKeyClose?`: `boolean` — If true
|
|
24
|
-
* - `disableBodyScroll?`: `boolean` — Prevents body
|
|
25
|
-
* - `
|
|
26
|
-
* - `
|
|
27
|
-
* - `
|
|
28
|
-
* - `
|
|
29
|
-
* - `
|
|
30
|
-
* - `
|
|
31
|
-
* - `
|
|
32
|
-
*
|
|
33
|
-
* ## Slots
|
|
34
|
-
*
|
|
35
|
-
* This component uses `children` as a rendered snippet via `{@render}`.
|
|
36
|
-
*
|
|
37
|
-
* ## Overlay
|
|
38
|
-
*
|
|
39
|
-
* The `<Overlay>` component is used to darken the background and optionally block interaction. It is clickable and will trigger the drawer to close unless stopped by event propagation. You can customize its styling using the `overlayClasses` prop.
|
|
24
|
+
* - `escapeKeyClose?`: `boolean` — If `true`, allows closing the drawer via the Escape key. Default: `true`.
|
|
25
|
+
* - `disableBodyScroll?`: `boolean` — Prevents body scroll while drawer is open. Default: `true`.
|
|
26
|
+
* - `inertId?`: `string` — DOM element ID to set `inert` while the drawer is open (for accessibility).
|
|
27
|
+
* - `ariaLabel?`: `string` — ARIA label for screen readers. Default: `"Drawer"`.
|
|
28
|
+
* - `position?`: `"left" | "right" | "top" | "bottom"` — Drawer slide direction. Default: `"left"`.
|
|
29
|
+
* - `transitionDuration?`: `number` — Drawer animation duration in milliseconds.
|
|
30
|
+
* - `transitionDistance?`: `number` — Distance the drawer flies in from (px).
|
|
31
|
+
* - `overlayClasses?`: `string` — Custom Tailwind classes for the backdrop overlay.
|
|
32
|
+
* - `class?`: `ClassNameValue` — Classes passed to the drawer container.
|
|
33
|
+
* - `children?`: `Snippet` — Svelte content rendered inside the drawer.
|
|
40
34
|
*
|
|
41
35
|
* ## Usage
|
|
42
36
|
*
|
|
43
37
|
* ```svelte
|
|
44
38
|
* <script>
|
|
45
|
-
* import Drawer from '@
|
|
46
|
-
* let
|
|
39
|
+
* import { Drawer } from '@your-lib';
|
|
40
|
+
* let open = false;
|
|
47
41
|
* </script>
|
|
48
42
|
*
|
|
49
|
-
* <Drawer bind:open={
|
|
43
|
+
* <Drawer bind:open={open} position="right" inertId="main-content">
|
|
50
44
|
* <p class="p-4">Drawer content goes here</p>
|
|
51
45
|
* </Drawer>
|
|
52
46
|
* ```
|
|
@@ -2,15 +2,20 @@
|
|
|
2
2
|
import { type Snippet } from 'svelte';
|
|
3
3
|
import type { ClassNameValue } from 'tailwind-merge';
|
|
4
4
|
import {twMerge} from 'tailwind-merge';
|
|
5
|
-
import Overlay from '../overlay/
|
|
5
|
+
import { Overlay } from '../overlay/index.js';
|
|
6
6
|
import { onKeydown } from '../../actions/on-keydown.js';
|
|
7
|
+
import { Portal } from "../portal/index.js";
|
|
8
|
+
import { config } from '../../configs/config.js';
|
|
9
|
+
import { fly } from 'svelte/transition';
|
|
10
|
+
import { trapFocus } from '../../actions/trap-focus.js';
|
|
7
11
|
|
|
8
12
|
export type ModalProps = {
|
|
9
13
|
open?: boolean;
|
|
10
14
|
escapeKeyClose?: boolean;
|
|
11
15
|
disableBodyScroll?: boolean;
|
|
12
16
|
ariaLabel?: string;
|
|
13
|
-
|
|
17
|
+
inertId?: string;
|
|
18
|
+
transitionDuration?: number;
|
|
14
19
|
overlayClasses?: string;
|
|
15
20
|
children?: Snippet;
|
|
16
21
|
class?: ClassNameValue;
|
|
@@ -19,10 +24,24 @@
|
|
|
19
24
|
</script>
|
|
20
25
|
|
|
21
26
|
<script lang="ts">
|
|
22
|
-
let {open=$bindable(false), escapeKeyClose=true, disableBodyScroll=true,
|
|
27
|
+
let {open=$bindable(false), escapeKeyClose=true, disableBodyScroll=true, transitionDuration=config.transitionDuration, inertId, ariaLabel="Modal", overlayClasses="", children, ...props}: ModalProps = $props();
|
|
28
|
+
|
|
29
|
+
let modalElement: HTMLDivElement | undefined = $state();
|
|
30
|
+
|
|
31
|
+
$effect(() => {
|
|
32
|
+
if (open && modalElement) {
|
|
33
|
+
if(inertId)
|
|
34
|
+
document.getElementById(inertId)?.setAttribute("inert", "true");
|
|
35
|
+
modalElement?.focus();
|
|
36
|
+
|
|
37
|
+
}
|
|
38
|
+
});
|
|
23
39
|
|
|
24
40
|
export function closeModal() {
|
|
25
41
|
open = false;
|
|
42
|
+
if(inertId)
|
|
43
|
+
document.getElementById(inertId)?.removeAttribute("inert");
|
|
44
|
+
|
|
26
45
|
};
|
|
27
46
|
|
|
28
47
|
function handleKeydown() {
|
|
@@ -34,15 +53,18 @@
|
|
|
34
53
|
</script>
|
|
35
54
|
|
|
36
55
|
{#if open}
|
|
37
|
-
<
|
|
38
|
-
|
|
39
|
-
<div role="dialog" aria-modal="true" aria-label={ariaLabel} tabindex=
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
56
|
+
<Portal class="fixed z-50">
|
|
57
|
+
<Overlay {transitionDuration} {disableBodyScroll} class={overlayClasses} onclick={closeModal} />
|
|
58
|
+
<div bind:this={modalElement} use:trapFocus role="dialog" aria-modal="true" aria-label={ariaLabel} tabindex={open ? 0 : -1} aria-hidden={!open}
|
|
59
|
+
class={twMerge("fixed w-full max-w-2xl top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 h-6/12 overflow-y-auto focus:outline-none bg-white rounded-primary", props.class)}
|
|
60
|
+
use:onKeydown={{key: "Escape", callback: handleKeydown}}
|
|
61
|
+
in:fly={{y: 150, duration: transitionDuration}}
|
|
62
|
+
out:fly={{y: 150, duration: transitionDuration}}>
|
|
63
|
+
|
|
64
|
+
{@render children?.()}
|
|
44
65
|
|
|
45
|
-
</
|
|
66
|
+
</div>
|
|
67
|
+
</Portal>
|
|
46
68
|
{/if}
|
|
47
69
|
|
|
48
70
|
|
|
@@ -5,7 +5,8 @@ export type ModalProps = {
|
|
|
5
5
|
escapeKeyClose?: boolean;
|
|
6
6
|
disableBodyScroll?: boolean;
|
|
7
7
|
ariaLabel?: string;
|
|
8
|
-
|
|
8
|
+
inertId?: string;
|
|
9
|
+
transitionDuration?: number;
|
|
9
10
|
overlayClasses?: string;
|
|
10
11
|
children?: Snippet;
|
|
11
12
|
class?: ClassNameValue;
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
ariaLabel?: string;
|
|
12
12
|
transitionDuration?: number;
|
|
13
13
|
children?: Snippet;
|
|
14
|
-
onclick?: (event:
|
|
14
|
+
onclick?: (event: Event) => void;
|
|
15
15
|
class?: ClassNameValue;
|
|
16
16
|
};
|
|
17
17
|
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
let { disableBodyScroll=true, transitionDuration=config.transitionDuration, ariaLabel="Overlay", onclick, children, ...props }: OverlayProps = $props();
|
|
22
22
|
</script>
|
|
23
23
|
|
|
24
|
-
<div
|
|
24
|
+
<div transition:fade={{duration: transitionDuration}} class={twMerge("fixed inset-0 bg-primary-overlay", props.class)} role="presentation" {onclick} use:disableScroll={disableBodyScroll} >
|
|
25
25
|
{@render children?.()}
|
|
26
26
|
</div>
|
|
27
27
|
|
|
@@ -5,7 +5,7 @@ export type OverlayProps = {
|
|
|
5
5
|
ariaLabel?: string;
|
|
6
6
|
transitionDuration?: number;
|
|
7
7
|
children?: Snippet;
|
|
8
|
-
onclick?: (event:
|
|
8
|
+
onclick?: (event: Event) => void;
|
|
9
9
|
class?: ClassNameValue;
|
|
10
10
|
};
|
|
11
11
|
declare const Overlay: import("svelte").Component<OverlayProps, {}, "">;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { default as Overlay } from
|
|
1
|
+
export { default as Overlay } from "./Overlay.svelte";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { default as Overlay } from
|
|
1
|
+
export { default as Overlay } from "./Overlay.svelte";
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
<!--
|
|
2
2
|
@component Portal
|
|
3
3
|
|
|
4
|
-
A utility component that renders its children outside the current DOM hierarchy by prepending them into a target DOM element (e.g., `document.body`). Useful for modals, drawers, toasts, and overlays to avoid z-index and layout conflicts.
|
|
4
|
+
A utility component that renders its children outside the current DOM hierarchy by appending or prepending them into a target DOM element (e.g., `document.body`). Useful for modals, drawers, toasts, and overlays to avoid z-index and layout conflicts.
|
|
5
5
|
|
|
6
6
|
## Props
|
|
7
7
|
|
|
8
8
|
- `target?`: `HTMLElement` — The DOM node to render the content into. Defaults to `document.body` if not provided. Useful for directing content to a specific container.
|
|
9
|
-
- `
|
|
9
|
+
- `append?: boolean` — If true (default), the portal will be appended to the end of the target node. If false, it will be prepended to the begining. This controls the render order of layered content.
|
|
10
10
|
- `children?`: `Snippet` — The Svelte children content to render inside the portal. Use `{@render}` to render dynamic fragments.
|
|
11
11
|
- `class?`: `ClassNameValue` — Tailwind CSS or custom classes applied to the outer container div.
|
|
12
12
|
|
|
@@ -36,30 +36,30 @@ A utility component that renders its children outside the current DOM hierarchy
|
|
|
36
36
|
|
|
37
37
|
<script lang="ts" module>
|
|
38
38
|
import { type Snippet } from 'svelte';
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
39
|
+
import type { ClassNameValue } from 'tailwind-merge';
|
|
40
|
+
import {twMerge} from 'tailwind-merge';
|
|
41
|
+
import { onMount, onDestroy } from 'svelte';
|
|
42
|
+
import { browser } from '$app/environment';
|
|
43
|
+
|
|
44
|
+
export type PortalProps = {
|
|
45
|
+
target?: HTMLElement;
|
|
46
|
+
append?: boolean;
|
|
47
|
+
children?: Snippet;
|
|
48
|
+
class?: ClassNameValue;
|
|
49
|
+
};
|
|
50
50
|
|
|
51
51
|
</script>
|
|
52
52
|
|
|
53
53
|
<script lang="ts">
|
|
54
|
-
let { target = browser ? document.body : undefined,
|
|
54
|
+
let { target = browser ? document.body : undefined, append = true, children, ...props }: PortalProps = $props();
|
|
55
55
|
let el: HTMLDivElement;
|
|
56
56
|
|
|
57
57
|
onMount(() => {
|
|
58
58
|
if (!browser || !el) return;
|
|
59
|
-
if(
|
|
60
|
-
target?.prepend(el);
|
|
61
|
-
else
|
|
59
|
+
if(append)
|
|
62
60
|
target?.append(el);
|
|
61
|
+
else
|
|
62
|
+
target?.prepend(el);
|
|
63
63
|
});
|
|
64
64
|
|
|
65
65
|
onDestroy(() => {
|
|
@@ -2,19 +2,19 @@ import { type Snippet } from 'svelte';
|
|
|
2
2
|
import type { ClassNameValue } from 'tailwind-merge';
|
|
3
3
|
export type PortalProps = {
|
|
4
4
|
target?: HTMLElement;
|
|
5
|
-
|
|
5
|
+
append?: boolean;
|
|
6
6
|
children?: Snippet;
|
|
7
7
|
class?: ClassNameValue;
|
|
8
8
|
};
|
|
9
9
|
/**
|
|
10
10
|
* Portal
|
|
11
11
|
*
|
|
12
|
-
* A utility component that renders its children outside the current DOM hierarchy by prepending them into a target DOM element (e.g., `document.body`). Useful for modals, drawers, toasts, and overlays to avoid z-index and layout conflicts.
|
|
12
|
+
* A utility component that renders its children outside the current DOM hierarchy by appending or prepending them into a target DOM element (e.g., `document.body`). Useful for modals, drawers, toasts, and overlays to avoid z-index and layout conflicts.
|
|
13
13
|
*
|
|
14
14
|
* ## Props
|
|
15
15
|
*
|
|
16
16
|
* - `target?`: `HTMLElement` — The DOM node to render the content into. Defaults to `document.body` if not provided. Useful for directing content to a specific container.
|
|
17
|
-
* - `
|
|
17
|
+
* - `append?: boolean` — If true (default), the portal will be appended to the end of the target node. If false, it will be prepended to the begining. This controls the render order of layered content.
|
|
18
18
|
* - `children?`: `Snippet` — The Svelte children content to render inside the portal. Use `{@render}` to render dynamic fragments.
|
|
19
19
|
* - `class?`: `ClassNameValue` — Tailwind CSS or custom classes applied to the outer container div.
|
|
20
20
|
*
|
|
@@ -99,7 +99,7 @@ Supports both desktop and mobile rendering, multi-select checkboxes, and custom
|
|
|
99
99
|
-->
|
|
100
100
|
|
|
101
101
|
<script lang="ts" module>
|
|
102
|
-
import type
|
|
102
|
+
import { onMount, type Snippet } from 'svelte';
|
|
103
103
|
import type { ClassNameValue } from 'tailwind-merge';
|
|
104
104
|
|
|
105
105
|
/**
|
|
@@ -171,6 +171,8 @@ Supports both desktop and mobile rendering, multi-select checkboxes, and custom
|
|
|
171
171
|
let { showMultiSelect=$bindable(false), selected=$bindable([]), rows=$bindable([]), getKey, headings, tableRow, tableRowMobile, multiSelectTh, multiSelectTd,
|
|
172
172
|
tableMobileTdClass, outerDivClass, headingsRowClass, multiSelectThClass, tableRowClass, multiSelectTdClass, tableRowMobileClass, checkboxClass, ...props }: TableProps<T> = $props();
|
|
173
173
|
|
|
174
|
+
let tableElement: HTMLTableElement | undefined = $state();
|
|
175
|
+
|
|
174
176
|
function addSelected(rowkey: string){
|
|
175
177
|
if(selected.includes(rowkey)){
|
|
176
178
|
selected = selected.filter((item) => item !== rowkey);
|
|
@@ -181,27 +183,48 @@ Supports both desktop and mobile rendering, multi-select checkboxes, and custom
|
|
|
181
183
|
|
|
182
184
|
function handleSelectAll(event: any){
|
|
183
185
|
if (event.target.checked) {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
186
|
+
for(let i = 0; i < rows.length; i++){
|
|
187
|
+
selected.push(getKey(rows[i]));
|
|
188
|
+
}
|
|
187
189
|
}
|
|
188
190
|
else{
|
|
189
191
|
selected = [];
|
|
190
192
|
}
|
|
191
193
|
}
|
|
192
194
|
|
|
195
|
+
/*function applyStickyToSecondColumn(table: HTMLTableElement) {
|
|
196
|
+
if (!table) return;
|
|
197
|
+
|
|
198
|
+
const toAdd = showMultiSelect ? 'left-[3rem]' : "left-0"; // 3 rem is the width of the checkbox column
|
|
199
|
+
const toRemove = showMultiSelect ? 'left-0' : "left-[3rem]";
|
|
200
|
+
|
|
201
|
+
// thead > tr > th:nth-child(2)
|
|
202
|
+
const headCells = table.querySelectorAll('thead tr > th:nth-child(2)');
|
|
203
|
+
for (const cell of headCells) {
|
|
204
|
+
cell.classList.remove(toRemove);
|
|
205
|
+
cell.classList.add('sticky', toAdd);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// tbody > tr > td:nth-child(2)
|
|
209
|
+
const bodyCells = table.querySelectorAll('tbody tr > td:nth-child(2)');
|
|
210
|
+
for (const cell of bodyCells) {
|
|
211
|
+
cell.classList.remove(toRemove);
|
|
212
|
+
cell.classList.add('sticky', toAdd);
|
|
213
|
+
}
|
|
214
|
+
}*/
|
|
215
|
+
|
|
193
216
|
</script>
|
|
194
217
|
|
|
195
218
|
|
|
196
219
|
<div class={twMerge("rounded-lg border-primary-table-border w-full overflow-x-auto", tableRowMobile ? "border-0 md:border bg-transparent" : "border bg-white", outerDivClass)}>
|
|
197
|
-
<table class={twMerge("table-fixed w-full text-sm", tableRowMobile ? "border-separate border-spacing-y-2 md:border-collapse md:border-spacing-y-0": "", props.class)} cellpadding="10px">
|
|
220
|
+
<table bind:this={tableElement} class={twMerge("table-fixed w-full text-sm", tableRowMobile ? "border-separate border-spacing-y-2 md:border-collapse md:border-spacing-y-0": "", props.class)} cellpadding="10px">
|
|
198
221
|
<thead class={twMerge("bg-primary-table-heading", tableRowMobile ? "hidden md:table-header-group" : "")}>
|
|
199
222
|
<tr class={twMerge("text-xs text-primary-table-heading-text whitespace-nowrap bg-inherit", headingsRowClass)}>
|
|
200
223
|
{#if showMultiSelect}
|
|
201
224
|
{#if multiSelectTh}
|
|
202
225
|
{@render multiSelectTh()}
|
|
203
226
|
{:else}
|
|
204
|
-
<th class={twMerge("w-12 px-4 py-2 text-center bg-inherit", multiSelectThClass)}>
|
|
227
|
+
<th class={twMerge("w-12 px-4 py-2 text-center bg-inherit sticky left-0", multiSelectThClass)}>
|
|
205
228
|
<Checkbox id="header-multiselect-checkbox" onchange={handleSelectAll} checked={ rows ? selected.length === rows.length: false} class={checkboxClass} />
|
|
206
229
|
</th>
|
|
207
230
|
{/if}
|
|
@@ -222,7 +245,7 @@ Supports both desktop and mobile rendering, multi-select checkboxes, and custom
|
|
|
222
245
|
{#if multiSelectTd}
|
|
223
246
|
{@render multiSelectTd(row)}
|
|
224
247
|
{:else}
|
|
225
|
-
<td class={twMerge("text-center px-4 py-2 bg-inherit", multiSelectTdClass )}>
|
|
248
|
+
<td class={twMerge("text-center px-4 py-2 bg-inherit sticky left-0", multiSelectTdClass )}>
|
|
226
249
|
<Checkbox id={"checkbox-" + rowKey} value={rowKey} checked={isSelected} onchange={()=>{addSelected(rowKey)}} class={checkboxClass} />
|
|
227
250
|
</td>
|
|
228
251
|
{/if}
|