@human-kit/svelte-components 1.0.0-alpha.6 → 1.0.0-alpha.7
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/button/README.md +48 -0
- package/dist/button/TODO.md +13 -0
- package/dist/button/index.d.ts +5 -0
- package/dist/button/index.js +4 -0
- package/dist/button/index.parts.d.ts +1 -0
- package/dist/button/index.parts.js +1 -0
- package/dist/button/root/README.md +43 -0
- package/dist/button/root/button-root.svelte +362 -0
- package/dist/button/root/button-root.svelte.d.ts +21 -0
- package/dist/button/root/button-test.svelte +76 -0
- package/dist/button/root/button-test.svelte.d.ts +11 -0
- package/dist/calendar/trigger-next/calendar-trigger-next.svelte +9 -4
- package/dist/calendar/trigger-next/calendar-trigger-next.svelte.d.ts +2 -1
- package/dist/calendar/trigger-previous/calendar-trigger-previous.svelte +9 -4
- package/dist/calendar/trigger-previous/calendar-trigger-previous.svelte.d.ts +2 -1
- package/dist/combobox/button/combobox-button.svelte +5 -4
- package/dist/combobox/list/combobox-listbox.svelte.d.ts +1 -1
- package/dist/combobox/popover/combobox-popover.svelte +34 -4
- package/dist/combobox/root/combobox.svelte +8 -0
- package/dist/combobox/tag-remove/combobox-tag-remove.svelte +3 -2
- package/dist/datepicker/trigger/date-picker-trigger.svelte +5 -5
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/listbox/item/README.md +2 -1
- package/dist/listbox/item/listbox-item.svelte +129 -1
- package/dist/listbox/item/listbox-item.svelte.d.ts +2 -0
- package/dist/listbox/root/listbox-test.svelte +14 -2
- package/dist/listbox/root/listbox-test.svelte.d.ts +1 -0
- package/dist/listbox/root/listbox.svelte.d.ts +2 -2
- package/dist/popover/trigger/popover-trigger-button.svelte +4 -3
- package/dist/table/root/table-root.svelte.d.ts +1 -1
- package/dist/timepicker/trigger/time-picker-trigger.svelte +5 -5
- package/package.json +6 -1
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Button
|
|
2
|
+
|
|
3
|
+
## Description
|
|
4
|
+
|
|
5
|
+
`Button` is a headless native button with RAC-aligned pending semantics, pressed-state exposure, and modality-aware focus data attributes.
|
|
6
|
+
|
|
7
|
+
## Anatomy
|
|
8
|
+
|
|
9
|
+
- `Button.Root`
|
|
10
|
+
|
|
11
|
+
```svelte
|
|
12
|
+
<Button.Root>
|
|
13
|
+
{#snippet children({ isPending, isPressed })}
|
|
14
|
+
{#if isPending}
|
|
15
|
+
<SavingSpinner />
|
|
16
|
+
{:else}
|
|
17
|
+
<span class:is-pressed={isPressed}>Save</span>
|
|
18
|
+
{/if}
|
|
19
|
+
{/snippet}
|
|
20
|
+
</Button.Root>
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Usage guidelines
|
|
24
|
+
|
|
25
|
+
- Use native button props such as `type`, `name`, `value`, and form attributes directly on `Button.Root`.
|
|
26
|
+
- Use `isPending` to keep the button focusable while blocking activation and hover state.
|
|
27
|
+
- Style interaction states with `data-hovered`, `data-pressed`, `data-focused`, `data-focus-visible`, `data-disabled`, and `data-pending`.
|
|
28
|
+
|
|
29
|
+
## API reference
|
|
30
|
+
|
|
31
|
+
`Button.Root` supports:
|
|
32
|
+
|
|
33
|
+
- `isPending?: boolean`
|
|
34
|
+
- `isDisabled?: boolean`
|
|
35
|
+
- `children?: Snippet<[ButtonRenderState]> | Snippet`
|
|
36
|
+
- `type?: 'button' | 'submit' | 'reset'`
|
|
37
|
+
- `...restProps: HTMLButtonAttributes`
|
|
38
|
+
|
|
39
|
+
## Accessibility
|
|
40
|
+
|
|
41
|
+
- `Button.Root` renders a native `<button>`.
|
|
42
|
+
- `isPending` applies `aria-disabled="true"`, preserves focusability, blocks press behavior, and announces the pending state through an internal polite live region.
|
|
43
|
+
- `data-focus-visible` follows the shared modality contract and is only exposed for keyboard or virtual focus.
|
|
44
|
+
|
|
45
|
+
## Notes
|
|
46
|
+
|
|
47
|
+
- When `type="submit"` and `isPending` is true, the rendered button type switches to `button` to prevent implicit and explicit form submission.
|
|
48
|
+
- Pending does not serialize `data-disabled`; it is represented by `data-pending`.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Button TODO
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
|
|
5
|
+
Track Button work with a single mandatory TODO format.
|
|
6
|
+
|
|
7
|
+
## Backlog
|
|
8
|
+
|
|
9
|
+
- [x] [S][P0][Area: Architecture][Owner: Unassigned][Target: Done] Create base `root` part with namespace exports.
|
|
10
|
+
- [x] [S][P0][Area: Interaction][Owner: Unassigned][Target: Done] Implement native press, hover, focus, and focus-visible state exposure.
|
|
11
|
+
- [x] [S][P0][Area: Accessibility][Owner: Unassigned][Target: Done] Match RAC-style pending semantics with `aria-disabled`, live announcement, and submit suppression.
|
|
12
|
+
- [x] [S][P0][Area: Testing][Owner: Unassigned][Target: Done] Add baseline tests for press, pending, disabled, focus, and form behavior.
|
|
13
|
+
- [ ] [S][P1][Area: API][Owner: Unassigned][Target: TBD] Evaluate whether to expose a dedicated `pendingLabel` override if consumers need localized announcement text without custom child content.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as Root } from './root/button-root.svelte';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as Root } from './root/button-root.svelte';
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Button Root
|
|
2
|
+
|
|
3
|
+
## API reference
|
|
4
|
+
|
|
5
|
+
### Button.Root
|
|
6
|
+
|
|
7
|
+
Name: `Button.Root`
|
|
8
|
+
Description: Native button root with pressed, hovered, focused, focus-visible, disabled, and pending render state.
|
|
9
|
+
|
|
10
|
+
| Prop | Type | Default | Description |
|
|
11
|
+
| -------------- | ----------------------------------------- | ------------- | ---------------------------------------------------------------------------------------------------- |
|
|
12
|
+
| `id` | `string` | `$props.id()` | Stable id for the native button and pending announcement labelling. |
|
|
13
|
+
| `type` | `'button' \| 'submit' \| 'reset'` | `'button'` | Native button type. While pending, `submit` is rendered as `button`. |
|
|
14
|
+
| `isPending` | `boolean` | `false` | Keeps the button focusable while disabling press and hover behavior. |
|
|
15
|
+
| `isDisabled` | `boolean` | `false` | Disables the native button. |
|
|
16
|
+
| `children` | `Snippet<[ButtonRenderState]> \| Snippet` | `undefined` | Optional renderer receiving the current interaction state. |
|
|
17
|
+
| `class` | `string` | `''` | CSS class names applied to the button element. |
|
|
18
|
+
| `...restProps` | `HTMLButtonAttributes` | `-` | Additional native button attributes forwarded to the element, excluding reserved disabled semantics. |
|
|
19
|
+
|
|
20
|
+
### Types
|
|
21
|
+
|
|
22
|
+
Name: `ButtonRenderState`
|
|
23
|
+
Description: Render-state payload available to the `children` snippet.
|
|
24
|
+
|
|
25
|
+
| Prop | Type | Default | Description |
|
|
26
|
+
| ---------------- | --------- | ------- | --------------------------------------------------------------------- |
|
|
27
|
+
| `isHovered` | `boolean` | `false` | Whether the button is currently hovered by a mouse. |
|
|
28
|
+
| `isPressed` | `boolean` | `false` | Whether the button is currently being pressed. Cleared while pending. |
|
|
29
|
+
| `isFocused` | `boolean` | `false` | Whether the button currently holds DOM focus. |
|
|
30
|
+
| `isFocusVisible` | `boolean` | `false` | Whether focus is visibly keyboard or virtual driven. |
|
|
31
|
+
| `isDisabled` | `boolean` | `false` | Whether the button is disabled. |
|
|
32
|
+
| `isPending` | `boolean` | `false` | Whether the button is pending. |
|
|
33
|
+
|
|
34
|
+
```svelte
|
|
35
|
+
<Button.Root type="submit" isPending={saving} aria-label="Save changes">
|
|
36
|
+
{#snippet children({ isPending, isPressed })}
|
|
37
|
+
<span class:opacity-0={isPending}>Save</span>
|
|
38
|
+
{#if isPending}
|
|
39
|
+
<ProgressCircle aria-label="Saving" isIndeterminate />
|
|
40
|
+
{/if}
|
|
41
|
+
{/snippet}
|
|
42
|
+
</Button.Root>
|
|
43
|
+
```
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { untrack, type Snippet } from 'svelte';
|
|
3
|
+
import type { HTMLButtonAttributes } from 'svelte/elements';
|
|
4
|
+
import {
|
|
5
|
+
shouldShowFocusVisible,
|
|
6
|
+
trackInteractionModality
|
|
7
|
+
} from '../../primitives/input-modality';
|
|
8
|
+
import { cn } from '../../utils/cn';
|
|
9
|
+
|
|
10
|
+
export type ButtonRenderState = {
|
|
11
|
+
isHovered: boolean;
|
|
12
|
+
isPressed: boolean;
|
|
13
|
+
isFocused: boolean;
|
|
14
|
+
isFocusVisible: boolean;
|
|
15
|
+
isDisabled: boolean;
|
|
16
|
+
isPending: boolean;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
type ButtonRootProps = Omit<
|
|
20
|
+
HTMLButtonAttributes,
|
|
21
|
+
'children' | 'class' | 'disabled' | 'aria-disabled'
|
|
22
|
+
> & {
|
|
23
|
+
children?: Snippet<[ButtonRenderState]> | Snippet;
|
|
24
|
+
class?: string;
|
|
25
|
+
isPending?: boolean;
|
|
26
|
+
isDisabled?: boolean;
|
|
27
|
+
element?: HTMLButtonElement | null;
|
|
28
|
+
pressed?: boolean;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
function composeEventHandlers<TEvent extends Event>(
|
|
32
|
+
internalHandler: ((event: TEvent) => void) | undefined,
|
|
33
|
+
externalHandler: ((event: TEvent) => void) | undefined,
|
|
34
|
+
options?: { skipExternalOnDefaultPrevented?: boolean }
|
|
35
|
+
): (event: TEvent) => void {
|
|
36
|
+
return (event: TEvent) => {
|
|
37
|
+
let preventDefaultCalled = false;
|
|
38
|
+
const originalPreventDefault = event.preventDefault.bind(event);
|
|
39
|
+
event.preventDefault = () => {
|
|
40
|
+
preventDefaultCalled = true;
|
|
41
|
+
originalPreventDefault();
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
internalHandler?.(event);
|
|
45
|
+
event.preventDefault = originalPreventDefault;
|
|
46
|
+
|
|
47
|
+
if (
|
|
48
|
+
options?.skipExternalOnDefaultPrevented &&
|
|
49
|
+
(event.defaultPrevented || preventDefaultCalled)
|
|
50
|
+
) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
externalHandler?.(event);
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const generatedId = $props.id();
|
|
59
|
+
|
|
60
|
+
let {
|
|
61
|
+
id,
|
|
62
|
+
type = 'button',
|
|
63
|
+
children,
|
|
64
|
+
class: className = '',
|
|
65
|
+
isPending = false,
|
|
66
|
+
isDisabled = false,
|
|
67
|
+
element = $bindable<HTMLButtonElement | null>(null),
|
|
68
|
+
pressed: pressedOverride,
|
|
69
|
+
onclick: onClickExternal,
|
|
70
|
+
onfocus: onFocusExternal,
|
|
71
|
+
onblur: onBlurExternal,
|
|
72
|
+
onkeydown: onKeyDownExternal,
|
|
73
|
+
onkeyup: onKeyUpExternal,
|
|
74
|
+
onpointerdown: onPointerDownExternal,
|
|
75
|
+
onpointerup: onPointerUpExternal,
|
|
76
|
+
onpointercancel: onPointerCancelExternal,
|
|
77
|
+
onpointerenter: onPointerEnterExternal,
|
|
78
|
+
onpointerleave: onPointerLeaveExternal,
|
|
79
|
+
onmousedown: onMouseDownExternal,
|
|
80
|
+
onmouseup: onMouseUpExternal,
|
|
81
|
+
onmouseenter: onMouseEnterExternal,
|
|
82
|
+
onmouseleave: onMouseLeaveExternal,
|
|
83
|
+
'aria-label': ariaLabel,
|
|
84
|
+
'aria-labelledby': ariaLabelledby,
|
|
85
|
+
...restProps
|
|
86
|
+
}: ButtonRootProps = $props();
|
|
87
|
+
|
|
88
|
+
const resolvedId = untrack(() => id) ?? generatedId;
|
|
89
|
+
|
|
90
|
+
let buttonRef: HTMLButtonElement | null = $state(null);
|
|
91
|
+
let hovered = $state(false);
|
|
92
|
+
let pressed = $state(false);
|
|
93
|
+
let focused = $state(false);
|
|
94
|
+
let focusVisible = $state(false);
|
|
95
|
+
let pressedKey: 'Enter' | 'Space' | null = $state(null);
|
|
96
|
+
|
|
97
|
+
const renderedType = $derived(type === 'submit' && isPending ? 'button' : type);
|
|
98
|
+
const renderedPressed = $derived(
|
|
99
|
+
pressedOverride !== undefined ? Boolean(pressedOverride) && !isPending : pressed && !isPending
|
|
100
|
+
);
|
|
101
|
+
const renderState = $derived.by<ButtonRenderState>(() => ({
|
|
102
|
+
isHovered: hovered,
|
|
103
|
+
isPressed: renderedPressed,
|
|
104
|
+
isFocused: focused,
|
|
105
|
+
isFocusVisible: focusVisible,
|
|
106
|
+
isDisabled,
|
|
107
|
+
isPending
|
|
108
|
+
}));
|
|
109
|
+
const pendingAnnouncement = $derived.by(() =>
|
|
110
|
+
isPending ? `${resolveButtonLabel() || 'Button'}, pending` : ''
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
function resolveReferencedLabel(ids: string | null | undefined): string {
|
|
114
|
+
if (!ids || !buttonRef) return '';
|
|
115
|
+
const ownerDocument = buttonRef.ownerDocument;
|
|
116
|
+
return ids
|
|
117
|
+
.split(/\s+/)
|
|
118
|
+
.map((token) => ownerDocument.getElementById(token)?.textContent?.trim() ?? '')
|
|
119
|
+
.filter(Boolean)
|
|
120
|
+
.join(' ')
|
|
121
|
+
.trim();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function resolveButtonLabel(): string {
|
|
125
|
+
const directLabel = ariaLabel?.trim();
|
|
126
|
+
if (directLabel) return directLabel;
|
|
127
|
+
|
|
128
|
+
const labelledByLabel = resolveReferencedLabel(ariaLabelledby);
|
|
129
|
+
if (labelledByLabel) return labelledByLabel;
|
|
130
|
+
|
|
131
|
+
if (!buttonRef) return '';
|
|
132
|
+
|
|
133
|
+
const liveRegion = buttonRef.querySelector('[data-button-live-region="true"]');
|
|
134
|
+
return Array.from(buttonRef.childNodes)
|
|
135
|
+
.filter((node) => node !== liveRegion)
|
|
136
|
+
.map((node) => node.textContent?.trim() ?? '')
|
|
137
|
+
.filter(Boolean)
|
|
138
|
+
.join(' ')
|
|
139
|
+
.trim();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function clearInteractionState() {
|
|
143
|
+
hovered = false;
|
|
144
|
+
pressed = false;
|
|
145
|
+
pressedKey = null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
$effect(() => {
|
|
149
|
+
element = buttonRef;
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
$effect(() => {
|
|
153
|
+
if (!isPending && !isDisabled) return;
|
|
154
|
+
clearInteractionState();
|
|
155
|
+
if (isDisabled) {
|
|
156
|
+
focusVisible = false;
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
function handlePointerDown(event: PointerEvent) {
|
|
161
|
+
trackInteractionModality(event, buttonRef);
|
|
162
|
+
focusVisible = false;
|
|
163
|
+
|
|
164
|
+
if (isDisabled || isPending) {
|
|
165
|
+
event.preventDefault();
|
|
166
|
+
clearInteractionState();
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (event.button !== 0) return;
|
|
171
|
+
pressed = true;
|
|
172
|
+
pressedKey = null;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function handlePointerUp(event: PointerEvent) {
|
|
176
|
+
if (event.button !== 0) return;
|
|
177
|
+
pressed = false;
|
|
178
|
+
pressedKey = null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function handlePointerCancel() {
|
|
182
|
+
pressed = false;
|
|
183
|
+
pressedKey = null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function handlePointerEnter(event: PointerEvent) {
|
|
187
|
+
if (isDisabled || isPending) return;
|
|
188
|
+
|
|
189
|
+
if ((event.buttons & 1) === 1 && pressedKey === null) {
|
|
190
|
+
pressed = true;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function handlePointerLeave() {
|
|
195
|
+
if (pressedKey === null) {
|
|
196
|
+
pressed = false;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function handleMouseDown(event: MouseEvent) {
|
|
201
|
+
trackInteractionModality(event, buttonRef);
|
|
202
|
+
focusVisible = false;
|
|
203
|
+
|
|
204
|
+
if (isDisabled || isPending) {
|
|
205
|
+
event.preventDefault();
|
|
206
|
+
clearInteractionState();
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (event.button !== 0) return;
|
|
211
|
+
pressed = true;
|
|
212
|
+
pressedKey = null;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function handleMouseUp(event: MouseEvent) {
|
|
216
|
+
if (event.button !== 0) return;
|
|
217
|
+
if (pressedKey === null) {
|
|
218
|
+
pressed = false;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function handleMouseEnter() {
|
|
223
|
+
if (isDisabled || isPending) {
|
|
224
|
+
hovered = false;
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
hovered = true;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function handleMouseLeave() {
|
|
232
|
+
hovered = false;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function handleFocus() {
|
|
236
|
+
focused = true;
|
|
237
|
+
focusVisible = shouldShowFocusVisible(buttonRef);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function handleBlur() {
|
|
241
|
+
focused = false;
|
|
242
|
+
focusVisible = false;
|
|
243
|
+
clearInteractionState();
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function handleKeyDown(event: KeyboardEvent) {
|
|
247
|
+
const key =
|
|
248
|
+
event.key === 'Enter'
|
|
249
|
+
? 'Enter'
|
|
250
|
+
: event.key === ' ' || event.key === 'Spacebar'
|
|
251
|
+
? 'Space'
|
|
252
|
+
: null;
|
|
253
|
+
|
|
254
|
+
if (!key) return;
|
|
255
|
+
|
|
256
|
+
trackInteractionModality(event, buttonRef);
|
|
257
|
+
focusVisible = focused ? true : shouldShowFocusVisible(buttonRef);
|
|
258
|
+
|
|
259
|
+
if (isDisabled || isPending) {
|
|
260
|
+
event.preventDefault();
|
|
261
|
+
clearInteractionState();
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (event.repeat && pressed && pressedKey === key) return;
|
|
266
|
+
|
|
267
|
+
pressed = true;
|
|
268
|
+
pressedKey = key;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function handleKeyUp(event: KeyboardEvent) {
|
|
272
|
+
const key =
|
|
273
|
+
event.key === 'Enter'
|
|
274
|
+
? 'Enter'
|
|
275
|
+
: event.key === ' ' || event.key === 'Spacebar'
|
|
276
|
+
? 'Space'
|
|
277
|
+
: null;
|
|
278
|
+
|
|
279
|
+
if (!key) return;
|
|
280
|
+
|
|
281
|
+
trackInteractionModality(event, buttonRef);
|
|
282
|
+
focusVisible = focused ? true : shouldShowFocusVisible(buttonRef);
|
|
283
|
+
|
|
284
|
+
if (isDisabled || isPending) {
|
|
285
|
+
event.preventDefault();
|
|
286
|
+
clearInteractionState();
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (pressedKey === key) {
|
|
291
|
+
pressed = false;
|
|
292
|
+
pressedKey = null;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function handleClick(event: MouseEvent) {
|
|
297
|
+
if (event.detail > 0) {
|
|
298
|
+
trackInteractionModality(event, buttonRef);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (isPending) {
|
|
302
|
+
event.preventDefault();
|
|
303
|
+
clearInteractionState();
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
</script>
|
|
307
|
+
|
|
308
|
+
<button
|
|
309
|
+
{...restProps}
|
|
310
|
+
bind:this={buttonRef}
|
|
311
|
+
id={resolvedId}
|
|
312
|
+
type={renderedType}
|
|
313
|
+
disabled={isDisabled}
|
|
314
|
+
aria-label={ariaLabel}
|
|
315
|
+
aria-labelledby={ariaLabelledby}
|
|
316
|
+
aria-disabled={isPending ? 'true' : undefined}
|
|
317
|
+
data-button-root="true"
|
|
318
|
+
data-disabled={isDisabled || undefined}
|
|
319
|
+
data-pending={isPending || undefined}
|
|
320
|
+
data-hovered={hovered || undefined}
|
|
321
|
+
data-pressed={renderedPressed || undefined}
|
|
322
|
+
data-focused={focused || undefined}
|
|
323
|
+
data-focus-visible={focusVisible || undefined}
|
|
324
|
+
class={cn('outline-none', className)}
|
|
325
|
+
onclick={composeEventHandlers(handleClick, onClickExternal ?? undefined, {
|
|
326
|
+
skipExternalOnDefaultPrevented: true
|
|
327
|
+
})}
|
|
328
|
+
onfocus={composeEventHandlers(handleFocus, onFocusExternal ?? undefined)}
|
|
329
|
+
onblur={composeEventHandlers(handleBlur, onBlurExternal ?? undefined)}
|
|
330
|
+
onkeydown={composeEventHandlers(handleKeyDown, onKeyDownExternal ?? undefined, {
|
|
331
|
+
skipExternalOnDefaultPrevented: true
|
|
332
|
+
})}
|
|
333
|
+
onkeyup={composeEventHandlers(handleKeyUp, onKeyUpExternal ?? undefined, {
|
|
334
|
+
skipExternalOnDefaultPrevented: true
|
|
335
|
+
})}
|
|
336
|
+
onpointerdown={composeEventHandlers(handlePointerDown, onPointerDownExternal ?? undefined, {
|
|
337
|
+
skipExternalOnDefaultPrevented: true
|
|
338
|
+
})}
|
|
339
|
+
onpointerup={composeEventHandlers(handlePointerUp, onPointerUpExternal ?? undefined)}
|
|
340
|
+
onpointercancel={composeEventHandlers(handlePointerCancel, onPointerCancelExternal ?? undefined)}
|
|
341
|
+
onpointerenter={composeEventHandlers(handlePointerEnter, onPointerEnterExternal ?? undefined)}
|
|
342
|
+
onpointerleave={composeEventHandlers(handlePointerLeave, onPointerLeaveExternal ?? undefined)}
|
|
343
|
+
onmousedown={composeEventHandlers(handleMouseDown, onMouseDownExternal ?? undefined, {
|
|
344
|
+
skipExternalOnDefaultPrevented: true
|
|
345
|
+
})}
|
|
346
|
+
onmouseup={composeEventHandlers(handleMouseUp, onMouseUpExternal ?? undefined)}
|
|
347
|
+
onmouseenter={composeEventHandlers(handleMouseEnter, onMouseEnterExternal ?? undefined)}
|
|
348
|
+
onmouseleave={composeEventHandlers(handleMouseLeave, onMouseLeaveExternal ?? undefined)}
|
|
349
|
+
>
|
|
350
|
+
{#if children}
|
|
351
|
+
{@render (children as Snippet<[ButtonRenderState]>)(renderState)}
|
|
352
|
+
{/if}
|
|
353
|
+
|
|
354
|
+
<span
|
|
355
|
+
data-button-live-region="true"
|
|
356
|
+
role="status"
|
|
357
|
+
aria-live="polite"
|
|
358
|
+
aria-atomic="true"
|
|
359
|
+
style="position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0;"
|
|
360
|
+
>{pendingAnnouncement}</span
|
|
361
|
+
>
|
|
362
|
+
</button>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type Snippet } from 'svelte';
|
|
2
|
+
import type { HTMLButtonAttributes } from 'svelte/elements';
|
|
3
|
+
export type ButtonRenderState = {
|
|
4
|
+
isHovered: boolean;
|
|
5
|
+
isPressed: boolean;
|
|
6
|
+
isFocused: boolean;
|
|
7
|
+
isFocusVisible: boolean;
|
|
8
|
+
isDisabled: boolean;
|
|
9
|
+
isPending: boolean;
|
|
10
|
+
};
|
|
11
|
+
type ButtonRootProps = Omit<HTMLButtonAttributes, 'children' | 'class' | 'disabled' | 'aria-disabled'> & {
|
|
12
|
+
children?: Snippet<[ButtonRenderState]> | Snippet;
|
|
13
|
+
class?: string;
|
|
14
|
+
isPending?: boolean;
|
|
15
|
+
isDisabled?: boolean;
|
|
16
|
+
element?: HTMLButtonElement | null;
|
|
17
|
+
pressed?: boolean;
|
|
18
|
+
};
|
|
19
|
+
declare const ButtonRoot: import("svelte").Component<ButtonRootProps, {}, "element">;
|
|
20
|
+
type ButtonRoot = ReturnType<typeof ButtonRoot>;
|
|
21
|
+
export default ButtonRoot;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Button } from '../index';
|
|
3
|
+
|
|
4
|
+
type Props = {
|
|
5
|
+
isDisabled?: boolean;
|
|
6
|
+
isPending?: boolean;
|
|
7
|
+
type?: 'button' | 'submit' | 'reset';
|
|
8
|
+
ariaLabel?: string;
|
|
9
|
+
onMouseEnter?: (event: MouseEvent) => void;
|
|
10
|
+
onFocus?: (event: FocusEvent) => void;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
let {
|
|
14
|
+
isDisabled = false,
|
|
15
|
+
isPending = false,
|
|
16
|
+
type = 'button',
|
|
17
|
+
ariaLabel = 'Save',
|
|
18
|
+
onMouseEnter,
|
|
19
|
+
onFocus
|
|
20
|
+
}: Props = $props();
|
|
21
|
+
|
|
22
|
+
let pendingOverride = $state<boolean | null>(null);
|
|
23
|
+
let clickCount = $state(0);
|
|
24
|
+
let submitCount = $state(0);
|
|
25
|
+
const pending = $derived(pendingOverride ?? isPending);
|
|
26
|
+
|
|
27
|
+
function handleClick() {
|
|
28
|
+
clickCount += 1;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function handleSubmit(event: SubmitEvent) {
|
|
32
|
+
event.preventDefault();
|
|
33
|
+
submitCount += 1;
|
|
34
|
+
}
|
|
35
|
+
</script>
|
|
36
|
+
|
|
37
|
+
<form onsubmit={handleSubmit}>
|
|
38
|
+
<label for="name">Name</label>
|
|
39
|
+
<input id="name" type="text" />
|
|
40
|
+
|
|
41
|
+
<Button.Root
|
|
42
|
+
{type}
|
|
43
|
+
{isDisabled}
|
|
44
|
+
isPending={pending}
|
|
45
|
+
onclick={handleClick}
|
|
46
|
+
onmouseenter={onMouseEnter}
|
|
47
|
+
onfocus={onFocus}
|
|
48
|
+
aria-label={ariaLabel}
|
|
49
|
+
class="inline-flex items-center"
|
|
50
|
+
>
|
|
51
|
+
{#snippet children(state)}
|
|
52
|
+
<span data-button-label>{pending ? 'Saving' : 'Save'}</span>
|
|
53
|
+
<span
|
|
54
|
+
data-render-state="true"
|
|
55
|
+
data-render-pressed={state.isPressed || undefined}
|
|
56
|
+
data-render-hovered={state.isHovered || undefined}
|
|
57
|
+
data-render-pending={state.isPending || undefined}
|
|
58
|
+
data-render-focused={state.isFocused || undefined}
|
|
59
|
+
data-render-focus-visible={state.isFocusVisible || undefined}
|
|
60
|
+
>
|
|
61
|
+
{state.isPressed ? 'pressed' : state.isPending ? 'pending' : 'idle'}
|
|
62
|
+
</span>
|
|
63
|
+
{/snippet}
|
|
64
|
+
</Button.Root>
|
|
65
|
+
|
|
66
|
+
<button type="button" data-set-pending onclick={() => (pendingOverride = true)}
|
|
67
|
+
>Set pending</button
|
|
68
|
+
>
|
|
69
|
+
<button type="button" data-clear-pending onclick={() => (pendingOverride = false)}>
|
|
70
|
+
Clear pending
|
|
71
|
+
</button>
|
|
72
|
+
|
|
73
|
+
<output data-click-count>{String(clickCount)}</output>
|
|
74
|
+
<output data-submit-count>{String(submitCount)}</output>
|
|
75
|
+
<output data-pending-state>{String(pending)}</output>
|
|
76
|
+
</form>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
type Props = {
|
|
2
|
+
isDisabled?: boolean;
|
|
3
|
+
isPending?: boolean;
|
|
4
|
+
type?: 'button' | 'submit' | 'reset';
|
|
5
|
+
ariaLabel?: string;
|
|
6
|
+
onMouseEnter?: (event: MouseEvent) => void;
|
|
7
|
+
onFocus?: (event: FocusEvent) => void;
|
|
8
|
+
};
|
|
9
|
+
declare const ButtonTest: import("svelte").Component<Props, {}, "">;
|
|
10
|
+
type ButtonTest = ReturnType<typeof ButtonTest>;
|
|
11
|
+
export default ButtonTest;
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
3
|
import type { HTMLButtonAttributes } from 'svelte/elements';
|
|
4
|
+
import { ButtonRoot } from '../../button/index.js';
|
|
4
5
|
import { useCalendarContext } from '../root/context';
|
|
5
6
|
|
|
6
|
-
type CalendarTriggerNextProps = Omit<
|
|
7
|
+
type CalendarTriggerNextProps = Omit<
|
|
8
|
+
HTMLButtonAttributes,
|
|
9
|
+
'children' | 'class' | 'disabled' | 'aria-disabled'
|
|
10
|
+
> & {
|
|
11
|
+
class?: string;
|
|
7
12
|
children?: Snippet;
|
|
8
13
|
};
|
|
9
14
|
|
|
@@ -22,11 +27,11 @@
|
|
|
22
27
|
}
|
|
23
28
|
</script>
|
|
24
29
|
|
|
25
|
-
<
|
|
30
|
+
<ButtonRoot
|
|
26
31
|
type="button"
|
|
27
32
|
class={className}
|
|
28
33
|
aria-label="Next page"
|
|
29
|
-
|
|
34
|
+
{isDisabled}
|
|
30
35
|
onclick={handleClick}
|
|
31
36
|
{...restProps}
|
|
32
37
|
>
|
|
@@ -35,4 +40,4 @@
|
|
|
35
40
|
{:else}
|
|
36
41
|
Next
|
|
37
42
|
{/if}
|
|
38
|
-
</
|
|
43
|
+
</ButtonRoot>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
2
|
import type { HTMLButtonAttributes } from 'svelte/elements';
|
|
3
|
-
type CalendarTriggerNextProps = Omit<HTMLButtonAttributes, 'children'> & {
|
|
3
|
+
type CalendarTriggerNextProps = Omit<HTMLButtonAttributes, 'children' | 'class' | 'disabled' | 'aria-disabled'> & {
|
|
4
|
+
class?: string;
|
|
4
5
|
children?: Snippet;
|
|
5
6
|
};
|
|
6
7
|
declare const CalendarTriggerNext: import("svelte").Component<CalendarTriggerNextProps, {}, "">;
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
3
|
import type { HTMLButtonAttributes } from 'svelte/elements';
|
|
4
|
+
import { ButtonRoot } from '../../button/index.js';
|
|
4
5
|
import { useCalendarContext } from '../root/context';
|
|
5
6
|
|
|
6
|
-
type CalendarTriggerPreviousProps = Omit<
|
|
7
|
+
type CalendarTriggerPreviousProps = Omit<
|
|
8
|
+
HTMLButtonAttributes,
|
|
9
|
+
'children' | 'class' | 'disabled' | 'aria-disabled'
|
|
10
|
+
> & {
|
|
11
|
+
class?: string;
|
|
7
12
|
children?: Snippet;
|
|
8
13
|
};
|
|
9
14
|
|
|
@@ -22,11 +27,11 @@
|
|
|
22
27
|
}
|
|
23
28
|
</script>
|
|
24
29
|
|
|
25
|
-
<
|
|
30
|
+
<ButtonRoot
|
|
26
31
|
type="button"
|
|
27
32
|
class={className}
|
|
28
33
|
aria-label="Previous page"
|
|
29
|
-
|
|
34
|
+
{isDisabled}
|
|
30
35
|
onclick={handleClick}
|
|
31
36
|
{...restProps}
|
|
32
37
|
>
|
|
@@ -35,4 +40,4 @@
|
|
|
35
40
|
{:else}
|
|
36
41
|
Prev
|
|
37
42
|
{/if}
|
|
38
|
-
</
|
|
43
|
+
</ButtonRoot>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
2
|
import type { HTMLButtonAttributes } from 'svelte/elements';
|
|
3
|
-
type CalendarTriggerPreviousProps = Omit<HTMLButtonAttributes, 'children'> & {
|
|
3
|
+
type CalendarTriggerPreviousProps = Omit<HTMLButtonAttributes, 'children' | 'class' | 'disabled' | 'aria-disabled'> & {
|
|
4
|
+
class?: string;
|
|
4
5
|
children?: Snippet;
|
|
5
6
|
};
|
|
6
7
|
declare const CalendarTriggerPrevious: import("svelte").Component<CalendarTriggerPreviousProps, {}, "">;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
3
|
import type { HTMLButtonAttributes } from 'svelte/elements';
|
|
4
|
+
import { ButtonRoot } from '../../button/index.js';
|
|
4
5
|
import { useComboBoxContext } from '../root/context';
|
|
5
6
|
|
|
6
7
|
type ComboBoxButtonProps = HTMLButtonAttributes & {
|
|
@@ -26,14 +27,14 @@
|
|
|
26
27
|
|
|
27
28
|
<!-- * CHANGE: CHANGE NAME FROM BUTTON TO TRIGGER -->
|
|
28
29
|
|
|
29
|
-
<
|
|
30
|
+
<ButtonRoot
|
|
30
31
|
type="button"
|
|
31
32
|
{tabindex}
|
|
32
33
|
aria-label={ctx.isOpen ? 'Close menu' : 'Open menu'}
|
|
33
34
|
aria-expanded={ctx.isOpen}
|
|
34
35
|
aria-controls={`combobox-listbox-${ctx.instanceId}`}
|
|
35
|
-
|
|
36
|
-
|
|
36
|
+
isDisabled={ctx.isDisabled}
|
|
37
|
+
pressed={ctx.isOpen}
|
|
37
38
|
onmousedown={handleMouseDown}
|
|
38
39
|
class={className}
|
|
39
40
|
{...restProps}
|
|
@@ -56,4 +57,4 @@
|
|
|
56
57
|
<path d="m6 9 6 6 6-6" />
|
|
57
58
|
</svg>
|
|
58
59
|
{/if}
|
|
59
|
-
</
|
|
60
|
+
</ButtonRoot>
|
|
@@ -17,7 +17,7 @@ declare function $$render<T extends object = object>(): {
|
|
|
17
17
|
} & {
|
|
18
18
|
context?: ListBoxContext;
|
|
19
19
|
element?: HTMLElement;
|
|
20
|
-
}, "children" | "id" | "
|
|
20
|
+
}, "children" | "value" | "id" | "element" | "selectionMode" | "selectionBehavior" | "defaultValue" | "onChange" | "items" | "context"> & {
|
|
21
21
|
/** Optional items for dynamic rendering - overrides items from ComboBox context */
|
|
22
22
|
items?: Iterable<T>;
|
|
23
23
|
/** Content of the listbox. Receives item in dynamic mode. */
|
|
@@ -26,11 +26,41 @@
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
function handleOpenChange(open: boolean, details?: PopoverOpenChangeDetails) {
|
|
29
|
-
if (!open
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
if (!open) {
|
|
30
|
+
// Cancel Popover.Root's close to prevent scheduleTriggerCloseFocus from
|
|
31
|
+
// setting stale data-focused on the trigger. The combobox passes triggerRef
|
|
32
|
+
// via prop (not Popover.Trigger), so Popover.Root never registers a
|
|
33
|
+
// blur-cleanup listener and any data-focused it sets persists forever.
|
|
34
|
+
//
|
|
35
|
+
// IMPORTANT: the actual state change is deferred to a microtask so that
|
|
36
|
+
// when Popover.Root's closePopover re-reads `isOpen` after the callback,
|
|
37
|
+
// the derived value is still `true` and the guard succeeds. A synchronous
|
|
38
|
+
// ctx.onOpenChange(false) would update the upstream signal immediately,
|
|
39
|
+
// making the derived `false` and bypassing the guard despite the cancel.
|
|
40
|
+
details?.cancel();
|
|
41
|
+
|
|
42
|
+
if (details?.reason === 'outside-press') {
|
|
43
|
+
const target = resolveFocusTarget(details);
|
|
44
|
+
queueMicrotask(() => {
|
|
45
|
+
ctx.onOpenChange(false);
|
|
46
|
+
|
|
47
|
+
if (target) {
|
|
48
|
+
focusWithModality(target, 'pointer' satisfies InputModality);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// If focus is still on the input (non-focusable target), blur it
|
|
52
|
+
// so focusWithin becomes false.
|
|
53
|
+
if (document.activeElement === ctx.inputRef) {
|
|
54
|
+
ctx.inputRef?.blur();
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
return;
|
|
33
58
|
}
|
|
59
|
+
|
|
60
|
+
queueMicrotask(() => {
|
|
61
|
+
ctx.onOpenChange(false);
|
|
62
|
+
});
|
|
63
|
+
return;
|
|
34
64
|
}
|
|
35
65
|
|
|
36
66
|
ctx.onOpenChange(open);
|
|
@@ -346,6 +346,13 @@
|
|
|
346
346
|
!!wrapperRef && !!document.activeElement && wrapperRef.contains(document.activeElement);
|
|
347
347
|
if (!focusWithin) {
|
|
348
348
|
focusVisible = false;
|
|
349
|
+
// Clean up stale data-focused/data-focus-visible that Popover.Root's
|
|
350
|
+
// focus-state module may have set imperatively on the wrapper (the
|
|
351
|
+
// wrapper is briefly used as triggerRef before the input overrides it).
|
|
352
|
+
if (wrapperRef) {
|
|
353
|
+
delete wrapperRef.dataset.focused;
|
|
354
|
+
delete wrapperRef.dataset.focusVisible;
|
|
355
|
+
}
|
|
349
356
|
}
|
|
350
357
|
}
|
|
351
358
|
|
|
@@ -717,6 +724,7 @@
|
|
|
717
724
|
aria-labelledby={ariaLabelledby}
|
|
718
725
|
class={className}
|
|
719
726
|
data-combobox
|
|
727
|
+
data-focused={focusWithin || undefined}
|
|
720
728
|
data-disabled={isDisabled || undefined}
|
|
721
729
|
data-readonly={isReadOnly || undefined}
|
|
722
730
|
data-focus-within={focusWithin || undefined}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
3
|
import type { HTMLButtonAttributes } from 'svelte/elements';
|
|
4
4
|
import { getContext } from 'svelte';
|
|
5
|
+
import { ButtonRoot } from '../../button/index.js';
|
|
5
6
|
import { cn } from '../../utils/cn';
|
|
6
7
|
import { TAG_CONTEXT_KEY, type TagContext } from '../tag/combobox-tag.svelte';
|
|
7
8
|
|
|
@@ -27,7 +28,7 @@
|
|
|
27
28
|
</script>
|
|
28
29
|
|
|
29
30
|
{#if !tagCtx.disabled}
|
|
30
|
-
<
|
|
31
|
+
<ButtonRoot
|
|
31
32
|
type="button"
|
|
32
33
|
onclick={handleClick}
|
|
33
34
|
aria-label={`Remove ${tagCtx.label}`}
|
|
@@ -49,5 +50,5 @@
|
|
|
49
50
|
/>
|
|
50
51
|
</svg>
|
|
51
52
|
{/if}
|
|
52
|
-
</
|
|
53
|
+
</ButtonRoot>
|
|
53
54
|
{/if}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
3
|
import type { HTMLButtonAttributes } from 'svelte/elements';
|
|
4
|
+
import { ButtonRoot } from '../../button/index.js';
|
|
4
5
|
import { useDatePickerContext } from '../root/context';
|
|
5
6
|
import {
|
|
6
7
|
shouldShowFocusVisible,
|
|
@@ -86,14 +87,13 @@
|
|
|
86
87
|
</script>
|
|
87
88
|
|
|
88
89
|
{#if !datePicker.isReadOnly}
|
|
89
|
-
<
|
|
90
|
-
bind:
|
|
90
|
+
<ButtonRoot
|
|
91
|
+
bind:element={buttonRef}
|
|
91
92
|
type="button"
|
|
92
|
-
|
|
93
|
+
isDisabled={datePicker.isDisabled}
|
|
93
94
|
class={className}
|
|
94
95
|
aria-haspopup="dialog"
|
|
95
96
|
aria-expanded={datePicker.open}
|
|
96
|
-
data-disabled={datePicker.isDisabled || undefined}
|
|
97
97
|
data-focused={isFocused || undefined}
|
|
98
98
|
data-focus-visible={isFocused && datePicker.focusVisible ? 'true' : undefined}
|
|
99
99
|
onmousedown={handleMouseDown}
|
|
@@ -106,5 +106,5 @@
|
|
|
106
106
|
{#if children}
|
|
107
107
|
{@render children()}
|
|
108
108
|
{/if}
|
|
109
|
-
</
|
|
109
|
+
</ButtonRoot>
|
|
110
110
|
{/if}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export { Button } from './button/index.js';
|
|
1
2
|
export { Checkbox } from './checkbox/index.js';
|
|
2
3
|
export { ComboBox } from './combobox/index.js';
|
|
3
4
|
export { Calendar } from './calendar/index.js';
|
|
@@ -13,6 +14,7 @@ export { default as Label } from './label/index.js';
|
|
|
13
14
|
export { default as LocaleProvider } from './locale-provider/index.js';
|
|
14
15
|
export { Portal } from './portal/index.js';
|
|
15
16
|
export * from './locale-provider/index.js';
|
|
17
|
+
export * from './button/index.js';
|
|
16
18
|
export * from './checkbox/index.js';
|
|
17
19
|
export * from './combobox/index.js';
|
|
18
20
|
export * from './calendar/index.js';
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Main library entry point
|
|
2
2
|
// Components (namespace exports)
|
|
3
|
+
export { Button } from './button/index.js';
|
|
3
4
|
export { Checkbox } from './checkbox/index.js';
|
|
4
5
|
export { ComboBox } from './combobox/index.js';
|
|
5
6
|
export { Calendar } from './calendar/index.js';
|
|
@@ -17,6 +18,7 @@ export { default as LocaleProvider } from './locale-provider/index.js';
|
|
|
17
18
|
export { Portal } from './portal/index.js';
|
|
18
19
|
export * from './locale-provider/index.js';
|
|
19
20
|
// Re-export named exports from components
|
|
21
|
+
export * from './button/index.js';
|
|
20
22
|
export * from './checkbox/index.js';
|
|
21
23
|
export * from './combobox/index.js';
|
|
22
24
|
export * from './calendar/index.js';
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
### ListBox.Item
|
|
6
6
|
|
|
7
7
|
Name: `ListBox.Item`
|
|
8
|
-
Description: Selectable option element with built-in selected, focused, hovered, and disabled states.
|
|
8
|
+
Description: Selectable option element with built-in selected, focused, hovered, pressed, and disabled states.
|
|
9
9
|
|
|
10
10
|
| Prop | Type | Default | Description |
|
|
11
11
|
| ---------------------- | -------------------------------- | -------------------- | -------------------------------------------------------- |
|
|
@@ -21,4 +21,5 @@ Description: Selectable option element with built-in selected, focused, hovered,
|
|
|
21
21
|
| `onResolvedTextValue` | `(label: string) => void` | `undefined` | Called when item text value is resolved. |
|
|
22
22
|
| `scrollOnFocus` | `boolean` | `false` | Scrolls item into view when focused. |
|
|
23
23
|
| `isParentDisabled` | `boolean` | `false` | Additional disabled state inherited from parent wrapper. |
|
|
24
|
+
| `pressed` | `boolean` | `undefined` | Forces the visual pressed state from parent composition. |
|
|
24
25
|
| `...restProps` | `HTMLAttributes<HTMLDivElement>` | `-` | Additional option attributes. |
|
|
@@ -34,6 +34,8 @@
|
|
|
34
34
|
scrollOnFocus?: boolean;
|
|
35
35
|
/** Additional disabled state from parent. */
|
|
36
36
|
isParentDisabled?: boolean;
|
|
37
|
+
/** Override the visual pressed state. When provided, this value is used instead of internal press tracking. */
|
|
38
|
+
pressed?: boolean;
|
|
37
39
|
};
|
|
38
40
|
|
|
39
41
|
let {
|
|
@@ -50,6 +52,7 @@
|
|
|
50
52
|
onResolvedTextValue,
|
|
51
53
|
scrollOnFocus = false,
|
|
52
54
|
isParentDisabled = false,
|
|
55
|
+
pressed: pressedOverride,
|
|
53
56
|
...restProps
|
|
54
57
|
}: ListBoxItemProps = $props();
|
|
55
58
|
|
|
@@ -59,6 +62,8 @@
|
|
|
59
62
|
let isSelected = $state(false);
|
|
60
63
|
let isFocused = $state(false);
|
|
61
64
|
let isHovered = $state(false);
|
|
65
|
+
let isPressed = $state(false);
|
|
66
|
+
let pressedKey: 'Enter' | 'Space' | null = $state(null);
|
|
62
67
|
|
|
63
68
|
// Focus: use override if provided, otherwise use internal state
|
|
64
69
|
const isFocusedComputed = $derived(
|
|
@@ -67,6 +72,11 @@
|
|
|
67
72
|
const isDisabledComputed = $derived(
|
|
68
73
|
disabled || listboxCtx.disabledIds.has(id) || isParentDisabled
|
|
69
74
|
);
|
|
75
|
+
const isPressedComputed = $derived(
|
|
76
|
+
pressedOverride !== undefined
|
|
77
|
+
? Boolean(pressedOverride) && !isDisabledComputed
|
|
78
|
+
: isPressed && !isDisabledComputed
|
|
79
|
+
);
|
|
70
80
|
|
|
71
81
|
// ID: use custom if provided, otherwise generate
|
|
72
82
|
const uniqueId = $derived(customId ?? `listbox-item-${id}`);
|
|
@@ -113,6 +123,17 @@
|
|
|
113
123
|
}
|
|
114
124
|
});
|
|
115
125
|
|
|
126
|
+
$effect(() => {
|
|
127
|
+
if (!isDisabledComputed) return;
|
|
128
|
+
clearPressedState();
|
|
129
|
+
isHovered = false;
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
function clearPressedState() {
|
|
133
|
+
isPressed = false;
|
|
134
|
+
pressedKey = null;
|
|
135
|
+
}
|
|
136
|
+
|
|
116
137
|
function handleClick() {
|
|
117
138
|
if (isDisabledComputed) return;
|
|
118
139
|
|
|
@@ -135,6 +156,42 @@
|
|
|
135
156
|
|
|
136
157
|
function handleBlur() {}
|
|
137
158
|
|
|
159
|
+
function handlePointerDown(event: PointerEvent) {
|
|
160
|
+
if (isDisabledComputed) {
|
|
161
|
+
event.preventDefault();
|
|
162
|
+
clearPressedState();
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (event.button !== 0) return;
|
|
167
|
+
isPressed = true;
|
|
168
|
+
pressedKey = null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function handlePointerUp(event: PointerEvent) {
|
|
172
|
+
if (event.button !== 0) return;
|
|
173
|
+
isPressed = false;
|
|
174
|
+
pressedKey = null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function handlePointerCancel() {
|
|
178
|
+
clearPressedState();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function handlePointerEnter(event: PointerEvent) {
|
|
182
|
+
if (isDisabledComputed) return;
|
|
183
|
+
|
|
184
|
+
if ((event.buttons & 1) === 1 && pressedKey === null) {
|
|
185
|
+
isPressed = true;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function handlePointerLeave() {
|
|
190
|
+
if (pressedKey === null) {
|
|
191
|
+
isPressed = false;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
138
195
|
function handleMouseEnter() {
|
|
139
196
|
if (!isDisabledComputed) {
|
|
140
197
|
isHovered = true;
|
|
@@ -143,17 +200,80 @@
|
|
|
143
200
|
|
|
144
201
|
function handleMouseLeave() {
|
|
145
202
|
isHovered = false;
|
|
203
|
+
if (pressedKey === null) {
|
|
204
|
+
isPressed = false;
|
|
205
|
+
}
|
|
146
206
|
}
|
|
147
207
|
|
|
148
208
|
// Keyboard is handled by parent container
|
|
149
|
-
function handleKeydown() {
|
|
209
|
+
function handleKeydown(event: KeyboardEvent) {
|
|
210
|
+
const key =
|
|
211
|
+
event.key === 'Enter'
|
|
212
|
+
? 'Enter'
|
|
213
|
+
: event.key === ' ' || event.key === 'Spacebar'
|
|
214
|
+
? 'Space'
|
|
215
|
+
: null;
|
|
216
|
+
|
|
217
|
+
if (!key) return;
|
|
218
|
+
|
|
219
|
+
if (isDisabledComputed) {
|
|
220
|
+
event.preventDefault();
|
|
221
|
+
clearPressedState();
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (event.repeat && isPressed && pressedKey === key) return;
|
|
226
|
+
|
|
227
|
+
isPressed = true;
|
|
228
|
+
pressedKey = key;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function handleKeyup(event: KeyboardEvent) {
|
|
232
|
+
const key =
|
|
233
|
+
event.key === 'Enter'
|
|
234
|
+
? 'Enter'
|
|
235
|
+
: event.key === ' ' || event.key === 'Spacebar'
|
|
236
|
+
? 'Space'
|
|
237
|
+
: null;
|
|
238
|
+
|
|
239
|
+
if (!key) return;
|
|
240
|
+
|
|
241
|
+
if (isDisabledComputed) {
|
|
242
|
+
event.preventDefault();
|
|
243
|
+
clearPressedState();
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (pressedKey === key) {
|
|
248
|
+
clearPressedState();
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
150
252
|
function handleMouseDown(event: MouseEvent) {
|
|
151
253
|
// Prevent focus stealing when used in ComboBox (disableFocusHandling=true)
|
|
152
254
|
// This keeps the focus on the input while allowing click selection
|
|
255
|
+
if (isDisabledComputed) {
|
|
256
|
+
event.preventDefault();
|
|
257
|
+
clearPressedState();
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (event.button === 0) {
|
|
262
|
+
isPressed = true;
|
|
263
|
+
pressedKey = null;
|
|
264
|
+
}
|
|
265
|
+
|
|
153
266
|
if (disableFocusHandling) {
|
|
154
267
|
event.preventDefault();
|
|
155
268
|
}
|
|
156
269
|
}
|
|
270
|
+
|
|
271
|
+
function handleMouseUp(event: MouseEvent) {
|
|
272
|
+
if (event.button !== 0) return;
|
|
273
|
+
if (pressedKey === null) {
|
|
274
|
+
clearPressedState();
|
|
275
|
+
}
|
|
276
|
+
}
|
|
157
277
|
</script>
|
|
158
278
|
|
|
159
279
|
<div
|
|
@@ -171,9 +291,17 @@
|
|
|
171
291
|
data-disabled={isDisabledComputed || undefined}
|
|
172
292
|
data-focused={isFocusedComputed || undefined}
|
|
173
293
|
data-hovered={isHovered || undefined}
|
|
294
|
+
data-pressed={isPressedComputed || undefined}
|
|
295
|
+
onpointerdown={handlePointerDown}
|
|
296
|
+
onpointerup={handlePointerUp}
|
|
297
|
+
onpointercancel={handlePointerCancel}
|
|
298
|
+
onpointerenter={handlePointerEnter}
|
|
299
|
+
onpointerleave={handlePointerLeave}
|
|
174
300
|
onmousedown={handleMouseDown}
|
|
301
|
+
onmouseup={handleMouseUp}
|
|
175
302
|
onclick={handleClick}
|
|
176
303
|
onkeydown={handleKeydown}
|
|
304
|
+
onkeyup={handleKeyup}
|
|
177
305
|
onfocus={handleFocus}
|
|
178
306
|
onblur={handleBlur}
|
|
179
307
|
onmouseenter={handleMouseEnter}
|
|
@@ -28,6 +28,8 @@ type ListBoxItemProps = Omit<HTMLAttributes<HTMLDivElement>, 'id' | 'children'>
|
|
|
28
28
|
scrollOnFocus?: boolean;
|
|
29
29
|
/** Additional disabled state from parent. */
|
|
30
30
|
isParentDisabled?: boolean;
|
|
31
|
+
/** Override the visual pressed state. When provided, this value is used instead of internal press tracking. */
|
|
32
|
+
pressed?: boolean;
|
|
31
33
|
};
|
|
32
34
|
declare const ListboxItem: import("svelte").Component<ListBoxItemProps, {}, "">;
|
|
33
35
|
type ListboxItem = ReturnType<typeof ListboxItem>;
|
|
@@ -5,9 +5,17 @@
|
|
|
5
5
|
selectionMode?: 'single' | 'multiple';
|
|
6
6
|
selectionBehavior?: 'toggle' | 'replace';
|
|
7
7
|
disabledIds?: Iterable<string | number>;
|
|
8
|
+
pressedIds?: Iterable<string | number>;
|
|
8
9
|
};
|
|
9
10
|
|
|
10
|
-
let {
|
|
11
|
+
let {
|
|
12
|
+
selectionMode = 'single',
|
|
13
|
+
selectionBehavior = 'toggle',
|
|
14
|
+
disabledIds,
|
|
15
|
+
pressedIds
|
|
16
|
+
}: Props = $props();
|
|
17
|
+
|
|
18
|
+
const pressedIdSet = $derived(new Set(pressedIds ?? []));
|
|
11
19
|
|
|
12
20
|
const fruits = [
|
|
13
21
|
{ id: 'apple', name: 'Apple' },
|
|
@@ -20,7 +28,11 @@
|
|
|
20
28
|
|
|
21
29
|
<ListBox.Root {selectionMode} {selectionBehavior} {disabledIds} aria-label="Fruits list">
|
|
22
30
|
{#each fruits as fruit (fruit.id)}
|
|
23
|
-
<ListBox.Item
|
|
31
|
+
<ListBox.Item
|
|
32
|
+
id={fruit.id}
|
|
33
|
+
textValue={fruit.name}
|
|
34
|
+
pressed={pressedIdSet.has(fruit.id) ? true : undefined}
|
|
35
|
+
>
|
|
24
36
|
{fruit.name}
|
|
25
37
|
</ListBox.Item>
|
|
26
38
|
{/each}
|
|
@@ -2,6 +2,7 @@ type Props = {
|
|
|
2
2
|
selectionMode?: 'single' | 'multiple';
|
|
3
3
|
selectionBehavior?: 'toggle' | 'replace';
|
|
4
4
|
disabledIds?: Iterable<string | number>;
|
|
5
|
+
pressedIds?: Iterable<string | number>;
|
|
5
6
|
};
|
|
6
7
|
declare const ListboxTest: import("svelte").Component<Props, {}, "">;
|
|
7
8
|
type ListboxTest = ReturnType<typeof ListboxTest>;
|
|
@@ -31,7 +31,7 @@ declare function $$render<T extends object = object>(): {
|
|
|
31
31
|
element?: HTMLElement;
|
|
32
32
|
};
|
|
33
33
|
exports: {};
|
|
34
|
-
bindings: "value" | "
|
|
34
|
+
bindings: "value" | "element" | "context";
|
|
35
35
|
slots: {};
|
|
36
36
|
events: {};
|
|
37
37
|
};
|
|
@@ -39,7 +39,7 @@ declare class __sveltets_Render<T extends object = object> {
|
|
|
39
39
|
props(): ReturnType<typeof $$render<T>>['props'];
|
|
40
40
|
events(): ReturnType<typeof $$render<T>>['events'];
|
|
41
41
|
slots(): ReturnType<typeof $$render<T>>['slots'];
|
|
42
|
-
bindings(): "value" | "
|
|
42
|
+
bindings(): "value" | "element" | "context";
|
|
43
43
|
exports(): {};
|
|
44
44
|
}
|
|
45
45
|
interface $$IsomorphicComponent {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
3
|
import type { HTMLButtonAttributes } from 'svelte/elements';
|
|
4
|
+
import { ButtonRoot } from '../../button/index.js';
|
|
4
5
|
import { getPopoverContext } from '../root/context';
|
|
5
6
|
|
|
6
7
|
type PopoverTriggerButtonProps = Omit<
|
|
@@ -30,8 +31,8 @@
|
|
|
30
31
|
}
|
|
31
32
|
</script>
|
|
32
33
|
|
|
33
|
-
<
|
|
34
|
-
bind:
|
|
34
|
+
<ButtonRoot
|
|
35
|
+
bind:element={buttonRef}
|
|
35
36
|
class={className}
|
|
36
37
|
type="button"
|
|
37
38
|
aria-expanded={ctx?.isOpen ?? false}
|
|
@@ -42,4 +43,4 @@
|
|
|
42
43
|
{#if children}
|
|
43
44
|
{@render children()}
|
|
44
45
|
{/if}
|
|
45
|
-
</
|
|
46
|
+
</ButtonRoot>
|
|
@@ -24,6 +24,6 @@ type TableRootProps = Omit<HTMLAttributes<HTMLTableElement>, 'children'> & {
|
|
|
24
24
|
context?: TableContext;
|
|
25
25
|
element?: HTMLTableElement;
|
|
26
26
|
};
|
|
27
|
-
declare const TableRoot: import("svelte").Component<TableRootProps, {}, "
|
|
27
|
+
declare const TableRoot: import("svelte").Component<TableRootProps, {}, "element" | "context" | "hiddenColumns" | "selectedKeys" | "sortDescriptor" | "columnWidths">;
|
|
28
28
|
type TableRoot = ReturnType<typeof TableRoot>;
|
|
29
29
|
export default TableRoot;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
3
|
import type { HTMLButtonAttributes } from 'svelte/elements';
|
|
4
|
+
import { ButtonRoot } from '../../button/index.js';
|
|
4
5
|
import { useTimePickerContext } from '../root/context';
|
|
5
6
|
import {
|
|
6
7
|
shouldShowFocusVisible,
|
|
@@ -94,14 +95,13 @@
|
|
|
94
95
|
</script>
|
|
95
96
|
|
|
96
97
|
{#if !timePicker.isReadOnly}
|
|
97
|
-
<
|
|
98
|
-
bind:
|
|
98
|
+
<ButtonRoot
|
|
99
|
+
bind:element={buttonRef}
|
|
99
100
|
type="button"
|
|
100
|
-
|
|
101
|
+
isDisabled={timePicker.isDisabled}
|
|
101
102
|
class={className}
|
|
102
103
|
aria-haspopup="dialog"
|
|
103
104
|
aria-expanded={timePicker.open}
|
|
104
|
-
data-disabled={timePicker.isDisabled || undefined}
|
|
105
105
|
data-focused={isFocused || undefined}
|
|
106
106
|
data-focus-visible={isFocused && timePicker.focusVisible ? 'true' : undefined}
|
|
107
107
|
{...restProps}
|
|
@@ -118,5 +118,5 @@
|
|
|
118
118
|
{#if children}
|
|
119
119
|
{@render children()}
|
|
120
120
|
{/if}
|
|
121
|
-
</
|
|
121
|
+
</ButtonRoot>
|
|
122
122
|
{/if}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@human-kit/svelte-components",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.7",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"svelte": "./dist/index.js",
|
|
@@ -11,6 +11,11 @@
|
|
|
11
11
|
"svelte": "./dist/index.js",
|
|
12
12
|
"default": "./dist/index.js"
|
|
13
13
|
},
|
|
14
|
+
"./button": {
|
|
15
|
+
"types": "./dist/button/index.d.ts",
|
|
16
|
+
"svelte": "./dist/button/index.js",
|
|
17
|
+
"default": "./dist/button/index.js"
|
|
18
|
+
},
|
|
14
19
|
"./checkbox": {
|
|
15
20
|
"types": "./dist/checkbox/index.d.ts",
|
|
16
21
|
"svelte": "./dist/checkbox/index.js",
|