@human-kit/svelte-components 1.0.0-alpha.11 → 1.0.0-alpha.12
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/item/combobox-listboxitem.svelte +11 -2
- package/dist/combobox/root/combobox-multiselect-test.svelte +4 -2
- package/dist/combobox/root/combobox-multiselect-test.svelte.d.ts +1 -0
- package/dist/combobox/root/combobox.svelte +3 -0
- package/dist/hooks/use-virtual-focus.svelte.js +3 -1
- package/dist/listbox/item/listbox-item.svelte +81 -5
- package/dist/listbox/item/listbox-item.svelte.d.ts +2 -0
- package/dist/listbox/root/context.d.ts +6 -0
- package/dist/listbox/root/context.js +23 -13
- package/dist/listbox/root/listbox.svelte +4 -13
- package/dist/primitives/keyboard-navigation.d.ts +1 -0
- package/dist/primitives/keyboard-navigation.js +17 -0
- package/package.json +1 -1
|
@@ -56,6 +56,7 @@
|
|
|
56
56
|
|
|
57
57
|
// Normalized input for filtering comparison
|
|
58
58
|
const normalizedInput = $derived(ctx.inputValue.trim().toLowerCase());
|
|
59
|
+
const isDisabled = $derived(Boolean(props.disabled) || ctx.isDisabled);
|
|
59
60
|
|
|
60
61
|
// Automatic filtering: if text is not resolved yet, keep item visible until mount resolves it.
|
|
61
62
|
const isVisible = $derived(
|
|
@@ -77,14 +78,15 @@
|
|
|
77
78
|
// Reactive registration: register when visible, unregister when hidden
|
|
78
79
|
$effect(() => {
|
|
79
80
|
const visible = isVisible;
|
|
81
|
+
const disabled = isDisabled;
|
|
80
82
|
const label = effectiveTextValue || String(id);
|
|
81
83
|
const itemId = id;
|
|
82
84
|
|
|
83
85
|
untrack(() => {
|
|
84
|
-
if (visible && !isRegistered) {
|
|
86
|
+
if (visible && !disabled && !isRegistered) {
|
|
85
87
|
ctx.registerItem(itemId, label);
|
|
86
88
|
isRegistered = true;
|
|
87
|
-
} else if (!visible && isRegistered) {
|
|
89
|
+
} else if ((!visible || disabled) && isRegistered) {
|
|
88
90
|
ctx.unregisterItem(itemId);
|
|
89
91
|
isRegistered = false;
|
|
90
92
|
}
|
|
@@ -109,8 +111,14 @@
|
|
|
109
111
|
|
|
110
112
|
// Custom select handler that uses ComboBox context
|
|
111
113
|
function handleSelect(itemId: string | number, label: string) {
|
|
114
|
+
ctx.setFocusedItemId(itemId);
|
|
112
115
|
ctx.select(itemId, label);
|
|
113
116
|
}
|
|
117
|
+
|
|
118
|
+
function handleHoverStart(itemId: string | number) {
|
|
119
|
+
ctx.setFocusVisible(false);
|
|
120
|
+
ctx.setFocusedItemId(itemId);
|
|
121
|
+
}
|
|
114
122
|
</script>
|
|
115
123
|
|
|
116
124
|
{#if isVisible}
|
|
@@ -124,6 +132,7 @@
|
|
|
124
132
|
isFocusVisibleOverride={isFocusVisible}
|
|
125
133
|
onItemSelect={handleSelect}
|
|
126
134
|
onResolvedTextValue={handleResolvedTextValue}
|
|
135
|
+
onItemHoverStart={handleHoverStart}
|
|
127
136
|
scrollOnFocus={true}
|
|
128
137
|
isParentDisabled={ctx.isDisabled}
|
|
129
138
|
/>
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
onValueChange?: (value: (string | number)[]) => void;
|
|
8
8
|
trigger?: 'focus' | 'input' | 'press';
|
|
9
9
|
closeOnSelect?: boolean;
|
|
10
|
+
disabledIds?: string[];
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
let {
|
|
@@ -20,7 +21,8 @@
|
|
|
20
21
|
value = $bindable([]),
|
|
21
22
|
onValueChange,
|
|
22
23
|
trigger = 'press',
|
|
23
|
-
closeOnSelect = false
|
|
24
|
+
closeOnSelect = false,
|
|
25
|
+
disabledIds = []
|
|
24
26
|
}: Props = $props();
|
|
25
27
|
|
|
26
28
|
function handleChange(newValue: string | number | (string | number)[] | undefined) {
|
|
@@ -53,7 +55,7 @@
|
|
|
53
55
|
<ComboBox.Popover>
|
|
54
56
|
<ComboBox.List>
|
|
55
57
|
{#each items as item (item.id)}
|
|
56
|
-
<ComboBox.Item id={item.id} textValue={item.name}>
|
|
58
|
+
<ComboBox.Item id={item.id} textValue={item.name} disabled={disabledIds.includes(item.id)}>
|
|
57
59
|
{item.name}
|
|
58
60
|
<ComboBox.ItemIndicator />
|
|
59
61
|
</ComboBox.Item>
|
|
@@ -7,6 +7,7 @@ interface Props {
|
|
|
7
7
|
onValueChange?: (value: (string | number)[]) => void;
|
|
8
8
|
trigger?: 'focus' | 'input' | 'press';
|
|
9
9
|
closeOnSelect?: boolean;
|
|
10
|
+
disabledIds?: string[];
|
|
10
11
|
}
|
|
11
12
|
declare const ComboboxMultiselectTest: import("svelte").Component<Props, {}, "value">;
|
|
12
13
|
type ComboboxMultiselectTest = ReturnType<typeof ComboboxMultiselectTest>;
|
|
@@ -381,6 +381,9 @@
|
|
|
381
381
|
// Use navigation hook methods for keyboard navigation
|
|
382
382
|
function selectFocusedItem() {
|
|
383
383
|
if (navigation.focusedId !== null) {
|
|
384
|
+
if (listboxCtxRef?.isDisabled(navigation.focusedId)) {
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
384
387
|
const label = navigation.itemLabels.get(navigation.focusedId) ?? String(navigation.focusedId);
|
|
385
388
|
selectItem(navigation.focusedId, label);
|
|
386
389
|
}
|
|
@@ -62,7 +62,9 @@ export function useVirtualFocus(options) {
|
|
|
62
62
|
if (fullId && fullId.startsWith(prefix)) {
|
|
63
63
|
const rawId = fullId.substring(prefix.length);
|
|
64
64
|
const registeredId = itemIds.find((id) => String(id) === rawId);
|
|
65
|
-
|
|
65
|
+
if (registeredId !== undefined) {
|
|
66
|
+
orderedIds.push(registeredId);
|
|
67
|
+
}
|
|
66
68
|
}
|
|
67
69
|
});
|
|
68
70
|
cachedItemOrder = orderedIds;
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { useListBoxContext } from '../root/context';
|
|
5
5
|
import { onMount, onDestroy } from 'svelte';
|
|
6
6
|
import {
|
|
7
|
+
focusWithModality,
|
|
7
8
|
shouldShowFocusVisible,
|
|
8
9
|
trackInteractionModality
|
|
9
10
|
} from '../../primitives/input-modality';
|
|
@@ -36,6 +37,8 @@
|
|
|
36
37
|
onItemSelect?: (id: string | number, label: string) => void;
|
|
37
38
|
/** Callback with resolved text value when mounted (from prop or rendered content). */
|
|
38
39
|
onResolvedTextValue?: (label: string) => void;
|
|
40
|
+
/** Callback when pointer hover should move logical focus to this item. */
|
|
41
|
+
onItemHoverStart?: (id: string | number, label: string) => void;
|
|
39
42
|
/** Whether to scroll this item into view when focused. Useful for virtual focus patterns. */
|
|
40
43
|
scrollOnFocus?: boolean;
|
|
41
44
|
/** Additional disabled state from parent. */
|
|
@@ -57,6 +60,7 @@
|
|
|
57
60
|
isFocusVisibleOverride,
|
|
58
61
|
onItemSelect,
|
|
59
62
|
onResolvedTextValue,
|
|
63
|
+
onItemHoverStart,
|
|
60
64
|
scrollOnFocus = false,
|
|
61
65
|
isParentDisabled = false,
|
|
62
66
|
pressed: pressedOverride,
|
|
@@ -69,9 +73,11 @@
|
|
|
69
73
|
let isSelected = $state(false);
|
|
70
74
|
let isFocused = $state(false);
|
|
71
75
|
let isFocusVisible = $state(false);
|
|
76
|
+
let listFocusVisible = $state(false);
|
|
72
77
|
let isHovered = $state(false);
|
|
73
78
|
let isPressed = $state(false);
|
|
74
79
|
let pressedKey: 'Enter' | 'Space' | null = $state(null);
|
|
80
|
+
let suppressNextFocusVisible = $state(false);
|
|
75
81
|
|
|
76
82
|
// Focus: use override if provided, otherwise use internal state
|
|
77
83
|
const isFocusedComputed = $derived(
|
|
@@ -88,12 +94,20 @@
|
|
|
88
94
|
const isFocusVisibleComputed = $derived(
|
|
89
95
|
isFocusVisibleOverride !== undefined ? isFocusVisibleOverride : isFocusVisible
|
|
90
96
|
);
|
|
97
|
+
const isActiveFocusVisible = $derived(
|
|
98
|
+
isFocusVisibleOverride !== undefined
|
|
99
|
+
? isFocusVisibleComputed
|
|
100
|
+
: isFocusedComputed && listFocusVisible
|
|
101
|
+
);
|
|
102
|
+
const showFocusVisible = $derived(isActiveFocusVisible && !isHovered);
|
|
103
|
+
const showHovered = $derived(isHovered && !isActiveFocusVisible);
|
|
91
104
|
|
|
92
105
|
// ID: use custom if provided, otherwise generate
|
|
93
106
|
const uniqueId = $derived(customId ?? `listbox-item-${id}`);
|
|
94
107
|
|
|
95
108
|
let unsubscribeSelection: (() => void) | null = null;
|
|
96
109
|
let unsubscribeFocus: (() => void) | null = null;
|
|
110
|
+
let unsubscribeFocusVisible: (() => void) | null = null;
|
|
97
111
|
|
|
98
112
|
function getResolvedTextValue() {
|
|
99
113
|
return textValue || elementRef?.textContent?.trim() || String(id);
|
|
@@ -115,6 +129,9 @@
|
|
|
115
129
|
unsubscribeFocus = listboxCtx.subscribeToFocus(id, (focused) => {
|
|
116
130
|
isFocused = focused;
|
|
117
131
|
});
|
|
132
|
+
unsubscribeFocusVisible = listboxCtx.subscribeToFocusVisible((visible) => {
|
|
133
|
+
listFocusVisible = visible;
|
|
134
|
+
});
|
|
118
135
|
listboxCtx.keyboardNav.updateItems();
|
|
119
136
|
}
|
|
120
137
|
});
|
|
@@ -123,6 +140,7 @@
|
|
|
123
140
|
listboxCtx.unregisterItem(id);
|
|
124
141
|
unsubscribeSelection?.();
|
|
125
142
|
unsubscribeFocus?.();
|
|
143
|
+
unsubscribeFocusVisible?.();
|
|
126
144
|
});
|
|
127
145
|
|
|
128
146
|
// Scroll into view when focused (if enabled)
|
|
@@ -146,6 +164,30 @@
|
|
|
146
164
|
pressedKey = null;
|
|
147
165
|
}
|
|
148
166
|
|
|
167
|
+
function applyPointerFocusState() {
|
|
168
|
+
suppressNextFocusVisible = true;
|
|
169
|
+
listboxCtx.setFocusVisible(false);
|
|
170
|
+
listboxCtx.setFocusedId(id);
|
|
171
|
+
listboxCtx.keyboardNav.setCurrentId(id);
|
|
172
|
+
if (elementRef) {
|
|
173
|
+
focusWithModality(elementRef, 'pointer');
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function transferHoverFocus() {
|
|
178
|
+
const label = getResolvedTextValue();
|
|
179
|
+
if (onItemHoverStart) {
|
|
180
|
+
onItemHoverStart(id, label);
|
|
181
|
+
} else if (!disableFocusHandling) {
|
|
182
|
+
applyPointerFocusState();
|
|
183
|
+
requestAnimationFrame(() => {
|
|
184
|
+
if (isHovered && !isDisabledComputed) {
|
|
185
|
+
applyPointerFocusState();
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
149
191
|
function handleClick() {
|
|
150
192
|
if (isDisabledComputed) return;
|
|
151
193
|
|
|
@@ -156,25 +198,40 @@
|
|
|
156
198
|
onItemSelect(id, label);
|
|
157
199
|
} else {
|
|
158
200
|
listboxCtx.select(id);
|
|
159
|
-
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (!disableFocusHandling) {
|
|
204
|
+
listboxCtx.keyboardNav.focusById(id);
|
|
160
205
|
}
|
|
161
206
|
}
|
|
162
207
|
|
|
163
208
|
function handleFocus() {
|
|
164
209
|
if (isDisabledComputed) return;
|
|
165
|
-
isFocusVisible = shouldShowFocusVisible(elementRef);
|
|
210
|
+
isFocusVisible = suppressNextFocusVisible ? false : shouldShowFocusVisible(elementRef);
|
|
211
|
+
suppressNextFocusVisible = false;
|
|
212
|
+
if (isFocusVisible) {
|
|
213
|
+
isHovered = false;
|
|
214
|
+
}
|
|
166
215
|
if (!disableFocusHandling) {
|
|
216
|
+
listboxCtx.setFocusVisible(isFocusVisible);
|
|
167
217
|
listboxCtx.setFocusedId(id);
|
|
168
218
|
}
|
|
169
219
|
}
|
|
170
220
|
|
|
171
221
|
function handleBlur() {
|
|
172
222
|
isFocusVisible = false;
|
|
223
|
+
if (!disableFocusHandling && listboxCtx.isFocused(id)) {
|
|
224
|
+
listboxCtx.setFocusVisible(false);
|
|
225
|
+
listboxCtx.setFocusedId(null);
|
|
226
|
+
}
|
|
173
227
|
}
|
|
174
228
|
|
|
175
229
|
function handlePointerDown(event: PointerEvent) {
|
|
176
230
|
trackInteractionModality(event, elementRef);
|
|
177
231
|
isFocusVisible = false;
|
|
232
|
+
if (!disableFocusHandling) {
|
|
233
|
+
listboxCtx.setFocusVisible(false);
|
|
234
|
+
}
|
|
178
235
|
|
|
179
236
|
if (isDisabledComputed) {
|
|
180
237
|
event.preventDefault();
|
|
@@ -200,6 +257,12 @@
|
|
|
200
257
|
function handlePointerEnter(event: PointerEvent) {
|
|
201
258
|
if (isDisabledComputed) return;
|
|
202
259
|
|
|
260
|
+
trackInteractionModality(event, elementRef);
|
|
261
|
+
if (!disableFocusHandling) {
|
|
262
|
+
listboxCtx.setFocusVisible(false);
|
|
263
|
+
}
|
|
264
|
+
transferHoverFocus();
|
|
265
|
+
|
|
203
266
|
if ((event.buttons & 1) === 1 && pressedKey === null) {
|
|
204
267
|
isPressed = true;
|
|
205
268
|
}
|
|
@@ -211,9 +274,15 @@
|
|
|
211
274
|
}
|
|
212
275
|
}
|
|
213
276
|
|
|
214
|
-
function handleMouseEnter() {
|
|
277
|
+
function handleMouseEnter(event: MouseEvent) {
|
|
215
278
|
if (!isDisabledComputed) {
|
|
279
|
+
trackInteractionModality(event, elementRef);
|
|
216
280
|
isHovered = true;
|
|
281
|
+
isFocusVisible = false;
|
|
282
|
+
if (!disableFocusHandling) {
|
|
283
|
+
listboxCtx.setFocusVisible(false);
|
|
284
|
+
transferHoverFocus();
|
|
285
|
+
}
|
|
217
286
|
}
|
|
218
287
|
}
|
|
219
288
|
|
|
@@ -228,7 +297,11 @@
|
|
|
228
297
|
function handleKeydown(event: KeyboardEvent) {
|
|
229
298
|
trackInteractionModality(event, elementRef);
|
|
230
299
|
if (isFocusedComputed) {
|
|
300
|
+
isHovered = false;
|
|
231
301
|
isFocusVisible = true;
|
|
302
|
+
if (!disableFocusHandling) {
|
|
303
|
+
listboxCtx.setFocusVisible(true);
|
|
304
|
+
}
|
|
232
305
|
}
|
|
233
306
|
|
|
234
307
|
const key =
|
|
@@ -276,6 +349,9 @@
|
|
|
276
349
|
function handleMouseDown(event: MouseEvent) {
|
|
277
350
|
trackInteractionModality(event, elementRef);
|
|
278
351
|
isFocusVisible = false;
|
|
352
|
+
if (!disableFocusHandling) {
|
|
353
|
+
listboxCtx.setFocusVisible(false);
|
|
354
|
+
}
|
|
279
355
|
|
|
280
356
|
// Prevent focus stealing when used in ComboBox (disableFocusHandling=true)
|
|
281
357
|
// This keeps the focus on the input while allowing click selection
|
|
@@ -317,8 +393,8 @@
|
|
|
317
393
|
data-selected={isSelected || undefined}
|
|
318
394
|
data-disabled={isDisabledComputed || undefined}
|
|
319
395
|
data-focused={isFocusedComputed || undefined}
|
|
320
|
-
data-focus-visible={
|
|
321
|
-
data-hovered={
|
|
396
|
+
data-focus-visible={showFocusVisible || undefined}
|
|
397
|
+
data-hovered={showHovered || undefined}
|
|
322
398
|
data-pressed={isPressedComputed || undefined}
|
|
323
399
|
onpointerdown={handlePointerDown}
|
|
324
400
|
onpointerup={handlePointerUp}
|
|
@@ -26,6 +26,8 @@ type ListBoxItemProps = Omit<HTMLAttributes<HTMLDivElement>, 'id' | 'children'>
|
|
|
26
26
|
onItemSelect?: (id: string | number, label: string) => void;
|
|
27
27
|
/** Callback with resolved text value when mounted (from prop or rendered content). */
|
|
28
28
|
onResolvedTextValue?: (label: string) => void;
|
|
29
|
+
/** Callback when pointer hover should move logical focus to this item. */
|
|
30
|
+
onItemHoverStart?: (id: string | number, label: string) => void;
|
|
29
31
|
/** Whether to scroll this item into view when focused. Useful for virtual focus patterns. */
|
|
30
32
|
scrollOnFocus?: boolean;
|
|
31
33
|
/** Additional disabled state from parent. */
|
|
@@ -20,6 +20,8 @@ export type ListBoxContext = {
|
|
|
20
20
|
isDisabled: (id: string | number) => boolean;
|
|
21
21
|
/** Checks if an item is focused. */
|
|
22
22
|
isFocused: (id: string | number) => boolean;
|
|
23
|
+
/** Whether keyboard focus-visible should be shown for the currently focused item. */
|
|
24
|
+
getFocusVisible: () => boolean;
|
|
23
25
|
/** Keyboard navigation controller from the shared primitive. */
|
|
24
26
|
keyboardNav: KeyboardNavigationReturn;
|
|
25
27
|
/** Map of registered items with their metadata. */
|
|
@@ -45,10 +47,14 @@ export type ListBoxContext = {
|
|
|
45
47
|
setSelection: (selection: Set<string | number>) => void;
|
|
46
48
|
/** Sets the focused item ID. */
|
|
47
49
|
setFocusedId: (id: string | number | null) => void;
|
|
50
|
+
/** Sets whether the focused item should render keyboard focus-visible. */
|
|
51
|
+
setFocusVisible: (visible: boolean) => void;
|
|
48
52
|
/** Subscribes to selection changes for a specific item. Returns unsubscribe function. */
|
|
49
53
|
subscribeToItem: (id: string | number, callback: (selected: boolean) => void) => () => void;
|
|
50
54
|
/** Subscribes to focus changes for a specific item. Returns unsubscribe function. */
|
|
51
55
|
subscribeToFocus: (id: string | number, callback: (focused: boolean) => void) => () => void;
|
|
56
|
+
/** Subscribes to focus-visible state changes. Returns unsubscribe function. */
|
|
57
|
+
subscribeToFocusVisible: (callback: (visible: boolean) => void) => () => void;
|
|
52
58
|
/** Returns the next item ID respecting loop setting, or null if at end. */
|
|
53
59
|
getNextItemId: (currentId: string | number | null) => string | number | null;
|
|
54
60
|
/** Returns the previous item ID respecting loop setting, or null if at start. */
|
|
@@ -53,30 +53,30 @@ export function createListBoxContext(options = {}) {
|
|
|
53
53
|
}
|
|
54
54
|
let focusedId = null;
|
|
55
55
|
const focusCallbacks = new Map();
|
|
56
|
+
let focusVisible = false;
|
|
57
|
+
const focusVisibleCallbacks = new Set();
|
|
56
58
|
function getFocusedId() {
|
|
57
59
|
return focusedId;
|
|
58
60
|
}
|
|
59
61
|
function isFocused(id) {
|
|
60
62
|
return focusedId === id || String(focusedId) === String(id);
|
|
61
63
|
}
|
|
62
|
-
function
|
|
63
|
-
|
|
64
|
-
if (callbacks) {
|
|
65
|
-
callbacks.forEach((cb) => cb(focused));
|
|
66
|
-
}
|
|
64
|
+
function getFocusVisible() {
|
|
65
|
+
return focusVisible;
|
|
67
66
|
}
|
|
68
67
|
function setFocusedId(newId) {
|
|
69
|
-
if (focusedId === newId)
|
|
70
|
-
return;
|
|
71
|
-
const previousId = focusedId;
|
|
72
68
|
focusedId = newId;
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
if (newId !== null) {
|
|
77
|
-
notifyFocus(newId, true);
|
|
69
|
+
for (const [id, callbacks] of focusCallbacks) {
|
|
70
|
+
const focused = newId !== null && (id === newId || String(id) === String(newId));
|
|
71
|
+
callbacks.forEach((callback) => callback(focused));
|
|
78
72
|
}
|
|
79
73
|
}
|
|
74
|
+
function setFocusVisible(visible) {
|
|
75
|
+
if (focusVisible === visible)
|
|
76
|
+
return;
|
|
77
|
+
focusVisible = visible;
|
|
78
|
+
focusVisibleCallbacks.forEach((callback) => callback(visible));
|
|
79
|
+
}
|
|
80
80
|
function subscribeToFocus(id, callback) {
|
|
81
81
|
if (!focusCallbacks.has(id)) {
|
|
82
82
|
focusCallbacks.set(id, new Set());
|
|
@@ -93,6 +93,13 @@ export function createListBoxContext(options = {}) {
|
|
|
93
93
|
}
|
|
94
94
|
};
|
|
95
95
|
}
|
|
96
|
+
function subscribeToFocusVisible(callback) {
|
|
97
|
+
focusVisibleCallbacks.add(callback);
|
|
98
|
+
callback(focusVisible);
|
|
99
|
+
return () => {
|
|
100
|
+
focusVisibleCallbacks.delete(callback);
|
|
101
|
+
};
|
|
102
|
+
}
|
|
96
103
|
function subscribeToItem(id, callback) {
|
|
97
104
|
if (!itemCallbacks.has(id)) {
|
|
98
105
|
itemCallbacks.set(id, new Set());
|
|
@@ -221,6 +228,7 @@ export function createListBoxContext(options = {}) {
|
|
|
221
228
|
isSelected,
|
|
222
229
|
isDisabled,
|
|
223
230
|
isFocused,
|
|
231
|
+
getFocusVisible,
|
|
224
232
|
keyboardNav,
|
|
225
233
|
items,
|
|
226
234
|
registerItem,
|
|
@@ -232,8 +240,10 @@ export function createListBoxContext(options = {}) {
|
|
|
232
240
|
selectAll,
|
|
233
241
|
setSelection,
|
|
234
242
|
setFocusedId,
|
|
243
|
+
setFocusVisible,
|
|
235
244
|
subscribeToItem,
|
|
236
245
|
subscribeToFocus,
|
|
246
|
+
subscribeToFocusVisible,
|
|
237
247
|
getNextItemId,
|
|
238
248
|
getPreviousItemId
|
|
239
249
|
};
|
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
<script lang="ts" generics="T extends object = object">
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
3
|
import { createListBoxContext, type ListBoxContext } from './context';
|
|
4
|
-
import {
|
|
5
|
-
shouldShowFocusVisible,
|
|
6
|
-
trackInteractionModality
|
|
7
|
-
} from '../../primitives/input-modality';
|
|
4
|
+
import { trackInteractionModality } from '../../primitives/input-modality';
|
|
8
5
|
|
|
9
6
|
/**
|
|
10
7
|
* Props for the ListBox component.
|
|
@@ -120,21 +117,16 @@
|
|
|
120
117
|
const hasItems = $derived(itemsArray.length > 0 || itemCount > 0);
|
|
121
118
|
|
|
122
119
|
let focusWithin = $state(false);
|
|
123
|
-
let focusVisible = $state(false);
|
|
124
120
|
|
|
125
121
|
function syncFocusWithin() {
|
|
126
122
|
focusWithin =
|
|
127
123
|
!!listboxElement &&
|
|
128
124
|
!!document.activeElement &&
|
|
129
125
|
listboxElement.contains(document.activeElement);
|
|
130
|
-
if (!focusWithin) {
|
|
131
|
-
focusVisible = false;
|
|
132
|
-
}
|
|
133
126
|
}
|
|
134
127
|
|
|
135
|
-
function handleFocusIn(
|
|
128
|
+
function handleFocusIn() {
|
|
136
129
|
focusWithin = true;
|
|
137
|
-
focusVisible = shouldShowFocusVisible(event.target as HTMLElement | null);
|
|
138
130
|
}
|
|
139
131
|
|
|
140
132
|
function handleFocusOut() {
|
|
@@ -143,13 +135,13 @@
|
|
|
143
135
|
|
|
144
136
|
function handleMouseDown(event: MouseEvent) {
|
|
145
137
|
trackInteractionModality(event, event.target as HTMLElement | null);
|
|
146
|
-
|
|
138
|
+
ctx.setFocusVisible(false);
|
|
147
139
|
}
|
|
148
140
|
|
|
149
141
|
function handleKeyDown(event: KeyboardEvent) {
|
|
150
142
|
trackInteractionModality(event, event.target as HTMLElement | null);
|
|
151
143
|
if (focusWithin) {
|
|
152
|
-
|
|
144
|
+
ctx.setFocusVisible(true);
|
|
153
145
|
}
|
|
154
146
|
}
|
|
155
147
|
</script>
|
|
@@ -163,7 +155,6 @@
|
|
|
163
155
|
class={className}
|
|
164
156
|
tabindex="0"
|
|
165
157
|
data-focus-within={focusWithin || undefined}
|
|
166
|
-
data-focus-visible={focusVisible || undefined}
|
|
167
158
|
use:keyboardAction
|
|
168
159
|
onfocusin={handleFocusIn}
|
|
169
160
|
onfocusout={handleFocusOut}
|
|
@@ -53,6 +53,7 @@ export type KeyboardNavigationReturn = {
|
|
|
53
53
|
focusFirst: () => void;
|
|
54
54
|
focusLast: () => void;
|
|
55
55
|
focusById: (id: string | number) => void;
|
|
56
|
+
setCurrentId: (id: string | number | null) => void;
|
|
56
57
|
/** Update items (call after DOM changes) */
|
|
57
58
|
updateItems: () => void;
|
|
58
59
|
};
|
|
@@ -130,6 +130,22 @@ export function createKeyboardNavigation(options = {}) {
|
|
|
130
130
|
focusItem(element);
|
|
131
131
|
}
|
|
132
132
|
}
|
|
133
|
+
function setCurrentId(id) {
|
|
134
|
+
if (id === null) {
|
|
135
|
+
focusedId.set(null);
|
|
136
|
+
focusedElement.set(null);
|
|
137
|
+
onFocusChange?.(null, null);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
items = getItems();
|
|
141
|
+
const element = items.find((el) => {
|
|
142
|
+
const itemId = getItemId(el);
|
|
143
|
+
return itemId === id || String(itemId) === String(id);
|
|
144
|
+
});
|
|
145
|
+
focusedId.set(id);
|
|
146
|
+
focusedElement.set(element ?? null);
|
|
147
|
+
onFocusChange?.(id, element ?? null);
|
|
148
|
+
}
|
|
133
149
|
function handleTypeahead(char) {
|
|
134
150
|
if (!typeahead)
|
|
135
151
|
return;
|
|
@@ -254,6 +270,7 @@ export function createKeyboardNavigation(options = {}) {
|
|
|
254
270
|
focusFirst,
|
|
255
271
|
focusLast,
|
|
256
272
|
focusById,
|
|
273
|
+
setCurrentId,
|
|
257
274
|
updateItems
|
|
258
275
|
};
|
|
259
276
|
}
|