@classic-homes/theme-svelte 0.1.55 → 1.0.0
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/dist/__tests__/nav-access.test.js +145 -0
- package/dist/__tests__/sidebar-expansion.test.js +59 -0
- package/dist/lib/components/Button.svelte +17 -1
- package/dist/lib/components/Checkbox.svelte +3 -2
- package/dist/lib/components/Combobox.svelte +4 -4
- package/dist/lib/components/DataPanel.svelte +11 -5
- package/dist/lib/components/DateTimePicker.svelte +3 -2
- package/dist/lib/components/FileUpload.svelte +4 -3
- package/dist/lib/components/HeaderSearch.svelte +5 -0
- package/dist/lib/components/RadioGroup.svelte +3 -2
- package/dist/lib/components/Select.svelte +3 -2
- package/dist/lib/components/Signature.svelte +3 -2
- package/dist/lib/components/Tabs.svelte +3 -2
- package/dist/lib/components/data-table/DataTableColumnFilter.svelte +2 -2
- package/dist/lib/components/data-table/DataTableColumnFilter.svelte.d.ts +33 -13
- package/dist/lib/components/data-table/DataTableColumnVisibility.svelte +2 -2
- package/dist/lib/components/data-table/DataTableColumnVisibility.svelte.d.ts +27 -7
- package/dist/lib/components/data-table/DataTableFacetedFilter.svelte +2 -2
- package/dist/lib/components/data-table/DataTableFacetedFilter.svelte.d.ts +36 -17
- package/dist/lib/components/data-table/DataTablePagination.svelte +2 -2
- package/dist/lib/components/data-table/DataTablePagination.svelte.d.ts +31 -11
- package/dist/lib/components/data-table/DataTableSearch.svelte +2 -2
- package/dist/lib/components/data-table/DataTableSearch.svelte.d.ts +31 -11
- package/dist/lib/components/data-table/DataTableToolbar.svelte +2 -2
- package/dist/lib/components/data-table/DataTableToolbar.svelte.d.ts +37 -17
- package/dist/lib/components/layout/DashboardLayout.svelte +1 -0
- package/dist/lib/components/layout/Sidebar.svelte +22 -17
- package/dist/lib/components/layout/Sidebar.svelte.d.ts +10 -1
- package/dist/lib/composables/useAnnounce.svelte.js +9 -9
- package/dist/lib/composables/useArrowNavigation.svelte.js +5 -4
- package/dist/lib/composables/useEscapeKey.svelte.d.ts +4 -2
- package/dist/lib/composables/useEscapeKey.svelte.js +3 -2
- package/dist/lib/composables/useFocusRef.svelte.js +4 -3
- package/dist/lib/composables/useFocusTrap.svelte.d.ts +9 -3
- package/dist/lib/composables/useFocusTrap.svelte.js +34 -28
- package/dist/lib/composables/useRovingTabindex.svelte.js +4 -3
- package/dist/lib/schemas/auth.d.ts +18 -8
- package/dist/lib/schemas/auth.js +4 -1
- package/dist/lib/schemas/index.d.ts +1 -1
- package/dist/lib/stores/sidebar.svelte.d.ts +7 -2
- package/dist/lib/stores/sidebar.svelte.js +9 -3
- package/dist/lib/types/layout.d.ts +52 -4
- package/dist/lib/utils/nav-access.d.ts +61 -0
- package/dist/lib/utils/nav-access.js +74 -0
- package/dist/lib/utils.d.ts +0 -10
- package/dist/lib/utils.js +0 -13
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<script lang="ts">
|
|
1
|
+
<script lang="ts" generics="TData extends object">
|
|
2
2
|
/**
|
|
3
3
|
* DataTablePagination - Pagination controls for DataTable
|
|
4
4
|
*
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
interface Props {
|
|
14
14
|
/** The native DataTable instance */
|
|
15
|
-
table: DataTableInstance<
|
|
15
|
+
table: DataTableInstance<TData>;
|
|
16
16
|
/** Available page sizes */
|
|
17
17
|
pageSizeOptions?: number[];
|
|
18
18
|
/** Show selected row count */
|
|
@@ -5,16 +5,36 @@
|
|
|
5
5
|
* Works with the native Svelte 5 DataTable implementation.
|
|
6
6
|
*/
|
|
7
7
|
import type { DataTableInstance } from './createDataTable.svelte.js';
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
8
|
+
declare function $$render<TData extends object>(): {
|
|
9
|
+
props: {
|
|
10
|
+
/** The native DataTable instance */
|
|
11
|
+
table: DataTableInstance<TData>;
|
|
12
|
+
/** Available page sizes */
|
|
13
|
+
pageSizeOptions?: number[];
|
|
14
|
+
/** Show selected row count */
|
|
15
|
+
showSelectedCount?: boolean;
|
|
16
|
+
/** Additional classes */
|
|
17
|
+
class?: string;
|
|
18
|
+
};
|
|
19
|
+
exports: {};
|
|
20
|
+
bindings: "";
|
|
21
|
+
slots: {};
|
|
22
|
+
events: {};
|
|
23
|
+
};
|
|
24
|
+
declare class __sveltets_Render<TData extends object> {
|
|
25
|
+
props(): ReturnType<typeof $$render<TData>>['props'];
|
|
26
|
+
events(): ReturnType<typeof $$render<TData>>['events'];
|
|
27
|
+
slots(): ReturnType<typeof $$render<TData>>['slots'];
|
|
28
|
+
bindings(): "";
|
|
29
|
+
exports(): {};
|
|
17
30
|
}
|
|
18
|
-
|
|
19
|
-
|
|
31
|
+
interface $$IsomorphicComponent {
|
|
32
|
+
new <TData extends object>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<TData>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<TData>['props']>, ReturnType<__sveltets_Render<TData>['events']>, ReturnType<__sveltets_Render<TData>['slots']>> & {
|
|
33
|
+
$$bindings?: ReturnType<__sveltets_Render<TData>['bindings']>;
|
|
34
|
+
} & ReturnType<__sveltets_Render<TData>['exports']>;
|
|
35
|
+
<TData extends object>(internal: unknown, props: ReturnType<__sveltets_Render<TData>['props']> & {}): ReturnType<__sveltets_Render<TData>['exports']>;
|
|
36
|
+
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
|
|
37
|
+
}
|
|
38
|
+
declare const DataTablePagination: $$IsomorphicComponent;
|
|
39
|
+
type DataTablePagination<TData extends object> = InstanceType<typeof DataTablePagination<TData>>;
|
|
20
40
|
export default DataTablePagination;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<script lang="ts">
|
|
1
|
+
<script lang="ts" generics="TData extends object">
|
|
2
2
|
/**
|
|
3
3
|
* DataTableSearch - Global search input for DataTable
|
|
4
4
|
*
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
interface Props {
|
|
13
13
|
/** The native DataTable instance */
|
|
14
|
-
table: DataTableInstance<
|
|
14
|
+
table: DataTableInstance<TData>;
|
|
15
15
|
/** Placeholder text */
|
|
16
16
|
placeholder?: string;
|
|
17
17
|
/** Debounce delay in ms */
|
|
@@ -5,16 +5,36 @@
|
|
|
5
5
|
* Works with the native Svelte 5 DataTable implementation.
|
|
6
6
|
*/
|
|
7
7
|
import type { DataTableInstance } from './createDataTable.svelte.js';
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
8
|
+
declare function $$render<TData extends object>(): {
|
|
9
|
+
props: {
|
|
10
|
+
/** The native DataTable instance */
|
|
11
|
+
table: DataTableInstance<TData>;
|
|
12
|
+
/** Placeholder text */
|
|
13
|
+
placeholder?: string;
|
|
14
|
+
/** Debounce delay in ms */
|
|
15
|
+
debounceMs?: number;
|
|
16
|
+
/** Additional classes */
|
|
17
|
+
class?: string;
|
|
18
|
+
};
|
|
19
|
+
exports: {};
|
|
20
|
+
bindings: "";
|
|
21
|
+
slots: {};
|
|
22
|
+
events: {};
|
|
23
|
+
};
|
|
24
|
+
declare class __sveltets_Render<TData extends object> {
|
|
25
|
+
props(): ReturnType<typeof $$render<TData>>['props'];
|
|
26
|
+
events(): ReturnType<typeof $$render<TData>>['events'];
|
|
27
|
+
slots(): ReturnType<typeof $$render<TData>>['slots'];
|
|
28
|
+
bindings(): "";
|
|
29
|
+
exports(): {};
|
|
17
30
|
}
|
|
18
|
-
|
|
19
|
-
|
|
31
|
+
interface $$IsomorphicComponent {
|
|
32
|
+
new <TData extends object>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<TData>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<TData>['props']>, ReturnType<__sveltets_Render<TData>['events']>, ReturnType<__sveltets_Render<TData>['slots']>> & {
|
|
33
|
+
$$bindings?: ReturnType<__sveltets_Render<TData>['bindings']>;
|
|
34
|
+
} & ReturnType<__sveltets_Render<TData>['exports']>;
|
|
35
|
+
<TData extends object>(internal: unknown, props: ReturnType<__sveltets_Render<TData>['props']> & {}): ReturnType<__sveltets_Render<TData>['exports']>;
|
|
36
|
+
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
|
|
37
|
+
}
|
|
38
|
+
declare const DataTableSearch: $$IsomorphicComponent;
|
|
39
|
+
type DataTableSearch<TData extends object> = InstanceType<typeof DataTableSearch<TData>>;
|
|
20
40
|
export default DataTableSearch;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<script lang="ts">
|
|
1
|
+
<script lang="ts" generics="TData extends object">
|
|
2
2
|
/**
|
|
3
3
|
* DataTableToolbar - Combined toolbar with search and controls
|
|
4
4
|
*
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
interface Props {
|
|
16
16
|
/** The native DataTable instance */
|
|
17
|
-
table: DataTableInstance<
|
|
17
|
+
table: DataTableInstance<TData>;
|
|
18
18
|
/** Show global search */
|
|
19
19
|
showSearch?: boolean;
|
|
20
20
|
/** Show column visibility toggle */
|
|
@@ -6,22 +6,42 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import type { Snippet } from 'svelte';
|
|
8
8
|
import type { DataTableInstance } from './createDataTable.svelte.js';
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
9
|
+
declare function $$render<TData extends object>(): {
|
|
10
|
+
props: {
|
|
11
|
+
/** The native DataTable instance */
|
|
12
|
+
table: DataTableInstance<TData>;
|
|
13
|
+
/** Show global search */
|
|
14
|
+
showSearch?: boolean;
|
|
15
|
+
/** Show column visibility toggle */
|
|
16
|
+
showColumnVisibility?: boolean;
|
|
17
|
+
/** Search placeholder */
|
|
18
|
+
searchPlaceholder?: string;
|
|
19
|
+
/** Custom filters slot */
|
|
20
|
+
filters?: Snippet;
|
|
21
|
+
/** Custom actions slot */
|
|
22
|
+
actions?: Snippet;
|
|
23
|
+
/** Additional classes */
|
|
24
|
+
class?: string;
|
|
25
|
+
};
|
|
26
|
+
exports: {};
|
|
27
|
+
bindings: "";
|
|
28
|
+
slots: {};
|
|
29
|
+
events: {};
|
|
30
|
+
};
|
|
31
|
+
declare class __sveltets_Render<TData extends object> {
|
|
32
|
+
props(): ReturnType<typeof $$render<TData>>['props'];
|
|
33
|
+
events(): ReturnType<typeof $$render<TData>>['events'];
|
|
34
|
+
slots(): ReturnType<typeof $$render<TData>>['slots'];
|
|
35
|
+
bindings(): "";
|
|
36
|
+
exports(): {};
|
|
24
37
|
}
|
|
25
|
-
|
|
26
|
-
|
|
38
|
+
interface $$IsomorphicComponent {
|
|
39
|
+
new <TData extends object>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<TData>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<TData>['props']>, ReturnType<__sveltets_Render<TData>['events']>, ReturnType<__sveltets_Render<TData>['slots']>> & {
|
|
40
|
+
$$bindings?: ReturnType<__sveltets_Render<TData>['bindings']>;
|
|
41
|
+
} & ReturnType<__sveltets_Render<TData>['exports']>;
|
|
42
|
+
<TData extends object>(internal: unknown, props: ReturnType<__sveltets_Render<TData>['props']> & {}): ReturnType<__sveltets_Render<TData>['exports']>;
|
|
43
|
+
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
|
|
44
|
+
}
|
|
45
|
+
declare const DataTableToolbar: $$IsomorphicComponent;
|
|
46
|
+
type DataTableToolbar<TData extends object> = InstanceType<typeof DataTableToolbar<TData>>;
|
|
27
47
|
export default DataTableToolbar;
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import type { Snippet } from 'svelte';
|
|
16
16
|
import type { NavSection, NavItem, QuickLink } from '../../types/layout.js';
|
|
17
17
|
import { cn } from '../../utils.js';
|
|
18
|
+
import { filterNavItems } from '../../utils/nav-access.js';
|
|
18
19
|
import { sidebarStore } from '../../stores/sidebar.svelte.js';
|
|
19
20
|
import LogoMain from '../LogoMain.svelte';
|
|
20
21
|
import QuickLinks from './QuickLinks.svelte';
|
|
@@ -25,8 +26,17 @@
|
|
|
25
26
|
interface Props {
|
|
26
27
|
/** Navigation sections */
|
|
27
28
|
navigation: NavSection[];
|
|
28
|
-
/**
|
|
29
|
+
/**
|
|
30
|
+
* Every role the current user holds. Items are matched against the whole
|
|
31
|
+
* set, so a user with several roles sees the union of what they allow.
|
|
32
|
+
*/
|
|
29
33
|
userRoles?: string[];
|
|
34
|
+
/** Every permission the current user holds, for permission-gated items. */
|
|
35
|
+
userPermissions?: string[];
|
|
36
|
+
/** Require ALL of an item's `roles` rather than any (default: any). */
|
|
37
|
+
requireAllRoles?: boolean;
|
|
38
|
+
/** Require ALL of an item's `permissions` rather than any (default: any). */
|
|
39
|
+
requireAllPermissions?: boolean;
|
|
30
40
|
/** Whether sidebar is collapsed (desktop) */
|
|
31
41
|
collapsed?: boolean;
|
|
32
42
|
/** Whether currently on mobile */
|
|
@@ -80,6 +90,9 @@
|
|
|
80
90
|
let {
|
|
81
91
|
navigation,
|
|
82
92
|
userRoles,
|
|
93
|
+
userPermissions,
|
|
94
|
+
requireAllRoles = false,
|
|
95
|
+
requireAllPermissions = false,
|
|
83
96
|
collapsed = false,
|
|
84
97
|
isMobile = false,
|
|
85
98
|
mobileOpen = false,
|
|
@@ -106,27 +119,19 @@
|
|
|
106
119
|
class: className,
|
|
107
120
|
}: Props = $props();
|
|
108
121
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// Recursively filter items based on roles
|
|
117
|
-
function filterItems(items: NavItem[]): NavItem[] {
|
|
118
|
-
return items.filter(isItemVisible).map((item) => ({
|
|
119
|
-
...item,
|
|
120
|
-
children: item.children ? filterItems(item.children) : undefined,
|
|
121
|
-
}));
|
|
122
|
-
}
|
|
122
|
+
const accessContext = $derived({
|
|
123
|
+
userRoles,
|
|
124
|
+
userPermissions,
|
|
125
|
+
requireAllRoles,
|
|
126
|
+
requireAllPermissions,
|
|
127
|
+
});
|
|
123
128
|
|
|
124
|
-
// Filter navigation
|
|
129
|
+
// Filter navigation by what the user holds, dropping sections left empty.
|
|
125
130
|
const roleFilteredNavigation = $derived(
|
|
126
131
|
navigation
|
|
127
132
|
.map((section) => ({
|
|
128
133
|
...section,
|
|
129
|
-
items:
|
|
134
|
+
items: filterNavItems(section.items, accessContext),
|
|
130
135
|
}))
|
|
131
136
|
.filter((section) => section.items.length > 0)
|
|
132
137
|
);
|
|
@@ -16,8 +16,17 @@ import type { NavSection, NavItem, QuickLink } from '../../types/layout.js';
|
|
|
16
16
|
interface Props {
|
|
17
17
|
/** Navigation sections */
|
|
18
18
|
navigation: NavSection[];
|
|
19
|
-
/**
|
|
19
|
+
/**
|
|
20
|
+
* Every role the current user holds. Items are matched against the whole
|
|
21
|
+
* set, so a user with several roles sees the union of what they allow.
|
|
22
|
+
*/
|
|
20
23
|
userRoles?: string[];
|
|
24
|
+
/** Every permission the current user holds, for permission-gated items. */
|
|
25
|
+
userPermissions?: string[];
|
|
26
|
+
/** Require ALL of an item's `roles` rather than any (default: any). */
|
|
27
|
+
requireAllRoles?: boolean;
|
|
28
|
+
/** Require ALL of an item's `permissions` rather than any (default: any). */
|
|
29
|
+
requireAllPermissions?: boolean;
|
|
21
30
|
/** Whether sidebar is collapsed (desktop) */
|
|
22
31
|
collapsed?: boolean;
|
|
23
32
|
/** Whether currently on mobile */
|
|
@@ -28,18 +28,18 @@
|
|
|
28
28
|
export function useAnnounce(defaultOptions) {
|
|
29
29
|
let message = $state('');
|
|
30
30
|
let politeness = $state(defaultOptions?.politeness ?? 'polite');
|
|
31
|
-
let
|
|
31
|
+
let clearTimer = null;
|
|
32
32
|
function clear() {
|
|
33
33
|
message = '';
|
|
34
|
-
if (
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
if (clearTimer) {
|
|
35
|
+
clearTimeout(clearTimer);
|
|
36
|
+
clearTimer = null;
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
function announce(newMessage, options) {
|
|
40
40
|
// Clear any pending timeout
|
|
41
|
-
if (
|
|
42
|
-
|
|
41
|
+
if (clearTimer) {
|
|
42
|
+
clearTimeout(clearTimer);
|
|
43
43
|
}
|
|
44
44
|
// Update politeness if provided
|
|
45
45
|
if (options?.politeness) {
|
|
@@ -50,7 +50,7 @@ export function useAnnounce(defaultOptions) {
|
|
|
50
50
|
// Set up auto-clear if delay > 0
|
|
51
51
|
const delay = options?.clearDelay ?? defaultOptions?.clearDelay ?? 1000;
|
|
52
52
|
if (delay > 0) {
|
|
53
|
-
|
|
53
|
+
clearTimer = setTimeout(() => {
|
|
54
54
|
message = '';
|
|
55
55
|
}, delay);
|
|
56
56
|
}
|
|
@@ -58,8 +58,8 @@ export function useAnnounce(defaultOptions) {
|
|
|
58
58
|
// Cleanup on destroy
|
|
59
59
|
$effect(() => {
|
|
60
60
|
return () => {
|
|
61
|
-
if (
|
|
62
|
-
|
|
61
|
+
if (clearTimer) {
|
|
62
|
+
clearTimeout(clearTimer);
|
|
63
63
|
}
|
|
64
64
|
};
|
|
65
65
|
});
|
|
@@ -23,10 +23,11 @@ const DEFAULT_SELECTOR = '[role="menuitem"], [role="option"], [role="tab"], butt
|
|
|
23
23
|
* ```
|
|
24
24
|
*/
|
|
25
25
|
export function useArrowNavigation(options = {}) {
|
|
26
|
-
|
|
27
|
-
let
|
|
28
|
-
let
|
|
29
|
-
let
|
|
26
|
+
// Plain locals: only read imperatively (handlers/action update), never tracked
|
|
27
|
+
let orientation = options.orientation ?? 'vertical';
|
|
28
|
+
let loop = options.loop ?? true;
|
|
29
|
+
let itemSelector = options.itemSelector ?? DEFAULT_SELECTOR;
|
|
30
|
+
let onFocusChange = options.onFocusChange;
|
|
30
31
|
let container = null;
|
|
31
32
|
let focusedIndex = $state(-1);
|
|
32
33
|
function getFocusableItems() {
|
|
@@ -5,9 +5,11 @@ export interface UseEscapeKeyOptions {
|
|
|
5
5
|
/**
|
|
6
6
|
* Whether the escape key handler is enabled.
|
|
7
7
|
* Useful for conditionally enabling based on component state (e.g., dialog open).
|
|
8
|
+
* Pass a getter (`() => isOpen`) for a value that should stay reactive —
|
|
9
|
+
* a plain boolean is captured once when the composable is called.
|
|
8
10
|
* Default: true
|
|
9
11
|
*/
|
|
10
|
-
enabled?: boolean;
|
|
12
|
+
enabled?: boolean | (() => boolean);
|
|
11
13
|
/**
|
|
12
14
|
* Whether to prevent default behavior when Escape is pressed.
|
|
13
15
|
* Default: true
|
|
@@ -37,7 +39,7 @@ export interface UseEscapeKeyOptions {
|
|
|
37
39
|
* useEscapeKey(() => {
|
|
38
40
|
* isOpen = false;
|
|
39
41
|
* onClose?.();
|
|
40
|
-
* }, { enabled: isOpen });
|
|
42
|
+
* }, { enabled: () => isOpen });
|
|
41
43
|
* </script>
|
|
42
44
|
*
|
|
43
45
|
* {#if isOpen}
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* useEscapeKey(() => {
|
|
17
17
|
* isOpen = false;
|
|
18
18
|
* onClose?.();
|
|
19
|
-
* }, { enabled: isOpen });
|
|
19
|
+
* }, { enabled: () => isOpen });
|
|
20
20
|
* </script>
|
|
21
21
|
*
|
|
22
22
|
* {#if isOpen}
|
|
@@ -25,10 +25,11 @@
|
|
|
25
25
|
* ```
|
|
26
26
|
*/
|
|
27
27
|
export function useEscapeKey(callback, options) {
|
|
28
|
-
const enabled = $derived(options?.enabled ?? true);
|
|
29
28
|
const preventDefault = options?.preventDefault ?? true;
|
|
30
29
|
const stopPropagation = options?.stopPropagation ?? false;
|
|
31
30
|
$effect(() => {
|
|
31
|
+
// Resolve inside the effect so a getter's reactive reads are tracked
|
|
32
|
+
const enabled = typeof options?.enabled === 'function' ? options.enabled() : (options?.enabled ?? true);
|
|
32
33
|
if (!enabled)
|
|
33
34
|
return;
|
|
34
35
|
function handleKeyDown(event) {
|
|
@@ -27,9 +27,10 @@
|
|
|
27
27
|
* ```
|
|
28
28
|
*/
|
|
29
29
|
export function useFocusRef(options = {}) {
|
|
30
|
-
|
|
31
|
-
let
|
|
32
|
-
let
|
|
30
|
+
// Plain locals: only read imperatively (action lifecycle), never tracked
|
|
31
|
+
let focusOnMount = options.focusOnMount ?? false;
|
|
32
|
+
let preventScroll = options.preventScroll ?? false;
|
|
33
|
+
let delay = options.delay ?? 0;
|
|
33
34
|
let element = null;
|
|
34
35
|
let timeoutId = null;
|
|
35
36
|
let frameId = null;
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
export interface UseFocusTrapOptions {
|
|
2
|
-
/**
|
|
3
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Whether the focus trap is active.
|
|
4
|
+
* Pass a getter (`() => isOpen`) for a value that should stay reactive —
|
|
5
|
+
* a plain boolean is captured once when the composable is called.
|
|
6
|
+
*/
|
|
7
|
+
active?: boolean | (() => boolean);
|
|
4
8
|
/** Element to return focus to when trap is deactivated */
|
|
5
9
|
returnFocusTo?: HTMLElement | null;
|
|
6
10
|
/** Selector for the element to focus initially (defaults to first focusable) */
|
|
@@ -15,12 +19,14 @@ export interface UseFocusTrapOptions {
|
|
|
15
19
|
* focusable elements without escaping. Useful for modals, dialogs,
|
|
16
20
|
* and dropdown menus.
|
|
17
21
|
*
|
|
22
|
+
* Must be called during component initialization (it registers a `$effect`).
|
|
23
|
+
*
|
|
18
24
|
* @example
|
|
19
25
|
* ```svelte
|
|
20
26
|
* <script>
|
|
21
27
|
* import { useFocusTrap } from '@classic-homes/theme-svelte';
|
|
22
28
|
*
|
|
23
|
-
* const { action } = useFocusTrap({ active: isOpen });
|
|
29
|
+
* const { action } = useFocusTrap({ active: () => isOpen });
|
|
24
30
|
* </script>
|
|
25
31
|
*
|
|
26
32
|
* {#if isOpen}
|
|
@@ -22,12 +22,14 @@ function getFocusableElements(container) {
|
|
|
22
22
|
* focusable elements without escaping. Useful for modals, dialogs,
|
|
23
23
|
* and dropdown menus.
|
|
24
24
|
*
|
|
25
|
+
* Must be called during component initialization (it registers a `$effect`).
|
|
26
|
+
*
|
|
25
27
|
* @example
|
|
26
28
|
* ```svelte
|
|
27
29
|
* <script>
|
|
28
30
|
* import { useFocusTrap } from '@classic-homes/theme-svelte';
|
|
29
31
|
*
|
|
30
|
-
* const { action } = useFocusTrap({ active: isOpen });
|
|
32
|
+
* const { action } = useFocusTrap({ active: () => isOpen });
|
|
31
33
|
* </script>
|
|
32
34
|
*
|
|
33
35
|
* {#if isOpen}
|
|
@@ -40,15 +42,18 @@ function getFocusableElements(container) {
|
|
|
40
42
|
* ```
|
|
41
43
|
*/
|
|
42
44
|
export function useFocusTrap(options = {}) {
|
|
43
|
-
let
|
|
44
|
-
let
|
|
45
|
-
let initialFocus = $state(options.initialFocus);
|
|
46
|
-
let preventScroll = $state(options.preventScroll ?? false);
|
|
47
|
-
let container = null;
|
|
45
|
+
let currentOptions = $state(options);
|
|
46
|
+
let container = $state(null);
|
|
48
47
|
let previousActiveElement = null;
|
|
48
|
+
let trapped = false;
|
|
49
|
+
function isActive() {
|
|
50
|
+
const active = currentOptions.active;
|
|
51
|
+
return typeof active === 'function' ? active() : (active ?? true);
|
|
52
|
+
}
|
|
49
53
|
function handleKeyDown(event) {
|
|
50
|
-
if (!
|
|
54
|
+
if (!trapped || event.key !== 'Tab' || !container)
|
|
51
55
|
return;
|
|
56
|
+
const preventScroll = currentOptions.preventScroll ?? false;
|
|
52
57
|
const focusableElements = getFocusableElements(container);
|
|
53
58
|
if (focusableElements.length === 0)
|
|
54
59
|
return;
|
|
@@ -70,16 +75,18 @@ export function useFocusTrap(options = {}) {
|
|
|
70
75
|
}
|
|
71
76
|
}
|
|
72
77
|
function activate() {
|
|
73
|
-
if (!container)
|
|
78
|
+
if (!container || trapped)
|
|
74
79
|
return;
|
|
80
|
+
trapped = true;
|
|
75
81
|
// Save current active element to return focus later
|
|
76
82
|
previousActiveElement = document.activeElement;
|
|
77
83
|
// Focus initial element
|
|
84
|
+
const preventScroll = currentOptions.preventScroll ?? false;
|
|
78
85
|
const focusableElements = getFocusableElements(container);
|
|
79
86
|
if (focusableElements.length > 0) {
|
|
80
87
|
let elementToFocus = null;
|
|
81
|
-
if (initialFocus) {
|
|
82
|
-
elementToFocus = container.querySelector(initialFocus);
|
|
88
|
+
if (currentOptions.initialFocus) {
|
|
89
|
+
elementToFocus = container.querySelector(currentOptions.initialFocus);
|
|
83
90
|
}
|
|
84
91
|
if (!elementToFocus) {
|
|
85
92
|
elementToFocus = focusableElements[0];
|
|
@@ -91,39 +98,38 @@ export function useFocusTrap(options = {}) {
|
|
|
91
98
|
document.addEventListener('keydown', handleKeyDown);
|
|
92
99
|
}
|
|
93
100
|
function deactivate() {
|
|
101
|
+
if (!trapped)
|
|
102
|
+
return;
|
|
103
|
+
trapped = false;
|
|
94
104
|
document.removeEventListener('keydown', handleKeyDown);
|
|
95
105
|
// Return focus when trap is deactivated
|
|
96
|
-
const
|
|
106
|
+
const preventScroll = currentOptions.preventScroll ?? false;
|
|
107
|
+
const elementToReturn = currentOptions.returnFocusTo ?? previousActiveElement;
|
|
97
108
|
if (elementToReturn && typeof elementToReturn.focus === 'function') {
|
|
98
109
|
elementToReturn.focus({ preventScroll });
|
|
99
110
|
}
|
|
100
111
|
}
|
|
112
|
+
// Activate/deactivate as the container attaches/detaches, `active` changes
|
|
113
|
+
// (via getter or action update), or the component is destroyed.
|
|
114
|
+
$effect(() => {
|
|
115
|
+
if (container !== null && isActive()) {
|
|
116
|
+
activate();
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
deactivate();
|
|
120
|
+
}
|
|
121
|
+
});
|
|
101
122
|
/**
|
|
102
123
|
* Svelte action to apply focus trap to an element
|
|
103
124
|
*/
|
|
104
125
|
function action(node) {
|
|
105
126
|
container = node;
|
|
106
|
-
if (active) {
|
|
107
|
-
activate();
|
|
108
|
-
}
|
|
109
127
|
return {
|
|
110
128
|
update(newOptions) {
|
|
111
|
-
|
|
112
|
-
active = newOptions.active ?? true;
|
|
113
|
-
returnFocusTo = newOptions.returnFocusTo ?? null;
|
|
114
|
-
initialFocus = newOptions.initialFocus;
|
|
115
|
-
preventScroll = newOptions.preventScroll ?? false;
|
|
116
|
-
if (!wasActive && active) {
|
|
117
|
-
activate();
|
|
118
|
-
}
|
|
119
|
-
else if (wasActive && !active) {
|
|
120
|
-
deactivate();
|
|
121
|
-
}
|
|
129
|
+
currentOptions = newOptions;
|
|
122
130
|
},
|
|
123
131
|
destroy() {
|
|
124
|
-
|
|
125
|
-
deactivate();
|
|
126
|
-
}
|
|
132
|
+
deactivate();
|
|
127
133
|
container = null;
|
|
128
134
|
},
|
|
129
135
|
};
|
|
@@ -32,10 +32,11 @@ const DEFAULT_SELECTOR = '[data-roving-tabindex-item]';
|
|
|
32
32
|
* ```
|
|
33
33
|
*/
|
|
34
34
|
export function useRovingTabindex(options = {}) {
|
|
35
|
-
|
|
36
|
-
let
|
|
35
|
+
// Plain locals: only read imperatively (handlers/action update), never tracked
|
|
36
|
+
let loop = options.loop ?? true;
|
|
37
|
+
let itemSelector = options.itemSelector ?? DEFAULT_SELECTOR;
|
|
37
38
|
let activeIndex = $state(options.initialIndex ?? 0);
|
|
38
|
-
let onIndexChange =
|
|
39
|
+
let onIndexChange = options.onIndexChange;
|
|
39
40
|
let container = null;
|
|
40
41
|
function getItems() {
|
|
41
42
|
if (!container)
|