@juspay/svelte-ui-components 4.27.5 → 4.28.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 +140 -36
- package/dist/Animations/ModalAnimation.svelte +15 -1
- package/dist/Animations/OverlayAnimation.svelte +7 -1
- package/dist/AreaChart/AreaChart.svelte +366 -85
- package/dist/AspectRatio/AspectRatio.svelte +51 -0
- package/dist/AspectRatio/AspectRatio.svelte.d.ts +3 -0
- package/dist/AspectRatio/properties.d.ts +27 -0
- package/dist/AspectRatio/properties.js +1 -0
- package/dist/Banner/Banner.svelte +10 -4
- package/dist/Banner/properties.d.ts +8 -0
- package/dist/BarChart/BarChart.svelte +156 -49
- package/dist/Book/Book.svelte +14 -1
- package/dist/BrandLoader/BrandLoader.svelte +76 -12
- package/dist/Button/Button.svelte +56 -11
- package/dist/Calendar/Calendar.svelte +88 -61
- package/dist/Card/Card.svelte +1 -1
- package/dist/Carousel/Carousel.svelte +6 -3
- package/dist/ChatComposer/ChatComposer.svelte +54 -4
- package/dist/ChatComposer/dictationState.d.ts +15 -0
- package/dist/ChatComposer/dictationState.js +9 -0
- package/dist/ChatComposer/properties.d.ts +28 -1
- package/dist/ChatMessage/ChatMessage.svelte +27 -1
- package/dist/CheckListItem/CheckListItem.svelte +3 -3
- package/dist/Checkbox/Checkbox.svelte +84 -3
- package/dist/Checkbox/properties.d.ts +45 -13
- package/dist/ChipInput/ChipInput.svelte +70 -3
- package/dist/ChipInput/properties.d.ts +16 -0
- package/dist/Choicebox/Choicebox.svelte +156 -1
- package/dist/Choicebox/group.d.ts +63 -0
- package/dist/Choicebox/group.js +127 -0
- package/dist/Choicebox/properties.d.ts +46 -0
- package/dist/ColorPicker/ColorPicker.svelte +169 -20
- package/dist/ColorPicker/properties.d.ts +16 -0
- package/dist/Combobox/Combobox.svelte +87 -25
- package/dist/Combobox/properties.d.ts +23 -0
- package/dist/CommandMenu/CommandMenu.svelte +27 -11
- package/dist/ContextMenu/ContextMenu.svelte +31 -37
- package/dist/DateRangePicker/DateRangePicker.svelte +171 -73
- package/dist/DeltaIndicator/DeltaIndicator.svelte +3 -3
- package/dist/Draggable/Draggable.svelte +12 -7
- package/dist/DualAxisBarChart/DualAxisBarChart.svelte +11 -4
- package/dist/EmptyState/EmptyState.svelte +1 -1
- package/dist/FileDropzoneTrigger/FileDropzoneTrigger.svelte +1 -1
- package/dist/FileInput/FileInput.svelte +50 -1
- package/dist/FileInput/properties.d.ts +7 -0
- package/dist/FunnelChart/FunnelChart.svelte +40 -12
- package/dist/FunnelChart/properties.d.ts +7 -1
- package/dist/Gallery/Gallery.svelte +20 -13
- package/dist/Gallery/properties.d.ts +8 -0
- package/dist/GridItem/GridItem.svelte +31 -1
- package/dist/HITL/HITL.svelte +35 -9
- package/dist/Icon/Icon.svelte +19 -3
- package/dist/Input/Input.svelte +24 -10
- package/dist/Input/properties.d.ts +37 -1
- package/dist/InputButton/InputButton.svelte +86 -11
- package/dist/InputButton/properties.d.ts +10 -1
- package/dist/Label/Label.svelte +54 -0
- package/dist/Label/Label.svelte.d.ts +3 -0
- package/dist/Label/properties.d.ts +26 -0
- package/dist/Label/properties.js +1 -0
- package/dist/LineChart/LineChart.svelte +385 -94
- package/dist/ListItem/ListItem.svelte +121 -15
- package/dist/Loader/Loader.svelte +22 -2
- package/dist/LoadingDots/LoadingDots.svelte +21 -0
- package/dist/MediaUpload/MediaUpload.svelte +13 -3
- package/dist/Menu/Menu.svelte +56 -28
- package/dist/Modal/Modal.svelte +77 -42
- package/dist/Pagination/Pagination.svelte +9 -2
- package/dist/Phone/Phone.svelte +4 -0
- package/dist/PieChart/PieChart.svelte +282 -38
- package/dist/PieChart/properties.d.ts +9 -0
- package/dist/Pill/Pill.svelte +14 -5
- package/dist/Progress/Progress.svelte +20 -0
- package/dist/Radio/Radio.svelte +58 -1
- package/dist/Radio/properties.d.ts +23 -0
- package/dist/RatingGroup/RatingGroup.svelte +283 -0
- package/dist/RatingGroup/RatingGroup.svelte.d.ts +4 -0
- package/dist/RatingGroup/properties.d.ts +73 -0
- package/dist/RatingGroup/properties.js +1 -0
- package/dist/RelativeTime/RelativeTime.svelte +1 -1
- package/dist/SankeyChart/SankeyChart.svelte +272 -50
- package/dist/SankeyChart/properties.d.ts +9 -0
- package/dist/Scroller/Scroller.svelte +37 -2
- package/dist/Select/Select.svelte +166 -46
- package/dist/Select/properties.d.ts +7 -0
- package/dist/Separator/Separator.svelte +48 -0
- package/dist/Separator/Separator.svelte.d.ts +3 -0
- package/dist/Separator/properties.d.ts +22 -0
- package/dist/Separator/properties.js +1 -0
- package/dist/Sheet/Sheet.svelte +60 -21
- package/dist/Shimmer/Shimmer.svelte +15 -0
- package/dist/Slider/Slider.svelte +87 -6
- package/dist/Slider/properties.d.ts +26 -0
- package/dist/Snippet/Snippet.svelte +4 -0
- package/dist/Snippet/copyState.svelte.d.ts +13 -1
- package/dist/Snippet/copyState.svelte.js +27 -1
- package/dist/Snippet/properties.d.ts +7 -0
- package/dist/SoundKit/SoundKit.js +9 -8
- package/dist/SplitInput/SplitInput.svelte +68 -2
- package/dist/SplitInput/properties.d.ts +23 -0
- package/dist/StatCard/StatCard.svelte +6 -6
- package/dist/Status/Status.svelte +2 -2
- package/dist/Stepper/Step.svelte +19 -6
- package/dist/Stepper/Stepper.svelte +108 -35
- package/dist/Table/BuiltinCell.svelte +2 -2
- package/dist/Table/Table.svelte +373 -55
- package/dist/Table/properties.d.ts +134 -3
- package/dist/Tabs/Tabs.svelte +263 -65
- package/dist/Tabs/properties.d.ts +23 -0
- package/dist/TaskList/TaskList.svelte +14 -5
- package/dist/ThemeSwitcher/ThemeSwitcher.svelte +23 -0
- package/dist/ThinkingIndicator/ThinkingIndicator.svelte +13 -5
- package/dist/Toast/Toast.svelte +27 -2
- package/dist/Toggle/Toggle.svelte +79 -7
- package/dist/Toggle/Toggle.svelte.d.ts +1 -1
- package/dist/Toggle/properties.d.ts +30 -7
- package/dist/ToolCallLog/ToolCallLog.svelte +59 -21
- package/dist/ToolCallLog/properties.d.ts +10 -0
- package/dist/Toolbar/Toolbar.svelte +1 -1
- package/dist/Tooltip/Tooltip.svelte +20 -2
- package/dist/Tooltip/tooltip-action.js +39 -4
- package/dist/TypewriterText/TypewriterText.svelte +34 -2
- package/dist/TypewriterText/properties.d.ts +18 -5
- package/dist/_chart/ChartContainer.svelte +12 -0
- package/dist/_chart/ChartTooltip.svelte +18 -4
- package/dist/_chart/Legend.svelte +21 -0
- package/dist/_chart/Legend.svelte.d.ts +15 -0
- package/dist/_chart/geometry.d.ts +54 -1
- package/dist/_chart/geometry.js +122 -14
- package/dist/_chart/interactions.d.ts +5 -1
- package/dist/_chart/interactions.js +9 -5
- package/dist/_chart/scales.js +16 -0
- package/dist/_chart/temporal.d.ts +45 -0
- package/dist/_chart/temporal.js +116 -0
- package/dist/_field/description.d.ts +50 -0
- package/dist/_field/description.js +51 -0
- package/dist/_interaction/dismissal.d.ts +67 -0
- package/dist/_interaction/dismissal.js +137 -0
- package/dist/_interaction/focus.d.ts +73 -0
- package/dist/_interaction/focus.js +143 -0
- package/dist/assets/star.svg +5 -0
- package/dist/index.d.ts +13 -1
- package/dist/index.js +21 -1
- package/dist/reduced-motion.svelte.d.ts +37 -0
- package/dist/reduced-motion.svelte.js +44 -0
- package/dist/styles/theme-dark.css +778 -0
- package/dist/types.d.ts +4 -2
- package/dist/utils.d.ts +62 -1
- package/dist/utils.js +80 -21
- package/dist-codemod/{bin.js → codemod/bin.js} +13 -2
- package/dist-codemod/{cli.js → codemod/cli.js} +48 -3
- package/dist-codemod/codemod/migrate-audit.js +395 -0
- package/dist-codemod/codemod/migration-assets/legacy-display.css +120 -0
- package/dist-codemod/codemod/migration-assets/legacy-palette.css +106 -0
- package/dist-codemod/codemod/migration-assets/wc-display.json +492 -0
- package/dist-codemod/migrate/analyze.js +804 -0
- package/dist-codemod/migrate/chart-tooltip-selector.js +301 -0
- package/dist-wc/index.js +13992 -4323
- package/package.json +13 -6
- package/dist/Modal/focus-trap.d.ts +0 -42
- package/dist/Modal/focus-trap.js +0 -57
- /package/dist-codemod/{legacy-pairs.js → codemod/legacy-pairs.js} +0 -0
- /package/dist-codemod/{transform.js → codemod/transform.js} +0 -0
- /package/dist-codemod/{wc-children.js → codemod/wc-children.js} +0 -0
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { eventHitsInside, registerDismissible } from '../_interaction/dismissal';
|
|
3
|
+
import { getActiveElement } from '../_interaction/focus';
|
|
4
|
+
import { describeField } from '../_field/description';
|
|
2
5
|
import { onMount, tick } from 'svelte';
|
|
3
6
|
import type { SelectItem, SelectPlacement, SelectProperties } from './properties';
|
|
4
7
|
import Pill from '../Pill/Pill.svelte';
|
|
@@ -126,7 +129,6 @@
|
|
|
126
129
|
|
|
127
130
|
const instanceId = $props.id();
|
|
128
131
|
const listboxId = `select-listbox-${instanceId}`;
|
|
129
|
-
const errorMessageId = `${listboxId}-error`;
|
|
130
132
|
|
|
131
133
|
/* `searchable` alone no longer says where the input goes, so every place
|
|
132
134
|
that used to branch on it now asks which of the two it meant. Filtering
|
|
@@ -134,8 +136,28 @@
|
|
|
134
136
|
const searchInTrigger = $derived(searchable && searchPosition !== 'menu');
|
|
135
137
|
const searchInMenu = $derived(searchable && searchPosition === 'menu');
|
|
136
138
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
+
/* `error` is an independent gate, not merely "is errorMessage non-empty" --
|
|
140
|
+
`error=false` with `errorMessage` still set is a state this component's two-prop
|
|
141
|
+
API explicitly permits (see properties.ts) and must render nothing, matching
|
|
142
|
+
today's behaviour before this ported to describeField. Passing `error ?
|
|
143
|
+
errorMessage : null` preserves that gate; passing `error` through `invalid` too
|
|
144
|
+
means aria-invalid tracks `error` alone, exactly like the literal `error ?
|
|
145
|
+
'true' : null` this replaces, whether or not a message is set.
|
|
146
|
+
describeField's own hasText check does not trim (`value !== ''`), while this
|
|
147
|
+
gate did (`.trim().length > 0`) -- so a whitespace-only errorMessage rendered
|
|
148
|
+
nothing before and, ported as-is, would render an empty-looking alert div after.
|
|
149
|
+
The trim is kept here, in the glue, rather than tightened into hasText itself:
|
|
150
|
+
hasText is shared by five other consumers (Radio, Checkbox, Slider, Toggle,
|
|
151
|
+
FileInput) this change does not otherwise touch, and tightening it was out of
|
|
152
|
+
scope for a surgical port of Select alone. */
|
|
153
|
+
const field = $derived(
|
|
154
|
+
describeField(listboxId, {
|
|
155
|
+
error:
|
|
156
|
+
error && typeof errorMessage === 'string' && errorMessage.trim().length > 0
|
|
157
|
+
? errorMessage
|
|
158
|
+
: null,
|
|
159
|
+
invalid: error
|
|
160
|
+
})
|
|
139
161
|
);
|
|
140
162
|
|
|
141
163
|
/* Nothing selected means nothing to clear, so the control is not rendered at
|
|
@@ -245,15 +267,30 @@
|
|
|
245
267
|
});
|
|
246
268
|
|
|
247
269
|
/**
|
|
248
|
-
*
|
|
270
|
+
* Where a portalled node is allowed to land. Svelte scopes a custom element's
|
|
271
|
+
* CSS to its shadow root, so `document.body` is the one place the panel must
|
|
272
|
+
* never go from inside `<sui-select>`: it keeps its `svelte-*` scoping class
|
|
273
|
+
* and loses every rule behind it, plus the host's custom properties. Measured
|
|
274
|
+
* in Chromium -- position fell back to `static`, background to transparent,
|
|
275
|
+
* border and shadow to none, so the options rendered as bare text over the
|
|
276
|
+
* page. Its own root holds that stylesheet and still sits above every
|
|
277
|
+
* overflow/scroll ancestor between the two.
|
|
278
|
+
*/
|
|
279
|
+
const portalTarget = (node: Node): Node => {
|
|
280
|
+
const root = node.getRootNode();
|
|
281
|
+
return root instanceof ShadowRoot ? root : document.body;
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Svelte action: relocates the dropdown out to its root when usePortal is set,
|
|
249
286
|
* so a position:fixed panel is never clipped by an overflow/scroll ancestor
|
|
250
287
|
* (e.g. a table cell). No-op otherwise; `use:` actions never run during SSR.
|
|
251
288
|
*/
|
|
252
|
-
const
|
|
289
|
+
const portalToRoot = (node: HTMLElement) => {
|
|
253
290
|
if (!usePortal) {
|
|
254
291
|
return;
|
|
255
292
|
}
|
|
256
|
-
|
|
293
|
+
portalTarget(node).appendChild(node);
|
|
257
294
|
return { destroy: () => node.remove() };
|
|
258
295
|
};
|
|
259
296
|
|
|
@@ -360,6 +397,9 @@
|
|
|
360
397
|
if (disabled) {
|
|
361
398
|
return;
|
|
362
399
|
}
|
|
400
|
+
if (items.find((item) => item.id === id)?.disabled === true) {
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
363
403
|
if (multiple) {
|
|
364
404
|
value = value.includes(id) ? value.filter((v) => v !== id) : [...value, id];
|
|
365
405
|
} else {
|
|
@@ -426,6 +466,40 @@
|
|
|
426
466
|
}
|
|
427
467
|
}
|
|
428
468
|
|
|
469
|
+
// The select-all row is always selectable; only an item row can opt out via
|
|
470
|
+
// SelectItem.disabled. There is no group-header row kind to skip.
|
|
471
|
+
function isRowSelectable(row: SelectRow): boolean {
|
|
472
|
+
return row.kind === 'select-all' || row.item.disabled !== true;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function hasModifierKey(event: KeyboardEvent): boolean {
|
|
476
|
+
return event.ctrlKey || event.metaKey || event.altKey || event.shiftKey;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Home/End: jump the highlight to the first/last selectable row, skipping
|
|
481
|
+
* disabled options the same way `moveHighlight` skips nothing -- it just
|
|
482
|
+
* targets a different index and reuses the same scroll-into-view step. A
|
|
483
|
+
* no-op when no row in that direction is selectable, including an empty list.
|
|
484
|
+
*/
|
|
485
|
+
async function moveHighlightToEdge(edge: 'first' | 'last'): Promise<void> {
|
|
486
|
+
const target =
|
|
487
|
+
edge === 'first'
|
|
488
|
+
? optionRows.findIndex(isRowSelectable)
|
|
489
|
+
: optionRows.findLastIndex(isRowSelectable);
|
|
490
|
+
if (target < 0) {
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
highlightedIndex = target;
|
|
494
|
+
await tick();
|
|
495
|
+
if (dropdownEl !== null) {
|
|
496
|
+
const el = dropdownEl.querySelector('.select-option.highlighted');
|
|
497
|
+
if (el instanceof HTMLElement) {
|
|
498
|
+
el.scrollIntoView({ block: 'nearest' });
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
429
503
|
function handleTriggerClick(event: MouseEvent): void {
|
|
430
504
|
if (event.target instanceof HTMLInputElement) {
|
|
431
505
|
if (!open) {
|
|
@@ -500,12 +574,18 @@
|
|
|
500
574
|
event.preventDefault();
|
|
501
575
|
moveHighlight(-1);
|
|
502
576
|
break;
|
|
503
|
-
case '
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
577
|
+
case 'Home':
|
|
578
|
+
// A caret-bearing text filter input owns Home/End for itself; only the
|
|
579
|
+
// dropdown's own chrome (or a non-searchable trigger) hands them to us.
|
|
580
|
+
if (open && !(event.target instanceof HTMLInputElement) && !hasModifierKey(event)) {
|
|
581
|
+
event.preventDefault();
|
|
582
|
+
moveHighlightToEdge('first');
|
|
583
|
+
}
|
|
584
|
+
break;
|
|
585
|
+
case 'End':
|
|
586
|
+
if (open && !(event.target instanceof HTMLInputElement) && !hasModifierKey(event)) {
|
|
587
|
+
event.preventDefault();
|
|
588
|
+
moveHighlightToEdge('last');
|
|
509
589
|
}
|
|
510
590
|
break;
|
|
511
591
|
case 'Backspace':
|
|
@@ -525,7 +605,7 @@
|
|
|
525
605
|
close();
|
|
526
606
|
break;
|
|
527
607
|
}
|
|
528
|
-
const active =
|
|
608
|
+
const active = getActiveElement(triggerEl);
|
|
529
609
|
if (event.shiftKey) {
|
|
530
610
|
/* Backwards out of the trigger genuinely leaves the widget; every
|
|
531
611
|
other Shift+Tab walks back through the menu and must not close
|
|
@@ -596,24 +676,36 @@
|
|
|
596
676
|
}
|
|
597
677
|
}
|
|
598
678
|
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
679
|
+
// Registered for exactly the dropdown's open span (the `{#if open && !disabled}`
|
|
680
|
+
// block below), same as Menu's dismissalAction, so this dropdown only answers
|
|
681
|
+
// Escape/outside-press while it is the topmost dismissible surface -- a select
|
|
682
|
+
// opened inside a Modal, Sheet, Menu, ContextMenu or CommandMenu no longer closes
|
|
683
|
+
// the layer behind it, and pressing Escape closes only this dropdown rather than
|
|
684
|
+
// being intercepted by an enclosing layer's own Escape handler.
|
|
685
|
+
function dismissalAction(_node: HTMLDivElement) {
|
|
686
|
+
const release = registerDismissible({
|
|
687
|
+
element: () => containerEl,
|
|
688
|
+
// A portaled dropdown lives outside containerEl, so a click on an option is
|
|
689
|
+
// not contained by it — treat the dropdown node as "inside" too, otherwise a
|
|
690
|
+
// multi-select would close on every pick.
|
|
691
|
+
onOutside: (event) => {
|
|
692
|
+
if (eventHitsInside(dropdownEl, event)) {
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
close();
|
|
696
|
+
},
|
|
697
|
+
onEscape: () => {
|
|
698
|
+
close();
|
|
699
|
+
if (!searchInTrigger && triggerEl !== null) {
|
|
700
|
+
triggerEl.focus();
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
});
|
|
704
|
+
return { destroy: release };
|
|
611
705
|
}
|
|
612
706
|
|
|
613
707
|
onMount(() => {
|
|
614
|
-
document.addEventListener('click', handleClickOutside);
|
|
615
708
|
return () => {
|
|
616
|
-
document.removeEventListener('click', handleClickOutside);
|
|
617
709
|
if (open) {
|
|
618
710
|
onclose?.();
|
|
619
711
|
}
|
|
@@ -650,8 +742,8 @@
|
|
|
650
742
|
aria-expanded={open}
|
|
651
743
|
aria-haspopup="listbox"
|
|
652
744
|
aria-controls={listboxId}
|
|
653
|
-
aria-invalid={
|
|
654
|
-
aria-describedby={
|
|
745
|
+
aria-invalid={field.ariaInvalid}
|
|
746
|
+
aria-describedby={field.describedBy}
|
|
655
747
|
{...highlightedOptionId !== null ? { 'aria-activedescendant': highlightedOptionId } : {}}
|
|
656
748
|
tabindex={disabled ? -1 : searchInTrigger ? -1 : 0}
|
|
657
749
|
>
|
|
@@ -678,8 +770,8 @@
|
|
|
678
770
|
bind:this={searchInputEl}
|
|
679
771
|
placeholder={value.length === 0 ? placeholder : ''}
|
|
680
772
|
{disabled}
|
|
681
|
-
aria-invalid={
|
|
682
|
-
aria-describedby={
|
|
773
|
+
aria-invalid={field.ariaInvalid}
|
|
774
|
+
aria-describedby={field.describedBy}
|
|
683
775
|
autocomplete="off"
|
|
684
776
|
tabindex={disabled ? -1 : 0}
|
|
685
777
|
data-pw={typeof testId === 'string' ? `${testId}-search` : null}
|
|
@@ -706,8 +798,8 @@
|
|
|
706
798
|
bind:this={searchInputEl}
|
|
707
799
|
placeholder={value.length === 0 ? placeholder : ''}
|
|
708
800
|
{disabled}
|
|
709
|
-
aria-invalid={
|
|
710
|
-
aria-describedby={
|
|
801
|
+
aria-invalid={field.ariaInvalid}
|
|
802
|
+
aria-describedby={field.describedBy}
|
|
711
803
|
autocomplete="off"
|
|
712
804
|
tabindex={disabled ? -1 : 0}
|
|
713
805
|
data-pw={typeof testId === 'string' ? `${testId}-search` : null}
|
|
@@ -727,8 +819,8 @@
|
|
|
727
819
|
bind:this={searchInputEl}
|
|
728
820
|
placeholder={searchPlaceholder}
|
|
729
821
|
{disabled}
|
|
730
|
-
aria-invalid={
|
|
731
|
-
aria-describedby={
|
|
822
|
+
aria-invalid={field.ariaInvalid}
|
|
823
|
+
aria-describedby={field.describedBy}
|
|
732
824
|
autocomplete="off"
|
|
733
825
|
tabindex={disabled ? -1 : 0}
|
|
734
826
|
data-pw={typeof testId === 'string' ? `${testId}-search` : null}
|
|
@@ -761,9 +853,9 @@
|
|
|
761
853
|
{/if}
|
|
762
854
|
</div>
|
|
763
855
|
|
|
764
|
-
{#if
|
|
856
|
+
{#if field.showsError}
|
|
765
857
|
<div
|
|
766
|
-
id={
|
|
858
|
+
id={field.errorId}
|
|
767
859
|
role="alert"
|
|
768
860
|
class="select-error-message"
|
|
769
861
|
data-pw={typeof testId === 'string' ? `${testId}-error-message` : null}
|
|
@@ -833,9 +925,11 @@
|
|
|
833
925
|
class:tickable={showSelectedTick && !multiple}
|
|
834
926
|
class:selected={value.includes(row.item.id)}
|
|
835
927
|
class:highlighted={index === highlightedIndex}
|
|
928
|
+
class:select-option-disabled={row.item.disabled === true}
|
|
836
929
|
role="option"
|
|
837
930
|
id={`${listboxId}-option-${index}`}
|
|
838
931
|
aria-selected={value.includes(row.item.id)}
|
|
932
|
+
aria-disabled={row.item.disabled === true ? 'true' : null}
|
|
839
933
|
tabindex="-1"
|
|
840
934
|
{...typeof row.item.testId === 'string'
|
|
841
935
|
? { 'data-pw': row.item.testId, testID: row.item.testId }
|
|
@@ -848,7 +942,11 @@
|
|
|
848
942
|
? { 'data-pw': `${testId}-${row.item.id}`, testID: `${testId}-${row.item.id}` }
|
|
849
943
|
: {}}
|
|
850
944
|
onclick={() => selectItem(row.item.id)}
|
|
851
|
-
onmouseenter={() =>
|
|
945
|
+
onmouseenter={() => {
|
|
946
|
+
if (row.item.disabled !== true) {
|
|
947
|
+
highlightedIndex = index;
|
|
948
|
+
}
|
|
949
|
+
}}
|
|
852
950
|
>
|
|
853
951
|
{#if multiple}
|
|
854
952
|
{#if typeof optionIndicator === 'function'}
|
|
@@ -915,7 +1013,8 @@
|
|
|
915
1013
|
bind:clientHeight={dropdownHeight}
|
|
916
1014
|
style={portalStyle}
|
|
917
1015
|
use:menuKeys
|
|
918
|
-
use:
|
|
1016
|
+
use:portalToRoot
|
|
1017
|
+
use:dismissalAction
|
|
919
1018
|
>
|
|
920
1019
|
<input
|
|
921
1020
|
class="select-menu-search"
|
|
@@ -971,7 +1070,8 @@
|
|
|
971
1070
|
id={listboxId}
|
|
972
1071
|
aria-multiselectable={multiple}
|
|
973
1072
|
style={portalStyle}
|
|
974
|
-
use:
|
|
1073
|
+
use:portalToRoot
|
|
1074
|
+
use:dismissalAction
|
|
975
1075
|
>
|
|
976
1076
|
{@render menuOptions()}
|
|
977
1077
|
</div>
|
|
@@ -1122,7 +1222,7 @@
|
|
|
1122
1222
|
|
|
1123
1223
|
.select-placeholder {
|
|
1124
1224
|
flex: 1;
|
|
1125
|
-
color: var(--select-placeholder-color, #
|
|
1225
|
+
color: var(--select-placeholder-color, #666666);
|
|
1126
1226
|
white-space: nowrap;
|
|
1127
1227
|
overflow: hidden;
|
|
1128
1228
|
text-overflow: ellipsis;
|
|
@@ -1143,7 +1243,7 @@
|
|
|
1143
1243
|
}
|
|
1144
1244
|
|
|
1145
1245
|
.select-search::placeholder {
|
|
1146
|
-
color: var(--select-placeholder-color, #
|
|
1246
|
+
color: var(--select-placeholder-color, #6b7280);
|
|
1147
1247
|
}
|
|
1148
1248
|
|
|
1149
1249
|
.select-arrow {
|
|
@@ -1154,7 +1254,8 @@
|
|
|
1154
1254
|
height: var(--select-arrow-size, 16px);
|
|
1155
1255
|
color: var(--select-arrow-color, #666666);
|
|
1156
1256
|
flex-shrink: 0;
|
|
1157
|
-
transition: transform 0.15s
|
|
1257
|
+
transition: transform var(--select-arrow-transition-duration, var(--motion-duration, 0.15s))
|
|
1258
|
+
var(--select-arrow-transition-easing, var(--motion-easing, ease));
|
|
1158
1259
|
}
|
|
1159
1260
|
|
|
1160
1261
|
.select.open .select-arrow {
|
|
@@ -1262,7 +1363,8 @@
|
|
|
1262
1363
|
color: var(--select-option-color, #333333);
|
|
1263
1364
|
font-size: var(--select-option-font-size, inherit);
|
|
1264
1365
|
cursor: pointer;
|
|
1265
|
-
transition: background 0.1s
|
|
1366
|
+
transition: background var(--select-option-transition-duration, var(--motion-duration, 0.1s))
|
|
1367
|
+
var(--select-option-transition-easing, var(--motion-easing, ease));
|
|
1266
1368
|
}
|
|
1267
1369
|
|
|
1268
1370
|
/* Per-option leading icon (SelectItem.icon). Kept inline + vertically centred so
|
|
@@ -1294,6 +1396,12 @@
|
|
|
1294
1396
|
color: var(--select-option-selected-color, var(--select-option-color, #333333));
|
|
1295
1397
|
}
|
|
1296
1398
|
|
|
1399
|
+
.select-option-disabled {
|
|
1400
|
+
opacity: var(--select-option-disabled-opacity, 0.4);
|
|
1401
|
+
cursor: var(--select-option-disabled-cursor, not-allowed);
|
|
1402
|
+
pointer-events: none;
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1297
1405
|
.select-option.selected.highlighted {
|
|
1298
1406
|
background: var(
|
|
1299
1407
|
--select-option-selected-hover-background,
|
|
@@ -1326,8 +1434,11 @@
|
|
|
1326
1434
|
background-color: var(--select-option-indicator-background, transparent);
|
|
1327
1435
|
color: var(--select-option-indicator-color, currentColor);
|
|
1328
1436
|
transition:
|
|
1329
|
-
background-color
|
|
1330
|
-
|
|
1437
|
+
background-color
|
|
1438
|
+
var(--select-option-indicator-transition-duration, var(--motion-duration, 0.15s))
|
|
1439
|
+
var(--select-option-indicator-transition-easing, var(--motion-easing, ease)),
|
|
1440
|
+
border-color var(--select-option-indicator-transition-duration, var(--motion-duration, 0.15s))
|
|
1441
|
+
var(--select-option-indicator-transition-easing, var(--motion-easing, ease));
|
|
1331
1442
|
}
|
|
1332
1443
|
|
|
1333
1444
|
.select-option-indicator.checked {
|
|
@@ -1430,4 +1541,13 @@
|
|
|
1430
1541
|
width: 100%;
|
|
1431
1542
|
height: 100%;
|
|
1432
1543
|
}
|
|
1544
|
+
|
|
1545
|
+
/* The chevron rotates between closed and open. Small, but it is a transform,
|
|
1546
|
+
and the two end states are each meaningful, so dropping the tween just makes
|
|
1547
|
+
the flip instant. */
|
|
1548
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1549
|
+
.select-arrow {
|
|
1550
|
+
transition: none;
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1433
1553
|
</style>
|
|
@@ -20,6 +20,13 @@ export type SelectItem = {
|
|
|
20
20
|
* the current selection in the closed trigger.
|
|
21
21
|
*/
|
|
22
22
|
icon?: string;
|
|
23
|
+
/**
|
|
24
|
+
* When `true`, the option is rendered non-interactive (dimmed, `aria-disabled`)
|
|
25
|
+
* and cannot be chosen by click or Enter/Space. `Home` and `End` skip past
|
|
26
|
+
* disabled options to land on the nearest selectable row at each end of the
|
|
27
|
+
* list — mirroring `ComboboxItem.disabled`.
|
|
28
|
+
*/
|
|
29
|
+
disabled?: boolean;
|
|
23
30
|
};
|
|
24
31
|
export type MandatorySelectProperties = {
|
|
25
32
|
items: SelectItem[] | string[];
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { SeparatorProperties } from './properties';
|
|
3
|
+
|
|
4
|
+
let {
|
|
5
|
+
orientation = 'horizontal',
|
|
6
|
+
decorative = true,
|
|
7
|
+
testId,
|
|
8
|
+
classes
|
|
9
|
+
}: SeparatorProperties = $props();
|
|
10
|
+
|
|
11
|
+
const isVertical = $derived(orientation === 'vertical');
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<!--
|
|
15
|
+
Only a *meaningful* separator belongs in the accessibility tree (design
|
|
16
|
+
principle #4: a11y reasoned about per-state, not bolted on). A decorative
|
|
17
|
+
rule between unrelated content has no `role` at all and is `aria-hidden`, so
|
|
18
|
+
it is skipped entirely rather than read as an empty, nameless landmark. A
|
|
19
|
+
non-decorative one takes `role="separator"` -- `aria-orientation` is only
|
|
20
|
+
set for `vertical`, matching Tabs' convention elsewhere in this library,
|
|
21
|
+
since the ARIA default for the separator role is already `horizontal`.
|
|
22
|
+
-->
|
|
23
|
+
<div
|
|
24
|
+
class="separator {classes ?? ''}"
|
|
25
|
+
class:vertical={isVertical}
|
|
26
|
+
data-orientation={orientation}
|
|
27
|
+
data-pw={typeof testId === 'string' ? testId : null}
|
|
28
|
+
testID={typeof testId === 'string' ? testId : null}
|
|
29
|
+
role={decorative ? null : 'separator'}
|
|
30
|
+
aria-orientation={!decorative && isVertical ? 'vertical' : null}
|
|
31
|
+
aria-hidden={decorative ? 'true' : null}
|
|
32
|
+
></div>
|
|
33
|
+
|
|
34
|
+
<style>
|
|
35
|
+
.separator {
|
|
36
|
+
flex-shrink: 0;
|
|
37
|
+
border: none;
|
|
38
|
+
width: var(--separator-length, 100%);
|
|
39
|
+
height: var(--separator-thickness, 1px);
|
|
40
|
+
background: var(--separator-background, #e0e0e0);
|
|
41
|
+
margin: var(--separator-margin, 0);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.separator.vertical {
|
|
45
|
+
width: var(--separator-thickness, 1px);
|
|
46
|
+
height: var(--separator-length, 100%);
|
|
47
|
+
}
|
|
48
|
+
</style>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export type SeparatorOrientation = 'horizontal' | 'vertical';
|
|
2
|
+
export type SeparatorProperties = OptionalSeparatorProperties;
|
|
3
|
+
export type MandatorySeparatorProperties = Record<string, never>;
|
|
4
|
+
export type OptionalSeparatorProperties = {
|
|
5
|
+
/**
|
|
6
|
+
* Axis the dividing line runs across. `'horizontal'` renders a full-width
|
|
7
|
+
* line (for stacking content vertically); `'vertical'` renders a
|
|
8
|
+
* full-height line (for stacking content side by side).
|
|
9
|
+
*/
|
|
10
|
+
orientation?: SeparatorOrientation;
|
|
11
|
+
/**
|
|
12
|
+
* A decorative separator (the default) carries no meaning of its own — it's
|
|
13
|
+
* purely a visual rule between unrelated content — so it is hidden from
|
|
14
|
+
* assistive technology (`aria-hidden="true"`) and takes no ARIA role. Set
|
|
15
|
+
* `false` when the separator actually demarcates two semantically distinct
|
|
16
|
+
* sections (e.g. two groups in a menu); it then takes `role="separator"`
|
|
17
|
+
* with a matching `aria-orientation` so the boundary is announced too.
|
|
18
|
+
*/
|
|
19
|
+
decorative?: boolean;
|
|
20
|
+
testId?: string;
|
|
21
|
+
classes?: string;
|
|
22
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/Sheet/Sheet.svelte
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { reducedMotion } from '../reduced-motion.svelte';
|
|
2
3
|
import type { SheetProperties } from './properties';
|
|
3
4
|
import { fly, fade } from 'svelte/transition';
|
|
4
5
|
import { lockBodyScroll, unlockBodyScroll } from '../utils';
|
|
5
6
|
import { tick } from 'svelte';
|
|
6
7
|
import Button from '../Button/Button.svelte';
|
|
8
|
+
import { focusTrapTabTarget, getActiveElement } from '../_interaction/focus';
|
|
9
|
+
import { registerDismissible } from '../_interaction/dismissal';
|
|
7
10
|
|
|
8
11
|
let {
|
|
9
12
|
open = $bindable(false),
|
|
@@ -27,6 +30,26 @@
|
|
|
27
30
|
let sheetPanel: HTMLDivElement | null = $state(null);
|
|
28
31
|
|
|
29
32
|
let flyParams = $derived.by(() => {
|
|
33
|
+
// Under reduced motion every side behaves as `center` already does. That is
|
|
34
|
+
// not an invented fallback: the `center` case below documents it in the
|
|
35
|
+
// component's own words -- "a centered dialog only fades, so x/y stay at 0
|
|
36
|
+
// and `fly` degrades to exactly the overlay's own fade transition". The
|
|
37
|
+
// panel still appears, it simply stops travelling 400px to get there.
|
|
38
|
+
//
|
|
39
|
+
// A directive's parameters are a JS object, so no @media block can reach
|
|
40
|
+
// them; the guard has to be here.
|
|
41
|
+
//
|
|
42
|
+
// Reading `reducedMotion.current` (real `$state`) rather than calling
|
|
43
|
+
// `prefersReducedMotion()` (a plain, un-reactive read) is what keeps this
|
|
44
|
+
// derived subscribed even on this early-return branch, where `side` below
|
|
45
|
+
// is never reached. Sheet's demo keeps one instance mounted across many
|
|
46
|
+
// open/close cycles (`{#if open}` lives inside Sheet, not around it), so a
|
|
47
|
+
// preference the user toggles mid-session has to reach an already-mounted
|
|
48
|
+
// panel -- a plain read here would latch whatever it saw on first
|
|
49
|
+
// evaluation and never revisit it.
|
|
50
|
+
if (reducedMotion.current) {
|
|
51
|
+
return { x: 0, y: 0, duration: 300 };
|
|
52
|
+
}
|
|
30
53
|
switch (side) {
|
|
31
54
|
case 'left':
|
|
32
55
|
return { x: -400, y: 0, duration: 300 };
|
|
@@ -57,12 +80,14 @@
|
|
|
57
80
|
}
|
|
58
81
|
}
|
|
59
82
|
|
|
83
|
+
// Escape moved out to the shared dismissal module (see registerDismissible
|
|
84
|
+
// in scrollLockAction below) so only the topmost dismissible layer -- this
|
|
85
|
+
// sheet, or a Menu/CommandMenu opened on top of it -- answers Escape,
|
|
86
|
+
// instead of every open surface's own keydown handler reacting to the same
|
|
87
|
+
// press. Tab and Enter/Space stay here: neither is a cross-component
|
|
88
|
+
// ownership question, just this panel's own trap and its overlay's
|
|
89
|
+
// click-equivalent activation.
|
|
60
90
|
function handleKeyDown(event: KeyboardEvent) {
|
|
61
|
-
if (event.key === 'Escape') {
|
|
62
|
-
close();
|
|
63
|
-
return;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
91
|
// The overlay only carries role="button" while it is dismissible, and a
|
|
67
92
|
// role="button" has to answer Enter/Space as well as click (WCAG 2.1
|
|
68
93
|
// SC 2.1.1) -- tabindex="-1" keeps it out of the Tab sequence, but the
|
|
@@ -80,29 +105,37 @@
|
|
|
80
105
|
return;
|
|
81
106
|
}
|
|
82
107
|
|
|
83
|
-
if (event.key === 'Tab'
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
if (first === null || last === null) {
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const atEdge = document.activeElement === (event.shiftKey ? first : last);
|
|
95
|
-
if (atEdge) {
|
|
108
|
+
if (event.key === 'Tab') {
|
|
109
|
+
const target = focusTrapTabTarget({
|
|
110
|
+
container: sheetPanel,
|
|
111
|
+
activeElement: getActiveElement(sheetPanel),
|
|
112
|
+
shiftKey: event.shiftKey
|
|
113
|
+
});
|
|
114
|
+
if (target !== null) {
|
|
96
115
|
event.preventDefault();
|
|
97
|
-
|
|
116
|
+
target.focus();
|
|
98
117
|
}
|
|
99
118
|
}
|
|
100
119
|
}
|
|
101
120
|
|
|
102
121
|
function scrollLockAction(node: HTMLElement) {
|
|
103
|
-
|
|
122
|
+
// getActiveElement rather than document.activeElement: when the sheet is
|
|
123
|
+
// used through its custom element the opener lives inside a shadow root,
|
|
124
|
+
// where document.activeElement reports only the host and the real opener
|
|
125
|
+
// could never be handed focus back.
|
|
126
|
+
const previousFocus = getActiveElement(node);
|
|
104
127
|
let active = true;
|
|
105
128
|
lockBodyScroll();
|
|
129
|
+
// Registered here rather than in an onMount/onDestroy pair: this action's
|
|
130
|
+
// mount/destroy already brackets exactly the open span (the overlay only
|
|
131
|
+
// exists while `open` is true, per the `{#if open}` below), the same
|
|
132
|
+
// span the scroll lock above uses. `element` reads `sheetPanel` lazily --
|
|
133
|
+
// it is not bound yet at this point in a fresh open, same reason
|
|
134
|
+
// sheetPanel.focus() below is deferred through tick().
|
|
135
|
+
const releaseDismissible = registerDismissible({
|
|
136
|
+
element: () => sheetPanel,
|
|
137
|
+
onEscape: close
|
|
138
|
+
});
|
|
106
139
|
tick().then(() => {
|
|
107
140
|
if (active && sheetPanel !== null) {
|
|
108
141
|
sheetPanel.focus();
|
|
@@ -111,11 +144,17 @@
|
|
|
111
144
|
return {
|
|
112
145
|
destroy() {
|
|
113
146
|
active = false;
|
|
147
|
+
releaseDismissible();
|
|
114
148
|
unlockBodyScroll();
|
|
149
|
+
// Only take focus back when it is still somewhere the sheet is
|
|
150
|
+
// responsible for -- inside the panel, or nowhere at all. If the user
|
|
151
|
+
// has already moved on to another control, stealing it back would be
|
|
152
|
+
// the more disruptive bug.
|
|
153
|
+
const current = getActiveElement(node);
|
|
115
154
|
if (
|
|
116
155
|
previousFocus instanceof HTMLElement &&
|
|
117
156
|
previousFocus.isConnected &&
|
|
118
|
-
(
|
|
157
|
+
(current === null || current === document.body || node.contains(current))
|
|
119
158
|
) {
|
|
120
159
|
previousFocus.focus();
|
|
121
160
|
}
|
|
@@ -45,4 +45,19 @@
|
|
|
45
45
|
transform: translateX(100%);
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
|
+
/* An indefinite animation is the case this preference exists for: it never
|
|
49
|
+
ends, so a user who asked the OS to minimise motion gets a permanent loop.
|
|
50
|
+
The animation stops, but the element must still read as "busy" -- a guard
|
|
51
|
+
that leaves nothing on screen, or leaves a frame that means something else,
|
|
52
|
+
is worse than the motion it removed. This block lives in the component's own
|
|
53
|
+
<style> because that is the only stylesheet that reaches inside the shadow
|
|
54
|
+
root a custom-element consumer gets. */
|
|
55
|
+
@media (prefers-reduced-motion: reduce) {
|
|
56
|
+
.shimmer::after {
|
|
57
|
+
/* The sweep is the only animated part; the base block underneath is the
|
|
58
|
+
skeleton. Removing the sweep leaves exactly that -- a flat placeholder
|
|
59
|
+
-- rather than a highlight band frozen somewhere across it. */
|
|
60
|
+
display: none;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
48
63
|
</style>
|