@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
|
@@ -4,7 +4,7 @@ export declare const stepperVariants: ((props?: {
|
|
|
4
4
|
orientation?: "horizontal" | "vertical" | undefined;
|
|
5
5
|
size?: "sm" | "md" | "lg" | undefined;
|
|
6
6
|
variant?: "default" | "outlined" | "minimal" | undefined;
|
|
7
|
-
stepState?: "
|
|
7
|
+
stepState?: "error" | "warning" | "active" | "inactive" | "complete" | undefined;
|
|
8
8
|
clickable?: boolean | undefined;
|
|
9
9
|
stepDisabled?: boolean | undefined;
|
|
10
10
|
separatorComplete?: boolean | undefined;
|
|
@@ -14,7 +14,7 @@ export declare const stepperVariants: ((props?: {
|
|
|
14
14
|
orientation?: "horizontal" | "vertical" | undefined;
|
|
15
15
|
size?: "sm" | "md" | "lg" | undefined;
|
|
16
16
|
variant?: "default" | "outlined" | "minimal" | undefined;
|
|
17
|
-
stepState?: "
|
|
17
|
+
stepState?: "error" | "warning" | "active" | "inactive" | "complete" | undefined;
|
|
18
18
|
clickable?: boolean | undefined;
|
|
19
19
|
stepDisabled?: boolean | undefined;
|
|
20
20
|
separatorComplete?: boolean | undefined;
|
|
@@ -26,7 +26,7 @@ export declare const stepperVariants: ((props?: {
|
|
|
26
26
|
orientation?: "horizontal" | "vertical" | undefined;
|
|
27
27
|
size?: "sm" | "md" | "lg" | undefined;
|
|
28
28
|
variant?: "default" | "outlined" | "minimal" | undefined;
|
|
29
|
-
stepState?: "
|
|
29
|
+
stepState?: "error" | "warning" | "active" | "inactive" | "complete" | undefined;
|
|
30
30
|
clickable?: boolean | undefined;
|
|
31
31
|
stepDisabled?: boolean | undefined;
|
|
32
32
|
separatorComplete?: boolean | undefined;
|
|
@@ -38,7 +38,7 @@ export declare const stepperVariants: ((props?: {
|
|
|
38
38
|
orientation?: "horizontal" | "vertical" | undefined;
|
|
39
39
|
size?: "sm" | "md" | "lg" | undefined;
|
|
40
40
|
variant?: "default" | "outlined" | "minimal" | undefined;
|
|
41
|
-
stepState?: "
|
|
41
|
+
stepState?: "error" | "warning" | "active" | "inactive" | "complete" | undefined;
|
|
42
42
|
clickable?: boolean | undefined;
|
|
43
43
|
stepDisabled?: boolean | undefined;
|
|
44
44
|
separatorComplete?: boolean | undefined;
|
|
@@ -50,7 +50,7 @@ export declare const stepperVariants: ((props?: {
|
|
|
50
50
|
orientation?: "horizontal" | "vertical" | undefined;
|
|
51
51
|
size?: "sm" | "md" | "lg" | undefined;
|
|
52
52
|
variant?: "default" | "outlined" | "minimal" | undefined;
|
|
53
|
-
stepState?: "
|
|
53
|
+
stepState?: "error" | "warning" | "active" | "inactive" | "complete" | undefined;
|
|
54
54
|
clickable?: boolean | undefined;
|
|
55
55
|
stepDisabled?: boolean | undefined;
|
|
56
56
|
separatorComplete?: boolean | undefined;
|
|
@@ -62,7 +62,7 @@ export declare const stepperVariants: ((props?: {
|
|
|
62
62
|
orientation?: "horizontal" | "vertical" | undefined;
|
|
63
63
|
size?: "sm" | "md" | "lg" | undefined;
|
|
64
64
|
variant?: "default" | "outlined" | "minimal" | undefined;
|
|
65
|
-
stepState?: "
|
|
65
|
+
stepState?: "error" | "warning" | "active" | "inactive" | "complete" | undefined;
|
|
66
66
|
clickable?: boolean | undefined;
|
|
67
67
|
stepDisabled?: boolean | undefined;
|
|
68
68
|
separatorComplete?: boolean | undefined;
|
|
@@ -74,7 +74,7 @@ export declare const stepperVariants: ((props?: {
|
|
|
74
74
|
orientation?: "horizontal" | "vertical" | undefined;
|
|
75
75
|
size?: "sm" | "md" | "lg" | undefined;
|
|
76
76
|
variant?: "default" | "outlined" | "minimal" | undefined;
|
|
77
|
-
stepState?: "
|
|
77
|
+
stepState?: "error" | "warning" | "active" | "inactive" | "complete" | undefined;
|
|
78
78
|
clickable?: boolean | undefined;
|
|
79
79
|
stepDisabled?: boolean | undefined;
|
|
80
80
|
separatorComplete?: boolean | undefined;
|
|
@@ -86,7 +86,7 @@ export declare const stepperVariants: ((props?: {
|
|
|
86
86
|
orientation?: "horizontal" | "vertical" | undefined;
|
|
87
87
|
size?: "sm" | "md" | "lg" | undefined;
|
|
88
88
|
variant?: "default" | "outlined" | "minimal" | undefined;
|
|
89
|
-
stepState?: "
|
|
89
|
+
stepState?: "error" | "warning" | "active" | "inactive" | "complete" | undefined;
|
|
90
90
|
clickable?: boolean | undefined;
|
|
91
91
|
stepDisabled?: boolean | undefined;
|
|
92
92
|
separatorComplete?: boolean | undefined;
|
|
@@ -98,7 +98,7 @@ export declare const stepperVariants: ((props?: {
|
|
|
98
98
|
orientation?: "horizontal" | "vertical" | undefined;
|
|
99
99
|
size?: "sm" | "md" | "lg" | undefined;
|
|
100
100
|
variant?: "default" | "outlined" | "minimal" | undefined;
|
|
101
|
-
stepState?: "
|
|
101
|
+
stepState?: "error" | "warning" | "active" | "inactive" | "complete" | undefined;
|
|
102
102
|
clickable?: boolean | undefined;
|
|
103
103
|
stepDisabled?: boolean | undefined;
|
|
104
104
|
separatorComplete?: boolean | undefined;
|
|
@@ -110,7 +110,7 @@ export declare const stepperVariants: ((props?: {
|
|
|
110
110
|
orientation?: "horizontal" | "vertical" | undefined;
|
|
111
111
|
size?: "sm" | "md" | "lg" | undefined;
|
|
112
112
|
variant?: "default" | "outlined" | "minimal" | undefined;
|
|
113
|
-
stepState?: "
|
|
113
|
+
stepState?: "error" | "warning" | "active" | "inactive" | "complete" | undefined;
|
|
114
114
|
clickable?: boolean | undefined;
|
|
115
115
|
stepDisabled?: boolean | undefined;
|
|
116
116
|
separatorComplete?: boolean | undefined;
|
|
@@ -122,7 +122,7 @@ export declare const stepperVariants: ((props?: {
|
|
|
122
122
|
orientation?: "horizontal" | "vertical" | undefined;
|
|
123
123
|
size?: "sm" | "md" | "lg" | undefined;
|
|
124
124
|
variant?: "default" | "outlined" | "minimal" | undefined;
|
|
125
|
-
stepState?: "
|
|
125
|
+
stepState?: "error" | "warning" | "active" | "inactive" | "complete" | undefined;
|
|
126
126
|
clickable?: boolean | undefined;
|
|
127
127
|
stepDisabled?: boolean | undefined;
|
|
128
128
|
separatorComplete?: boolean | undefined;
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
slotClasses: slotClassesProp = {},
|
|
30
30
|
preset,
|
|
31
31
|
oninput: userOnInput,
|
|
32
|
+
'aria-describedby': ariaDescribedby,
|
|
32
33
|
...restProps
|
|
33
34
|
}: TextareaProps = $props();
|
|
34
35
|
|
|
@@ -50,6 +51,14 @@
|
|
|
50
51
|
disabled
|
|
51
52
|
}));
|
|
52
53
|
|
|
54
|
+
// Consumer-supplied `aria-describedby` (e.g. an external hint rendered
|
|
55
|
+
// outside the component) merges with the internal error/helper chain instead
|
|
56
|
+
// of replacing it — internal descriptions first, the consumer's supplemental
|
|
57
|
+
// one last (mirrors the Input role model, XC-2).
|
|
58
|
+
const describedBy = $derived(
|
|
59
|
+
[ff.describedBy, ariaDescribedby].filter(Boolean).join(' ') || undefined
|
|
60
|
+
);
|
|
61
|
+
|
|
53
62
|
const charCount = $derived(typeof value === 'string' ? value.length : 0);
|
|
54
63
|
const counterState = $derived.by(() => {
|
|
55
64
|
if (!maxlength) return 'normal' as const;
|
|
@@ -145,7 +154,7 @@
|
|
|
145
154
|
{readonly}
|
|
146
155
|
{required}
|
|
147
156
|
aria-invalid={ff.invalid ? 'true' : undefined}
|
|
148
|
-
aria-describedby={
|
|
157
|
+
aria-describedby={describedBy}
|
|
149
158
|
oninput={handleInput}></textarea>
|
|
150
159
|
|
|
151
160
|
{#if showFooter}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { useBlocksI18n } from '../..';
|
|
3
|
+
import { untrack } from 'svelte';
|
|
3
4
|
import { fly } from 'svelte/transition';
|
|
4
5
|
import { getOverlayMotion } from '../../utils';
|
|
5
6
|
import { toastVariants, type ToastVariants } from './toast.variants';
|
|
@@ -56,8 +57,74 @@
|
|
|
56
57
|
return toaster.registerSubscriber();
|
|
57
58
|
});
|
|
58
59
|
|
|
60
|
+
// Sonner-style hover-to-pause. The pointer or keyboard focus being anywhere in
|
|
61
|
+
// the toaster region freezes the whole visible stack (all auto-dismiss timers
|
|
62
|
+
// + the progress bars) and leaving resumes each from its remaining time.
|
|
63
|
+
//
|
|
64
|
+
// These are bubbling over/out + focusin/focusout, NOT pointerenter/leave: the
|
|
65
|
+
// container is `pointer-events-none` (so clicks fall through the gaps between
|
|
66
|
+
// toasts to the page), which means it never becomes a pointer target itself —
|
|
67
|
+
// only its `pointer-events-auto` toast children do. enter/leave don't bubble
|
|
68
|
+
// and so would never fire here; over/out bubble up from the toasts. The
|
|
69
|
+
// `relatedTarget` containment check collapses the noisy per-descendant out/blur
|
|
70
|
+
// events into a single "the pointer/focus actually left the region" signal.
|
|
71
|
+
let pointerInside = $state(false);
|
|
72
|
+
let focusInside = $state(false);
|
|
73
|
+
|
|
74
|
+
$effect(() => {
|
|
75
|
+
if (pointerInside || focusInside) toaster.pause();
|
|
76
|
+
else toaster.resume();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Safety net: if the Toaster unmounts while the stack is frozen (pointer/focus
|
|
80
|
+
// was still inside), un-freeze the singleton store so its timers aren't stranded.
|
|
81
|
+
$effect(() => {
|
|
82
|
+
return () => toaster.resume();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
function leftRegion(event: PointerEvent | FocusEvent) {
|
|
86
|
+
const next = event.relatedTarget as Node | null;
|
|
87
|
+
return !(event.currentTarget as HTMLElement).contains(next);
|
|
88
|
+
}
|
|
89
|
+
|
|
59
90
|
const visibleToasts = $derived(toaster.toasts.slice(-max));
|
|
60
91
|
|
|
92
|
+
// Stranded-pause guard. A toast removed from under the cursor (e.g. its own
|
|
93
|
+
// close button) does not reliably fire `pointerout`, so `pointerInside` can
|
|
94
|
+
// stay stuck `true` and freeze every remaining timer — worst case a fresh
|
|
95
|
+
// toast is then born paused and never counts down. Rather than trust the flag,
|
|
96
|
+
// when the rendered stack SHRINKS we re-derive containment from the real cursor
|
|
97
|
+
// position. The cursor is tracked from the region's own bubbling pointer events
|
|
98
|
+
// (the container is `pointer-events-none`, so these fire only over a toast
|
|
99
|
+
// child, which is exactly when the coords matter).
|
|
100
|
+
let containerEl: HTMLElement | undefined;
|
|
101
|
+
let pointerX = 0;
|
|
102
|
+
let pointerY = 0;
|
|
103
|
+
let prevVisibleCount = 0;
|
|
104
|
+
|
|
105
|
+
function reconcilePointerInside() {
|
|
106
|
+
// Only a stranded `true` needs rescuing; SSR-safe — effects/handlers never
|
|
107
|
+
// run on the server, and `document` is guarded regardless.
|
|
108
|
+
if (typeof document === 'undefined' || !pointerInside || !containerEl) return;
|
|
109
|
+
const hit = document.elementFromPoint(pointerX, pointerY);
|
|
110
|
+
const toastEl = (hit?.closest('[data-toast-id]') ?? null) as HTMLElement | null;
|
|
111
|
+
// Inside only if the cursor sits over a toast that is (a) in THIS region and
|
|
112
|
+
// (b) still live — not the just-removed one, whose node lingers through its
|
|
113
|
+
// fly-out outro.
|
|
114
|
+
pointerInside =
|
|
115
|
+
!!toastEl &&
|
|
116
|
+
containerEl.contains(toastEl) &&
|
|
117
|
+
visibleToasts.some((t) => t.id === toastEl.dataset.toastId);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
$effect(() => {
|
|
121
|
+
const count = visibleToasts.length;
|
|
122
|
+
untrack(() => {
|
|
123
|
+
if (count < prevVisibleCount) reconcilePointerInside();
|
|
124
|
+
prevVisibleCount = count;
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
61
128
|
const styles = $derived(
|
|
62
129
|
unstyled
|
|
63
130
|
? {
|
|
@@ -121,14 +188,37 @@
|
|
|
121
188
|
</script>
|
|
122
189
|
|
|
123
190
|
<div
|
|
191
|
+
bind:this={containerEl}
|
|
124
192
|
class={[slot('container'), className].filter(Boolean).join(' ')}
|
|
125
193
|
aria-live="polite"
|
|
126
194
|
aria-relevant="additions removals"
|
|
195
|
+
onpointerover={(e) => {
|
|
196
|
+
pointerX = e.clientX;
|
|
197
|
+
pointerY = e.clientY;
|
|
198
|
+
pointerInside = true;
|
|
199
|
+
}}
|
|
200
|
+
onpointermove={(e) => {
|
|
201
|
+
pointerX = e.clientX;
|
|
202
|
+
pointerY = e.clientY;
|
|
203
|
+
}}
|
|
204
|
+
onpointerout={(e) => {
|
|
205
|
+
if (leftRegion(e)) pointerInside = false;
|
|
206
|
+
}}
|
|
207
|
+
onfocusin={() => (focusInside = true)}
|
|
208
|
+
onfocusout={(e) => {
|
|
209
|
+
if (leftRegion(e)) focusInside = false;
|
|
210
|
+
}}
|
|
127
211
|
{...restProps}
|
|
128
212
|
>
|
|
129
213
|
{#each visibleToasts as toast (toast.id)}
|
|
130
214
|
{@const IntentIcon = INTENT_ICON_MAP[toast.intent] ?? INTENT_ICON_MAP.neutral}
|
|
131
|
-
<div
|
|
215
|
+
<div
|
|
216
|
+
class={slot('toast', toast.intent)}
|
|
217
|
+
role="alert"
|
|
218
|
+
aria-atomic="true"
|
|
219
|
+
data-toast-id={toast.id}
|
|
220
|
+
transition:fly={flyParams()}
|
|
221
|
+
>
|
|
132
222
|
{#if toast.loading}
|
|
133
223
|
<span class={slot('icon', toast.intent)}><Spinner size="sm" /></span>
|
|
134
224
|
{:else}
|
|
@@ -177,10 +267,20 @@
|
|
|
177
267
|
</button>
|
|
178
268
|
{/if}
|
|
179
269
|
|
|
180
|
-
{#if toast.showProgress && toast.duration > 0}
|
|
270
|
+
{#if toast.showProgress && Number.isFinite(toast.duration) && toast.duration > 0}
|
|
271
|
+
<!-- The countdown animation lives in the `<style>` rule below (not inline)
|
|
272
|
+
so `@media (prefers-reduced-motion: reduce)` can reach it — an inline
|
|
273
|
+
`animation` shorthand is invisible to media queries. Only the real
|
|
274
|
+
per-toast duration is passed inline, as a custom property.
|
|
275
|
+
`animation-play-state` (also inline, bound to the store's paused flag)
|
|
276
|
+
freezes the bar with the timer on hover/focus and resumes it in place;
|
|
277
|
+
inline wins over the rule's implicit `running`, and toggling only the
|
|
278
|
+
play-state never restarts the animation. -->
|
|
181
279
|
<div
|
|
182
|
-
class={slot('progress', toast.intent)}
|
|
183
|
-
style="
|
|
280
|
+
class={[slot('progress', toast.intent), 'blocks-toast-progress-bar']}
|
|
281
|
+
style="--toast-progress-duration: {toast.duration}ms; animation-play-state: {toaster.paused
|
|
282
|
+
? 'paused'
|
|
283
|
+
: 'running'};"
|
|
184
284
|
></div>
|
|
185
285
|
{/if}
|
|
186
286
|
</div>
|
|
@@ -196,4 +296,22 @@
|
|
|
196
296
|
width: 0%;
|
|
197
297
|
}
|
|
198
298
|
}
|
|
299
|
+
|
|
300
|
+
/* The countdown bar animates via this class (not an inline `animation`) so the
|
|
301
|
+
reduced-motion query below can reach it; the per-toast duration arrives as
|
|
302
|
+
the `--toast-progress-duration` custom property. `:global` keeps it off
|
|
303
|
+
Svelte's scoping, consistent with the global keyframe above. */
|
|
304
|
+
:global(.blocks-toast-progress-bar) {
|
|
305
|
+
animation: blocks-toast-progress var(--toast-progress-duration) linear forwards;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/* A countdown bar carries no information once it can't animate, so under
|
|
309
|
+
reduced motion we hide it outright rather than leave it frozen. This mirrors
|
|
310
|
+
interaction.css collapsing the motion tokens; the auto-dismiss timing itself
|
|
311
|
+
is unaffected. */
|
|
312
|
+
@media (prefers-reduced-motion: reduce) {
|
|
313
|
+
:global(.blocks-toast-progress-bar) {
|
|
314
|
+
display: none;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
199
317
|
</style>
|
|
@@ -14,6 +14,13 @@ import type { ToastPlacement } from './toast.variants.js';
|
|
|
14
14
|
declare class ToastStore {
|
|
15
15
|
toasts: ToastData[];
|
|
16
16
|
placement: ToastPlacement;
|
|
17
|
+
/**
|
|
18
|
+
* Whether the whole visible stack is currently frozen because the pointer or
|
|
19
|
+
* keyboard focus is inside the toaster region. Drives each progress bar's
|
|
20
|
+
* `animation-play-state`; the auto-dismiss timers freeze together with it. Set
|
|
21
|
+
* by `<Toaster>` via {@link pause} / {@link resume}.
|
|
22
|
+
*/
|
|
23
|
+
paused: boolean;
|
|
17
24
|
private timers;
|
|
18
25
|
private subscriberCount;
|
|
19
26
|
private hasWarnedNoSubscriber;
|
|
@@ -22,6 +29,15 @@ declare class ToastStore {
|
|
|
22
29
|
* the store knows whether a renderer is present. Returns an unsubscribe fn.
|
|
23
30
|
*/
|
|
24
31
|
registerSubscriber(): () => void;
|
|
32
|
+
/**
|
|
33
|
+
* Arm the auto-dismiss timer for `id` — or, if the stack is currently paused,
|
|
34
|
+
* pre-register it frozen so it starts counting when the stack resumes.
|
|
35
|
+
* Persistent toasts (`duration <= 0` or non-finite, e.g. `Infinity`, and the
|
|
36
|
+
* loading leg of a promise toast) get no timer at all.
|
|
37
|
+
*/
|
|
38
|
+
private armTimer;
|
|
39
|
+
/** Cancel and forget the timer for `id` (if any). */
|
|
40
|
+
private clearTimer;
|
|
25
41
|
/** Create a toast with full control over all options. Returns the toast ID for programmatic dismissal. */
|
|
26
42
|
add(input: ToastInput): string;
|
|
27
43
|
/**
|
|
@@ -50,8 +66,23 @@ declare class ToastStore {
|
|
|
50
66
|
promise<T>(promise: Promise<T>, opts: ToastPromiseOptions<T>): string;
|
|
51
67
|
/** Remove a single toast by ID. Clears its auto-dismiss timer. */
|
|
52
68
|
dismiss(id: string): void;
|
|
53
|
-
/** Remove all toasts
|
|
69
|
+
/** Remove all toasts, cancel their timers, and un-freeze the stack. */
|
|
54
70
|
clear(): void;
|
|
71
|
+
/**
|
|
72
|
+
* Freeze the whole visible stack (Sonner-style hover-to-pause): cancel every
|
|
73
|
+
* running auto-dismiss timer, banking the time each has left, and flag
|
|
74
|
+
* {@link paused} so the progress bars stop. Idempotent — `<Toaster>` calls it
|
|
75
|
+
* on pointer-enter / focus-in of the region. Persistent toasts and a promise's
|
|
76
|
+
* loading leg have no timer and are untouched; the banked remainders are
|
|
77
|
+
* restarted by {@link resume}.
|
|
78
|
+
*/
|
|
79
|
+
pause(): void;
|
|
80
|
+
/**
|
|
81
|
+
* Resume the stack: restart every frozen timer from its banked remaining time
|
|
82
|
+
* (not from the full duration) and clear {@link paused} so the progress bars
|
|
83
|
+
* run again. Idempotent — called on pointer-leave / focus-out of the region.
|
|
84
|
+
*/
|
|
85
|
+
resume(): void;
|
|
55
86
|
/** Shorthand for `add()` with `intent: 'info'`. Shows an info icon. */
|
|
56
87
|
info(title: string, opts?: ToastShorthandOpts): string;
|
|
57
88
|
/** Shorthand for `add()` with `intent: 'success'`. Shows a check icon. */
|
|
@@ -16,6 +16,13 @@ function uid() {
|
|
|
16
16
|
class ToastStore {
|
|
17
17
|
toasts = $state([]);
|
|
18
18
|
placement = $state('bottom-right');
|
|
19
|
+
/**
|
|
20
|
+
* Whether the whole visible stack is currently frozen because the pointer or
|
|
21
|
+
* keyboard focus is inside the toaster region. Drives each progress bar's
|
|
22
|
+
* `animation-play-state`; the auto-dismiss timers freeze together with it. Set
|
|
23
|
+
* by `<Toaster>` via {@link pause} / {@link resume}.
|
|
24
|
+
*/
|
|
25
|
+
paused = $state(false);
|
|
19
26
|
timers = new Map();
|
|
20
27
|
subscriberCount = 0;
|
|
21
28
|
hasWarnedNoSubscriber = false;
|
|
@@ -31,6 +38,34 @@ class ToastStore {
|
|
|
31
38
|
this.subscriberCount = Math.max(0, this.subscriberCount - 1);
|
|
32
39
|
};
|
|
33
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* Arm the auto-dismiss timer for `id` — or, if the stack is currently paused,
|
|
43
|
+
* pre-register it frozen so it starts counting when the stack resumes.
|
|
44
|
+
* Persistent toasts (`duration <= 0` or non-finite, e.g. `Infinity`, and the
|
|
45
|
+
* loading leg of a promise toast) get no timer at all.
|
|
46
|
+
*/
|
|
47
|
+
armTimer(id, duration) {
|
|
48
|
+
if (!Number.isFinite(duration) || duration <= 0)
|
|
49
|
+
return;
|
|
50
|
+
if (this.paused) {
|
|
51
|
+
// A toast that arrives while the user is hovering the stack starts frozen;
|
|
52
|
+
// resume() lights its timer once the pointer/focus leaves.
|
|
53
|
+
this.timers.set(id, { handle: undefined, remaining: duration, startedAt: 0 });
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
this.timers.set(id, {
|
|
57
|
+
handle: setTimeout(() => this.dismiss(id), duration),
|
|
58
|
+
remaining: duration,
|
|
59
|
+
startedAt: Date.now()
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
/** Cancel and forget the timer for `id` (if any). */
|
|
63
|
+
clearTimer(id) {
|
|
64
|
+
const entry = this.timers.get(id);
|
|
65
|
+
if (entry?.handle !== undefined)
|
|
66
|
+
clearTimeout(entry.handle);
|
|
67
|
+
this.timers.delete(id);
|
|
68
|
+
}
|
|
34
69
|
/** Create a toast with full control over all options. Returns the toast ID for programmatic dismissal. */
|
|
35
70
|
add(input) {
|
|
36
71
|
if (this.subscriberCount === 0 &&
|
|
@@ -56,9 +91,7 @@ class ToastStore {
|
|
|
56
91
|
loading: input.loading ?? false
|
|
57
92
|
};
|
|
58
93
|
this.toasts = [...this.toasts, toast];
|
|
59
|
-
|
|
60
|
-
this.timers.set(id, setTimeout(() => this.dismiss(id), toast.duration));
|
|
61
|
-
}
|
|
94
|
+
this.armTimer(id, toast.duration);
|
|
62
95
|
return id;
|
|
63
96
|
}
|
|
64
97
|
/**
|
|
@@ -73,14 +106,9 @@ class ToastStore {
|
|
|
73
106
|
return;
|
|
74
107
|
const next = { ...this.toasts[idx], ...input };
|
|
75
108
|
this.toasts = [...this.toasts.slice(0, idx), next, ...this.toasts.slice(idx + 1)];
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
this.timers.delete(id);
|
|
80
|
-
}
|
|
81
|
-
if (next.duration > 0) {
|
|
82
|
-
this.timers.set(id, setTimeout(() => this.dismiss(id), next.duration));
|
|
83
|
-
}
|
|
109
|
+
// Reset the auto-dismiss clock to the new duration (respecting a current pause).
|
|
110
|
+
this.clearTimer(id);
|
|
111
|
+
this.armTimer(id, next.duration);
|
|
84
112
|
}
|
|
85
113
|
/**
|
|
86
114
|
* Drive a toast through a promise's lifecycle (Sonner-style). Shows a
|
|
@@ -150,19 +178,56 @@ class ToastStore {
|
|
|
150
178
|
}
|
|
151
179
|
/** Remove a single toast by ID. Clears its auto-dismiss timer. */
|
|
152
180
|
dismiss(id) {
|
|
153
|
-
|
|
154
|
-
if (timer) {
|
|
155
|
-
clearTimeout(timer);
|
|
156
|
-
this.timers.delete(id);
|
|
157
|
-
}
|
|
181
|
+
this.clearTimer(id);
|
|
158
182
|
this.toasts = this.toasts.filter((t) => t.id !== id);
|
|
159
183
|
}
|
|
160
|
-
/** Remove all toasts
|
|
184
|
+
/** Remove all toasts, cancel their timers, and un-freeze the stack. */
|
|
161
185
|
clear() {
|
|
162
|
-
for (const
|
|
163
|
-
|
|
186
|
+
for (const entry of this.timers.values()) {
|
|
187
|
+
if (entry.handle !== undefined)
|
|
188
|
+
clearTimeout(entry.handle);
|
|
189
|
+
}
|
|
164
190
|
this.timers.clear();
|
|
165
191
|
this.toasts = [];
|
|
192
|
+
this.paused = false;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Freeze the whole visible stack (Sonner-style hover-to-pause): cancel every
|
|
196
|
+
* running auto-dismiss timer, banking the time each has left, and flag
|
|
197
|
+
* {@link paused} so the progress bars stop. Idempotent — `<Toaster>` calls it
|
|
198
|
+
* on pointer-enter / focus-in of the region. Persistent toasts and a promise's
|
|
199
|
+
* loading leg have no timer and are untouched; the banked remainders are
|
|
200
|
+
* restarted by {@link resume}.
|
|
201
|
+
*/
|
|
202
|
+
pause() {
|
|
203
|
+
if (this.paused)
|
|
204
|
+
return;
|
|
205
|
+
this.paused = true;
|
|
206
|
+
const now = Date.now();
|
|
207
|
+
for (const entry of this.timers.values()) {
|
|
208
|
+
if (entry.handle === undefined)
|
|
209
|
+
continue;
|
|
210
|
+
clearTimeout(entry.handle);
|
|
211
|
+
entry.remaining = Math.max(0, entry.remaining - (now - entry.startedAt));
|
|
212
|
+
entry.handle = undefined;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Resume the stack: restart every frozen timer from its banked remaining time
|
|
217
|
+
* (not from the full duration) and clear {@link paused} so the progress bars
|
|
218
|
+
* run again. Idempotent — called on pointer-leave / focus-out of the region.
|
|
219
|
+
*/
|
|
220
|
+
resume() {
|
|
221
|
+
if (!this.paused)
|
|
222
|
+
return;
|
|
223
|
+
this.paused = false;
|
|
224
|
+
const now = Date.now();
|
|
225
|
+
for (const [id, entry] of this.timers) {
|
|
226
|
+
if (entry.handle !== undefined)
|
|
227
|
+
continue;
|
|
228
|
+
entry.startedAt = now;
|
|
229
|
+
entry.handle = setTimeout(() => this.dismiss(id), entry.remaining);
|
|
230
|
+
}
|
|
166
231
|
}
|
|
167
232
|
/** Shorthand for `add()` with `intent: 'info'`. Shows an info icon. */
|
|
168
233
|
info(title, opts) {
|
|
@@ -1,70 +1,70 @@
|
|
|
1
1
|
import { type SlotNames, type VariantProps } from '../../utils/variants.js';
|
|
2
2
|
export declare const toastVariants: ((props?: {
|
|
3
|
-
intent?: "
|
|
3
|
+
intent?: "primary" | "info" | "success" | "warning" | "danger" | "neutral" | undefined;
|
|
4
4
|
placement?: "bottom-right" | "top-right" | "bottom-left" | "top-left" | "top-center" | "bottom-center" | undefined;
|
|
5
5
|
} | undefined) => {
|
|
6
6
|
container: (props?: ({
|
|
7
|
-
intent?: "
|
|
7
|
+
intent?: "primary" | "info" | "success" | "warning" | "danger" | "neutral" | undefined;
|
|
8
8
|
placement?: "bottom-right" | "top-right" | "bottom-left" | "top-left" | "top-center" | "bottom-center" | undefined;
|
|
9
9
|
} & {
|
|
10
10
|
class?: string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | /*elided*/ any | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined;
|
|
11
11
|
}) | undefined) => string;
|
|
12
12
|
toast: (props?: ({
|
|
13
|
-
intent?: "
|
|
13
|
+
intent?: "primary" | "info" | "success" | "warning" | "danger" | "neutral" | undefined;
|
|
14
14
|
placement?: "bottom-right" | "top-right" | "bottom-left" | "top-left" | "top-center" | "bottom-center" | undefined;
|
|
15
15
|
} & {
|
|
16
16
|
class?: string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | /*elided*/ any | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined;
|
|
17
17
|
}) | undefined) => string;
|
|
18
18
|
icon: (props?: ({
|
|
19
|
-
intent?: "
|
|
19
|
+
intent?: "primary" | "info" | "success" | "warning" | "danger" | "neutral" | undefined;
|
|
20
20
|
placement?: "bottom-right" | "top-right" | "bottom-left" | "top-left" | "top-center" | "bottom-center" | undefined;
|
|
21
21
|
} & {
|
|
22
22
|
class?: string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | /*elided*/ any | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined;
|
|
23
23
|
}) | undefined) => string;
|
|
24
24
|
content: (props?: ({
|
|
25
|
-
intent?: "
|
|
25
|
+
intent?: "primary" | "info" | "success" | "warning" | "danger" | "neutral" | undefined;
|
|
26
26
|
placement?: "bottom-right" | "top-right" | "bottom-left" | "top-left" | "top-center" | "bottom-center" | undefined;
|
|
27
27
|
} & {
|
|
28
28
|
class?: string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | /*elided*/ any | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined;
|
|
29
29
|
}) | undefined) => string;
|
|
30
30
|
title: (props?: ({
|
|
31
|
-
intent?: "
|
|
31
|
+
intent?: "primary" | "info" | "success" | "warning" | "danger" | "neutral" | undefined;
|
|
32
32
|
placement?: "bottom-right" | "top-right" | "bottom-left" | "top-left" | "top-center" | "bottom-center" | undefined;
|
|
33
33
|
} & {
|
|
34
34
|
class?: string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | /*elided*/ any | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined;
|
|
35
35
|
}) | undefined) => string;
|
|
36
36
|
description: (props?: ({
|
|
37
|
-
intent?: "
|
|
37
|
+
intent?: "primary" | "info" | "success" | "warning" | "danger" | "neutral" | undefined;
|
|
38
38
|
placement?: "bottom-right" | "top-right" | "bottom-left" | "top-left" | "top-center" | "bottom-center" | undefined;
|
|
39
39
|
} & {
|
|
40
40
|
class?: string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | /*elided*/ any | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined;
|
|
41
41
|
}) | undefined) => string;
|
|
42
42
|
actions: (props?: ({
|
|
43
|
-
intent?: "
|
|
43
|
+
intent?: "primary" | "info" | "success" | "warning" | "danger" | "neutral" | undefined;
|
|
44
44
|
placement?: "bottom-right" | "top-right" | "bottom-left" | "top-left" | "top-center" | "bottom-center" | undefined;
|
|
45
45
|
} & {
|
|
46
46
|
class?: string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | /*elided*/ any | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined;
|
|
47
47
|
}) | undefined) => string;
|
|
48
48
|
actionButton: (props?: ({
|
|
49
|
-
intent?: "
|
|
49
|
+
intent?: "primary" | "info" | "success" | "warning" | "danger" | "neutral" | undefined;
|
|
50
50
|
placement?: "bottom-right" | "top-right" | "bottom-left" | "top-left" | "top-center" | "bottom-center" | undefined;
|
|
51
51
|
} & {
|
|
52
52
|
class?: string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | /*elided*/ any | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined;
|
|
53
53
|
}) | undefined) => string;
|
|
54
54
|
cancelButton: (props?: ({
|
|
55
|
-
intent?: "
|
|
55
|
+
intent?: "primary" | "info" | "success" | "warning" | "danger" | "neutral" | undefined;
|
|
56
56
|
placement?: "bottom-right" | "top-right" | "bottom-left" | "top-left" | "top-center" | "bottom-center" | undefined;
|
|
57
57
|
} & {
|
|
58
58
|
class?: string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | /*elided*/ any | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined;
|
|
59
59
|
}) | undefined) => string;
|
|
60
60
|
dismissButton: (props?: ({
|
|
61
|
-
intent?: "
|
|
61
|
+
intent?: "primary" | "info" | "success" | "warning" | "danger" | "neutral" | undefined;
|
|
62
62
|
placement?: "bottom-right" | "top-right" | "bottom-left" | "top-left" | "top-center" | "bottom-center" | undefined;
|
|
63
63
|
} & {
|
|
64
64
|
class?: string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | /*elided*/ any | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined;
|
|
65
65
|
}) | undefined) => string;
|
|
66
66
|
progress: (props?: ({
|
|
67
|
-
intent?: "
|
|
67
|
+
intent?: "primary" | "info" | "success" | "warning" | "danger" | "neutral" | undefined;
|
|
68
68
|
placement?: "bottom-right" | "top-right" | "bottom-left" | "top-left" | "top-center" | "bottom-center" | undefined;
|
|
69
69
|
} & {
|
|
70
70
|
class?: string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | /*elided*/ any | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined;
|
|
@@ -28,6 +28,8 @@
|
|
|
28
28
|
slotClasses: slotClassesProp = {},
|
|
29
29
|
preset,
|
|
30
30
|
withBorder = false,
|
|
31
|
+
'aria-describedby': ariaDescribedby,
|
|
32
|
+
'aria-label': ariaLabel,
|
|
31
33
|
...restProps
|
|
32
34
|
}: ToggleProps = $props();
|
|
33
35
|
|
|
@@ -42,6 +44,14 @@
|
|
|
42
44
|
disabled
|
|
43
45
|
}));
|
|
44
46
|
|
|
47
|
+
// Consumer-supplied `aria-describedby` (e.g. an external hint rendered
|
|
48
|
+
// outside the component) merges with the internal error/helper chain instead
|
|
49
|
+
// of replacing it — internal descriptions first, the consumer's supplemental
|
|
50
|
+
// one last (mirrors the Input role model, XC-2).
|
|
51
|
+
const describedBy = $derived(
|
|
52
|
+
[ff.describedBy, ariaDescribedby].filter(Boolean).join(' ') || undefined
|
|
53
|
+
);
|
|
54
|
+
|
|
45
55
|
const blocksConfig = getBlocksConfig();
|
|
46
56
|
const unstyled = $derived(unstyledProp || blocksConfig?.unstyled || false);
|
|
47
57
|
|
|
@@ -101,7 +111,11 @@
|
|
|
101
111
|
: styles.control({ class: slotClasses?.control })}
|
|
102
112
|
for={ff.fieldId}
|
|
103
113
|
>
|
|
114
|
+
<!-- aria-label: a visible `label` wins (stays undefined); with no label a
|
|
115
|
+
consumer-supplied aria-label is preferred over the generic i18n fallback
|
|
116
|
+
so external labelling survives the restProps-first spread. -->
|
|
104
117
|
<input
|
|
118
|
+
{...restProps}
|
|
105
119
|
id={ff.fieldId}
|
|
106
120
|
type="checkbox"
|
|
107
121
|
role="switch"
|
|
@@ -112,11 +126,10 @@
|
|
|
112
126
|
{required}
|
|
113
127
|
class="peer sr-only"
|
|
114
128
|
aria-checked={checked}
|
|
115
|
-
aria-label={label ? undefined : bt('accessibility.toggle') || 'Toggle'}
|
|
116
|
-
aria-describedby={
|
|
129
|
+
aria-label={label ? undefined : (ariaLabel ?? (bt('accessibility.toggle') || 'Toggle'))}
|
|
130
|
+
aria-describedby={describedBy}
|
|
117
131
|
aria-invalid={ff.invalid ? 'true' : undefined}
|
|
118
132
|
onchange={handleChange}
|
|
119
|
-
{...restProps}
|
|
120
133
|
/>
|
|
121
134
|
|
|
122
135
|
<span
|