@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.
Files changed (140) hide show
  1. package/dist/combobox/TODO.md +175 -0
  2. package/dist/combobox/button/combobox-button.svelte +57 -0
  3. package/dist/combobox/button/combobox-button.svelte.d.ts +9 -0
  4. package/dist/combobox/index.d.ts +14 -0
  5. package/dist/combobox/index.js +18 -0
  6. package/dist/combobox/index.parts.d.ts +10 -0
  7. package/dist/combobox/index.parts.js +11 -0
  8. package/dist/combobox/input/combobox-input.svelte +98 -0
  9. package/dist/combobox/input/combobox-input.svelte.d.ts +13 -0
  10. package/dist/combobox/item/combobox-item-implicit-text-test.svelte +21 -0
  11. package/dist/combobox/item/combobox-item-implicit-text-test.svelte.d.ts +3 -0
  12. package/dist/combobox/item/combobox-listboxitem.svelte +136 -0
  13. package/dist/combobox/item/combobox-listboxitem.svelte.d.ts +18 -0
  14. package/dist/combobox/item-indicator/combobox-item-indicator.svelte +63 -0
  15. package/dist/combobox/item-indicator/combobox-item-indicator.svelte.d.ts +17 -0
  16. package/dist/combobox/list/combobox-listbox.svelte +76 -0
  17. package/dist/combobox/list/combobox-listbox.svelte.d.ts +47 -0
  18. package/dist/combobox/popover/combobox-popover.svelte +69 -0
  19. package/dist/combobox/popover/combobox-popover.svelte.d.ts +12 -0
  20. package/dist/combobox/root/combobox-filtered-test.svelte +51 -0
  21. package/dist/combobox/root/combobox-filtered-test.svelte.d.ts +7 -0
  22. package/dist/combobox/root/combobox-multiselect-test.svelte +76 -0
  23. package/dist/combobox/root/combobox-multiselect-test.svelte.d.ts +13 -0
  24. package/dist/combobox/root/combobox-numeric-string-id-test.svelte +20 -0
  25. package/dist/combobox/root/combobox-numeric-string-id-test.svelte.d.ts +3 -0
  26. package/dist/combobox/root/combobox-test.svelte +43 -0
  27. package/dist/combobox/root/combobox-test.svelte.d.ts +9 -0
  28. package/dist/combobox/root/combobox.svelte +696 -0
  29. package/dist/combobox/root/combobox.svelte.d.ts +58 -0
  30. package/dist/combobox/root/context.d.ts +90 -0
  31. package/dist/combobox/root/context.js +15 -0
  32. package/dist/combobox/tag/combobox-tag.svelte +58 -0
  33. package/dist/combobox/tag/combobox-tag.svelte.d.ts +22 -0
  34. package/dist/combobox/tag/tag-context-provider.svelte +36 -0
  35. package/dist/combobox/tag/tag-context-provider.svelte.d.ts +14 -0
  36. package/dist/combobox/tag-remove/combobox-tag-remove.svelte +53 -0
  37. package/dist/combobox/tag-remove/combobox-tag-remove.svelte.d.ts +14 -0
  38. package/dist/combobox/tags/combobox-tags.svelte +50 -0
  39. package/dist/combobox/tags/combobox-tags.svelte.d.ts +20 -0
  40. package/dist/dialog/content/dialog-content.svelte +121 -0
  41. package/dist/dialog/content/dialog-content.svelte.d.ts +19 -0
  42. package/dist/dialog/index.d.ts +10 -0
  43. package/dist/dialog/index.js +15 -0
  44. package/dist/dialog/index.parts.d.ts +5 -0
  45. package/dist/dialog/index.parts.js +6 -0
  46. package/dist/dialog/overlay/dialog-overlay.svelte +39 -0
  47. package/dist/dialog/overlay/dialog-overlay.svelte.d.ts +12 -0
  48. package/dist/dialog/portal/dialog-portal.svelte +32 -0
  49. package/dist/dialog/portal/dialog-portal.svelte.d.ts +12 -0
  50. package/dist/dialog/root/context.d.ts +25 -0
  51. package/dist/dialog/root/context.js +8 -0
  52. package/dist/dialog/root/dialog-root.svelte +99 -0
  53. package/dist/dialog/root/dialog-root.svelte.d.ts +21 -0
  54. package/dist/dialog/root/dialog-stack.d.ts +32 -0
  55. package/dist/dialog/root/dialog-stack.js +55 -0
  56. package/dist/dialog/root/dialog-test.svelte +38 -0
  57. package/dist/dialog/root/dialog-test.svelte.d.ts +10 -0
  58. package/dist/dialog/root/dialog-with-combobox-test.svelte +61 -0
  59. package/dist/dialog/root/dialog-with-combobox-test.svelte.d.ts +7 -0
  60. package/dist/dialog/root/nested-dialog-test.svelte +63 -0
  61. package/dist/dialog/root/nested-dialog-test.svelte.d.ts +8 -0
  62. package/dist/dialog/root/types.d.ts +10 -0
  63. package/dist/dialog/root/types.js +1 -0
  64. package/dist/dialog/trigger/dialog-trigger.svelte +71 -0
  65. package/dist/dialog/trigger/dialog-trigger.svelte.d.ts +12 -0
  66. package/dist/hooks/use-virtual-focus.svelte.d.ts +55 -0
  67. package/dist/hooks/use-virtual-focus.svelte.js +201 -0
  68. package/dist/index.d.ts +13 -0
  69. package/dist/index.js +19 -0
  70. package/dist/input/index.d.ts +3 -0
  71. package/dist/input/index.js +3 -0
  72. package/dist/input/input.svelte +19 -0
  73. package/dist/input/input.svelte.d.ts +8 -0
  74. package/dist/label/index.d.ts +3 -0
  75. package/dist/label/index.js +3 -0
  76. package/dist/label/label.svelte +21 -0
  77. package/dist/label/label.svelte.d.ts +8 -0
  78. package/dist/listbox/index.d.ts +6 -0
  79. package/dist/listbox/index.js +10 -0
  80. package/dist/listbox/index.parts.d.ts +2 -0
  81. package/dist/listbox/index.parts.js +3 -0
  82. package/dist/listbox/item/listbox-item.svelte +186 -0
  83. package/dist/listbox/item/listbox-item.svelte.d.ts +34 -0
  84. package/dist/listbox/root/context.d.ts +73 -0
  85. package/dist/listbox/root/context.js +249 -0
  86. package/dist/listbox/root/listbox-numeric-id-test.svelte +18 -0
  87. package/dist/listbox/root/listbox-numeric-id-test.svelte.d.ts +3 -0
  88. package/dist/listbox/root/listbox-test.svelte +27 -0
  89. package/dist/listbox/root/listbox-test.svelte.d.ts +8 -0
  90. package/dist/listbox/root/listbox.svelte +146 -0
  91. package/dist/listbox/root/listbox.svelte.d.ts +54 -0
  92. package/dist/popover/content/popover-content-test.svelte +43 -0
  93. package/dist/popover/content/popover-content-test.svelte.d.ts +12 -0
  94. package/dist/popover/content/popover-content.svelte +167 -0
  95. package/dist/popover/content/popover-content.svelte.d.ts +38 -0
  96. package/dist/popover/index.d.ts +8 -0
  97. package/dist/popover/index.js +14 -0
  98. package/dist/popover/index.parts.d.ts +4 -0
  99. package/dist/popover/index.parts.js +5 -0
  100. package/dist/popover/root/context.d.ts +24 -0
  101. package/dist/popover/root/context.js +10 -0
  102. package/dist/popover/root/popover-root.svelte +87 -0
  103. package/dist/popover/root/popover-root.svelte.d.ts +20 -0
  104. package/dist/popover/root/popover-test.svelte +40 -0
  105. package/dist/popover/root/popover-test.svelte.d.ts +11 -0
  106. package/dist/popover/trigger/popover-trigger-button.svelte +42 -0
  107. package/dist/popover/trigger/popover-trigger-button.svelte.d.ts +12 -0
  108. package/dist/popover/trigger/popover-trigger-in-dialog-test.svelte +29 -0
  109. package/dist/popover/trigger/popover-trigger-in-dialog-test.svelte.d.ts +18 -0
  110. package/dist/popover/trigger/popover-trigger.svelte +71 -0
  111. package/dist/popover/trigger/popover-trigger.svelte.d.ts +12 -0
  112. package/dist/portal/index.d.ts +1 -0
  113. package/dist/portal/index.js +1 -0
  114. package/dist/portal/portal.svelte +44 -0
  115. package/dist/portal/portal.svelte.d.ts +10 -0
  116. package/dist/primitives/aria-hide-outside.d.ts +38 -0
  117. package/dist/primitives/aria-hide-outside.js +152 -0
  118. package/dist/primitives/click-outside.d.ts +26 -0
  119. package/dist/primitives/click-outside.js +66 -0
  120. package/dist/primitives/floating.d.ts +57 -0
  121. package/dist/primitives/floating.js +179 -0
  122. package/dist/primitives/focus-trap.d.ts +19 -0
  123. package/dist/primitives/focus-trap.js +102 -0
  124. package/dist/primitives/index.d.ts +6 -0
  125. package/dist/primitives/index.js +7 -0
  126. package/dist/primitives/keyboard-navigation.d.ts +88 -0
  127. package/dist/primitives/keyboard-navigation.js +274 -0
  128. package/dist/primitives/scroll-lock.d.ts +19 -0
  129. package/dist/primitives/scroll-lock.js +62 -0
  130. package/dist/test-mocks/app-environment.d.ts +7 -0
  131. package/dist/test-mocks/app-environment.js +7 -0
  132. package/dist/test-mocks/app-navigation.d.ts +11 -0
  133. package/dist/test-mocks/app-navigation.js +11 -0
  134. package/dist/test-mocks/app-stores.d.ts +16 -0
  135. package/dist/test-mocks/app-stores.js +18 -0
  136. package/dist/utils/cn.d.ts +2 -0
  137. package/dist/utils/cn.js +5 -0
  138. package/dist/utils/index.d.ts +1 -0
  139. package/dist/utils/index.js +1 -0
  140. package/package.json +99 -0
@@ -0,0 +1,201 @@
1
+ /**
2
+ * useVirtualFocus - A reusable hook for virtual focus navigation.
3
+ *
4
+ * Implements the aria-activedescendant pattern for keyboard navigation
5
+ * in composite widgets like ComboBox, Menu, Select, TreeView, etc.
6
+ *
7
+ * Features:
8
+ * - Virtual focus state (focusedId)
9
+ * - Item registration/unregistration
10
+ * - Navigation (next, previous, first, last, pageUp, pageDown)
11
+ * - DOM order caching for performance
12
+ * - Scoped queries via containerRef
13
+ *
14
+ * @example
15
+ * ```svelte
16
+ * <script>
17
+ * import { useVirtualFocus } from './use-virtual-focus.svelte';
18
+ *
19
+ * const nav = useVirtualFocus({
20
+ * instanceId: 'my-list',
21
+ * containerRef: () => listboxElement
22
+ * });
23
+ *
24
+ * // Use nav.focusedId, nav.next(), nav.register(), etc.
25
+ * // PageSize is calculated automatically based on item count
26
+ * </script>
27
+ * ```
28
+ */
29
+ import { SvelteMap } from 'svelte/reactivity';
30
+ export function useVirtualFocus(options) {
31
+ const { instanceId, itemPrefix = 'combobox-item', containerRef } = options;
32
+ // Internal state
33
+ let focusedId = $state(null);
34
+ let itemIds = $state([]);
35
+ const itemLabels = new SvelteMap();
36
+ let pendingFocusDirection = $state(null);
37
+ let cachedItemOrder = $state(null);
38
+ /**
39
+ * Smart page size calculation based on number of registered items.
40
+ * Ensures PageUp/PageDown jump a reasonable amount regardless of list size.
41
+ */
42
+ const pageSize = $derived(itemIds.length === 0 ? 10 : Math.max(5, Math.min(10, Math.ceil(itemIds.length / 3))));
43
+ /**
44
+ * Get item IDs in DOM order.
45
+ * Uses cache when available, queries DOM otherwise.
46
+ */
47
+ function getItemIdsInOrder() {
48
+ if (cachedItemOrder !== null) {
49
+ return cachedItemOrder;
50
+ }
51
+ const selector = `[id^="${itemPrefix}-${instanceId}-"]`;
52
+ const queryRoot = containerRef?.() ?? document;
53
+ const itemElements = queryRoot.querySelectorAll(selector);
54
+ if (itemElements.length === 0) {
55
+ cachedItemOrder = itemIds;
56
+ return cachedItemOrder;
57
+ }
58
+ const orderedIds = [];
59
+ const prefix = `${itemPrefix}-${instanceId}-`;
60
+ itemElements.forEach((el) => {
61
+ const fullId = el.getAttribute('id');
62
+ if (fullId && fullId.startsWith(prefix)) {
63
+ const rawId = fullId.substring(prefix.length);
64
+ const registeredId = itemIds.find((id) => String(id) === rawId);
65
+ orderedIds.push(registeredId ?? rawId);
66
+ }
67
+ });
68
+ cachedItemOrder = orderedIds;
69
+ return cachedItemOrder;
70
+ }
71
+ // Navigation functions
72
+ function next() {
73
+ const orderedIds = getItemIdsInOrder();
74
+ if (orderedIds.length === 0)
75
+ return;
76
+ if (focusedId === null) {
77
+ focusedId = orderedIds[0];
78
+ return;
79
+ }
80
+ const currentIndex = orderedIds.indexOf(focusedId);
81
+ if (currentIndex === -1) {
82
+ focusedId = orderedIds[0];
83
+ }
84
+ else if (currentIndex < orderedIds.length - 1) {
85
+ focusedId = orderedIds[currentIndex + 1];
86
+ }
87
+ }
88
+ function previous() {
89
+ const orderedIds = getItemIdsInOrder();
90
+ if (orderedIds.length === 0)
91
+ return;
92
+ if (focusedId === null) {
93
+ focusedId = orderedIds[orderedIds.length - 1];
94
+ return;
95
+ }
96
+ const currentIndex = orderedIds.indexOf(focusedId);
97
+ if (currentIndex === -1) {
98
+ focusedId = orderedIds[orderedIds.length - 1];
99
+ }
100
+ else if (currentIndex > 0) {
101
+ focusedId = orderedIds[currentIndex - 1];
102
+ }
103
+ }
104
+ function first() {
105
+ const orderedIds = getItemIdsInOrder();
106
+ if (orderedIds.length > 0) {
107
+ focusedId = orderedIds[0];
108
+ }
109
+ }
110
+ function last() {
111
+ const orderedIds = getItemIdsInOrder();
112
+ if (orderedIds.length > 0) {
113
+ focusedId = orderedIds[orderedIds.length - 1];
114
+ }
115
+ }
116
+ function pageUp() {
117
+ const orderedIds = getItemIdsInOrder();
118
+ if (orderedIds.length === 0)
119
+ return;
120
+ const currentIndex = focusedId !== null ? orderedIds.indexOf(focusedId) : orderedIds.length - 1;
121
+ const newIndex = Math.max(0, currentIndex - pageSize);
122
+ focusedId = orderedIds[newIndex];
123
+ }
124
+ function pageDown() {
125
+ const orderedIds = getItemIdsInOrder();
126
+ if (orderedIds.length === 0)
127
+ return;
128
+ const currentIndex = focusedId !== null ? orderedIds.indexOf(focusedId) : 0;
129
+ const newIndex = Math.min(orderedIds.length - 1, currentIndex + pageSize);
130
+ focusedId = orderedIds[newIndex];
131
+ }
132
+ // Registration functions
133
+ function register(id, label) {
134
+ if (!itemIds.includes(id)) {
135
+ itemIds = [...itemIds, id];
136
+ cachedItemOrder = null; // Invalidate cache
137
+ }
138
+ itemLabels.set(id, label);
139
+ // Apply pending focus if this is the target item
140
+ if (pendingFocusDirection !== null && itemIds.length > 0) {
141
+ if (pendingFocusDirection === 'first' && itemIds[0] === id) {
142
+ focusedId = id;
143
+ pendingFocusDirection = null;
144
+ }
145
+ else if (pendingFocusDirection === 'last') {
146
+ focusedId = id;
147
+ }
148
+ }
149
+ }
150
+ function unregister(id) {
151
+ itemIds = itemIds.filter((i) => i !== id);
152
+ itemLabels.delete(id);
153
+ cachedItemOrder = null; // Invalidate cache
154
+ if (focusedId === id) {
155
+ focusedId = null;
156
+ }
157
+ }
158
+ // Control functions
159
+ function setFocused(id) {
160
+ focusedId = id;
161
+ }
162
+ function setPendingDirection(direction) {
163
+ pendingFocusDirection = direction;
164
+ }
165
+ function reset() {
166
+ focusedId = null;
167
+ itemIds = [];
168
+ itemLabels.clear();
169
+ cachedItemOrder = null;
170
+ pendingFocusDirection = null;
171
+ }
172
+ function invalidateCache() {
173
+ cachedItemOrder = null;
174
+ }
175
+ return {
176
+ get focusedId() {
177
+ return focusedId;
178
+ },
179
+ get itemIds() {
180
+ return itemIds;
181
+ },
182
+ get itemLabels() {
183
+ return itemLabels;
184
+ },
185
+ get pendingFocusDirection() {
186
+ return pendingFocusDirection;
187
+ },
188
+ next,
189
+ previous,
190
+ first,
191
+ last,
192
+ pageUp,
193
+ pageDown,
194
+ register,
195
+ unregister,
196
+ setFocused,
197
+ setPendingDirection,
198
+ reset,
199
+ invalidateCache
200
+ };
201
+ }
@@ -0,0 +1,13 @@
1
+ export { ComboBox } from './combobox/index.ts';
2
+ export { Dialog } from './dialog/index.ts';
3
+ export { ListBox } from './listbox/index.ts';
4
+ export { Popover } from './popover/index.ts';
5
+ export { default as Input } from './input/index.ts';
6
+ export { default as Label } from './label/index.ts';
7
+ export { Portal } from './portal/index.ts';
8
+ export * from './combobox/index.ts';
9
+ export * from './dialog/index.ts';
10
+ export * from './listbox/index.ts';
11
+ export * from './popover/index.ts';
12
+ export * from './primitives/index.ts';
13
+ export { cn } from './utils/index.ts';
package/dist/index.js ADDED
@@ -0,0 +1,19 @@
1
+ // Main library entry point
2
+ // Components (namespace exports)
3
+ export { ComboBox } from './combobox/index.ts';
4
+ export { Dialog } from './dialog/index.ts';
5
+ export { ListBox } from './listbox/index.ts';
6
+ export { Popover } from './popover/index.ts';
7
+ // Simple components
8
+ export { default as Input } from './input/index.ts';
9
+ export { default as Label } from './label/index.ts';
10
+ export { Portal } from './portal/index.ts';
11
+ // Re-export named exports from components
12
+ export * from './combobox/index.ts';
13
+ export * from './dialog/index.ts';
14
+ export * from './listbox/index.ts';
15
+ export * from './popover/index.ts';
16
+ // Primitives
17
+ export * from './primitives/index.ts';
18
+ // Utilities
19
+ export { cn } from './utils/index.ts';
@@ -0,0 +1,3 @@
1
+ import Input from './input.svelte';
2
+ export { Input };
3
+ export default Input;
@@ -0,0 +1,3 @@
1
+ import Input from './input.svelte';
2
+ export { Input };
3
+ export default Input;
@@ -0,0 +1,19 @@
1
+ <script lang="ts">
2
+ import type { HTMLInputAttributes } from 'svelte/elements';
3
+ import type { ClassValue } from 'class-variance-authority/types';
4
+ import { cn } from '../utils/cn';
5
+
6
+ type InputProps = HTMLInputAttributes & {
7
+ class?: ClassValue;
8
+ };
9
+
10
+ let { class: className, ...props }: InputProps = $props();
11
+ </script>
12
+
13
+ <input
14
+ {...props}
15
+ class={cn(
16
+ `bg-depth-2 sunken placeholder:text-muted-foreground hover:bg-depth-1 focus:ring-border h-8 w-full rounded-xs border px-2 text-sm shadow-xs transition-all ease-out outline-none focus:ring focus:ring-offset-1 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 focus:data-[invalid=true]:ring-red-500`,
17
+ className
18
+ )}
19
+ />
@@ -0,0 +1,8 @@
1
+ import type { HTMLInputAttributes } from 'svelte/elements';
2
+ import type { ClassValue } from 'class-variance-authority/types';
3
+ type InputProps = HTMLInputAttributes & {
4
+ class?: ClassValue;
5
+ };
6
+ declare const Input: import("svelte").Component<InputProps, {}, "">;
7
+ type Input = ReturnType<typeof Input>;
8
+ export default Input;
@@ -0,0 +1,3 @@
1
+ import Label from './label.svelte';
2
+ export { Label };
3
+ export default Label;
@@ -0,0 +1,3 @@
1
+ import Label from './label.svelte';
2
+ export { Label };
3
+ export default Label;
@@ -0,0 +1,21 @@
1
+ <script lang="ts">
2
+ import type { HTMLLabelAttributes } from 'svelte/elements';
3
+ import type { ClassValue } from 'class-variance-authority/types';
4
+ import { cn } from '../utils/cn';
5
+
6
+ type LabelProps = HTMLLabelAttributes & {
7
+ class?: ClassValue;
8
+ };
9
+
10
+ let { class: className, children, ...props }: LabelProps = $props();
11
+ </script>
12
+
13
+ <label
14
+ {...props}
15
+ class={cn(
16
+ `text-sm leading-none font-medium peer-disabled:cursor-not-allowed peer-disabled:opacity-70`,
17
+ className
18
+ )}
19
+ >
20
+ {@render children?.()}
21
+ </label>
@@ -0,0 +1,8 @@
1
+ import type { HTMLLabelAttributes } from 'svelte/elements';
2
+ import type { ClassValue } from 'class-variance-authority/types';
3
+ type LabelProps = HTMLLabelAttributes & {
4
+ class?: ClassValue;
5
+ };
6
+ declare const Label: import("svelte").Component<LabelProps, {}, "">;
7
+ type Label = ReturnType<typeof Label>;
8
+ export default Label;
@@ -0,0 +1,6 @@
1
+ export * as ListBox from './index.parts.ts';
2
+ export { default as ListBoxRoot } from './root/listbox.svelte';
3
+ export { default as ListBoxItem } from './item/listbox-item.svelte';
4
+ export { useListBoxContext, createListBoxContext, type ListBoxContext } from './root/context.ts';
5
+ import * as ListBoxParts from './index.parts.ts';
6
+ export default ListBoxParts;
@@ -0,0 +1,10 @@
1
+ // Namespace export for component composition: <ListBox.Root>, <ListBox.Item>
2
+ export * as ListBox from './index.parts.ts';
3
+ // Direct named exports for individual imports
4
+ export { default as ListBoxRoot } from './root/listbox.svelte';
5
+ export { default as ListBoxItem } from './item/listbox-item.svelte';
6
+ // Context and types
7
+ export { useListBoxContext, createListBoxContext } from './root/context.ts';
8
+ // Default export as namespace object
9
+ import * as ListBoxParts from './index.parts.ts';
10
+ export default ListBoxParts;
@@ -0,0 +1,2 @@
1
+ export { default as Root } from './root/listbox.svelte';
2
+ export { default as Item } from './item/listbox-item.svelte';
@@ -0,0 +1,3 @@
1
+ // Short alias exports for namespace usage: ListBox.Root, ListBox.Item
2
+ export { default as Root } from './root/listbox.svelte';
3
+ export { default as Item } from './item/listbox-item.svelte';
@@ -0,0 +1,186 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import type { HTMLAttributes } from 'svelte/elements';
4
+ import { useListBoxContext } from '../root/context';
5
+ import { onMount, onDestroy } from 'svelte';
6
+
7
+ /**
8
+ * Props for the ListBox.Item component.
9
+ */
10
+ type ListBoxItemProps = Omit<HTMLAttributes<HTMLDivElement>, 'id' | 'children'> & {
11
+ /** Unique identifier for this item. Used for selection tracking. */
12
+ id: string | number;
13
+ /** Text value for typeahead search. If not provided, extracted from content. */
14
+ textValue?: string;
15
+ /** Whether this item is disabled and non-selectable. */
16
+ disabled?: boolean;
17
+ /** CSS class to apply to the item element. */
18
+ class?: string;
19
+ /** Content to render inside the item. */
20
+ children?: Snippet;
21
+
22
+ // Override props for composition (e.g., ComboBox.Item wrapping ListBox.Item)
23
+ /** Override the generated ID. Useful for components with custom ID requirements. */
24
+ customId?: string;
25
+ /** Disable real DOM focus handling. When true, no tabindex is set and focus events are skipped. */
26
+ disableFocusHandling?: boolean;
27
+ /** Override the focused state. When provided, this value is used instead of internal focus tracking. */
28
+ isFocusedOverride?: boolean;
29
+ /** Override the select behavior. When provided, called instead of default listbox selection. */
30
+ onItemSelect?: (id: string | number, label: string) => void;
31
+ /** Callback with resolved text value when mounted (from prop or rendered content). */
32
+ onResolvedTextValue?: (label: string) => void;
33
+ /** Whether to scroll this item into view when focused. Useful for virtual focus patterns. */
34
+ scrollOnFocus?: boolean;
35
+ /** Additional disabled state from parent. */
36
+ isParentDisabled?: boolean;
37
+ };
38
+
39
+ let {
40
+ id,
41
+ textValue,
42
+ disabled = false,
43
+ class: className = '',
44
+ children,
45
+ // Override props
46
+ customId,
47
+ disableFocusHandling = false,
48
+ isFocusedOverride,
49
+ onItemSelect,
50
+ onResolvedTextValue,
51
+ scrollOnFocus = false,
52
+ isParentDisabled = false,
53
+ ...restProps
54
+ }: ListBoxItemProps = $props();
55
+
56
+ const listboxCtx = useListBoxContext();
57
+
58
+ let elementRef: HTMLElement;
59
+ let isSelected = $state(false);
60
+ let isFocused = $state(false);
61
+ let isHovered = $state(false);
62
+
63
+ // Focus: use override if provided, otherwise use internal state
64
+ const isFocusedComputed = $derived(
65
+ isFocusedOverride !== undefined ? isFocusedOverride : isFocused
66
+ );
67
+ const isDisabledComputed = $derived(
68
+ disabled || listboxCtx.disabledIds.has(id) || isParentDisabled
69
+ );
70
+
71
+ // ID: use custom if provided, otherwise generate
72
+ const uniqueId = $derived(customId ?? `listbox-item-${id}`);
73
+
74
+ let unsubscribeSelection: (() => void) | null = null;
75
+ let unsubscribeFocus: (() => void) | null = null;
76
+
77
+ function getResolvedTextValue() {
78
+ return textValue || elementRef?.textContent?.trim() || String(id);
79
+ }
80
+
81
+ onMount(() => {
82
+ const computedTextValue = getResolvedTextValue();
83
+
84
+ // Register with ListBox context for selection state
85
+ listboxCtx.registerItem(id, computedTextValue, elementRef);
86
+ onResolvedTextValue?.(computedTextValue);
87
+
88
+ unsubscribeSelection = listboxCtx.subscribeToItem(id, (selected) => {
89
+ isSelected = selected;
90
+ });
91
+
92
+ // Only subscribe to ListBox focus if focus handling is enabled
93
+ if (!disableFocusHandling) {
94
+ unsubscribeFocus = listboxCtx.subscribeToFocus(id, (focused) => {
95
+ isFocused = focused;
96
+ });
97
+ listboxCtx.keyboardNav.updateItems();
98
+ }
99
+ });
100
+
101
+ onDestroy(() => {
102
+ listboxCtx.unregisterItem(id);
103
+ unsubscribeSelection?.();
104
+ unsubscribeFocus?.();
105
+ });
106
+
107
+ // Scroll into view when focused (if enabled)
108
+ $effect(() => {
109
+ if (scrollOnFocus && isFocusedComputed && elementRef) {
110
+ requestAnimationFrame(() => {
111
+ elementRef?.scrollIntoView({ block: 'nearest' });
112
+ });
113
+ }
114
+ });
115
+
116
+ function handleClick() {
117
+ if (isDisabledComputed) return;
118
+
119
+ const label = getResolvedTextValue();
120
+
121
+ // Use custom select handler if provided, otherwise use listbox default
122
+ if (onItemSelect) {
123
+ onItemSelect(id, label);
124
+ } else {
125
+ listboxCtx.select(id);
126
+ listboxCtx.setFocusedId(id);
127
+ }
128
+ }
129
+
130
+ function handleFocus() {
131
+ if (!disableFocusHandling) {
132
+ listboxCtx.setFocusedId(id);
133
+ }
134
+ }
135
+
136
+ function handleBlur() {}
137
+
138
+ function handleMouseEnter() {
139
+ if (!isDisabledComputed) {
140
+ isHovered = true;
141
+ }
142
+ }
143
+
144
+ function handleMouseLeave() {
145
+ isHovered = false;
146
+ }
147
+
148
+ // Keyboard is handled by parent container
149
+ function handleKeydown() {}
150
+ function handleMouseDown(event: MouseEvent) {
151
+ // Prevent focus stealing when used in ComboBox (disableFocusHandling=true)
152
+ // This keeps the focus on the input while allowing click selection
153
+ if (disableFocusHandling) {
154
+ event.preventDefault();
155
+ }
156
+ }
157
+ </script>
158
+
159
+ <div
160
+ bind:this={elementRef}
161
+ role="option"
162
+ id={uniqueId}
163
+ class={className}
164
+ tabindex={disableFocusHandling ? undefined : isFocusedComputed ? 0 : -1}
165
+ aria-selected={isSelected}
166
+ aria-disabled={isDisabledComputed || undefined}
167
+ data-navigation-item={!disableFocusHandling || undefined}
168
+ data-item-id={id}
169
+ data-item-id-type={typeof id === 'number' ? 'number' : 'string'}
170
+ data-selected={isSelected || undefined}
171
+ data-disabled={isDisabledComputed || undefined}
172
+ data-focused={isFocusedComputed || undefined}
173
+ data-hovered={isHovered || undefined}
174
+ onmousedown={handleMouseDown}
175
+ onclick={handleClick}
176
+ onkeydown={handleKeydown}
177
+ onfocus={handleFocus}
178
+ onblur={handleBlur}
179
+ onmouseenter={handleMouseEnter}
180
+ onmouseleave={handleMouseLeave}
181
+ {...restProps}
182
+ >
183
+ {#if children}
184
+ {@render children()}
185
+ {/if}
186
+ </div>
@@ -0,0 +1,34 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ /**
4
+ * Props for the ListBox.Item component.
5
+ */
6
+ type ListBoxItemProps = Omit<HTMLAttributes<HTMLDivElement>, 'id' | 'children'> & {
7
+ /** Unique identifier for this item. Used for selection tracking. */
8
+ id: string | number;
9
+ /** Text value for typeahead search. If not provided, extracted from content. */
10
+ textValue?: string;
11
+ /** Whether this item is disabled and non-selectable. */
12
+ disabled?: boolean;
13
+ /** CSS class to apply to the item element. */
14
+ class?: string;
15
+ /** Content to render inside the item. */
16
+ children?: Snippet;
17
+ /** Override the generated ID. Useful for components with custom ID requirements. */
18
+ customId?: string;
19
+ /** Disable real DOM focus handling. When true, no tabindex is set and focus events are skipped. */
20
+ disableFocusHandling?: boolean;
21
+ /** Override the focused state. When provided, this value is used instead of internal focus tracking. */
22
+ isFocusedOverride?: boolean;
23
+ /** Override the select behavior. When provided, called instead of default listbox selection. */
24
+ onItemSelect?: (id: string | number, label: string) => void;
25
+ /** Callback with resolved text value when mounted (from prop or rendered content). */
26
+ onResolvedTextValue?: (label: string) => void;
27
+ /** Whether to scroll this item into view when focused. Useful for virtual focus patterns. */
28
+ scrollOnFocus?: boolean;
29
+ /** Additional disabled state from parent. */
30
+ isParentDisabled?: boolean;
31
+ };
32
+ declare const ListboxItem: import("svelte").Component<ListBoxItemProps, {}, "">;
33
+ type ListboxItem = ReturnType<typeof ListboxItem>;
34
+ export default ListboxItem;
@@ -0,0 +1,73 @@
1
+ import { type KeyboardNavigationReturn } from '../../primitives/keyboard-navigation';
2
+ /**
3
+ * Context object shared between ListBox and ListBox.Item components.
4
+ * Provides state management and actions for selection and focus.
5
+ */
6
+ export type ListBoxContext = {
7
+ /** Current selection mode. */
8
+ selectionMode: 'single' | 'multiple';
9
+ /** Current selection behavior. */
10
+ selectionBehavior: 'toggle' | 'replace';
11
+ /** Set of disabled item IDs. */
12
+ disabledIds: Set<string | number>;
13
+ /** Returns a copy of the currently selected keys. */
14
+ getSelectedKeys: () => Set<string | number>;
15
+ /** Returns the currently focused item ID. */
16
+ getFocusedId: () => string | number | null;
17
+ /** Checks if an item is selected. */
18
+ isSelected: (id: string | number) => boolean;
19
+ /** Checks if an item is disabled. */
20
+ isDisabled: (id: string | number) => boolean;
21
+ /** Checks if an item is focused. */
22
+ isFocused: (id: string | number) => boolean;
23
+ /** Keyboard navigation controller from the shared primitive. */
24
+ keyboardNav: KeyboardNavigationReturn;
25
+ /** Map of registered items with their metadata. */
26
+ items: Map<string | number, {
27
+ textValue?: string;
28
+ element?: HTMLElement;
29
+ }>;
30
+ /** Registers an item in the listbox. */
31
+ registerItem: (id: string | number, textValue?: string, element?: HTMLElement) => void;
32
+ /** Unregisters an item from the listbox. */
33
+ unregisterItem: (id: string | number) => void;
34
+ /** Returns all registered item IDs. */
35
+ getItemIds: () => (string | number)[];
36
+ /** Returns the current item count. */
37
+ getItemCount: () => number;
38
+ /** Subscribes to item count changes. Returns unsubscribe function. */
39
+ subscribeToItemCount: (callback: (count: number) => void) => () => void;
40
+ /** Selects or toggles an item based on current mode and behavior. */
41
+ select: (id: string | number) => void;
42
+ /** Selects all enabled items (only works in multiple mode). */
43
+ selectAll: () => void;
44
+ /** Sets the selection programmatically (for controlled mode). */
45
+ setSelection: (selection: Set<string | number>) => void;
46
+ /** Sets the focused item ID. */
47
+ setFocusedId: (id: string | number | null) => void;
48
+ /** Subscribes to selection changes for a specific item. Returns unsubscribe function. */
49
+ subscribeToItem: (id: string | number, callback: (selected: boolean) => void) => () => void;
50
+ /** Subscribes to focus changes for a specific item. Returns unsubscribe function. */
51
+ subscribeToFocus: (id: string | number, callback: (focused: boolean) => void) => () => void;
52
+ /** Returns the next item ID respecting loop setting, or null if at end. */
53
+ getNextItemId: (currentId: string | number | null) => string | number | null;
54
+ /** Returns the previous item ID respecting loop setting, or null if at start. */
55
+ getPreviousItemId: (currentId: string | number | null) => string | number | null;
56
+ };
57
+ /**
58
+ * Options for creating a ListBox context.
59
+ */
60
+ export type CreateListBoxContextOptions = {
61
+ /** Selection mode: 'single' or 'multiple'. Default: 'single'. */
62
+ selectionMode?: 'single' | 'multiple';
63
+ /** Selection behavior: 'toggle' or 'replace'. Default: 'toggle'. */
64
+ selectionBehavior?: 'toggle' | 'replace';
65
+ /** Initial set of disabled item IDs. */
66
+ disabledIds?: Iterable<string | number>;
67
+ /** Initial selection for uncontrolled mode. */
68
+ initialSelection?: Set<string | number>;
69
+ /** Callback fired when selection changes. */
70
+ onSelectionChange?: (selection: Set<string | number>) => void;
71
+ };
72
+ export declare function createListBoxContext(options?: CreateListBoxContextOptions): ListBoxContext;
73
+ export declare function useListBoxContext(): ListBoxContext;