@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,249 @@
|
|
|
1
|
+
import { setContext, getContext } from 'svelte';
|
|
2
|
+
import { createKeyboardNavigation } from '../../primitives/keyboard-navigation';
|
|
3
|
+
const KEY = Symbol('listbox');
|
|
4
|
+
export function createListBoxContext(options = {}) {
|
|
5
|
+
const { selectionMode = 'single', selectionBehavior = 'toggle', disabledIds: initialDisabledIds, initialSelection = new Set(), onSelectionChange } = options;
|
|
6
|
+
const disabledIds = new Set(initialDisabledIds ?? []);
|
|
7
|
+
const items = new Map();
|
|
8
|
+
let selectedKeys = new Set(initialSelection);
|
|
9
|
+
const itemCallbacks = new Map();
|
|
10
|
+
// Item count tracking with subscription
|
|
11
|
+
const itemCountCallbacks = new Set();
|
|
12
|
+
function notifyItemCountChange() {
|
|
13
|
+
const count = items.size;
|
|
14
|
+
itemCountCallbacks.forEach((cb) => cb(count));
|
|
15
|
+
}
|
|
16
|
+
function registerItem(id, textValue, element) {
|
|
17
|
+
items.set(id, { textValue, element });
|
|
18
|
+
notifyItemCountChange();
|
|
19
|
+
}
|
|
20
|
+
function unregisterItem(id) {
|
|
21
|
+
items.delete(id);
|
|
22
|
+
itemCallbacks.delete(id);
|
|
23
|
+
notifyItemCountChange();
|
|
24
|
+
}
|
|
25
|
+
function getItemCount() {
|
|
26
|
+
return items.size;
|
|
27
|
+
}
|
|
28
|
+
function subscribeToItemCount(callback) {
|
|
29
|
+
itemCountCallbacks.add(callback);
|
|
30
|
+
// Immediately call with current count
|
|
31
|
+
callback(items.size);
|
|
32
|
+
return () => {
|
|
33
|
+
itemCountCallbacks.delete(callback);
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function getItemIds() {
|
|
37
|
+
return Array.from(items.keys());
|
|
38
|
+
}
|
|
39
|
+
function isDisabled(id) {
|
|
40
|
+
return disabledIds.has(id);
|
|
41
|
+
}
|
|
42
|
+
function isSelected(id) {
|
|
43
|
+
return selectedKeys.has(id);
|
|
44
|
+
}
|
|
45
|
+
function getSelectedKeys() {
|
|
46
|
+
return new Set(selectedKeys);
|
|
47
|
+
}
|
|
48
|
+
function notifyItem(id, selected) {
|
|
49
|
+
const callbacks = itemCallbacks.get(id);
|
|
50
|
+
if (callbacks) {
|
|
51
|
+
callbacks.forEach((cb) => cb(selected));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
let focusedId = null;
|
|
55
|
+
const focusCallbacks = new Map();
|
|
56
|
+
function getFocusedId() {
|
|
57
|
+
return focusedId;
|
|
58
|
+
}
|
|
59
|
+
function isFocused(id) {
|
|
60
|
+
return focusedId === id || String(focusedId) === String(id);
|
|
61
|
+
}
|
|
62
|
+
function notifyFocus(id, focused) {
|
|
63
|
+
const callbacks = focusCallbacks.get(id);
|
|
64
|
+
if (callbacks) {
|
|
65
|
+
callbacks.forEach((cb) => cb(focused));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function setFocusedId(newId) {
|
|
69
|
+
if (focusedId === newId)
|
|
70
|
+
return;
|
|
71
|
+
const previousId = focusedId;
|
|
72
|
+
focusedId = newId;
|
|
73
|
+
if (previousId !== null) {
|
|
74
|
+
notifyFocus(previousId, false);
|
|
75
|
+
}
|
|
76
|
+
if (newId !== null) {
|
|
77
|
+
notifyFocus(newId, true);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function subscribeToFocus(id, callback) {
|
|
81
|
+
if (!focusCallbacks.has(id)) {
|
|
82
|
+
focusCallbacks.set(id, new Set());
|
|
83
|
+
}
|
|
84
|
+
focusCallbacks.get(id).add(callback);
|
|
85
|
+
callback(isFocused(id));
|
|
86
|
+
return () => {
|
|
87
|
+
const callbacks = focusCallbacks.get(id);
|
|
88
|
+
if (callbacks) {
|
|
89
|
+
callbacks.delete(callback);
|
|
90
|
+
if (callbacks.size === 0) {
|
|
91
|
+
focusCallbacks.delete(id);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
function subscribeToItem(id, callback) {
|
|
97
|
+
if (!itemCallbacks.has(id)) {
|
|
98
|
+
itemCallbacks.set(id, new Set());
|
|
99
|
+
}
|
|
100
|
+
itemCallbacks.get(id).add(callback);
|
|
101
|
+
callback(selectedKeys.has(id));
|
|
102
|
+
return () => {
|
|
103
|
+
const callbacks = itemCallbacks.get(id);
|
|
104
|
+
if (callbacks) {
|
|
105
|
+
callbacks.delete(callback);
|
|
106
|
+
if (callbacks.size === 0) {
|
|
107
|
+
itemCallbacks.delete(id);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
function select(id) {
|
|
113
|
+
if (isDisabled(id))
|
|
114
|
+
return;
|
|
115
|
+
const wasSelected = selectedKeys.has(id);
|
|
116
|
+
const previouslySelected = new Set(selectedKeys);
|
|
117
|
+
if (selectionMode === 'single') {
|
|
118
|
+
if (selectionBehavior === 'toggle') {
|
|
119
|
+
selectedKeys = wasSelected ? new Set() : new Set([id]);
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
selectedKeys = new Set([id]);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
selectedKeys = new Set(selectedKeys);
|
|
127
|
+
if (selectionBehavior === 'toggle') {
|
|
128
|
+
if (wasSelected) {
|
|
129
|
+
selectedKeys.delete(id);
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
selectedKeys.add(id);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
selectedKeys.add(id);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
previouslySelected.forEach((prevId) => {
|
|
140
|
+
if (!selectedKeys.has(prevId)) {
|
|
141
|
+
notifyItem(prevId, false);
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
selectedKeys.forEach((newId) => {
|
|
145
|
+
if (!previouslySelected.has(newId)) {
|
|
146
|
+
notifyItem(newId, true);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
onSelectionChange?.(new Set(selectedKeys));
|
|
150
|
+
}
|
|
151
|
+
function selectAll() {
|
|
152
|
+
if (selectionMode !== 'multiple')
|
|
153
|
+
return;
|
|
154
|
+
const enabledIds = getItemIds().filter((id) => !isDisabled(id));
|
|
155
|
+
const previouslySelected = new Set(selectedKeys);
|
|
156
|
+
selectedKeys = new Set(enabledIds);
|
|
157
|
+
enabledIds.forEach((id) => {
|
|
158
|
+
if (!previouslySelected.has(id)) {
|
|
159
|
+
notifyItem(id, true);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
onSelectionChange?.(new Set(selectedKeys));
|
|
163
|
+
}
|
|
164
|
+
function setSelection(newSelection) {
|
|
165
|
+
const previouslySelected = new Set(selectedKeys);
|
|
166
|
+
selectedKeys = new Set(newSelection);
|
|
167
|
+
previouslySelected.forEach((prevId) => {
|
|
168
|
+
if (!selectedKeys.has(prevId)) {
|
|
169
|
+
notifyItem(prevId, false);
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
selectedKeys.forEach((newId) => {
|
|
173
|
+
if (!previouslySelected.has(newId)) {
|
|
174
|
+
notifyItem(newId, true);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
// Navigation helper functions for virtual focus (used by ComboBox)
|
|
179
|
+
const loop = false; // Same as keyboardNav loop setting
|
|
180
|
+
function getNextItemId(currentId) {
|
|
181
|
+
const itemIdsList = getItemIds();
|
|
182
|
+
if (itemIdsList.length === 0)
|
|
183
|
+
return null;
|
|
184
|
+
const currentIndex = currentId !== null ? itemIdsList.indexOf(currentId) : -1;
|
|
185
|
+
if (currentIndex < itemIdsList.length - 1) {
|
|
186
|
+
return itemIdsList[currentIndex + 1];
|
|
187
|
+
}
|
|
188
|
+
// At end - loop or stay
|
|
189
|
+
return loop ? itemIdsList[0] : null;
|
|
190
|
+
}
|
|
191
|
+
function getPreviousItemId(currentId) {
|
|
192
|
+
const itemIdsList = getItemIds();
|
|
193
|
+
if (itemIdsList.length === 0)
|
|
194
|
+
return null;
|
|
195
|
+
const currentIndex = currentId !== null ? itemIdsList.indexOf(currentId) : itemIdsList.length;
|
|
196
|
+
if (currentIndex > 0) {
|
|
197
|
+
return itemIdsList[currentIndex - 1];
|
|
198
|
+
}
|
|
199
|
+
// At start - loop or stay
|
|
200
|
+
return loop ? itemIdsList[itemIdsList.length - 1] : null;
|
|
201
|
+
}
|
|
202
|
+
const keyboardNav = createKeyboardNavigation({
|
|
203
|
+
orientation: 'vertical',
|
|
204
|
+
loop: loop,
|
|
205
|
+
itemSelector: '[data-navigation-item]:not([data-disabled])',
|
|
206
|
+
onSelect: (id) => {
|
|
207
|
+
select(id);
|
|
208
|
+
},
|
|
209
|
+
onFocusChange: (id) => {
|
|
210
|
+
setFocusedId(id);
|
|
211
|
+
},
|
|
212
|
+
onSelectAll: selectionMode === 'multiple' ? selectAll : undefined,
|
|
213
|
+
homeEndKeys: true
|
|
214
|
+
});
|
|
215
|
+
const ctx = {
|
|
216
|
+
selectionMode,
|
|
217
|
+
selectionBehavior,
|
|
218
|
+
disabledIds,
|
|
219
|
+
getSelectedKeys,
|
|
220
|
+
getFocusedId,
|
|
221
|
+
isSelected,
|
|
222
|
+
isDisabled,
|
|
223
|
+
isFocused,
|
|
224
|
+
keyboardNav,
|
|
225
|
+
items,
|
|
226
|
+
registerItem,
|
|
227
|
+
unregisterItem,
|
|
228
|
+
getItemIds,
|
|
229
|
+
getItemCount,
|
|
230
|
+
subscribeToItemCount,
|
|
231
|
+
select,
|
|
232
|
+
selectAll,
|
|
233
|
+
setSelection,
|
|
234
|
+
setFocusedId,
|
|
235
|
+
subscribeToItem,
|
|
236
|
+
subscribeToFocus,
|
|
237
|
+
getNextItemId,
|
|
238
|
+
getPreviousItemId
|
|
239
|
+
};
|
|
240
|
+
setContext(KEY, ctx);
|
|
241
|
+
return ctx;
|
|
242
|
+
}
|
|
243
|
+
export function useListBoxContext() {
|
|
244
|
+
const ctx = getContext(KEY);
|
|
245
|
+
if (!ctx) {
|
|
246
|
+
throw new Error('useListBoxContext must be used within a ListBox component');
|
|
247
|
+
}
|
|
248
|
+
return ctx;
|
|
249
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import ListBox from '../index';
|
|
3
|
+
|
|
4
|
+
let selected = $state<string | number | undefined>();
|
|
5
|
+
|
|
6
|
+
function handleChange(value: Set<string | number>) {
|
|
7
|
+
selected = Array.from(value)[0];
|
|
8
|
+
}
|
|
9
|
+
</script>
|
|
10
|
+
|
|
11
|
+
<ListBox.Root aria-label="Codes list" onChange={handleChange}>
|
|
12
|
+
<ListBox.Item id="01">Code 01</ListBox.Item>
|
|
13
|
+
<ListBox.Item id="02">Code 02</ListBox.Item>
|
|
14
|
+
<ListBox.Item id={3}>Code 3</ListBox.Item>
|
|
15
|
+
</ListBox.Root>
|
|
16
|
+
|
|
17
|
+
<div data-testid="selected">{selected === undefined ? '' : String(selected)}</div>
|
|
18
|
+
<div data-testid="selected-type">{selected === undefined ? '' : typeof selected}</div>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import ListBox from '../index';
|
|
3
|
+
|
|
4
|
+
type Props = {
|
|
5
|
+
selectionMode?: 'single' | 'multiple';
|
|
6
|
+
selectionBehavior?: 'toggle' | 'replace';
|
|
7
|
+
disabledIds?: Iterable<string | number>;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
let { selectionMode = 'single', selectionBehavior = 'toggle', disabledIds }: Props = $props();
|
|
11
|
+
|
|
12
|
+
const fruits = [
|
|
13
|
+
{ id: 'apple', name: 'Apple' },
|
|
14
|
+
{ id: 'banana', name: 'Banana' },
|
|
15
|
+
{ id: 'cherry', name: 'Cherry' },
|
|
16
|
+
{ id: 'grape', name: 'Grape' },
|
|
17
|
+
{ id: 'orange', name: 'Orange' }
|
|
18
|
+
];
|
|
19
|
+
</script>
|
|
20
|
+
|
|
21
|
+
<ListBox.Root {selectionMode} {selectionBehavior} {disabledIds} aria-label="Fruits list">
|
|
22
|
+
{#each fruits as fruit (fruit.id)}
|
|
23
|
+
<ListBox.Item id={fruit.id} textValue={fruit.name}>
|
|
24
|
+
{fruit.name}
|
|
25
|
+
</ListBox.Item>
|
|
26
|
+
{/each}
|
|
27
|
+
</ListBox.Root>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
type Props = {
|
|
2
|
+
selectionMode?: 'single' | 'multiple';
|
|
3
|
+
selectionBehavior?: 'toggle' | 'replace';
|
|
4
|
+
disabledIds?: Iterable<string | number>;
|
|
5
|
+
};
|
|
6
|
+
declare const ListboxTest: import("svelte").Component<Props, {}, "">;
|
|
7
|
+
type ListboxTest = ReturnType<typeof ListboxTest>;
|
|
8
|
+
export default ListboxTest;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
<script lang="ts" generics="T extends object = object">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import { createListBoxContext, type ListBoxContext } from './context';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Props for the ListBox component.
|
|
7
|
+
* @template T - The type of items when using dynamic rendering with the `items` prop.
|
|
8
|
+
*/
|
|
9
|
+
type ListBoxProps = {
|
|
10
|
+
/** How selection behaves: 'toggle' allows deselection, 'replace' always selects. */
|
|
11
|
+
selectionBehavior?: 'toggle' | 'replace';
|
|
12
|
+
/** Content shown when the list is empty. Can be a string or a Snippet. */
|
|
13
|
+
emptyPlaceholder?: string | Snippet;
|
|
14
|
+
/** Iterable of items for dynamic rendering. Used with a snippet that receives each item. */
|
|
15
|
+
items?: Iterable<T>;
|
|
16
|
+
/** IDs of items that should be disabled and non-selectable. */
|
|
17
|
+
disabledIds?: Iterable<string | number>;
|
|
18
|
+
/** Selection mode: 'single' allows one selection, 'multiple' allows many. */
|
|
19
|
+
selectionMode?: 'single' | 'multiple';
|
|
20
|
+
/** Controlled value. When provided, the component is in controlled mode. */
|
|
21
|
+
value?: Iterable<string | number>;
|
|
22
|
+
/** Initial selection for uncontrolled mode. */
|
|
23
|
+
defaultValue?: Iterable<string | number>;
|
|
24
|
+
/** Content of the listbox. Can be static children or a snippet receiving items. */
|
|
25
|
+
children?: Snippet | Snippet<[T]>;
|
|
26
|
+
/** CSS class to apply to the listbox container. */
|
|
27
|
+
class?: string;
|
|
28
|
+
/** HTML id attribute for the listbox element. */
|
|
29
|
+
id?: string;
|
|
30
|
+
/** Accessible label for the listbox. Announced by screen readers. */
|
|
31
|
+
'aria-label'?: string;
|
|
32
|
+
/** Callback fired when the selection changes. */
|
|
33
|
+
onChange?: (value: Set<string | number>) => void;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
let {
|
|
37
|
+
selectionBehavior = 'toggle',
|
|
38
|
+
emptyPlaceholder = 'No items selected',
|
|
39
|
+
items,
|
|
40
|
+
disabledIds,
|
|
41
|
+
selectionMode = 'single',
|
|
42
|
+
value = $bindable(),
|
|
43
|
+
defaultValue,
|
|
44
|
+
children,
|
|
45
|
+
class: className = '',
|
|
46
|
+
id,
|
|
47
|
+
'aria-label': ariaLabel,
|
|
48
|
+
onChange,
|
|
49
|
+
context = $bindable(),
|
|
50
|
+
element = $bindable()
|
|
51
|
+
}: ListBoxProps & { context?: ListBoxContext; element?: HTMLElement } = $props();
|
|
52
|
+
|
|
53
|
+
let listboxElement: HTMLElement;
|
|
54
|
+
|
|
55
|
+
// Expose element via bindable prop
|
|
56
|
+
$effect(() => {
|
|
57
|
+
element = listboxElement;
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
function parseSelection(val: Iterable<string | number> | undefined): Set<string | number> {
|
|
61
|
+
if (val === undefined) return new Set();
|
|
62
|
+
return new Set(val);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const isControlled = $derived(value !== undefined);
|
|
66
|
+
|
|
67
|
+
const ctx = createListBoxContext({
|
|
68
|
+
get selectionMode() {
|
|
69
|
+
return selectionMode;
|
|
70
|
+
},
|
|
71
|
+
get selectionBehavior() {
|
|
72
|
+
return selectionBehavior;
|
|
73
|
+
},
|
|
74
|
+
get disabledIds() {
|
|
75
|
+
return disabledIds;
|
|
76
|
+
},
|
|
77
|
+
// Use function to capture initial value only (not reactive)
|
|
78
|
+
initialSelection: (() => parseSelection(defaultValue))(),
|
|
79
|
+
onSelectionChange: (newSelection) => {
|
|
80
|
+
onChange?.(newSelection);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Expose context via bindable prop
|
|
85
|
+
context = ctx;
|
|
86
|
+
|
|
87
|
+
const { action: keyboardAction } = ctx.keyboardNav;
|
|
88
|
+
|
|
89
|
+
$effect(() => {
|
|
90
|
+
if (isControlled && value !== undefined) {
|
|
91
|
+
const newSelection = parseSelection(value);
|
|
92
|
+
ctx.setSelection(newSelection);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
$effect(() => {
|
|
97
|
+
ctx.disabledIds.clear();
|
|
98
|
+
if (disabledIds) {
|
|
99
|
+
for (const id of disabledIds) {
|
|
100
|
+
ctx.disabledIds.add(id);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
let itemCount = $state(0);
|
|
106
|
+
|
|
107
|
+
// Subscribe to item count changes for reactive empty state
|
|
108
|
+
$effect(() => {
|
|
109
|
+
const unsubscribe = ctx.subscribeToItemCount((count) => {
|
|
110
|
+
itemCount = count;
|
|
111
|
+
});
|
|
112
|
+
return unsubscribe;
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const itemsArray = $derived(items ? Array.from(items) : []);
|
|
116
|
+
const hasItems = $derived(itemsArray.length > 0 || itemCount > 0);
|
|
117
|
+
</script>
|
|
118
|
+
|
|
119
|
+
<div
|
|
120
|
+
bind:this={listboxElement}
|
|
121
|
+
role="listbox"
|
|
122
|
+
{id}
|
|
123
|
+
aria-multiselectable={selectionMode === 'multiple'}
|
|
124
|
+
aria-label={ariaLabel}
|
|
125
|
+
class={className}
|
|
126
|
+
tabindex="0"
|
|
127
|
+
use:keyboardAction
|
|
128
|
+
>
|
|
129
|
+
{#if items && children}
|
|
130
|
+
{#each itemsArray as item (item)}
|
|
131
|
+
{@render (children as Snippet<[T]>)(item)}
|
|
132
|
+
{/each}
|
|
133
|
+
{:else if children}
|
|
134
|
+
{@render (children as Snippet)()}
|
|
135
|
+
{/if}
|
|
136
|
+
|
|
137
|
+
{#if !hasItems && itemCount === 0}
|
|
138
|
+
{#if typeof emptyPlaceholder === 'string'}
|
|
139
|
+
<div role="option" aria-selected="false" aria-disabled="true" data-empty-placeholder>
|
|
140
|
+
{emptyPlaceholder}
|
|
141
|
+
</div>
|
|
142
|
+
{:else if emptyPlaceholder}
|
|
143
|
+
{@render emptyPlaceholder()}
|
|
144
|
+
{/if}
|
|
145
|
+
{/if}
|
|
146
|
+
</div>
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import { type ListBoxContext } from './context';
|
|
3
|
+
declare function $$render<T extends object = object>(): {
|
|
4
|
+
props: {
|
|
5
|
+
/** How selection behaves: 'toggle' allows deselection, 'replace' always selects. */
|
|
6
|
+
selectionBehavior?: "toggle" | "replace";
|
|
7
|
+
/** Content shown when the list is empty. Can be a string or a Snippet. */
|
|
8
|
+
emptyPlaceholder?: string | Snippet;
|
|
9
|
+
/** Iterable of items for dynamic rendering. Used with a snippet that receives each item. */
|
|
10
|
+
items?: Iterable<T>;
|
|
11
|
+
/** IDs of items that should be disabled and non-selectable. */
|
|
12
|
+
disabledIds?: Iterable<string | number>;
|
|
13
|
+
/** Selection mode: 'single' allows one selection, 'multiple' allows many. */
|
|
14
|
+
selectionMode?: "single" | "multiple";
|
|
15
|
+
/** Controlled value. When provided, the component is in controlled mode. */
|
|
16
|
+
value?: Iterable<string | number>;
|
|
17
|
+
/** Initial selection for uncontrolled mode. */
|
|
18
|
+
defaultValue?: Iterable<string | number>;
|
|
19
|
+
/** Content of the listbox. Can be static children or a snippet receiving items. */
|
|
20
|
+
children?: Snippet | Snippet<[T]>;
|
|
21
|
+
/** CSS class to apply to the listbox container. */
|
|
22
|
+
class?: string;
|
|
23
|
+
/** HTML id attribute for the listbox element. */
|
|
24
|
+
id?: string;
|
|
25
|
+
/** Accessible label for the listbox. Announced by screen readers. */
|
|
26
|
+
'aria-label'?: string;
|
|
27
|
+
/** Callback fired when the selection changes. */
|
|
28
|
+
onChange?: (value: Set<string | number>) => void;
|
|
29
|
+
} & {
|
|
30
|
+
context?: ListBoxContext;
|
|
31
|
+
element?: HTMLElement;
|
|
32
|
+
};
|
|
33
|
+
exports: {};
|
|
34
|
+
bindings: "value" | "context" | "element";
|
|
35
|
+
slots: {};
|
|
36
|
+
events: {};
|
|
37
|
+
};
|
|
38
|
+
declare class __sveltets_Render<T extends object = object> {
|
|
39
|
+
props(): ReturnType<typeof $$render<T>>['props'];
|
|
40
|
+
events(): ReturnType<typeof $$render<T>>['events'];
|
|
41
|
+
slots(): ReturnType<typeof $$render<T>>['slots'];
|
|
42
|
+
bindings(): "value" | "context" | "element";
|
|
43
|
+
exports(): {};
|
|
44
|
+
}
|
|
45
|
+
interface $$IsomorphicComponent {
|
|
46
|
+
new <T extends object = object>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
|
|
47
|
+
$$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
|
|
48
|
+
} & ReturnType<__sveltets_Render<T>['exports']>;
|
|
49
|
+
<T extends object = object>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
|
|
50
|
+
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
|
|
51
|
+
}
|
|
52
|
+
declare const Listbox: $$IsomorphicComponent;
|
|
53
|
+
type Listbox<T extends object = object> = InstanceType<typeof Listbox<T>>;
|
|
54
|
+
export default Listbox;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Popover } from '../index';
|
|
3
|
+
|
|
4
|
+
type Props = {
|
|
5
|
+
open?: boolean;
|
|
6
|
+
defaultOpen?: boolean;
|
|
7
|
+
isNonModal?: boolean;
|
|
8
|
+
shouldCloseOnInteractOutside?: boolean;
|
|
9
|
+
shouldCloseOnEscape?: boolean;
|
|
10
|
+
shouldCloseOnBlur?: boolean;
|
|
11
|
+
onOpenChange?: (open: boolean) => void;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
let {
|
|
15
|
+
open,
|
|
16
|
+
defaultOpen = false,
|
|
17
|
+
isNonModal = false,
|
|
18
|
+
shouldCloseOnInteractOutside = true,
|
|
19
|
+
shouldCloseOnEscape = true,
|
|
20
|
+
shouldCloseOnBlur,
|
|
21
|
+
onOpenChange
|
|
22
|
+
}: Props = $props();
|
|
23
|
+
</script>
|
|
24
|
+
|
|
25
|
+
<Popover.Root {open} {defaultOpen} {onOpenChange}>
|
|
26
|
+
<Popover.Trigger>
|
|
27
|
+
<button type="button">Open Popover</button>
|
|
28
|
+
</Popover.Trigger>
|
|
29
|
+
|
|
30
|
+
<Popover.Content
|
|
31
|
+
class="popover-content"
|
|
32
|
+
{isNonModal}
|
|
33
|
+
{shouldCloseOnInteractOutside}
|
|
34
|
+
{shouldCloseOnEscape}
|
|
35
|
+
{shouldCloseOnBlur}
|
|
36
|
+
>
|
|
37
|
+
<div class="popover-body">
|
|
38
|
+
<h3>Popover Title</h3>
|
|
39
|
+
<p>Popover content goes here.</p>
|
|
40
|
+
<button type="button" class="close-btn">Close</button>
|
|
41
|
+
</div>
|
|
42
|
+
</Popover.Content>
|
|
43
|
+
</Popover.Root>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
type Props = {
|
|
2
|
+
open?: boolean;
|
|
3
|
+
defaultOpen?: boolean;
|
|
4
|
+
isNonModal?: boolean;
|
|
5
|
+
shouldCloseOnInteractOutside?: boolean;
|
|
6
|
+
shouldCloseOnEscape?: boolean;
|
|
7
|
+
shouldCloseOnBlur?: boolean;
|
|
8
|
+
onOpenChange?: (open: boolean) => void;
|
|
9
|
+
};
|
|
10
|
+
declare const PopoverContentTest: import("svelte").Component<Props, {}, "">;
|
|
11
|
+
type PopoverContentTest = ReturnType<typeof PopoverContentTest>;
|
|
12
|
+
export default PopoverContentTest;
|