@human-kit/svelte-components 1.0.0-alpha.1
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/combobox/TODO.md +175 -0
- package/dist/combobox/button/combobox-button.svelte +57 -0
- package/dist/combobox/button/combobox-button.svelte.d.ts +9 -0
- package/dist/combobox/index.d.ts +14 -0
- package/dist/combobox/index.js +18 -0
- package/dist/combobox/index.parts.d.ts +10 -0
- package/dist/combobox/index.parts.js +11 -0
- package/dist/combobox/input/combobox-input.svelte +98 -0
- package/dist/combobox/input/combobox-input.svelte.d.ts +13 -0
- package/dist/combobox/item/combobox-item-implicit-text-test.svelte +21 -0
- package/dist/combobox/item/combobox-item-implicit-text-test.svelte.d.ts +3 -0
- package/dist/combobox/item/combobox-listboxitem.svelte +136 -0
- package/dist/combobox/item/combobox-listboxitem.svelte.d.ts +18 -0
- package/dist/combobox/item-indicator/combobox-item-indicator.svelte +63 -0
- package/dist/combobox/item-indicator/combobox-item-indicator.svelte.d.ts +17 -0
- package/dist/combobox/list/combobox-listbox.svelte +76 -0
- package/dist/combobox/list/combobox-listbox.svelte.d.ts +47 -0
- package/dist/combobox/popover/combobox-popover.svelte +69 -0
- package/dist/combobox/popover/combobox-popover.svelte.d.ts +12 -0
- package/dist/combobox/root/combobox-filtered-test.svelte +51 -0
- package/dist/combobox/root/combobox-filtered-test.svelte.d.ts +7 -0
- package/dist/combobox/root/combobox-multiselect-test.svelte +76 -0
- package/dist/combobox/root/combobox-multiselect-test.svelte.d.ts +13 -0
- package/dist/combobox/root/combobox-numeric-string-id-test.svelte +20 -0
- package/dist/combobox/root/combobox-numeric-string-id-test.svelte.d.ts +3 -0
- package/dist/combobox/root/combobox-test.svelte +43 -0
- package/dist/combobox/root/combobox-test.svelte.d.ts +9 -0
- package/dist/combobox/root/combobox.svelte +696 -0
- package/dist/combobox/root/combobox.svelte.d.ts +58 -0
- package/dist/combobox/root/context.d.ts +90 -0
- package/dist/combobox/root/context.js +15 -0
- package/dist/combobox/tag/combobox-tag.svelte +58 -0
- package/dist/combobox/tag/combobox-tag.svelte.d.ts +22 -0
- package/dist/combobox/tag/tag-context-provider.svelte +36 -0
- package/dist/combobox/tag/tag-context-provider.svelte.d.ts +14 -0
- package/dist/combobox/tag-remove/combobox-tag-remove.svelte +53 -0
- package/dist/combobox/tag-remove/combobox-tag-remove.svelte.d.ts +14 -0
- package/dist/combobox/tags/combobox-tags.svelte +50 -0
- package/dist/combobox/tags/combobox-tags.svelte.d.ts +20 -0
- package/dist/dialog/content/dialog-content.svelte +121 -0
- package/dist/dialog/content/dialog-content.svelte.d.ts +19 -0
- package/dist/dialog/index.d.ts +10 -0
- package/dist/dialog/index.js +15 -0
- package/dist/dialog/index.parts.d.ts +5 -0
- package/dist/dialog/index.parts.js +6 -0
- package/dist/dialog/overlay/dialog-overlay.svelte +39 -0
- package/dist/dialog/overlay/dialog-overlay.svelte.d.ts +12 -0
- package/dist/dialog/portal/dialog-portal.svelte +32 -0
- package/dist/dialog/portal/dialog-portal.svelte.d.ts +12 -0
- package/dist/dialog/root/context.d.ts +25 -0
- package/dist/dialog/root/context.js +8 -0
- package/dist/dialog/root/dialog-root.svelte +99 -0
- package/dist/dialog/root/dialog-root.svelte.d.ts +21 -0
- package/dist/dialog/root/dialog-stack.d.ts +32 -0
- package/dist/dialog/root/dialog-stack.js +55 -0
- package/dist/dialog/root/dialog-test.svelte +38 -0
- package/dist/dialog/root/dialog-test.svelte.d.ts +10 -0
- package/dist/dialog/root/dialog-with-combobox-test.svelte +61 -0
- package/dist/dialog/root/dialog-with-combobox-test.svelte.d.ts +7 -0
- package/dist/dialog/root/nested-dialog-test.svelte +63 -0
- package/dist/dialog/root/nested-dialog-test.svelte.d.ts +8 -0
- package/dist/dialog/root/types.d.ts +10 -0
- package/dist/dialog/root/types.js +1 -0
- package/dist/dialog/trigger/dialog-trigger.svelte +71 -0
- package/dist/dialog/trigger/dialog-trigger.svelte.d.ts +12 -0
- package/dist/hooks/use-virtual-focus.svelte.d.ts +55 -0
- package/dist/hooks/use-virtual-focus.svelte.js +201 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +19 -0
- package/dist/input/index.d.ts +3 -0
- package/dist/input/index.js +3 -0
- package/dist/input/input.svelte +19 -0
- package/dist/input/input.svelte.d.ts +8 -0
- package/dist/label/index.d.ts +3 -0
- package/dist/label/index.js +3 -0
- package/dist/label/label.svelte +21 -0
- package/dist/label/label.svelte.d.ts +8 -0
- package/dist/listbox/index.d.ts +6 -0
- package/dist/listbox/index.js +10 -0
- package/dist/listbox/index.parts.d.ts +2 -0
- package/dist/listbox/index.parts.js +3 -0
- package/dist/listbox/item/listbox-item.svelte +186 -0
- package/dist/listbox/item/listbox-item.svelte.d.ts +34 -0
- package/dist/listbox/root/context.d.ts +73 -0
- package/dist/listbox/root/context.js +249 -0
- package/dist/listbox/root/listbox-numeric-id-test.svelte +18 -0
- package/dist/listbox/root/listbox-numeric-id-test.svelte.d.ts +3 -0
- package/dist/listbox/root/listbox-test.svelte +27 -0
- package/dist/listbox/root/listbox-test.svelte.d.ts +8 -0
- package/dist/listbox/root/listbox.svelte +146 -0
- package/dist/listbox/root/listbox.svelte.d.ts +54 -0
- package/dist/popover/content/popover-content-test.svelte +43 -0
- package/dist/popover/content/popover-content-test.svelte.d.ts +12 -0
- package/dist/popover/content/popover-content.svelte +167 -0
- package/dist/popover/content/popover-content.svelte.d.ts +38 -0
- package/dist/popover/index.d.ts +8 -0
- package/dist/popover/index.js +14 -0
- package/dist/popover/index.parts.d.ts +4 -0
- package/dist/popover/index.parts.js +5 -0
- package/dist/popover/root/context.d.ts +24 -0
- package/dist/popover/root/context.js +10 -0
- package/dist/popover/root/popover-root.svelte +87 -0
- package/dist/popover/root/popover-root.svelte.d.ts +20 -0
- package/dist/popover/root/popover-test.svelte +40 -0
- package/dist/popover/root/popover-test.svelte.d.ts +11 -0
- package/dist/popover/trigger/popover-trigger-button.svelte +42 -0
- package/dist/popover/trigger/popover-trigger-button.svelte.d.ts +12 -0
- package/dist/popover/trigger/popover-trigger-in-dialog-test.svelte +29 -0
- package/dist/popover/trigger/popover-trigger-in-dialog-test.svelte.d.ts +18 -0
- package/dist/popover/trigger/popover-trigger.svelte +71 -0
- package/dist/popover/trigger/popover-trigger.svelte.d.ts +12 -0
- package/dist/portal/index.d.ts +1 -0
- package/dist/portal/index.js +1 -0
- package/dist/portal/portal.svelte +44 -0
- package/dist/portal/portal.svelte.d.ts +10 -0
- package/dist/primitives/aria-hide-outside.d.ts +38 -0
- package/dist/primitives/aria-hide-outside.js +152 -0
- package/dist/primitives/click-outside.d.ts +26 -0
- package/dist/primitives/click-outside.js +66 -0
- package/dist/primitives/floating.d.ts +57 -0
- package/dist/primitives/floating.js +179 -0
- package/dist/primitives/focus-trap.d.ts +19 -0
- package/dist/primitives/focus-trap.js +102 -0
- package/dist/primitives/index.d.ts +6 -0
- package/dist/primitives/index.js +7 -0
- package/dist/primitives/keyboard-navigation.d.ts +88 -0
- package/dist/primitives/keyboard-navigation.js +274 -0
- package/dist/primitives/scroll-lock.d.ts +19 -0
- package/dist/primitives/scroll-lock.js +62 -0
- package/dist/test-mocks/app-environment.d.ts +7 -0
- package/dist/test-mocks/app-environment.js +7 -0
- package/dist/test-mocks/app-navigation.d.ts +11 -0
- package/dist/test-mocks/app-navigation.js +11 -0
- package/dist/test-mocks/app-stores.d.ts +16 -0
- package/dist/test-mocks/app-stores.js +18 -0
- package/dist/utils/cn.d.ts +2 -0
- package/dist/utils/cn.js +5 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/package.json +99 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Focus trap primitive.
|
|
3
|
+
* Traps keyboard focus within a container element.
|
|
4
|
+
*/
|
|
5
|
+
const FOCUSABLE_SELECTOR = [
|
|
6
|
+
'a[href]',
|
|
7
|
+
'button:not([disabled])',
|
|
8
|
+
'input:not([disabled])',
|
|
9
|
+
'select:not([disabled])',
|
|
10
|
+
'textarea:not([disabled])',
|
|
11
|
+
'[tabindex]:not([tabindex="-1"])',
|
|
12
|
+
'[contenteditable="true"]'
|
|
13
|
+
].join(', ');
|
|
14
|
+
function getFocusableElements(container) {
|
|
15
|
+
return Array.from(container.querySelectorAll(FOCUSABLE_SELECTOR)).filter((el) => el.offsetParent !== null);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Svelte action that traps focus within an element.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```svelte
|
|
22
|
+
* <div use:focusTrap={isOpen}>
|
|
23
|
+
* <button>First</button>
|
|
24
|
+
* <button>Last</button>
|
|
25
|
+
* </div>
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export function focusTrap(node, enabled = true) {
|
|
29
|
+
let previousActiveElement = null;
|
|
30
|
+
function handleKeydown(event) {
|
|
31
|
+
if (event.key !== 'Tab')
|
|
32
|
+
return;
|
|
33
|
+
const focusableElements = getFocusableElements(node);
|
|
34
|
+
if (focusableElements.length === 0) {
|
|
35
|
+
event.preventDefault();
|
|
36
|
+
node.focus();
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const firstElement = focusableElements[0];
|
|
40
|
+
const lastElement = focusableElements[focusableElements.length - 1];
|
|
41
|
+
const focusIsInside = node.contains(document.activeElement);
|
|
42
|
+
if (!focusIsInside) {
|
|
43
|
+
event.preventDefault();
|
|
44
|
+
firstElement.focus();
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (event.shiftKey) {
|
|
48
|
+
if (document.activeElement === firstElement || document.activeElement === node) {
|
|
49
|
+
event.preventDefault();
|
|
50
|
+
lastElement.focus();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
if (document.activeElement === lastElement || document.activeElement === node) {
|
|
55
|
+
event.preventDefault();
|
|
56
|
+
firstElement.focus();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function activate() {
|
|
61
|
+
previousActiveElement = document.activeElement;
|
|
62
|
+
if (!node.hasAttribute('tabindex')) {
|
|
63
|
+
node.setAttribute('tabindex', '-1');
|
|
64
|
+
}
|
|
65
|
+
// Focus first focusable element, or the container if none
|
|
66
|
+
requestAnimationFrame(() => {
|
|
67
|
+
const focusableElements = getFocusableElements(node);
|
|
68
|
+
if (focusableElements.length > 0) {
|
|
69
|
+
focusableElements[0].focus();
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
node.focus();
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
document.addEventListener('keydown', handleKeydown, true);
|
|
76
|
+
}
|
|
77
|
+
function deactivate() {
|
|
78
|
+
document.removeEventListener('keydown', handleKeydown, true);
|
|
79
|
+
if (previousActiveElement && previousActiveElement.focus) {
|
|
80
|
+
previousActiveElement.focus();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (enabled) {
|
|
84
|
+
activate();
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
update(newEnabled) {
|
|
88
|
+
if (newEnabled && !enabled) {
|
|
89
|
+
activate();
|
|
90
|
+
}
|
|
91
|
+
else if (!newEnabled && enabled) {
|
|
92
|
+
deactivate();
|
|
93
|
+
}
|
|
94
|
+
enabled = newEnabled;
|
|
95
|
+
},
|
|
96
|
+
destroy() {
|
|
97
|
+
if (enabled) {
|
|
98
|
+
deactivate();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { type Writable } from 'svelte/store';
|
|
2
|
+
export type KeyboardNavigationOptions = {
|
|
3
|
+
/**
|
|
4
|
+
* Orientation of the navigation
|
|
5
|
+
* - 'vertical': ArrowUp/ArrowDown for navigation
|
|
6
|
+
* - 'horizontal': ArrowLeft/ArrowRight for navigation
|
|
7
|
+
* - 'both': All arrow keys for navigation
|
|
8
|
+
*/
|
|
9
|
+
orientation?: 'vertical' | 'horizontal' | 'both';
|
|
10
|
+
/**
|
|
11
|
+
* Whether navigation wraps around at the ends
|
|
12
|
+
*/
|
|
13
|
+
loop?: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Selector for finding navigable items within the container
|
|
16
|
+
*/
|
|
17
|
+
itemSelector?: string;
|
|
18
|
+
/**
|
|
19
|
+
* Callback when an item is selected (Enter/Space)
|
|
20
|
+
*/
|
|
21
|
+
onSelect?: (id: string | number, element: HTMLElement) => void;
|
|
22
|
+
/**
|
|
23
|
+
* Callback when focused item changes
|
|
24
|
+
*/
|
|
25
|
+
onFocusChange?: (id: string | number | null, element: HTMLElement | null) => void;
|
|
26
|
+
/**
|
|
27
|
+
* Callback for Ctrl+A (select all)
|
|
28
|
+
*/
|
|
29
|
+
onSelectAll?: () => void;
|
|
30
|
+
/**
|
|
31
|
+
* Whether to handle Home/End keys
|
|
32
|
+
*/
|
|
33
|
+
homeEndKeys?: boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Whether to handle typeahead (character search)
|
|
36
|
+
*/
|
|
37
|
+
typeahead?: boolean;
|
|
38
|
+
};
|
|
39
|
+
export type KeyboardNavigationState = {
|
|
40
|
+
focusedId: Writable<string | number | null>;
|
|
41
|
+
focusedElement: Writable<HTMLElement | null>;
|
|
42
|
+
};
|
|
43
|
+
export type KeyboardNavigationReturn = {
|
|
44
|
+
/** Current state stores */
|
|
45
|
+
state: KeyboardNavigationState;
|
|
46
|
+
/** Svelte action to attach to container */
|
|
47
|
+
action: (node: HTMLElement) => {
|
|
48
|
+
destroy: () => void;
|
|
49
|
+
};
|
|
50
|
+
/** Programmatic navigation methods */
|
|
51
|
+
focusNext: () => void;
|
|
52
|
+
focusPrevious: () => void;
|
|
53
|
+
focusFirst: () => void;
|
|
54
|
+
focusLast: () => void;
|
|
55
|
+
focusById: (id: string | number) => void;
|
|
56
|
+
/** Update items (call after DOM changes) */
|
|
57
|
+
updateItems: () => void;
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Creates a keyboard navigation controller for list-like components.
|
|
61
|
+
* Implements WAI-ARIA patterns for keyboard navigation.
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* ```svelte
|
|
65
|
+
* <script>
|
|
66
|
+
* import { createKeyboardNavigation } from './keyboard-navigation';
|
|
67
|
+
*
|
|
68
|
+
* const { action, state, focusNext } = createKeyboardNavigation({
|
|
69
|
+
* orientation: 'vertical',
|
|
70
|
+
* onSelect: (id) => console.log('Selected:', id)
|
|
71
|
+
* });
|
|
72
|
+
* </script>
|
|
73
|
+
*
|
|
74
|
+
* <div use:action>
|
|
75
|
+
* <div data-navigation-item data-item-id="1">Item 1</div>
|
|
76
|
+
* <div data-navigation-item data-item-id="2">Item 2</div>
|
|
77
|
+
* </div>
|
|
78
|
+
* ```
|
|
79
|
+
*/
|
|
80
|
+
export declare function createKeyboardNavigation(options?: KeyboardNavigationOptions): KeyboardNavigationReturn;
|
|
81
|
+
/**
|
|
82
|
+
* Simple Svelte action for roving tabindex without state management.
|
|
83
|
+
* Use this for simpler cases where you don't need programmatic control.
|
|
84
|
+
*/
|
|
85
|
+
export declare function rovingTabindex(container: HTMLElement, options?: Pick<KeyboardNavigationOptions, 'orientation' | 'loop' | 'itemSelector'>): {
|
|
86
|
+
update: () => void;
|
|
87
|
+
destroy: () => void;
|
|
88
|
+
};
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { writable } from 'svelte/store';
|
|
2
|
+
/**
|
|
3
|
+
* Creates a keyboard navigation controller for list-like components.
|
|
4
|
+
* Implements WAI-ARIA patterns for keyboard navigation.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```svelte
|
|
8
|
+
* <script>
|
|
9
|
+
* import { createKeyboardNavigation } from './keyboard-navigation';
|
|
10
|
+
*
|
|
11
|
+
* const { action, state, focusNext } = createKeyboardNavigation({
|
|
12
|
+
* orientation: 'vertical',
|
|
13
|
+
* onSelect: (id) => console.log('Selected:', id)
|
|
14
|
+
* });
|
|
15
|
+
* </script>
|
|
16
|
+
*
|
|
17
|
+
* <div use:action>
|
|
18
|
+
* <div data-navigation-item data-item-id="1">Item 1</div>
|
|
19
|
+
* <div data-navigation-item data-item-id="2">Item 2</div>
|
|
20
|
+
* </div>
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export function createKeyboardNavigation(options = {}) {
|
|
24
|
+
const { orientation = 'vertical', loop = false, itemSelector = '[data-navigation-item]:not([data-disabled])', onSelect, onFocusChange, onSelectAll, homeEndKeys = true, typeahead = false } = options;
|
|
25
|
+
const focusedId = writable(null);
|
|
26
|
+
const focusedElement = writable(null);
|
|
27
|
+
let container = null;
|
|
28
|
+
let items = [];
|
|
29
|
+
let typeaheadBuffer = '';
|
|
30
|
+
let typeaheadTimeout = null;
|
|
31
|
+
function getItems() {
|
|
32
|
+
if (!container)
|
|
33
|
+
return [];
|
|
34
|
+
return Array.from(container.querySelectorAll(itemSelector));
|
|
35
|
+
}
|
|
36
|
+
function updateItems() {
|
|
37
|
+
items = getItems();
|
|
38
|
+
// Note: tabIndex is now controlled by Svelte components via isFocused state
|
|
39
|
+
// The primitive only manages focus() calls and notifies about focus changes
|
|
40
|
+
}
|
|
41
|
+
function getItemId(element) {
|
|
42
|
+
const rawId = element.dataset.itemId;
|
|
43
|
+
if (rawId === undefined)
|
|
44
|
+
return null;
|
|
45
|
+
const idType = element.dataset.itemIdType;
|
|
46
|
+
if (idType === 'number') {
|
|
47
|
+
const parsed = Number(rawId);
|
|
48
|
+
return Number.isNaN(parsed) ? rawId : parsed;
|
|
49
|
+
}
|
|
50
|
+
return rawId;
|
|
51
|
+
}
|
|
52
|
+
function focusItem(element) {
|
|
53
|
+
if (!element) {
|
|
54
|
+
focusedId.set(null);
|
|
55
|
+
focusedElement.set(null);
|
|
56
|
+
onFocusChange?.(null, null);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const id = getItemId(element);
|
|
60
|
+
focusedId.set(id);
|
|
61
|
+
focusedElement.set(element);
|
|
62
|
+
// Note: tabIndex is controlled by Svelte via isFocused state
|
|
63
|
+
// We just call focus() and notify - the component will react to onFocusChange
|
|
64
|
+
element.focus();
|
|
65
|
+
onFocusChange?.(id, element);
|
|
66
|
+
}
|
|
67
|
+
function getCurrentIndex() {
|
|
68
|
+
let currentElement = null;
|
|
69
|
+
focusedElement.subscribe((el) => (currentElement = el))();
|
|
70
|
+
if (!currentElement) {
|
|
71
|
+
const active = document.activeElement;
|
|
72
|
+
const idx = items.indexOf(active);
|
|
73
|
+
return idx;
|
|
74
|
+
}
|
|
75
|
+
return items.indexOf(currentElement);
|
|
76
|
+
}
|
|
77
|
+
function focusNext() {
|
|
78
|
+
items = getItems();
|
|
79
|
+
if (items.length === 0)
|
|
80
|
+
return;
|
|
81
|
+
const currentIdx = getCurrentIndex();
|
|
82
|
+
let nextIdx;
|
|
83
|
+
if (currentIdx === -1) {
|
|
84
|
+
nextIdx = 0;
|
|
85
|
+
}
|
|
86
|
+
else if (loop) {
|
|
87
|
+
nextIdx = (currentIdx + 1) % items.length;
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
nextIdx = Math.min(currentIdx + 1, items.length - 1);
|
|
91
|
+
}
|
|
92
|
+
focusItem(items[nextIdx]);
|
|
93
|
+
}
|
|
94
|
+
function focusPrevious() {
|
|
95
|
+
items = getItems();
|
|
96
|
+
if (items.length === 0)
|
|
97
|
+
return;
|
|
98
|
+
const currentIdx = getCurrentIndex();
|
|
99
|
+
let prevIdx;
|
|
100
|
+
if (currentIdx === -1) {
|
|
101
|
+
prevIdx = items.length - 1;
|
|
102
|
+
}
|
|
103
|
+
else if (loop) {
|
|
104
|
+
prevIdx = (currentIdx - 1 + items.length) % items.length;
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
prevIdx = Math.max(currentIdx - 1, 0);
|
|
108
|
+
}
|
|
109
|
+
focusItem(items[prevIdx]);
|
|
110
|
+
}
|
|
111
|
+
function focusFirst() {
|
|
112
|
+
items = getItems();
|
|
113
|
+
if (items.length === 0)
|
|
114
|
+
return;
|
|
115
|
+
focusItem(items[0]);
|
|
116
|
+
}
|
|
117
|
+
function focusLast() {
|
|
118
|
+
items = getItems();
|
|
119
|
+
if (items.length === 0)
|
|
120
|
+
return;
|
|
121
|
+
focusItem(items[items.length - 1]);
|
|
122
|
+
}
|
|
123
|
+
function focusById(id) {
|
|
124
|
+
items = getItems();
|
|
125
|
+
const element = items.find((el) => {
|
|
126
|
+
const itemId = getItemId(el);
|
|
127
|
+
return itemId === id || String(itemId) === String(id);
|
|
128
|
+
});
|
|
129
|
+
if (element) {
|
|
130
|
+
focusItem(element);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function handleTypeahead(char) {
|
|
134
|
+
if (!typeahead)
|
|
135
|
+
return;
|
|
136
|
+
if (typeaheadTimeout) {
|
|
137
|
+
clearTimeout(typeaheadTimeout);
|
|
138
|
+
}
|
|
139
|
+
typeaheadBuffer += char.toLowerCase();
|
|
140
|
+
items = getItems();
|
|
141
|
+
const match = items.find((el) => {
|
|
142
|
+
const text = el.textContent?.trim().toLowerCase() || '';
|
|
143
|
+
return text.startsWith(typeaheadBuffer);
|
|
144
|
+
});
|
|
145
|
+
if (match) {
|
|
146
|
+
focusItem(match);
|
|
147
|
+
}
|
|
148
|
+
typeaheadTimeout = setTimeout(() => {
|
|
149
|
+
typeaheadBuffer = '';
|
|
150
|
+
}, 500);
|
|
151
|
+
}
|
|
152
|
+
function handleKeydown(event) {
|
|
153
|
+
const { key, ctrlKey, metaKey } = event;
|
|
154
|
+
const nextKeys = orientation === 'horizontal'
|
|
155
|
+
? ['ArrowRight']
|
|
156
|
+
: orientation === 'vertical'
|
|
157
|
+
? ['ArrowDown']
|
|
158
|
+
: ['ArrowDown', 'ArrowRight'];
|
|
159
|
+
const prevKeys = orientation === 'horizontal'
|
|
160
|
+
? ['ArrowLeft']
|
|
161
|
+
: orientation === 'vertical'
|
|
162
|
+
? ['ArrowUp']
|
|
163
|
+
: ['ArrowUp', 'ArrowLeft'];
|
|
164
|
+
if (nextKeys.includes(key)) {
|
|
165
|
+
event.preventDefault();
|
|
166
|
+
focusNext();
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (prevKeys.includes(key)) {
|
|
170
|
+
event.preventDefault();
|
|
171
|
+
focusPrevious();
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
// For all other keys, block repeat (prevent scroll but don't act)
|
|
175
|
+
if (event.repeat) {
|
|
176
|
+
event.preventDefault();
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
// Home/End - single press only
|
|
180
|
+
if (homeEndKeys) {
|
|
181
|
+
if (key === 'Home') {
|
|
182
|
+
event.preventDefault();
|
|
183
|
+
focusFirst();
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
if (key === 'End') {
|
|
187
|
+
event.preventDefault();
|
|
188
|
+
focusLast();
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
// Selection - single press only
|
|
193
|
+
if (key === 'Enter' || key === ' ') {
|
|
194
|
+
event.preventDefault();
|
|
195
|
+
const active = document.activeElement;
|
|
196
|
+
items = getItems();
|
|
197
|
+
if (active && items.includes(active)) {
|
|
198
|
+
const id = getItemId(active);
|
|
199
|
+
if (id !== null) {
|
|
200
|
+
onSelect?.(id, active);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
// Select all (Ctrl+A / Cmd+A)
|
|
206
|
+
if ((ctrlKey || metaKey) && (key === 'a' || key === 'A')) {
|
|
207
|
+
if (onSelectAll) {
|
|
208
|
+
event.preventDefault();
|
|
209
|
+
onSelectAll();
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
// Typeahead (single printable character)
|
|
214
|
+
if (typeahead && key.length === 1 && !ctrlKey && !metaKey) {
|
|
215
|
+
handleTypeahead(key);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
function action(node) {
|
|
219
|
+
container = node;
|
|
220
|
+
updateItems();
|
|
221
|
+
node.addEventListener('keydown', handleKeydown);
|
|
222
|
+
// Handle focus on container
|
|
223
|
+
function handleContainerFocus(event) {
|
|
224
|
+
// If focusing container directly (not an item), focus first item
|
|
225
|
+
if (event.target === node) {
|
|
226
|
+
items = getItems();
|
|
227
|
+
if (items.length > 0) {
|
|
228
|
+
// Focus the item with tabIndex 0 or first item
|
|
229
|
+
const tabbable = items.find((el) => el.tabIndex === 0) || items[0];
|
|
230
|
+
focusItem(tabbable);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
node.addEventListener('focus', handleContainerFocus, true);
|
|
235
|
+
return {
|
|
236
|
+
destroy() {
|
|
237
|
+
node.removeEventListener('keydown', handleKeydown);
|
|
238
|
+
node.removeEventListener('focus', handleContainerFocus, true);
|
|
239
|
+
container = null;
|
|
240
|
+
if (typeaheadTimeout) {
|
|
241
|
+
clearTimeout(typeaheadTimeout);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
return {
|
|
247
|
+
state: {
|
|
248
|
+
focusedId,
|
|
249
|
+
focusedElement
|
|
250
|
+
},
|
|
251
|
+
action,
|
|
252
|
+
focusNext,
|
|
253
|
+
focusPrevious,
|
|
254
|
+
focusFirst,
|
|
255
|
+
focusLast,
|
|
256
|
+
focusById,
|
|
257
|
+
updateItems
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Simple Svelte action for roving tabindex without state management.
|
|
262
|
+
* Use this for simpler cases where you don't need programmatic control.
|
|
263
|
+
*/
|
|
264
|
+
export function rovingTabindex(container, options = {}) {
|
|
265
|
+
if (typeof document === 'undefined') {
|
|
266
|
+
return { update: () => { }, destroy: () => { } };
|
|
267
|
+
}
|
|
268
|
+
const { action, updateItems } = createKeyboardNavigation(options);
|
|
269
|
+
const cleanup = action(container);
|
|
270
|
+
return {
|
|
271
|
+
update: updateItems,
|
|
272
|
+
destroy: cleanup.destroy
|
|
273
|
+
};
|
|
274
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scroll lock primitive.
|
|
3
|
+
* Prevents scrolling of the document body.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Svelte action that locks scrolling on the document body.
|
|
7
|
+
* Handles multiple nested scroll locks correctly.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```svelte
|
|
11
|
+
* <div use:scrollLock={isOpen}>
|
|
12
|
+
* Modal content
|
|
13
|
+
* </div>
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export declare function scrollLock(node: HTMLElement, enabled?: boolean): {
|
|
17
|
+
update(newEnabled: boolean): void;
|
|
18
|
+
destroy(): void;
|
|
19
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scroll lock primitive.
|
|
3
|
+
* Prevents scrolling of the document body.
|
|
4
|
+
*/
|
|
5
|
+
let lockCount = 0;
|
|
6
|
+
let originalOverflow = '';
|
|
7
|
+
let originalPaddingRight = '';
|
|
8
|
+
function getScrollbarWidth() {
|
|
9
|
+
return window.innerWidth - document.documentElement.clientWidth;
|
|
10
|
+
}
|
|
11
|
+
function lock() {
|
|
12
|
+
if (lockCount === 0) {
|
|
13
|
+
originalOverflow = document.body.style.overflow;
|
|
14
|
+
originalPaddingRight = document.body.style.paddingRight;
|
|
15
|
+
const scrollbarWidth = getScrollbarWidth();
|
|
16
|
+
document.body.style.overflow = 'hidden';
|
|
17
|
+
if (scrollbarWidth > 0) {
|
|
18
|
+
document.body.style.paddingRight = `${scrollbarWidth}px`;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
lockCount++;
|
|
22
|
+
}
|
|
23
|
+
function unlock() {
|
|
24
|
+
lockCount--;
|
|
25
|
+
if (lockCount === 0) {
|
|
26
|
+
document.body.style.overflow = originalOverflow;
|
|
27
|
+
document.body.style.paddingRight = originalPaddingRight;
|
|
28
|
+
}
|
|
29
|
+
lockCount = Math.max(0, lockCount);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Svelte action that locks scrolling on the document body.
|
|
33
|
+
* Handles multiple nested scroll locks correctly.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```svelte
|
|
37
|
+
* <div use:scrollLock={isOpen}>
|
|
38
|
+
* Modal content
|
|
39
|
+
* </div>
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
export function scrollLock(node, enabled = true) {
|
|
43
|
+
if (enabled) {
|
|
44
|
+
lock();
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
update(newEnabled) {
|
|
48
|
+
if (newEnabled && !enabled) {
|
|
49
|
+
lock();
|
|
50
|
+
}
|
|
51
|
+
else if (!newEnabled && enabled) {
|
|
52
|
+
unlock();
|
|
53
|
+
}
|
|
54
|
+
enabled = newEnabled;
|
|
55
|
+
},
|
|
56
|
+
destroy() {
|
|
57
|
+
if (enabled) {
|
|
58
|
+
unlock();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mock for $app/navigation used in vitest tests
|
|
3
|
+
*/
|
|
4
|
+
export declare const goto: () => Promise<void>;
|
|
5
|
+
export declare const invalidate: () => Promise<void>;
|
|
6
|
+
export declare const invalidateAll: () => Promise<void>;
|
|
7
|
+
export declare const prefetch: () => Promise<void>;
|
|
8
|
+
export declare const prefetchRoutes: () => Promise<void>;
|
|
9
|
+
export declare const beforeNavigate: () => void;
|
|
10
|
+
export declare const afterNavigate: () => void;
|
|
11
|
+
export declare const onNavigate: () => void;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mock for $app/navigation used in vitest tests
|
|
3
|
+
*/
|
|
4
|
+
export const goto = () => Promise.resolve();
|
|
5
|
+
export const invalidate = () => Promise.resolve();
|
|
6
|
+
export const invalidateAll = () => Promise.resolve();
|
|
7
|
+
export const prefetch = () => Promise.resolve();
|
|
8
|
+
export const prefetchRoutes = () => Promise.resolve();
|
|
9
|
+
export const beforeNavigate = () => { };
|
|
10
|
+
export const afterNavigate = () => { };
|
|
11
|
+
export const onNavigate = () => { };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export declare const page: import("svelte/store").Readable<{
|
|
2
|
+
url: URL;
|
|
3
|
+
params: {};
|
|
4
|
+
route: {
|
|
5
|
+
id: null;
|
|
6
|
+
};
|
|
7
|
+
status: number;
|
|
8
|
+
error: null;
|
|
9
|
+
data: {};
|
|
10
|
+
form: null;
|
|
11
|
+
}>;
|
|
12
|
+
export declare const navigating: import("svelte/store").Readable<null>;
|
|
13
|
+
export declare const updated: {
|
|
14
|
+
subscribe: (this: void, run: import("svelte/store").Subscriber<boolean>, invalidate?: () => void) => import("svelte/store").Unsubscriber;
|
|
15
|
+
check: () => Promise<boolean>;
|
|
16
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mock for $app/stores used in vitest tests
|
|
3
|
+
*/
|
|
4
|
+
import { readable } from 'svelte/store';
|
|
5
|
+
export const page = readable({
|
|
6
|
+
url: new URL('http://localhost'),
|
|
7
|
+
params: {},
|
|
8
|
+
route: { id: null },
|
|
9
|
+
status: 200,
|
|
10
|
+
error: null,
|
|
11
|
+
data: {},
|
|
12
|
+
form: null
|
|
13
|
+
});
|
|
14
|
+
export const navigating = readable(null);
|
|
15
|
+
export const updated = {
|
|
16
|
+
subscribe: readable(false).subscribe,
|
|
17
|
+
check: () => Promise.resolve(false)
|
|
18
|
+
};
|
package/dist/utils/cn.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { cn } from './cn.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { cn } from './cn.js';
|