@human-kit/svelte-components 1.0.0-alpha.12 → 1.0.0-alpha.14
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/checkbox/root/checkbox-root.svelte +22 -2
- package/dist/checkbox/root/checkbox-root.svelte.d.ts +4 -1
- package/dist/combobox/input/combobox-input.svelte +1 -0
- package/dist/combobox/list/combobox-listbox.svelte +1 -0
- package/dist/combobox/list/combobox-listbox.svelte.d.ts +1 -0
- package/dist/combobox/root/combobox-test.svelte +8 -2
- package/dist/combobox/root/combobox-test.svelte.d.ts +1 -0
- package/dist/combobox/root/combobox.svelte +16 -9
- package/dist/listbox/item/listbox-item.svelte +24 -2
- package/dist/listbox/root/listbox.svelte +14 -2
- package/dist/listbox/root/listbox.svelte.d.ts +2 -0
- package/dist/table/IMPLEMENTATION_NOTES.md +2 -1
- package/dist/table/PLAN.md +440 -17
- package/dist/table/TODO.md +39 -1
- package/dist/table/cell/table-cell.svelte +86 -79
- package/dist/table/checkbox/table-checkbox-test.svelte +7 -0
- package/dist/table/checkbox/table-checkbox-test.svelte.d.ts +3 -1
- package/dist/table/checkbox/table-checkbox.svelte +55 -30
- package/dist/table/index.d.ts +1 -1
- package/dist/table/root/context.d.ts +16 -1
- package/dist/table/root/context.js +199 -24
- package/dist/table/root/table-root.svelte +30 -0
- package/dist/table/root/table-root.svelte.d.ts +4 -1
- package/dist/table/root/table-test.svelte +29 -0
- package/dist/table/root/table-test.svelte.d.ts +5 -1
- package/dist/table/row/table-row.svelte +44 -67
- package/dist/table/utils/handle-body-keydown.d.ts +13 -0
- package/dist/table/utils/handle-body-keydown.js +67 -0
- package/package.json +1 -1
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
| 'value'
|
|
25
25
|
> & {
|
|
26
26
|
id?: string;
|
|
27
|
+
element?: HTMLSpanElement | null;
|
|
27
28
|
name?: string;
|
|
28
29
|
value?: string;
|
|
29
30
|
isChecked?: boolean;
|
|
@@ -39,8 +40,20 @@
|
|
|
39
40
|
class?: string;
|
|
40
41
|
'aria-label'?: string;
|
|
41
42
|
'aria-labelledby'?: string;
|
|
43
|
+
onclick?: HTMLAttributes<HTMLSpanElement>['onclick'];
|
|
44
|
+
onkeydown?: HTMLAttributes<HTMLSpanElement>['onkeydown'];
|
|
42
45
|
};
|
|
43
46
|
|
|
47
|
+
function composeEventHandlers<TEvent extends Event>(
|
|
48
|
+
internalHandler: ((event: TEvent) => void) | undefined,
|
|
49
|
+
externalHandler: ((event: TEvent) => void) | undefined
|
|
50
|
+
): (event: TEvent) => void {
|
|
51
|
+
return (event: TEvent) => {
|
|
52
|
+
internalHandler?.(event);
|
|
53
|
+
externalHandler?.(event);
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
44
57
|
function resolveState(isChecked: boolean, isIndeterminate: boolean): CheckboxState {
|
|
45
58
|
if (isIndeterminate) return 'indeterminate';
|
|
46
59
|
return isChecked ? 'checked' : 'unchecked';
|
|
@@ -56,6 +69,7 @@
|
|
|
56
69
|
|
|
57
70
|
let {
|
|
58
71
|
id,
|
|
72
|
+
element = $bindable(),
|
|
59
73
|
name,
|
|
60
74
|
value = 'on',
|
|
61
75
|
isChecked = $bindable(),
|
|
@@ -71,6 +85,8 @@
|
|
|
71
85
|
class: className = '',
|
|
72
86
|
'aria-label': ariaLabel,
|
|
73
87
|
'aria-labelledby': ariaLabelledby,
|
|
88
|
+
onclick: onClickExternal,
|
|
89
|
+
onkeydown: onKeyDownExternal,
|
|
74
90
|
...restProps
|
|
75
91
|
}: CheckboxRootProps = $props();
|
|
76
92
|
|
|
@@ -91,6 +107,10 @@
|
|
|
91
107
|
let rootRef: HTMLSpanElement | null = $state(null);
|
|
92
108
|
let inputRef: HTMLInputElement | null = $state(null);
|
|
93
109
|
|
|
110
|
+
$effect(() => {
|
|
111
|
+
element = rootRef;
|
|
112
|
+
});
|
|
113
|
+
|
|
94
114
|
if (untrack(() => isChecked) === undefined) {
|
|
95
115
|
isChecked = initialState === 'checked';
|
|
96
116
|
}
|
|
@@ -326,8 +346,8 @@
|
|
|
326
346
|
data-required={required || undefined}
|
|
327
347
|
data-focused={focused || undefined}
|
|
328
348
|
data-focus-visible={focusVisible || undefined}
|
|
329
|
-
onclick={handleClick}
|
|
330
|
-
onkeydown={handleKeyDown}
|
|
349
|
+
onclick={composeEventHandlers(onClickExternal ?? undefined, handleClick)}
|
|
350
|
+
onkeydown={composeEventHandlers(handleKeyDown, onKeyDownExternal ?? undefined)}
|
|
331
351
|
onkeyup={handleKeyUp}
|
|
332
352
|
onpointerdown={handlePointerDown}
|
|
333
353
|
onmousedown={handlePointerDown}
|
|
@@ -2,6 +2,7 @@ import { type Snippet } from 'svelte';
|
|
|
2
2
|
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
3
|
type CheckboxRootProps = Omit<HTMLAttributes<HTMLSpanElement>, 'children' | 'class' | 'id' | 'role' | 'tabindex' | 'aria-checked' | 'aria-disabled' | 'aria-readonly' | 'aria-required' | 'onclick' | 'onkeydown' | 'value'> & {
|
|
4
4
|
id?: string;
|
|
5
|
+
element?: HTMLSpanElement | null;
|
|
5
6
|
name?: string;
|
|
6
7
|
value?: string;
|
|
7
8
|
isChecked?: boolean;
|
|
@@ -17,7 +18,9 @@ type CheckboxRootProps = Omit<HTMLAttributes<HTMLSpanElement>, 'children' | 'cla
|
|
|
17
18
|
class?: string;
|
|
18
19
|
'aria-label'?: string;
|
|
19
20
|
'aria-labelledby'?: string;
|
|
21
|
+
onclick?: HTMLAttributes<HTMLSpanElement>['onclick'];
|
|
22
|
+
onkeydown?: HTMLAttributes<HTMLSpanElement>['onkeydown'];
|
|
20
23
|
};
|
|
21
|
-
declare const CheckboxRoot: import("svelte").Component<CheckboxRootProps, {}, "isChecked" | "isIndeterminate">;
|
|
24
|
+
declare const CheckboxRoot: import("svelte").Component<CheckboxRootProps, {}, "element" | "isChecked" | "isIndeterminate">;
|
|
22
25
|
type CheckboxRoot = ReturnType<typeof CheckboxRoot>;
|
|
23
26
|
export default CheckboxRoot;
|
|
@@ -14,6 +14,7 @@ declare function $$render<T extends object = object>(): {
|
|
|
14
14
|
id?: string;
|
|
15
15
|
'aria-label'?: string;
|
|
16
16
|
onChange?: ((value: Set<string | number>) => void) | undefined;
|
|
17
|
+
disableFocusHandling?: boolean;
|
|
17
18
|
} & {
|
|
18
19
|
context?: ListBoxContext;
|
|
19
20
|
element?: HTMLElement;
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
isPending?: boolean;
|
|
8
8
|
isReadOnly?: boolean;
|
|
9
9
|
trigger?: 'focus' | 'input' | 'press';
|
|
10
|
+
disabledIds?: string[];
|
|
10
11
|
};
|
|
11
12
|
|
|
12
13
|
let {
|
|
@@ -14,7 +15,8 @@
|
|
|
14
15
|
isDisabled = false,
|
|
15
16
|
isPending = false,
|
|
16
17
|
isReadOnly = false,
|
|
17
|
-
trigger = 'press'
|
|
18
|
+
trigger = 'press',
|
|
19
|
+
disabledIds = []
|
|
18
20
|
}: Props = $props();
|
|
19
21
|
|
|
20
22
|
let selectedValue = $state<string | number | undefined>();
|
|
@@ -40,7 +42,11 @@
|
|
|
40
42
|
<ComboBox.Popover>
|
|
41
43
|
<ComboBox.List emptyPlaceholder="No countries found">
|
|
42
44
|
{#each countries as country (country.id)}
|
|
43
|
-
<ComboBox.Item
|
|
45
|
+
<ComboBox.Item
|
|
46
|
+
id={country.id}
|
|
47
|
+
textValue={country.name}
|
|
48
|
+
disabled={disabledIds.includes(country.id)}
|
|
49
|
+
>
|
|
44
50
|
{country.name}
|
|
45
51
|
</ComboBox.Item>
|
|
46
52
|
{/each}
|
|
@@ -211,17 +211,26 @@
|
|
|
211
211
|
}
|
|
212
212
|
}
|
|
213
213
|
|
|
214
|
+
function syncInputValue(val: string, options?: { notifyInputChange?: boolean }) {
|
|
215
|
+
inputValueInternal = val;
|
|
216
|
+
inputValue = val;
|
|
217
|
+
|
|
218
|
+
if (options?.notifyInputChange ?? true) {
|
|
219
|
+
onInputChange?.(val);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
214
223
|
function selectItem(id: string | number, label: string) {
|
|
215
224
|
let newSelection: Set<string | number>;
|
|
216
225
|
|
|
217
226
|
if (selectionMode === 'single') {
|
|
218
227
|
newSelection = new Set([id]);
|
|
228
|
+
shouldFilter = false;
|
|
219
229
|
// Save the label persistently for restore on blur/escape
|
|
220
230
|
selectedLabel = label;
|
|
221
|
-
//
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
onInputChange?.(label);
|
|
231
|
+
// Keep the selected label visible in the input without re-triggering
|
|
232
|
+
// external filtering during the popover close animation.
|
|
233
|
+
syncInputValue(label, { notifyInputChange: false });
|
|
225
234
|
if (effectiveCloseOnSelect) {
|
|
226
235
|
closePopover(true); // Close and keep focus on input
|
|
227
236
|
}
|
|
@@ -318,6 +327,8 @@
|
|
|
318
327
|
onInputChange?.('');
|
|
319
328
|
}
|
|
320
329
|
// Otherwise user typed, keep their filter
|
|
330
|
+
} else {
|
|
331
|
+
shouldFilter = true;
|
|
321
332
|
}
|
|
322
333
|
setIsOpen(true);
|
|
323
334
|
// Auto-focus the selected item when opening with a selection
|
|
@@ -333,8 +344,6 @@
|
|
|
333
344
|
setIsOpen(false);
|
|
334
345
|
// Reset navigation state
|
|
335
346
|
navigation.reset();
|
|
336
|
-
// Re-enable filtering for next open
|
|
337
|
-
shouldFilter = true;
|
|
338
347
|
// Only refocus input when explicitly requested (e.g., after selection)
|
|
339
348
|
// Never refocus in focus mode to prevent re-opening
|
|
340
349
|
if (refocusInput && trigger !== 'focus') {
|
|
@@ -442,9 +451,7 @@
|
|
|
442
451
|
if (currentSelection.size > 0 && selectedLabel) {
|
|
443
452
|
if (selectedLabel !== currentInputValue) {
|
|
444
453
|
// Restore the selected label
|
|
445
|
-
|
|
446
|
-
inputValue = selectedLabel;
|
|
447
|
-
onInputChange?.(selectedLabel);
|
|
454
|
+
syncInputValue(selectedLabel, { notifyInputChange: false });
|
|
448
455
|
}
|
|
449
456
|
}
|
|
450
457
|
}
|
|
@@ -145,7 +145,7 @@
|
|
|
145
145
|
|
|
146
146
|
// Scroll into view when focused (if enabled)
|
|
147
147
|
$effect(() => {
|
|
148
|
-
if (scrollOnFocus && isFocusedComputed && elementRef) {
|
|
148
|
+
if (scrollOnFocus && isFocusedComputed && isFocusVisibleComputed && elementRef) {
|
|
149
149
|
requestAnimationFrame(() => {
|
|
150
150
|
elementRef?.scrollIntoView({ block: 'nearest' });
|
|
151
151
|
});
|
|
@@ -159,6 +159,19 @@
|
|
|
159
159
|
isFocusVisible = false;
|
|
160
160
|
});
|
|
161
161
|
|
|
162
|
+
$effect(() => {
|
|
163
|
+
if (!isFocusedComputed) {
|
|
164
|
+
if (pressedKey !== null) {
|
|
165
|
+
clearPressedState();
|
|
166
|
+
}
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (listFocusVisible || isFocusVisibleComputed) {
|
|
171
|
+
isHovered = false;
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
|
|
162
175
|
function clearPressedState() {
|
|
163
176
|
isPressed = false;
|
|
164
177
|
pressedKey = null;
|
|
@@ -193,6 +206,15 @@
|
|
|
193
206
|
|
|
194
207
|
const label = getResolvedTextValue();
|
|
195
208
|
|
|
209
|
+
if (!disableFocusHandling && elementRef) {
|
|
210
|
+
suppressNextFocusVisible = true;
|
|
211
|
+
isFocusVisible = false;
|
|
212
|
+
listboxCtx.setFocusVisible(false);
|
|
213
|
+
listboxCtx.setFocusedId(id);
|
|
214
|
+
listboxCtx.keyboardNav.setCurrentId(id);
|
|
215
|
+
focusWithModality(elementRef, 'pointer');
|
|
216
|
+
}
|
|
217
|
+
|
|
196
218
|
// Use custom select handler if provided, otherwise use listbox default
|
|
197
219
|
if (onItemSelect) {
|
|
198
220
|
onItemSelect(id, label);
|
|
@@ -201,7 +223,7 @@
|
|
|
201
223
|
}
|
|
202
224
|
|
|
203
225
|
if (!disableFocusHandling) {
|
|
204
|
-
listboxCtx.keyboardNav.
|
|
226
|
+
listboxCtx.keyboardNav.setCurrentId(id);
|
|
205
227
|
}
|
|
206
228
|
}
|
|
207
229
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts" generics="T extends object = object">
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
|
+
import { onMount } from 'svelte';
|
|
3
4
|
import { createListBoxContext, type ListBoxContext } from './context';
|
|
4
5
|
import { trackInteractionModality } from '../../primitives/input-modality';
|
|
5
6
|
|
|
@@ -32,6 +33,8 @@
|
|
|
32
33
|
'aria-label'?: string;
|
|
33
34
|
/** Callback fired when the selection changes. */
|
|
34
35
|
onChange?: (value: Set<string | number>) => void;
|
|
36
|
+
/** Disable DOM focus handling on the root container for virtual-focus compositions. */
|
|
37
|
+
disableFocusHandling?: boolean;
|
|
35
38
|
};
|
|
36
39
|
|
|
37
40
|
let {
|
|
@@ -47,6 +50,7 @@
|
|
|
47
50
|
id,
|
|
48
51
|
'aria-label': ariaLabel,
|
|
49
52
|
onChange,
|
|
53
|
+
disableFocusHandling = false,
|
|
50
54
|
context = $bindable(),
|
|
51
55
|
element = $bindable()
|
|
52
56
|
}: ListBoxProps & { context?: ListBoxContext; element?: HTMLElement } = $props();
|
|
@@ -115,9 +119,14 @@
|
|
|
115
119
|
|
|
116
120
|
const itemsArray = $derived(items ? Array.from(items) : []);
|
|
117
121
|
const hasItems = $derived(itemsArray.length > 0 || itemCount > 0);
|
|
122
|
+
let hasMounted = $state(false);
|
|
118
123
|
|
|
119
124
|
let focusWithin = $state(false);
|
|
120
125
|
|
|
126
|
+
onMount(() => {
|
|
127
|
+
hasMounted = true;
|
|
128
|
+
});
|
|
129
|
+
|
|
121
130
|
function syncFocusWithin() {
|
|
122
131
|
focusWithin =
|
|
123
132
|
!!listboxElement &&
|
|
@@ -136,6 +145,9 @@
|
|
|
136
145
|
function handleMouseDown(event: MouseEvent) {
|
|
137
146
|
trackInteractionModality(event, event.target as HTMLElement | null);
|
|
138
147
|
ctx.setFocusVisible(false);
|
|
148
|
+
if (disableFocusHandling) {
|
|
149
|
+
event.preventDefault();
|
|
150
|
+
}
|
|
139
151
|
}
|
|
140
152
|
|
|
141
153
|
function handleKeyDown(event: KeyboardEvent) {
|
|
@@ -153,7 +165,7 @@
|
|
|
153
165
|
aria-multiselectable={selectionMode === 'multiple'}
|
|
154
166
|
aria-label={ariaLabel}
|
|
155
167
|
class={className}
|
|
156
|
-
tabindex=
|
|
168
|
+
tabindex={disableFocusHandling ? undefined : 0}
|
|
157
169
|
data-focus-within={focusWithin || undefined}
|
|
158
170
|
use:keyboardAction
|
|
159
171
|
onfocusin={handleFocusIn}
|
|
@@ -169,7 +181,7 @@
|
|
|
169
181
|
{@render (children as Snippet)()}
|
|
170
182
|
{/if}
|
|
171
183
|
|
|
172
|
-
{#if !hasItems && itemCount === 0}
|
|
184
|
+
{#if hasMounted && !hasItems && itemCount === 0}
|
|
173
185
|
{#if typeof emptyPlaceholder === 'string'}
|
|
174
186
|
<div role="option" aria-selected="false" aria-disabled="true" data-empty-placeholder>
|
|
175
187
|
{emptyPlaceholder}
|
|
@@ -26,6 +26,8 @@ declare function $$render<T extends object = object>(): {
|
|
|
26
26
|
'aria-label'?: string;
|
|
27
27
|
/** Callback fired when the selection changes. */
|
|
28
28
|
onChange?: (value: Set<string | number>) => void;
|
|
29
|
+
/** Disable DOM focus handling on the root container for virtual-focus compositions. */
|
|
30
|
+
disableFocusHandling?: boolean;
|
|
29
31
|
} & {
|
|
30
32
|
context?: ListBoxContext;
|
|
31
33
|
element?: HTMLElement;
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
## Open questions
|
|
4
4
|
|
|
5
|
-
- Disabled body rows are currently rendered and
|
|
5
|
+
- Disabled body rows are currently rendered and treated with an all-or-nothing disabled model. The planned `disabledBehavior` API (`'selection' | 'all'`) will require splitting focus/action disabling from selection disabling.
|
|
6
6
|
- `Table.Column` is implemented as a logical wrapper and currently assumes the intended child is a single `Table.ColumnHeaderCell`.
|
|
7
7
|
- `Table.Footer` renders semantic table cells but is intentionally excluded from the roving-focus model in v1.
|
|
8
8
|
- Interactive controls nested inside `Table.Cell` are still intentionally out of scope for v1.
|
|
9
|
+
- `pressRow()` is currently selection-oriented. The planned `onRowAction` feature will require a clearer interaction pipeline so pointer click, double click, `Enter`, and `Space` can route to action and selection independently.
|