@urbicon-ui/blocks 6.22.0 → 6.24.0
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/components/Calendar/CalendarWeekdayHeader.svelte +4 -1
- package/dist/components/Calendar/calendar.variants.d.ts +63 -63
- package/dist/components/Sparkline/Sparkline.svelte +10 -4
- package/dist/components/Sparkline/index.d.ts +9 -1
- package/dist/i18n/index.d.ts +2 -378
- package/dist/primitives/Alert/alert.variants.d.ts +8 -8
- package/dist/primitives/Badge/Badge.svelte +8 -2
- package/dist/primitives/Badge/index.d.ts +67 -32
- package/dist/primitives/ButtonGroup/ButtonGroup.svelte +120 -3
- package/dist/primitives/Card/card.variants.js +2 -2
- package/dist/primitives/Checkbox/Checkbox.svelte +11 -2
- package/dist/primitives/Combobox/Combobox.svelte +35 -3
- package/dist/primitives/ConfirmDialog/index.d.ts +2 -1
- package/dist/primitives/Dialog/index.d.ts +4 -4
- package/dist/primitives/JourneyTimeline/journey-timeline.variants.d.ts +22 -22
- package/dist/primitives/Pagination/Pagination.svelte +28 -15
- package/dist/primitives/RadioGroup/RadioGroup.svelte +16 -3
- package/dist/primitives/RadioGroup/RadioItem.svelte +1 -1
- package/dist/primitives/Select/Select.svelte +20 -5
- package/dist/primitives/Spinner/spinner.variants.d.ts +16 -16
- package/dist/primitives/Stepper/stepper.variants.d.ts +11 -11
- package/dist/primitives/Textarea/Textarea.svelte +10 -1
- package/dist/primitives/Toast/Toaster.svelte +122 -4
- package/dist/primitives/Toast/toast.store.svelte.d.ts +32 -1
- package/dist/primitives/Toast/toast.store.svelte.js +84 -19
- package/dist/primitives/Toast/toast.variants.d.ts +12 -12
- package/dist/primitives/Toggle/Toggle.svelte +16 -3
- package/dist/primitives/Tooltip/tooltip.variants.d.ts +3 -3
- package/dist/style/semantic.css +1 -1
- package/package.json +3 -3
|
@@ -3,24 +3,13 @@ import type { HTMLAttributes } from 'svelte/elements';
|
|
|
3
3
|
import type { MintProp } from '../../mint/index.js';
|
|
4
4
|
import type { BadgePlacement, BadgeSlots, BadgeVariants } from './badge.variants.js';
|
|
5
5
|
/**
|
|
6
|
-
* Shared fields that apply to every Badge variant
|
|
7
|
-
* label-
|
|
6
|
+
* Shared fields that apply to every Badge arm. `variant`, `purpose`, and the
|
|
7
|
+
* label-only props (`children` / `counter` / `removable` / `interactive` /
|
|
8
|
+
* `onRemove`) are declared per-arm instead — that split is what lets *both*
|
|
9
|
+
* dot spellings (`variant="dot"` and the canonical `purpose="dot"`) forbid the
|
|
10
|
+
* label-only props at the type level.
|
|
8
11
|
*/
|
|
9
|
-
interface BadgeBaseProps extends Omit<BadgeVariants, 'variant' | 'counter'>, Omit<HTMLAttributes<HTMLElement>, 'children'> {
|
|
10
|
-
/**
|
|
11
|
-
* The badge's semantic purpose — the canonical axis that resolves what the
|
|
12
|
-
* badge *is*, since a bare Badge served five overlapping roles. Orchestrates
|
|
13
|
-
* the low-level visual props so you rarely set them directly:
|
|
14
|
-
* - `status` — a state marker (Active, Failed); pairs with `intent`.
|
|
15
|
-
* - `tag` — a neutral inline label (category, version).
|
|
16
|
-
* - `counter` — a compact numeric pill (replaces the `counter` boolean).
|
|
17
|
-
* - `dot` — a pure indicator, content hidden (replaces `variant="dot"`).
|
|
18
|
-
* - `chip` — a removable, interactive filter chip; pair with `removable`.
|
|
19
|
-
*
|
|
20
|
-
* Leave unset to drive the badge purely by the low-level props (back-compat).
|
|
21
|
-
* When set, `purpose` wins over `variant="dot"` / the `counter` boolean.
|
|
22
|
-
*/
|
|
23
|
-
purpose?: 'status' | 'tag' | 'counter' | 'dot' | 'chip';
|
|
12
|
+
interface BadgeBaseProps extends Omit<BadgeVariants, 'variant' | 'counter' | 'removable' | 'interactive'>, Omit<HTMLAttributes<HTMLElement>, 'children'> {
|
|
24
13
|
/** Add a pulsing animation to draw attention (e.g. for live indicators). */
|
|
25
14
|
pulse?: boolean;
|
|
26
15
|
/** Visually disable the badge (reduced opacity, no pointer events). */
|
|
@@ -29,7 +18,7 @@ interface BadgeBaseProps extends Omit<BadgeVariants, 'variant' | 'counter'>, Omi
|
|
|
29
18
|
border?: boolean;
|
|
30
19
|
/** Anchor the badge absolutely within a `position: relative` parent. */
|
|
31
20
|
placement?: BadgePlacement;
|
|
32
|
-
/** Click handler. Automatically enables interactive styles. */
|
|
21
|
+
/** Click handler. Automatically enables interactive styles (and `role="button"`). */
|
|
33
22
|
onclick?: (event: MouseEvent) => void;
|
|
34
23
|
/** Called when the hover state changes. */
|
|
35
24
|
onHover?: (hovered: boolean) => void;
|
|
@@ -46,30 +35,73 @@ interface BadgeBaseProps extends Omit<BadgeVariants, 'variant' | 'counter'>, Omi
|
|
|
46
35
|
* and make the custom look reusable across the project.
|
|
47
36
|
*/
|
|
48
37
|
preset?: string;
|
|
49
|
-
/**
|
|
50
|
-
|
|
38
|
+
/**
|
|
39
|
+
* ARIA role. A static badge is announced as `"status"`; an interactive badge
|
|
40
|
+
* (`onclick` or `purpose="chip"`, when not `disabled`) defaults to `"button"`
|
|
41
|
+
* so assistive tech announces its activation semantics. Set explicitly to
|
|
42
|
+
* override — e.g. `"alert"` for time-sensitive notifications. An explicit
|
|
43
|
+
* value always wins over the derived default.
|
|
44
|
+
*/
|
|
45
|
+
role?: 'status' | 'alert' | 'badge' | 'button';
|
|
51
46
|
/** Micro-interaction preset. Only applies when the badge is interactive. */
|
|
52
47
|
mint?: MintProp;
|
|
53
48
|
}
|
|
54
49
|
/**
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
* hover-scale on a 2.5 × 2.5 px dot is never the intended UI.
|
|
50
|
+
* The label-only props a pure-indicator dot forbids. An invisible remove
|
|
51
|
+
* button, counter shape, or hover-scale on a 2.5 × 2.5 px dot is never the
|
|
52
|
+
* intended UI, so every dot arm excludes them at the type level.
|
|
59
53
|
*/
|
|
60
|
-
interface
|
|
61
|
-
/** Visual variant. `dot` renders a pure indicator (content hidden); the label variants accept the full surface. */
|
|
62
|
-
variant: 'dot';
|
|
54
|
+
interface BadgeDotForbiddenProps {
|
|
63
55
|
children?: never;
|
|
64
56
|
counter?: never;
|
|
65
57
|
removable?: never;
|
|
66
58
|
interactive?: never;
|
|
67
59
|
onRemove?: never;
|
|
68
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* Dot badge selected by the canonical `purpose="dot"` — a pure indicator whose
|
|
63
|
+
* content is hidden. `variant` is inert here (the dot look always wins), and
|
|
64
|
+
* the label-only props are excluded by {@link BadgeDotForbiddenProps}.
|
|
65
|
+
*/
|
|
66
|
+
interface BadgeDotByPurposeProps extends BadgeBaseProps, BadgeDotForbiddenProps {
|
|
67
|
+
/**
|
|
68
|
+
* The canonical dot spelling — forces the pure-indicator look regardless of
|
|
69
|
+
* `variant`. For the label roles use `status` / `tag` / `counter` / `chip`
|
|
70
|
+
* (their own arm).
|
|
71
|
+
*/
|
|
72
|
+
purpose: 'dot';
|
|
73
|
+
/** Inert under `purpose="dot"`; accepted only so a leftover `variant` compiles. */
|
|
74
|
+
variant?: 'filled' | 'outlined' | 'soft' | 'dot';
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Dot badge selected by the deprecated `variant="dot"`. Prefer `purpose="dot"`.
|
|
78
|
+
* Same exclusions as {@link BadgeDotByPurposeProps}.
|
|
79
|
+
*/
|
|
80
|
+
interface BadgeDotProps extends BadgeBaseProps, BadgeDotForbiddenProps {
|
|
81
|
+
/** Visual variant. `dot` renders a pure indicator (content hidden); the label variants accept the full surface. */
|
|
82
|
+
variant: 'dot';
|
|
83
|
+
/** Only the dot purpose is non-contradictory with `variant="dot"`. */
|
|
84
|
+
purpose?: 'dot';
|
|
85
|
+
}
|
|
69
86
|
/**
|
|
70
87
|
* Label-style badge — accepts content, counter shape, remove button, etc.
|
|
71
88
|
*/
|
|
72
89
|
interface BadgeStandardProps extends BadgeBaseProps {
|
|
90
|
+
/**
|
|
91
|
+
* The badge's semantic purpose — the canonical axis that resolves what the
|
|
92
|
+
* badge *is*, since a bare Badge served overlapping roles. Orchestrates the
|
|
93
|
+
* low-level visual props so you rarely set them directly:
|
|
94
|
+
* - `status` — a state marker (Active, Failed); pairs with `intent`.
|
|
95
|
+
* - `tag` — a neutral inline label (category, version).
|
|
96
|
+
* - `counter` — a compact numeric pill (replaces the `counter` boolean).
|
|
97
|
+
* - `chip` — a removable, interactive filter chip; pair with `removable`.
|
|
98
|
+
*
|
|
99
|
+
* For a pure indicator use `purpose="dot"` (its own arm — it forbids
|
|
100
|
+
* content / counter / remove). Leave unset to drive the badge purely by the
|
|
101
|
+
* low-level props (back-compat); when set, `purpose` wins over the `counter`
|
|
102
|
+
* boolean.
|
|
103
|
+
*/
|
|
104
|
+
purpose?: 'status' | 'tag' | 'counter' | 'chip';
|
|
73
105
|
/** Visual variant. `dot` renders a pure indicator (content hidden); the label variants accept the full surface. @default 'filled' */
|
|
74
106
|
variant?: 'filled' | 'outlined' | 'soft';
|
|
75
107
|
/** Badge content (text, icons, numbers). */
|
|
@@ -93,10 +125,13 @@ interface BadgeStandardProps extends BadgeBaseProps {
|
|
|
93
125
|
* @related Alert
|
|
94
126
|
* @related Toast
|
|
95
127
|
*
|
|
96
|
-
* Badge props are a discriminated union
|
|
97
|
-
*
|
|
98
|
-
* `
|
|
99
|
-
*
|
|
128
|
+
* Badge props are a discriminated union. The pure-indicator dot — spelled
|
|
129
|
+
* canonically as `purpose="dot"` or via the deprecated `variant="dot"` —
|
|
130
|
+
* forbids `children` / `counter` / `removable` / `interactive` / `onRemove`
|
|
131
|
+
* at the type level, while the label arms (`filled` / `outlined` / `soft`;
|
|
132
|
+
* `purpose` `status` / `tag` / `counter` / `chip`) accept the full surface.
|
|
133
|
+
* An interactive badge (`onclick` or `purpose="chip"`) is announced as a
|
|
134
|
+
* `button`, a static one as `status` (override via `role`).
|
|
100
135
|
*
|
|
101
136
|
* @example Purpose-driven (canonical) — the intent reads from `purpose`
|
|
102
137
|
* ```svelte
|
|
@@ -114,6 +149,6 @@ interface BadgeStandardProps extends BadgeBaseProps {
|
|
|
114
149
|
* </div>
|
|
115
150
|
* ```
|
|
116
151
|
*/
|
|
117
|
-
export type BadgeProps = BadgeDotProps | BadgeStandardProps;
|
|
152
|
+
export type BadgeProps = BadgeDotByPurposeProps | BadgeDotProps | BadgeStandardProps;
|
|
118
153
|
export { default as Badge } from './Badge.svelte';
|
|
119
154
|
export { type BadgePlacement, type BadgeVariants, badgeVariants, PLACEMENT_VALUES } from './badge.variants.js';
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { buttonGroupVariants, type ButtonGroupVariants } from '..';
|
|
3
3
|
import { getBlocksConfig, resolveSlotClasses } from '../../provider';
|
|
4
|
+
import { edgeEnabledIndex, nextEnabledIndex } from '../../utils';
|
|
4
5
|
import { getTierContext, setTierContext } from '../../utils/tier-context';
|
|
5
6
|
import type { ButtonGroupContext, ButtonGroupProps } from './index';
|
|
6
7
|
import { setButtonGroupContext } from './buttonGroup.context';
|
|
@@ -65,7 +66,15 @@
|
|
|
65
66
|
}
|
|
66
67
|
});
|
|
67
68
|
|
|
69
|
+
// Child Button values in registration (= document) order. Buttons register
|
|
70
|
+
// during their render pass (before any effect runs), so this is fully
|
|
71
|
+
// populated in DOM order by the time roving reads it — it lets the radiogroup
|
|
72
|
+
// map the reactive selection back to a radio's position without the shared
|
|
73
|
+
// <Button> exposing its value in the DOM.
|
|
74
|
+
const buttonOrder: string[] = [];
|
|
75
|
+
|
|
68
76
|
function registerButton(buttonValue: string | undefined) {
|
|
77
|
+
if (buttonValue && !buttonOrder.includes(buttonValue)) buttonOrder.push(buttonValue);
|
|
69
78
|
return {
|
|
70
79
|
get isSelected() {
|
|
71
80
|
return buttonValue ? selectedValues.has(buttonValue) : false;
|
|
@@ -91,8 +100,14 @@
|
|
|
91
100
|
onSelectionChange?.(value, Array.from(selectedValues));
|
|
92
101
|
},
|
|
93
102
|
getButtonProps() {
|
|
94
|
-
|
|
95
|
-
|
|
103
|
+
// A value-less Button is an action button, not a selection option, so it
|
|
104
|
+
// gets no radio/checkbox role. This also keeps `rovingRadios()` (which
|
|
105
|
+
// collects `[role="radio"]`) index-aligned with `buttonOrder` (gated on
|
|
106
|
+
// `buttonValue`): a value-less radio would otherwise sit in the roving
|
|
107
|
+
// array but not the order registry, drifting the two index spaces and
|
|
108
|
+
// misplacing the tab stop / arrow-nav origin.
|
|
109
|
+
if (selection === 'none' || !buttonValue) return {};
|
|
110
|
+
const checked = selectedValues.has(buttonValue);
|
|
96
111
|
return {
|
|
97
112
|
role: selection === 'single' ? ('radio' as const) : ('checkbox' as const),
|
|
98
113
|
'aria-checked': checked
|
|
@@ -132,17 +147,119 @@
|
|
|
132
147
|
registerButton
|
|
133
148
|
});
|
|
134
149
|
|
|
150
|
+
let containerElement = $state<HTMLDivElement>();
|
|
151
|
+
|
|
152
|
+
// Single-select is a WAI-ARIA radiogroup: ONE tab stop, arrow keys move the
|
|
153
|
+
// selection (roving tabindex). Tab/SegmentGroup drive this from a value→element
|
|
154
|
+
// registry their own item components populate; ButtonGroup's items are shared
|
|
155
|
+
// <Button>s (outside this component), so the radiogroup owns the roving from
|
|
156
|
+
// the container instead — it queries the radio *elements* from the DOM (for
|
|
157
|
+
// focus + disabled state), reads the *selected* one from reactive state (see
|
|
158
|
+
// selectedRadioIndex), reuses the shared index math (utils/roving), skips
|
|
159
|
+
// disabled radios, and drives selection by clicking the target radio (Button's
|
|
160
|
+
// own click handler does the rest). `multiple`/`none` keep every button
|
|
161
|
+
// natively tabbable (checkbox-group / plain toolbar convention — not roved).
|
|
162
|
+
function rovingRadios(): HTMLButtonElement[] {
|
|
163
|
+
return containerElement
|
|
164
|
+
? Array.from(containerElement.querySelectorAll<HTMLButtonElement>('[role="radio"]'))
|
|
165
|
+
: [];
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Position of the selected radio, read from the reactive selection (not the
|
|
169
|
+
// DOM's aria-checked) so it is correct even on the first paint — a parent
|
|
170
|
+
// effect can run before the child <Button>s commit their aria, so the DOM
|
|
171
|
+
// isn't a reliable source there. `buttonOrder` is in document order, matching
|
|
172
|
+
// the radios queried above.
|
|
173
|
+
function selectedRadioIndex(): number {
|
|
174
|
+
for (let i = 0; i < buttonOrder.length; i++) {
|
|
175
|
+
if (selectedValues.has(buttonOrder[i])) return i;
|
|
176
|
+
}
|
|
177
|
+
return -1;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
$effect(() => {
|
|
181
|
+
if (selection !== 'single' || !containerElement) return;
|
|
182
|
+
const radios = rovingRadios();
|
|
183
|
+
if (radios.length === 0) return;
|
|
184
|
+
|
|
185
|
+
const checkedIndex = selectedRadioIndex();
|
|
186
|
+
// Nothing selected yet → the first enabled radio holds the tab stop, so the
|
|
187
|
+
// group stays reachable with Tab (standard radiogroup entry behaviour).
|
|
188
|
+
const activeIndex =
|
|
189
|
+
checkedIndex >= 0
|
|
190
|
+
? checkedIndex
|
|
191
|
+
: edgeEnabledIndex(radios.length, 1, (i) => radios[i].disabled);
|
|
192
|
+
|
|
193
|
+
radios.forEach((radio, i) => {
|
|
194
|
+
radio.tabIndex = i === activeIndex ? 0 : -1;
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// Restore native tabbability when the group stops roving (selection flips away
|
|
198
|
+
// from single, or it unmounts) — otherwise the imposed -1 would strand the
|
|
199
|
+
// buttons out of the tab order.
|
|
200
|
+
return () => {
|
|
201
|
+
for (const radio of radios) radio.removeAttribute('tabindex');
|
|
202
|
+
};
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// A disabled radio renders a `<button disabled>`, which can't hold focus, so
|
|
206
|
+
// arrow navigation must step over it — otherwise selection strands on an
|
|
207
|
+
// unfocusable radio (aria-checked set, focus stuck on the previous one). The
|
|
208
|
+
// index math lives in the shared roving helpers (utils/roving).
|
|
209
|
+
function handleKeyDown(event: KeyboardEvent) {
|
|
210
|
+
if (disabled || selection !== 'single') return;
|
|
211
|
+
|
|
212
|
+
const radios = rovingRadios();
|
|
213
|
+
if (radios.length === 0) return;
|
|
214
|
+
|
|
215
|
+
const currentIndex = selectedRadioIndex();
|
|
216
|
+
const isDisabled = (i: number) => radios[i].disabled;
|
|
217
|
+
let newIndex: number;
|
|
218
|
+
|
|
219
|
+
switch (event.key) {
|
|
220
|
+
case 'ArrowRight':
|
|
221
|
+
case 'ArrowDown':
|
|
222
|
+
event.preventDefault();
|
|
223
|
+
newIndex = nextEnabledIndex(radios.length, currentIndex, 1, isDisabled);
|
|
224
|
+
break;
|
|
225
|
+
case 'ArrowLeft':
|
|
226
|
+
case 'ArrowUp':
|
|
227
|
+
event.preventDefault();
|
|
228
|
+
newIndex = nextEnabledIndex(radios.length, currentIndex, -1, isDisabled);
|
|
229
|
+
break;
|
|
230
|
+
case 'Home':
|
|
231
|
+
event.preventDefault();
|
|
232
|
+
newIndex = edgeEnabledIndex(radios.length, 1, isDisabled);
|
|
233
|
+
break;
|
|
234
|
+
case 'End':
|
|
235
|
+
event.preventDefault();
|
|
236
|
+
newIndex = edgeEnabledIndex(radios.length, -1, isDisabled);
|
|
237
|
+
break;
|
|
238
|
+
default:
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (newIndex !== currentIndex && newIndex >= 0) {
|
|
243
|
+
const target = radios[newIndex];
|
|
244
|
+
target.click(); // Button's own click handler performs the selection
|
|
245
|
+
target.focus();
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
135
249
|
const ariaRole = $derived(selection === 'single' ? 'radiogroup' : 'group');
|
|
136
250
|
</script>
|
|
137
251
|
|
|
138
252
|
<div
|
|
253
|
+
bind:this={containerElement}
|
|
139
254
|
role={ariaRole}
|
|
140
255
|
class={unstyled
|
|
141
256
|
? [slotClasses?.base, className].filter(Boolean).join(' ')
|
|
142
257
|
: styles.base({ class: [slotClasses?.base, className] })}
|
|
143
258
|
aria-label={ariaLabel}
|
|
144
259
|
aria-labelledby={ariaLabelledBy}
|
|
145
|
-
aria-
|
|
260
|
+
aria-orientation={orientation}
|
|
261
|
+
aria-disabled={disabled || undefined}
|
|
262
|
+
onkeydown={handleKeyDown}
|
|
146
263
|
{...restProps}
|
|
147
264
|
>
|
|
148
265
|
{@render children?.()}
|
|
@@ -66,10 +66,10 @@ export const cardVariants = tv({
|
|
|
66
66
|
footer: 'pt-5'
|
|
67
67
|
}
|
|
68
68
|
},
|
|
69
|
-
// Opt-in slot separators. Default `false
|
|
69
|
+
// Opt-in slot separators. Default `false`: header/footer
|
|
70
70
|
// sit flush against the body, the slots are separated by spacing
|
|
71
71
|
// only. Set `dividers={true}` for traditional card-with-header look.
|
|
72
|
-
// Uses `border-hairline`
|
|
72
|
+
// Uses `border-hairline` — leiser als border-subtle.
|
|
73
73
|
dividers: {
|
|
74
74
|
true: {
|
|
75
75
|
header: 'border-b border-border-hairline',
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
unstyled: unstyledProp = false,
|
|
33
33
|
slotClasses: slotClassesProp = {},
|
|
34
34
|
preset,
|
|
35
|
+
'aria-describedby': ariaDescribedby,
|
|
35
36
|
...restProps
|
|
36
37
|
}: CheckboxProps = $props();
|
|
37
38
|
|
|
@@ -51,6 +52,14 @@
|
|
|
51
52
|
disabled
|
|
52
53
|
}));
|
|
53
54
|
|
|
55
|
+
// Consumer-supplied `aria-describedby` (e.g. an external hint rendered
|
|
56
|
+
// outside the component) merges with the internal error/helper chain instead
|
|
57
|
+
// of replacing it — internal descriptions first, the consumer's supplemental
|
|
58
|
+
// one last (mirrors the Input role model, XC-2).
|
|
59
|
+
const describedBy = $derived(
|
|
60
|
+
[ff.describedBy, ariaDescribedby].filter(Boolean).join(' ') || undefined
|
|
61
|
+
);
|
|
62
|
+
|
|
54
63
|
const blocksConfig = getBlocksConfig();
|
|
55
64
|
const unstyled = $derived(unstyledProp || blocksConfig?.unstyled || false);
|
|
56
65
|
|
|
@@ -117,6 +126,7 @@
|
|
|
117
126
|
for={id}
|
|
118
127
|
>
|
|
119
128
|
<input
|
|
129
|
+
{...restProps}
|
|
120
130
|
bind:this={inputRef}
|
|
121
131
|
{id}
|
|
122
132
|
type="checkbox"
|
|
@@ -128,9 +138,8 @@
|
|
|
128
138
|
class="peer sr-only"
|
|
129
139
|
aria-checked={indeterminate ? 'mixed' : undefined}
|
|
130
140
|
aria-invalid={ff.invalid ? 'true' : undefined}
|
|
131
|
-
aria-describedby={
|
|
141
|
+
aria-describedby={describedBy}
|
|
132
142
|
onchange={handleChange}
|
|
133
|
-
{...restProps}
|
|
134
143
|
/>
|
|
135
144
|
|
|
136
145
|
<span
|
|
@@ -56,6 +56,8 @@
|
|
|
56
56
|
slotClasses: slotClassesProp = {},
|
|
57
57
|
preset,
|
|
58
58
|
id: idProp,
|
|
59
|
+
'aria-describedby': ariaDescribedby,
|
|
60
|
+
'aria-label': ariaLabel,
|
|
59
61
|
...restProps
|
|
60
62
|
}: ComboboxProps<T> = $props();
|
|
61
63
|
|
|
@@ -80,6 +82,15 @@
|
|
|
80
82
|
disabled
|
|
81
83
|
}));
|
|
82
84
|
|
|
85
|
+
// Consumer-supplied `aria-describedby` (e.g. an external hint) merges with the
|
|
86
|
+
// internal error/helper chain instead of being dropped — restProps land on the
|
|
87
|
+
// wrapper div, so without this the description would never reach the focusable
|
|
88
|
+
// input. Internal descriptions first, the consumer's supplemental one last
|
|
89
|
+
// (mirrors Select + Input).
|
|
90
|
+
const describedBy = $derived(
|
|
91
|
+
[ff.describedBy, ariaDescribedby].filter(Boolean).join(' ') || undefined
|
|
92
|
+
);
|
|
93
|
+
|
|
83
94
|
const blocksConfig = getBlocksConfig();
|
|
84
95
|
const unstyled = $derived(unstyledProp || blocksConfig?.unstyled || false);
|
|
85
96
|
|
|
@@ -207,6 +218,18 @@
|
|
|
207
218
|
filteredGroups ? filteredGroups.flatMap((g) => g.options) : allOptions.filter(matchesQuery)
|
|
208
219
|
);
|
|
209
220
|
|
|
221
|
+
// Flat index of each option within `filtered`, precomputed so the grouped
|
|
222
|
+
// render path reads an option's keyboard-cursor index in O(1) instead of
|
|
223
|
+
// `filtered.indexOf(opt)` per option (O(n²) per keystroke on a large grouped
|
|
224
|
+
// list). Same first-occurrence semantics as `indexOf`.
|
|
225
|
+
const filteredIndexByOption = $derived.by(() => {
|
|
226
|
+
const map = new Map<ComboboxOption<T>, number>();
|
|
227
|
+
filtered.forEach((opt, i) => {
|
|
228
|
+
if (!map.has(opt)) map.set(opt, i);
|
|
229
|
+
});
|
|
230
|
+
return map;
|
|
231
|
+
});
|
|
232
|
+
|
|
210
233
|
// Debounced query runner. The effect tracks `query` (+ debounceMs); each change
|
|
211
234
|
// resets the timer, and the fetch runs a tick later so bursty typing collapses
|
|
212
235
|
// into one request. A per-run AbortController lets a newer query cancel an
|
|
@@ -750,7 +773,7 @@
|
|
|
750
773
|
{noResultsText}
|
|
751
774
|
</div>
|
|
752
775
|
{:else if filteredGroups}
|
|
753
|
-
{#each filteredGroups as group (group.label)}
|
|
776
|
+
{#each filteredGroups as group, i (`${group.label}-${i}`)}
|
|
754
777
|
<div
|
|
755
778
|
class={unstyled
|
|
756
779
|
? (slotClasses?.group ?? '')
|
|
@@ -766,7 +789,7 @@
|
|
|
766
789
|
{group.label}
|
|
767
790
|
</div>
|
|
768
791
|
{#each group.options as opt (opt.value)}
|
|
769
|
-
{@render optionButton(opt,
|
|
792
|
+
{@render optionButton(opt, filteredIndexByOption.get(opt) ?? -1)}
|
|
770
793
|
{/each}
|
|
771
794
|
</div>
|
|
772
795
|
{/each}
|
|
@@ -855,6 +878,14 @@
|
|
|
855
878
|
wiring lives in one place. `required` is single-mode only — in multi the input
|
|
856
879
|
is transient search text (cleared after each pick), so a native `required` on
|
|
857
880
|
it would wrongly block submit even with tags selected.
|
|
881
|
+
|
|
882
|
+
A consumer `aria-label` is forwarded onto the input (destructured out of
|
|
883
|
+
restProps so it never lands on the role-less wrapper `<div>` — axe
|
|
884
|
+
aria-prohibited-attr), but ONLY when no visible `label` renders. Unlike Select —
|
|
885
|
+
which names its trigger via `aria-labelledby` (HIGHER ARIA precedence than
|
|
886
|
+
aria-label) — Combobox names the input via a native `<label for>` (LOWER
|
|
887
|
+
precedence than aria-label), so an unconditional aria-label would override a
|
|
888
|
+
visible label. Gating on `label` keeps the visible label authoritative.
|
|
858
889
|
-->
|
|
859
890
|
{#snippet searchField(cls: string)}
|
|
860
891
|
<input
|
|
@@ -867,7 +898,8 @@
|
|
|
867
898
|
aria-activedescendant={activeDescendant}
|
|
868
899
|
aria-autocomplete="list"
|
|
869
900
|
aria-invalid={ff.invalid ? 'true' : undefined}
|
|
870
|
-
aria-describedby={
|
|
901
|
+
aria-describedby={describedBy}
|
|
902
|
+
aria-label={label ? undefined : ariaLabel}
|
|
871
903
|
autocomplete="off"
|
|
872
904
|
class={cls}
|
|
873
905
|
placeholder={effectivePlaceholder}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { HTMLDialogAttributes } from 'svelte/elements';
|
|
2
3
|
import type { DialogIntent } from '../Dialog/index.js';
|
|
3
4
|
import type { DialogSlots } from '../Dialog/dialog.variants.js';
|
|
4
5
|
/**
|
|
@@ -36,7 +37,7 @@ export type ConfirmIntent = Exclude<DialogIntent, 'neutral'>;
|
|
|
36
37
|
* />
|
|
37
38
|
* ```
|
|
38
39
|
*/
|
|
39
|
-
export interface ConfirmDialogProps {
|
|
40
|
+
export interface ConfirmDialogProps extends Omit<HTMLDialogAttributes, 'children' | 'open' | 'title' | 'draggable'> {
|
|
40
41
|
/** Controls visibility. Supports bind:open. */
|
|
41
42
|
open?: boolean;
|
|
42
43
|
/** Heading shown in the dialog header. */
|
|
@@ -41,10 +41,10 @@ export interface DialogProps extends Omit<HTMLDialogAttributes, 'children' | 'op
|
|
|
41
41
|
/** Vertical placement within the viewport. Use 'top' for command-palette style positioning. @default 'center' */
|
|
42
42
|
placement?: DialogVariants['placement'];
|
|
43
43
|
/**
|
|
44
|
-
* Semantic purpose marker (e.g. `danger` for destructive actions).
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
44
|
+
* Semantic purpose marker (e.g. `danger` for destructive actions). The
|
|
45
|
+
* Dialog itself no longer paints an accent bar — the value is exposed on
|
|
46
|
+
* the panel as `data-intent="…"` so consumers can hook presets, CSS
|
|
47
|
+
* overrides, or icon/title color via their own snippets.
|
|
48
48
|
* @default 'neutral'
|
|
49
49
|
*/
|
|
50
50
|
intent?: DialogVariants['intent'];
|