@urbicon-ui/blocks 6.23.0 → 6.25.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/README.md +14 -0
- package/dist/components/Calendar/CalendarTimeGrid.svelte +2 -1
- package/dist/components/Calendar/calendar.variants.js +5 -3
- package/dist/components/CommandPalette/CommandPalette.svelte +32 -16
- package/dist/components/CommandPalette/CommandPalette.svelte.d.ts +1 -1
- package/dist/components/CommandPalette/commandPalette.variants.d.ts +10 -0
- package/dist/components/CommandPalette/commandPalette.variants.js +9 -7
- package/dist/components/CommandPalette/index.d.ts +19 -0
- package/dist/components/CompositionBar/CompositionBar.svelte +1 -1
- package/dist/components/CompositionBar/composition-bar.variants.js +1 -1
- package/dist/components/Planner/planner.variants.js +1 -1
- package/dist/components/Sankey/sankey.variants.js +1 -1
- package/dist/components/Sparkline/Sparkline.svelte +10 -4
- package/dist/components/Sparkline/index.d.ts +9 -1
- package/dist/internal/charts/variants.js +2 -2
- package/dist/internal/date-grid/DateGridScaffold.svelte +9 -2
- package/dist/internal/date-grid/date-grid.svelte.d.ts +4 -2
- package/dist/internal/date-grid/date-grid.svelte.js +63 -7
- package/dist/primitives/Avatar/Avatar.svelte +38 -24
- package/dist/primitives/Avatar/index.d.ts +1 -1
- package/dist/primitives/Badge/Badge.svelte +8 -2
- package/dist/primitives/Badge/index.d.ts +67 -32
- package/dist/primitives/Button/Button.svelte +12 -25
- package/dist/primitives/ButtonGroup/ButtonGroup.svelte +5 -1
- package/dist/primitives/Checkbox/Checkbox.svelte +1 -1
- package/dist/primitives/Collapsible/Collapsible.svelte +10 -0
- package/dist/primitives/Collapsible/index.d.ts +13 -2
- package/dist/primitives/ConfirmDialog/ConfirmDialog.svelte +14 -0
- package/dist/primitives/ConfirmDialog/index.d.ts +15 -3
- package/dist/primitives/Dialog/Dialog.svelte +31 -8
- package/dist/primitives/Drawer/Drawer.svelte +29 -7
- package/dist/primitives/Input/Input.svelte +7 -1
- package/dist/primitives/JourneyTimeline/journey-timeline.variants.js +3 -3
- package/dist/primitives/Menu/Menu.svelte +8 -0
- package/dist/primitives/Menu/index.d.ts +11 -13
- package/dist/primitives/Pagination/Pagination.svelte +28 -15
- package/dist/primitives/Popover/Popover.svelte +107 -4
- package/dist/primitives/Popover/index.d.ts +15 -2
- package/dist/primitives/Popover/index.js +1 -1
- package/dist/primitives/Popover/popover.variants.d.ts +25 -0
- package/dist/primitives/Popover/popover.variants.js +35 -0
- package/dist/primitives/Progress/progress.variants.js +1 -1
- package/dist/primitives/RadioGroup/RadioGroup.svelte +6 -2
- package/dist/primitives/RadioGroup/RadioItem.svelte +1 -1
- package/dist/primitives/RadioGroup/radioGroup.variants.js +1 -1
- package/dist/primitives/Select/index.d.ts +8 -1
- package/dist/primitives/Slider/Slider.svelte +32 -3
- package/dist/primitives/Stepper/stepper.variants.js +1 -1
- package/dist/primitives/Textarea/Textarea.svelte +5 -1
- package/dist/primitives/Toast/Toaster.svelte +81 -7
- package/dist/primitives/Toggle/Toggle.svelte +6 -2
- package/dist/primitives/Tooltip/tooltip.variants.js +13 -2
- package/dist/style/foundation.css +30 -3
- package/dist/style/interaction.css +19 -2
- package/dist/style/semantic.css +72 -1
- package/dist/style/themes/neutral.css +10 -1
- package/dist/style/themes/ocean.css +5 -1
- package/dist/utils/compose-handlers.d.ts +72 -0
- package/dist/utils/compose-handlers.js +65 -0
- package/dist/utils/figma-token-export.js +1 -1
- package/dist/utils/guide.svelte.js +92 -29
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.js +1 -1
- package/dist/utils/overlay-tokens.d.ts +17 -0
- package/dist/utils/overlay-tokens.js +30 -0
- package/dist/utils/use-floating-panel.svelte.js +9 -3
- package/dist/utils/variants.js +5 -1
- package/package.json +3 -3
|
@@ -139,9 +139,10 @@
|
|
|
139
139
|
return () => handlePageChange(page);
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
-
// Computed state helpers
|
|
143
|
-
|
|
144
|
-
|
|
142
|
+
// Computed state helpers. Edge policy across all layouts is disabled-but-visible:
|
|
143
|
+
// Previous/Next stay mounted and go `disabled` at page 1 / N (never unmounted),
|
|
144
|
+
// so the arrow can't vanish from under the pointer (no layout shift, no focus
|
|
145
|
+
// loss). First/Last are a separate, redundancy-driven concern — see the markup.
|
|
145
146
|
const hasPreviousPage = $derived(currentPage > 1);
|
|
146
147
|
const hasNextPage = $derived(currentPage < totalPages);
|
|
147
148
|
|
|
@@ -220,19 +221,25 @@
|
|
|
220
221
|
{/if}
|
|
221
222
|
</div>
|
|
222
223
|
{:else if layout === 'navigation'}
|
|
223
|
-
<!-- Navigation Layout: Only Previous/Next
|
|
224
|
+
<!-- Navigation Layout: Only Previous/Next. Unified edge policy (matches the
|
|
225
|
+
table layout): both arrows stay mounted and go `disabled` at the boundary
|
|
226
|
+
(page 1 / N) rather than unmounting. This row is laid out `justify-between`,
|
|
227
|
+
so unmounting one arm would teleport the survivor across the whole bar
|
|
228
|
+
(start ↔ end) and drop focus — disabled-but-visible pins Previous left and
|
|
229
|
+
Next right. Prev/Next-only pagers therefore grey out the dead end here;
|
|
230
|
+
they do not disappear it. -->
|
|
224
231
|
<div
|
|
225
232
|
class={unstyled
|
|
226
233
|
? (slotClasses?.controls ?? '')
|
|
227
234
|
: styles.controls({ class: slotClasses?.controls })}
|
|
228
235
|
>
|
|
229
|
-
{#if showPreviousNext
|
|
236
|
+
{#if showPreviousNext}
|
|
230
237
|
<PaginationItem
|
|
231
238
|
{size}
|
|
232
239
|
{variant}
|
|
233
240
|
{intent}
|
|
234
241
|
{tier}
|
|
235
|
-
disabled={disabled || loading}
|
|
242
|
+
disabled={disabled || loading || !hasPreviousPage}
|
|
236
243
|
onPageClick={goToPrevious}
|
|
237
244
|
{mint}
|
|
238
245
|
>
|
|
@@ -242,15 +249,13 @@
|
|
|
242
249
|
{previousLabel}
|
|
243
250
|
{/if}
|
|
244
251
|
</PaginationItem>
|
|
245
|
-
{/if}
|
|
246
252
|
|
|
247
|
-
{#if showPreviousNext && hasNextPage}
|
|
248
253
|
<PaginationItem
|
|
249
254
|
{size}
|
|
250
255
|
{variant}
|
|
251
256
|
{intent}
|
|
252
257
|
{tier}
|
|
253
|
-
disabled={disabled || loading}
|
|
258
|
+
disabled={disabled || loading || !hasNextPage}
|
|
254
259
|
onPageClick={goToNext}
|
|
255
260
|
{mint}
|
|
256
261
|
>
|
|
@@ -276,7 +281,14 @@
|
|
|
276
281
|
? (slotClasses?.controls ?? '')
|
|
277
282
|
: styles.controls({ class: slotClasses?.controls })}
|
|
278
283
|
>
|
|
279
|
-
|
|
284
|
+
<!-- First / Last are gated by the ellipsis — by REDUNDANCY, not by the page
|
|
285
|
+
edge. `showStartEllipsis` is true only when page 1 sits OUTSIDE the visible
|
|
286
|
+
window; the moment it re-enters, page 1 is a directly-clickable number and
|
|
287
|
+
a "First" jump button would merely duplicate it. That predicate already
|
|
288
|
+
implies currentPage > 1, so (unlike Previous/Next) First/Last are never a
|
|
289
|
+
dead-end control needing disabled-but-visible: hiding one drops a duplicate,
|
|
290
|
+
not an edge stepper — no layout-shift/focus trap. -->
|
|
291
|
+
{#if showFirstLast && showStartEllipsis}
|
|
280
292
|
<PaginationItem
|
|
281
293
|
{size}
|
|
282
294
|
{variant}
|
|
@@ -294,13 +306,13 @@
|
|
|
294
306
|
</PaginationItem>
|
|
295
307
|
{/if}
|
|
296
308
|
|
|
297
|
-
{#if showPreviousNext
|
|
309
|
+
{#if showPreviousNext}
|
|
298
310
|
<PaginationItem
|
|
299
311
|
{size}
|
|
300
312
|
{variant}
|
|
301
313
|
{intent}
|
|
302
314
|
{tier}
|
|
303
|
-
disabled={disabled || loading}
|
|
315
|
+
disabled={disabled || loading || !hasPreviousPage}
|
|
304
316
|
onPageClick={goToPrevious}
|
|
305
317
|
{mint}
|
|
306
318
|
>
|
|
@@ -364,13 +376,13 @@
|
|
|
364
376
|
</div>
|
|
365
377
|
{/if}
|
|
366
378
|
|
|
367
|
-
{#if showPreviousNext
|
|
379
|
+
{#if showPreviousNext}
|
|
368
380
|
<PaginationItem
|
|
369
381
|
{size}
|
|
370
382
|
{variant}
|
|
371
383
|
{intent}
|
|
372
384
|
{tier}
|
|
373
|
-
disabled={disabled || loading}
|
|
385
|
+
disabled={disabled || loading || !hasNextPage}
|
|
374
386
|
onPageClick={goToNext}
|
|
375
387
|
{mint}
|
|
376
388
|
>
|
|
@@ -382,7 +394,8 @@
|
|
|
382
394
|
</PaginationItem>
|
|
383
395
|
{/if}
|
|
384
396
|
|
|
385
|
-
|
|
397
|
+
<!-- See the First-button note above: ellipsis-gated (redundancy), not edge-gated. -->
|
|
398
|
+
{#if showFirstLast && showEndEllipsis}
|
|
386
399
|
<PaginationItem
|
|
387
400
|
{size}
|
|
388
401
|
{variant}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { untrack } from 'svelte';
|
|
3
3
|
import { getBlocksConfig, resolveSlotClasses } from '../../provider';
|
|
4
|
-
import { useFloatingPanel, floatingPanelHidden } from '../../utils';
|
|
4
|
+
import { useFloatingPanel, floatingPanelHidden, maxTransitionDurationMs } from '../../utils';
|
|
5
5
|
import { popoverVariants } from './popover.variants';
|
|
6
6
|
import type { PopoverProps } from './index';
|
|
7
7
|
|
|
@@ -21,6 +21,9 @@
|
|
|
21
21
|
autoTrigger = true,
|
|
22
22
|
size = 'md',
|
|
23
23
|
|
|
24
|
+
transitionDuration,
|
|
25
|
+
transitionEasing,
|
|
26
|
+
|
|
24
27
|
onOpenChange,
|
|
25
28
|
onClickOutside: onClickOutsideProp,
|
|
26
29
|
onEscape: onEscapeProp,
|
|
@@ -47,11 +50,64 @@
|
|
|
47
50
|
resolveSlotClasses(blocksConfig, 'Popover', preset, { size }, slotClassesProp)
|
|
48
51
|
);
|
|
49
52
|
|
|
53
|
+
// Per-instance motion overrides. Set the shared popover CSS variables inline
|
|
54
|
+
// only when a prop is provided, so the unset default keeps inheriting the
|
|
55
|
+
// reduced-motion-aware token (mirrors Tooltip's transitionDuration path).
|
|
56
|
+
const popoverDurationInline = $derived(
|
|
57
|
+
transitionDuration != null ? `${transitionDuration}ms` : undefined
|
|
58
|
+
);
|
|
59
|
+
|
|
50
60
|
let internalTriggerElement = $state<HTMLElement | null>(null);
|
|
51
61
|
let popoverElement = $state<HTMLElement | null>(null);
|
|
52
62
|
|
|
53
63
|
const effectiveTriggerElement = $derived(triggerElement || internalTriggerElement);
|
|
54
64
|
|
|
65
|
+
// ── Exit-motion lag (ACC-3 rest) ───────────────────────────
|
|
66
|
+
//
|
|
67
|
+
// Closing used to tear three things down in one flush: `open` flips, the
|
|
68
|
+
// children block unmounts, and `useFloatingPanel` hides the panel. The CSS
|
|
69
|
+
// exit transition (see `popoverMotion` in popover.variants.ts) keeps the
|
|
70
|
+
// *panel element* painted via `allow-discrete`, but the children must
|
|
71
|
+
// outlive `open` or the panel fades out empty. `exiting` lags the teardown
|
|
72
|
+
// by the panel's actual computed transition duration — read from the live
|
|
73
|
+
// style so per-instance props, theme token overrides, and reduced motion all
|
|
74
|
+
// shorten it automatically. A zero duration (jsdom/node tests, browsers
|
|
75
|
+
// without the CSS, `unstyled` without rebuilt motion) tears down
|
|
76
|
+
// synchronously: exactly the pre-motion behaviour.
|
|
77
|
+
//
|
|
78
|
+
// `$effect.pre` matters: the flag must be `true` in the SAME flush that
|
|
79
|
+
// renders `open === false`, otherwise the children unmount one frame before
|
|
80
|
+
// the lag starts. The `prevOpenForExit` tracker (non-reactive, seeded via
|
|
81
|
+
// `untrack` like `prevPopoverMode` below) keeps the effect keyed on real
|
|
82
|
+
// transitions — without it, the `bind:this` assignment would re-run the
|
|
83
|
+
// effect on mount and flash a closed-by-default popover open for one lag.
|
|
84
|
+
const EXIT_MOTION_BUFFER_MS = 50;
|
|
85
|
+
let exiting = $state(false);
|
|
86
|
+
let exitTimer: ReturnType<typeof setTimeout> | undefined;
|
|
87
|
+
let prevOpenForExit = untrack(() => open);
|
|
88
|
+
$effect.pre(() => {
|
|
89
|
+
if (open === prevOpenForExit) return;
|
|
90
|
+
prevOpenForExit = open;
|
|
91
|
+
if (open) {
|
|
92
|
+
clearTimeout(exitTimer);
|
|
93
|
+
exiting = false;
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const ms = maxTransitionDurationMs(untrack(() => popoverElement));
|
|
97
|
+
if (ms <= 0) return;
|
|
98
|
+
exiting = true;
|
|
99
|
+
exitTimer = setTimeout(() => {
|
|
100
|
+
exiting = false;
|
|
101
|
+
}, ms + EXIT_MOTION_BUFFER_MS);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// While `exiting`, the panel is still fading: keep children mounted and (in
|
|
105
|
+
// the in-place modes) keep `display` un-hidden. `useFloatingPanel` stays on
|
|
106
|
+
// the raw `open` on purpose — `hidePopover()` fires immediately and the
|
|
107
|
+
// discrete transition carries the visual exit, so a re-open during the fade
|
|
108
|
+
// reverses smoothly instead of waiting out the lag.
|
|
109
|
+
const panelVisible = $derived(open || exiting);
|
|
110
|
+
|
|
55
111
|
// ── Floating UI positioning + native show/hide ─────────────
|
|
56
112
|
//
|
|
57
113
|
// Delegated to the shared `useFloatingPanel` helper — the same positioning
|
|
@@ -291,6 +347,27 @@
|
|
|
291
347
|
target?.focus();
|
|
292
348
|
}
|
|
293
349
|
|
|
350
|
+
// Un-arm `dismissedByTrigger` at the start of the NEXT pointer gesture,
|
|
351
|
+
// wherever it lands. Registered as a one-shot capture listener when the
|
|
352
|
+
// guard arms: capture order (document before trigger) guarantees a stale
|
|
353
|
+
// flag is cleared before any new arm, and `once` keeps at most one listener
|
|
354
|
+
// pending. This closes the aborted-click hole (pointerdown on the trigger,
|
|
355
|
+
// pointer released elsewhere → no click ever consumed the flag → the next
|
|
356
|
+
// trigger click was swallowed once).
|
|
357
|
+
function disarmDismissedByTrigger() {
|
|
358
|
+
dismissedByTrigger = false;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Unmount cleanup for the imperative leftovers: a pending exit-lag timer
|
|
362
|
+
// and a still-armed disarm listener. Dependency-free → runs once,
|
|
363
|
+
// teardown on destroy.
|
|
364
|
+
$effect(() => {
|
|
365
|
+
return () => {
|
|
366
|
+
clearTimeout(exitTimer);
|
|
367
|
+
document.removeEventListener('pointerdown', disarmDismissedByTrigger, true);
|
|
368
|
+
};
|
|
369
|
+
});
|
|
370
|
+
|
|
294
371
|
function handleTriggerPointerDown() {
|
|
295
372
|
// Arm the "this pointerdown already dismissed it" guard only in auto
|
|
296
373
|
// mode, where the browser's light dismiss really closes the popover
|
|
@@ -298,7 +375,15 @@
|
|
|
298
375
|
// re-opening it). In manual mode nothing light-dismisses — the click
|
|
299
376
|
// itself must toggle-close, so arming the guard there left the trigger
|
|
300
377
|
// unable to close its own popover.
|
|
301
|
-
if (open && popoverMode === 'auto')
|
|
378
|
+
if (open && popoverMode === 'auto') {
|
|
379
|
+
dismissedByTrigger = true;
|
|
380
|
+
// This event has already passed the document's capture phase, so the
|
|
381
|
+
// listener can only fire on a LATER pointerdown.
|
|
382
|
+
document.addEventListener('pointerdown', disarmDismissedByTrigger, {
|
|
383
|
+
capture: true,
|
|
384
|
+
once: true
|
|
385
|
+
});
|
|
386
|
+
}
|
|
302
387
|
}
|
|
303
388
|
|
|
304
389
|
function handleTriggerClick(event: MouseEvent) {
|
|
@@ -361,6 +446,20 @@
|
|
|
361
446
|
drops over a modal dialog (Codeberg #23).
|
|
362
447
|
-->
|
|
363
448
|
|
|
449
|
+
<!--
|
|
450
|
+
`data-state` sits after `{...restProps}` like the other load-bearing
|
|
451
|
+
attributes: it drives the enter/exit motion CSS (popoverMotion) and is the
|
|
452
|
+
documented styling hook for consumers rebuilding motion under `unstyled`.
|
|
453
|
+
`{#if panelVisible}` (not `open`) keeps the children mounted while the exit
|
|
454
|
+
transition plays; `style:display` follows the same lagged flag so the
|
|
455
|
+
in-place modes can fade out before they hide.
|
|
456
|
+
|
|
457
|
+
`inert` while closed (GuidePanel's pattern): the exit-fading children are
|
|
458
|
+
still mounted and displayed, so without it a Tab right after dismiss would
|
|
459
|
+
focus into the visually dismissed panel and Enter could re-fire a consumer
|
|
460
|
+
action; pointer-events-none (popoverMotion) only covers mouse/touch. Also
|
|
461
|
+
drops the fading subtree from the a11y tree immediately.
|
|
462
|
+
-->
|
|
364
463
|
<div
|
|
365
464
|
bind:this={popoverElement}
|
|
366
465
|
class={popoverClasses}
|
|
@@ -370,12 +469,16 @@
|
|
|
370
469
|
style:position={panel.strategy}
|
|
371
470
|
style:inset="auto"
|
|
372
471
|
style:margin="0"
|
|
373
|
-
style:display={floatingPanelHidden(panel,
|
|
472
|
+
style:display={floatingPanelHidden(panel, panelVisible) ? 'none' : null}
|
|
473
|
+
style:--blocks-popover-duration={popoverDurationInline}
|
|
474
|
+
style:--blocks-popover-easing={transitionEasing}
|
|
475
|
+
data-state={open ? 'open' : 'closed'}
|
|
476
|
+
inert={!open || undefined}
|
|
374
477
|
{role}
|
|
375
478
|
aria-modal={ariaModal || undefined}
|
|
376
479
|
{id}
|
|
377
480
|
>
|
|
378
|
-
{#if
|
|
481
|
+
{#if panelVisible}
|
|
379
482
|
{@render children()}
|
|
380
483
|
{/if}
|
|
381
484
|
</div>
|
|
@@ -70,6 +70,19 @@ export interface PopoverProps extends PopoverVariants, Omit<HTMLAttributes<HTMLD
|
|
|
70
70
|
open?: boolean;
|
|
71
71
|
/** When true (default), the trigger wrapper handles click and keyboard to toggle the popover. Set to `false` to manage `open` yourself. */
|
|
72
72
|
autoTrigger?: boolean;
|
|
73
|
+
/**
|
|
74
|
+
* Override the enter/exit fade duration in ms. Defaults to the shared token
|
|
75
|
+
* `--blocks-popover-duration` (150ms; collapses to 1ms under
|
|
76
|
+
* `prefers-reduced-motion`).
|
|
77
|
+
*/
|
|
78
|
+
transitionDuration?: number;
|
|
79
|
+
/**
|
|
80
|
+
* Override the enter/exit easing as a CSS `<easing-function>` (e.g.
|
|
81
|
+
* `'linear'`, `'ease-out'`, `'cubic-bezier(0.4,0,0.2,1)'`). Defaults to the
|
|
82
|
+
* token `--blocks-popover-easing`. A CSS string, not a JS easing fn, because
|
|
83
|
+
* the popover motion is a pure CSS transition.
|
|
84
|
+
*/
|
|
85
|
+
transitionEasing?: string;
|
|
73
86
|
/**
|
|
74
87
|
* Whether the popover closes on Escape key. Default `true`.
|
|
75
88
|
* Set to `false` for cases where Escape should be intercepted by an
|
|
@@ -98,7 +111,7 @@ export interface PopoverProps extends PopoverVariants, Omit<HTMLAttributes<HTMLD
|
|
|
98
111
|
onEscape?: () => void;
|
|
99
112
|
/** Extra classes merged onto the floating panel element. */
|
|
100
113
|
class?: string;
|
|
101
|
-
/** Strip all default tv() classes. Combine with `class` or `slotClasses` for full custom styling. */
|
|
114
|
+
/** Strip all default tv() classes (including the enter/exit motion). Combine with `class` or `slotClasses` for full custom styling; the panel always carries `data-state="open" | "closed"`, so custom motion can rebuild on that hook (see `popoverMotion` in popover.variants.ts for the reference implementation). */
|
|
102
115
|
unstyled?: boolean;
|
|
103
116
|
/** Per-slot class overrides. Available slots: `base` (the floating panel). */
|
|
104
117
|
slotClasses?: Partial<Record<'base', string>>;
|
|
@@ -111,4 +124,4 @@ export interface PopoverProps extends PopoverVariants, Omit<HTMLAttributes<HTMLD
|
|
|
111
124
|
preset?: string;
|
|
112
125
|
}
|
|
113
126
|
export { default as Popover } from './Popover.svelte';
|
|
114
|
-
export { type PopoverVariants, popoverVariants } from './popover.variants.js';
|
|
127
|
+
export { type PopoverVariants, popoverMotion, popoverVariants } from './popover.variants.js';
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { default as Popover } from './Popover.svelte';
|
|
2
|
-
export { popoverVariants } from './popover.variants.js';
|
|
2
|
+
export { popoverMotion, popoverVariants } from './popover.variants.js';
|
|
@@ -1,4 +1,29 @@
|
|
|
1
1
|
import { type VariantProps } from '../../utils/variants.js';
|
|
2
|
+
/**
|
|
3
|
+
* CSS-native enter/exit motion for the floating panel (ACC-3 rest), keyed on
|
|
4
|
+
* the `data-state` attribute Popover always stamps. Kept as its own fragment
|
|
5
|
+
* because two call sites need exactly these classes: `popoverVariants.base`
|
|
6
|
+
* below, and Menu — which renders its inner Popover `unstyled` (to avoid a
|
|
7
|
+
* double surface) and re-applies the fragment via Popover's `class` prop.
|
|
8
|
+
*
|
|
9
|
+
* How the two halves work:
|
|
10
|
+
* - **Enter** — the panel un-hides via `showPopover()` (top layer) or a
|
|
11
|
+
* `display` flip (in-place mode), so a plain transition has no before-state;
|
|
12
|
+
* `starting:` (`@starting-style`) supplies it.
|
|
13
|
+
* - **Exit** — `hidePopover()` / a native light dismiss yank the panel to
|
|
14
|
+
* `display: none` in the same style recalc that flips `data-state`;
|
|
15
|
+
* `transition-discrete` (`transition-behavior: allow-discrete`) on
|
|
16
|
+
* `display`/`overlay` keeps it painted (and in the top layer) until the
|
|
17
|
+
* fade lands. Popover lags the children-teardown to match — see the
|
|
18
|
+
* exit-motion block in Popover.svelte.
|
|
19
|
+
*
|
|
20
|
+
* Browsers without `@starting-style`/`allow-discrete` simply skip the motion
|
|
21
|
+
* and keep today's instant toggle. Duration/easing resolve through the
|
|
22
|
+
* `--blocks-popover-*` tokens (interaction.css), which reduced motion
|
|
23
|
+
* collapses to 1ms; `motion-reduce:duration-[1ms]` guards the inline
|
|
24
|
+
* per-instance override path, which can't see the media query.
|
|
25
|
+
*/
|
|
26
|
+
export declare const popoverMotion: string;
|
|
2
27
|
export declare const popoverVariants: ((props?: import("../../utils/variants.js").TVProps<{
|
|
3
28
|
size: {
|
|
4
29
|
sm: string;
|
|
@@ -1,9 +1,44 @@
|
|
|
1
1
|
import { tv } from '../../utils/variants.js';
|
|
2
|
+
/**
|
|
3
|
+
* CSS-native enter/exit motion for the floating panel (ACC-3 rest), keyed on
|
|
4
|
+
* the `data-state` attribute Popover always stamps. Kept as its own fragment
|
|
5
|
+
* because two call sites need exactly these classes: `popoverVariants.base`
|
|
6
|
+
* below, and Menu — which renders its inner Popover `unstyled` (to avoid a
|
|
7
|
+
* double surface) and re-applies the fragment via Popover's `class` prop.
|
|
8
|
+
*
|
|
9
|
+
* How the two halves work:
|
|
10
|
+
* - **Enter** — the panel un-hides via `showPopover()` (top layer) or a
|
|
11
|
+
* `display` flip (in-place mode), so a plain transition has no before-state;
|
|
12
|
+
* `starting:` (`@starting-style`) supplies it.
|
|
13
|
+
* - **Exit** — `hidePopover()` / a native light dismiss yank the panel to
|
|
14
|
+
* `display: none` in the same style recalc that flips `data-state`;
|
|
15
|
+
* `transition-discrete` (`transition-behavior: allow-discrete`) on
|
|
16
|
+
* `display`/`overlay` keeps it painted (and in the top layer) until the
|
|
17
|
+
* fade lands. Popover lags the children-teardown to match — see the
|
|
18
|
+
* exit-motion block in Popover.svelte.
|
|
19
|
+
*
|
|
20
|
+
* Browsers without `@starting-style`/`allow-discrete` simply skip the motion
|
|
21
|
+
* and keep today's instant toggle. Duration/easing resolve through the
|
|
22
|
+
* `--blocks-popover-*` tokens (interaction.css), which reduced motion
|
|
23
|
+
* collapses to 1ms; `motion-reduce:duration-[1ms]` guards the inline
|
|
24
|
+
* per-instance override path, which can't see the media query.
|
|
25
|
+
*/
|
|
26
|
+
export const popoverMotion = [
|
|
27
|
+
'transition-[opacity,scale,display,overlay] transition-discrete',
|
|
28
|
+
'duration-[var(--blocks-popover-duration)] ease-[var(--blocks-popover-easing)]',
|
|
29
|
+
'motion-reduce:duration-[1ms]',
|
|
30
|
+
// pointer-events-none while closed: during the exit fade the children are
|
|
31
|
+
// still mounted and painted — without this, a quick click on the fading
|
|
32
|
+
// panel would fire consumer handlers from a visually dismissed surface.
|
|
33
|
+
'data-[state=closed]:opacity-0 data-[state=closed]:scale-[0.98] data-[state=closed]:pointer-events-none',
|
|
34
|
+
'starting:data-[state=open]:opacity-0 starting:data-[state=open]:scale-[0.98]'
|
|
35
|
+
].join(' ');
|
|
2
36
|
export const popoverVariants = tv({
|
|
3
37
|
// tier: contain — floating panel surface.
|
|
4
38
|
base: [
|
|
5
39
|
'bg-surface-elevated border border-border-hairline rounded-contain',
|
|
6
40
|
'shadow-[var(--blocks-shadow-md)] backdrop-blur-sm',
|
|
41
|
+
popoverMotion,
|
|
7
42
|
// `calc(100dvh-4rem)` is the static design cap; Floating UI's `size`
|
|
8
43
|
// middleware (via useFloatingPanel) narrows it to the room actually left
|
|
9
44
|
// between the anchor and the visual viewport edge through
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
preset,
|
|
28
28
|
id,
|
|
29
29
|
'aria-describedby': ariaDescribedby,
|
|
30
|
+
'aria-labelledby': ariaLabelledby,
|
|
30
31
|
...restProps
|
|
31
32
|
}: RadioGroupProps = $props();
|
|
32
33
|
|
|
@@ -43,6 +44,9 @@
|
|
|
43
44
|
const unstyled = $derived(unstyledProp || blocksConfig?.unstyled || false);
|
|
44
45
|
|
|
45
46
|
const groupId = $derived(id || `radiogroup-${propsId}`);
|
|
47
|
+
// When a `label` is rendered it owns the group's `aria-labelledby`; otherwise a
|
|
48
|
+
// consumer-supplied `aria-labelledby` (an external heading) is used as the
|
|
49
|
+
// fallback so external labelling survives (see the group element below).
|
|
46
50
|
const labelId = $derived(label ? `${groupId}-label` : undefined);
|
|
47
51
|
// ARIA wiring is shared with every form primitive — see XC-2.
|
|
48
52
|
const ff = useFormField(() => ({
|
|
@@ -159,16 +163,16 @@
|
|
|
159
163
|
{/if}
|
|
160
164
|
|
|
161
165
|
<div
|
|
166
|
+
{...restProps}
|
|
162
167
|
bind:this={groupElement}
|
|
163
168
|
role="radiogroup"
|
|
164
169
|
id={groupId}
|
|
165
170
|
class={unstyled ? (slotClasses?.group ?? '') : styles.group({ class: slotClasses?.group })}
|
|
166
|
-
aria-labelledby={labelId}
|
|
171
|
+
aria-labelledby={labelId ?? ariaLabelledby}
|
|
167
172
|
aria-describedby={describedBy}
|
|
168
173
|
aria-required={required || undefined}
|
|
169
174
|
aria-invalid={ff.invalid ? 'true' : undefined}
|
|
170
175
|
onkeydown={handleKeydown}
|
|
171
|
-
{...restProps}
|
|
172
176
|
>
|
|
173
177
|
{@render children()}
|
|
174
178
|
</div>
|
|
@@ -71,6 +71,7 @@
|
|
|
71
71
|
for={id}
|
|
72
72
|
>
|
|
73
73
|
<input
|
|
74
|
+
{...restProps}
|
|
74
75
|
{id}
|
|
75
76
|
type="radio"
|
|
76
77
|
name={ctx.name}
|
|
@@ -80,7 +81,6 @@
|
|
|
80
81
|
class="peer sr-only"
|
|
81
82
|
tabindex={isChecked || (!ctx.value && !isDisabled) ? 0 : -1}
|
|
82
83
|
onchange={handleChange}
|
|
83
|
-
{...restProps}
|
|
84
84
|
/>
|
|
85
85
|
|
|
86
86
|
<span
|
|
@@ -256,7 +256,14 @@ interface SelectBaseProps<T extends SelectValue = string> extends Omit<SelectVar
|
|
|
256
256
|
* and make the custom look reusable across the project.
|
|
257
257
|
*/
|
|
258
258
|
preset?: string;
|
|
259
|
-
/**
|
|
259
|
+
/**
|
|
260
|
+
* Explicit `id` for the component. Auto-generated if omitted.
|
|
261
|
+
*
|
|
262
|
+
* The id lands on the component's wrapper, which is not a labelable element.
|
|
263
|
+
* The focusable trigger is derived from it as `` `${id}-trigger` `` — that is
|
|
264
|
+
* the id an external `<label for>` has to address, since a `for` pointing at
|
|
265
|
+
* the wrapper focuses nothing.
|
|
266
|
+
*/
|
|
260
267
|
id?: string;
|
|
261
268
|
}
|
|
262
269
|
/**
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
slotClasses: slotClassesProp = {},
|
|
36
36
|
preset,
|
|
37
37
|
'aria-describedby': ariaDescribedby,
|
|
38
|
+
'aria-labelledby': ariaLabelledby,
|
|
38
39
|
...restProps
|
|
39
40
|
}: SliderProps = $props();
|
|
40
41
|
|
|
@@ -53,6 +54,8 @@
|
|
|
53
54
|
disabled
|
|
54
55
|
}));
|
|
55
56
|
const statusId = `${uid}-status`;
|
|
57
|
+
const minLabelId = `${uid}-min-label`;
|
|
58
|
+
const maxLabelId = `${uid}-max-label`;
|
|
56
59
|
|
|
57
60
|
let trackRef = $state<HTMLDivElement>();
|
|
58
61
|
let dragging = $state<'single' | 'start' | 'end' | null>(null);
|
|
@@ -251,6 +254,17 @@
|
|
|
251
254
|
.join(' ') || undefined
|
|
252
255
|
);
|
|
253
256
|
|
|
257
|
+
// `aria-labelledby` has the same reach problem as `aria-describedby` above:
|
|
258
|
+
// restProps land on the roleless wrapper div, so an external visible label
|
|
259
|
+
// would never name the focusable thumbs. On the range arm one external label
|
|
260
|
+
// would name both thumbs identically ("Padding" / "Padding"), so it is
|
|
261
|
+
// composed with a visually-hidden minimum/maximum qualifier —
|
|
262
|
+
// aria-labelledby concatenates its references in order, yielding
|
|
263
|
+
// "Padding Minimum" / "Padding Maximum" and preserving the distinction the
|
|
264
|
+
// `label`-prop arm gets for free.
|
|
265
|
+
const startLabelledBy = $derived(ariaLabelledby ? `${ariaLabelledby} ${minLabelId}` : undefined);
|
|
266
|
+
const endLabelledBy = $derived(ariaLabelledby ? `${ariaLabelledby} ${maxLabelId}` : undefined);
|
|
267
|
+
|
|
254
268
|
$effect(() => {
|
|
255
269
|
if (trackRef && mint && mint !== 'none' && !disabled) {
|
|
256
270
|
return mintRegistry.apply(trackRef, mint);
|
|
@@ -429,13 +443,22 @@
|
|
|
429
443
|
{/if}
|
|
430
444
|
|
|
431
445
|
{#if range}
|
|
446
|
+
{#if ariaLabelledby}
|
|
447
|
+
<span id={minLabelId} class="sr-only">{bt('accessibility.minimum') || 'Minimum'}</span>
|
|
448
|
+
<span id={maxLabelId} class="sr-only">{bt('accessibility.maximum') || 'Maximum'}</span>
|
|
449
|
+
{/if}
|
|
432
450
|
<div
|
|
433
451
|
role="slider"
|
|
434
452
|
tabindex={disabled ? -1 : 0}
|
|
435
453
|
aria-valuemin={min}
|
|
436
454
|
aria-valuemax={rangeValue[1]}
|
|
437
455
|
aria-valuenow={rangeValue[0]}
|
|
438
|
-
aria-
|
|
456
|
+
aria-labelledby={startLabelledBy}
|
|
457
|
+
aria-label={startLabelledBy
|
|
458
|
+
? undefined
|
|
459
|
+
: label
|
|
460
|
+
? `${label} minimum`
|
|
461
|
+
: bt('accessibility.minimum') || 'Minimum'}
|
|
439
462
|
aria-describedby={describedBy}
|
|
440
463
|
aria-disabled={disabled || undefined}
|
|
441
464
|
class={unstyled ? (slotClasses?.thumb ?? '') : styles.thumb({ class: slotClasses?.thumb })}
|
|
@@ -449,7 +472,12 @@
|
|
|
449
472
|
aria-valuemin={rangeValue[0]}
|
|
450
473
|
aria-valuemax={max}
|
|
451
474
|
aria-valuenow={rangeValue[1]}
|
|
452
|
-
aria-
|
|
475
|
+
aria-labelledby={endLabelledBy}
|
|
476
|
+
aria-label={endLabelledBy
|
|
477
|
+
? undefined
|
|
478
|
+
: label
|
|
479
|
+
? `${label} maximum`
|
|
480
|
+
: bt('accessibility.maximum') || 'Maximum'}
|
|
453
481
|
aria-describedby={describedBy}
|
|
454
482
|
aria-disabled={disabled || undefined}
|
|
455
483
|
class={unstyled ? (slotClasses?.thumb ?? '') : styles.thumb({ class: slotClasses?.thumb })}
|
|
@@ -464,7 +492,8 @@
|
|
|
464
492
|
aria-valuemin={min}
|
|
465
493
|
aria-valuemax={max}
|
|
466
494
|
aria-valuenow={singleValue}
|
|
467
|
-
aria-
|
|
495
|
+
aria-labelledby={ariaLabelledby}
|
|
496
|
+
aria-label={ariaLabelledby ? undefined : label || bt('accessibility.slider') || 'Slider'}
|
|
468
497
|
aria-describedby={describedBy}
|
|
469
498
|
aria-disabled={disabled || undefined}
|
|
470
499
|
class={unstyled ? (slotClasses?.thumb ?? '') : styles.thumb({ class: slotClasses?.thumb })}
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
slotClasses: slotClassesProp = {},
|
|
30
30
|
preset,
|
|
31
31
|
oninput: userOnInput,
|
|
32
|
+
id: idProp,
|
|
32
33
|
'aria-describedby': ariaDescribedby,
|
|
33
34
|
...restProps
|
|
34
35
|
}: TextareaProps = $props();
|
|
@@ -42,9 +43,12 @@
|
|
|
42
43
|
let textareaRef = $state<HTMLTextAreaElement>();
|
|
43
44
|
|
|
44
45
|
// ARIA wiring is shared with every form primitive — see XC-2.
|
|
46
|
+
// A consumer-supplied `id` wins over the generated one, so an external
|
|
47
|
+
// `<label for>` can address the `<textarea>` (mirrors the Input role model).
|
|
45
48
|
const propsId = $props.id();
|
|
49
|
+
const fieldId = $derived(idProp ?? `textarea-${propsId}`);
|
|
46
50
|
const ff = useFormField(() => ({
|
|
47
|
-
fieldId
|
|
51
|
+
fieldId,
|
|
48
52
|
helper,
|
|
49
53
|
error,
|
|
50
54
|
required,
|