@juspay/svelte-ui-components 4.27.6 → 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
|
@@ -2,6 +2,21 @@ import type { JSONValue } from 'type-decoder';
|
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
3
|
import type { TooltipPosition } from '../Tooltip/properties';
|
|
4
4
|
export type SortDirection = 'asc' | 'desc';
|
|
5
|
+
/**
|
|
6
|
+
* Which column a table is sorted by, addressed by column ID rather than by
|
|
7
|
+
* column index.
|
|
8
|
+
*
|
|
9
|
+
* `columnId` is a `TableColumn.id` in the keyed model, and the column's index
|
|
10
|
+
* rendered as a string in the positional model (`tableHeaders`/`tableData`),
|
|
11
|
+
* which has no other identity to offer. The distinction is the whole point of
|
|
12
|
+
* the type: an index follows a POSITION, so reordering or hiding a column
|
|
13
|
+
* silently re-points a stored sort at whichever field moved into that slot,
|
|
14
|
+
* while an ID follows the FIELD and a hidden field simply stops being sorted.
|
|
15
|
+
*/
|
|
16
|
+
export type TableSortState = {
|
|
17
|
+
columnId: string;
|
|
18
|
+
direction: SortDirection;
|
|
19
|
+
};
|
|
5
20
|
/**
|
|
6
21
|
* Built-in cell renderer vocabulary for the keyed column model.
|
|
7
22
|
*
|
|
@@ -358,9 +373,29 @@ export type TableCheckboxSelectionConfig = {
|
|
|
358
373
|
*/
|
|
359
374
|
export type TablePaginationConfig = {
|
|
360
375
|
mode?: 'client' | 'server';
|
|
361
|
-
/**
|
|
376
|
+
/**
|
|
377
|
+
* 1-indexed current page — never 0-indexed, so an adapter over a
|
|
378
|
+
* zero-based engine (TanStack's `pageIndex`) converts once, here, and not
|
|
379
|
+
* again inside its own page handler.
|
|
380
|
+
*
|
|
381
|
+
* Server mode: authoritative. Table renders this page and `onPageChange` is
|
|
382
|
+
* a request the consumer may honor, delay, or refuse. Client mode: the
|
|
383
|
+
* starting page — Table then owns paging, and clamps the page it renders to
|
|
384
|
+
* the last page that actually has rows, so a dataset that shrinks from
|
|
385
|
+
* outside cannot leave the reader on a blank page past the end.
|
|
386
|
+
*/
|
|
362
387
|
page?: number;
|
|
363
|
-
/**
|
|
388
|
+
/**
|
|
389
|
+
* Rows per page. Default 10.
|
|
390
|
+
*
|
|
391
|
+
* Server mode: declaring it makes it authoritative, the same way
|
|
392
|
+
* `checkboxSelection.selectedIds` takes ownership of selection —
|
|
393
|
+
* `onPageSizeChange` becomes a request the consumer answers by changing
|
|
394
|
+
* this value, and refusing leaves both the rows and the chrome at the size
|
|
395
|
+
* the server actually served. Omit it in server mode to keep the older
|
|
396
|
+
* behavior where the page-size selector updates the chrome on its own.
|
|
397
|
+
* Client mode: the starting size, which the selector then owns.
|
|
398
|
+
*/
|
|
364
399
|
pageSize?: number;
|
|
365
400
|
/** Page-size selector options. Default [10, 25, 50, 100]; `[]` hides the selector. */
|
|
366
401
|
pageSizeOptions?: number[];
|
|
@@ -432,6 +467,38 @@ export type TableSearchConfig = {
|
|
|
432
467
|
searchableColumnIndices?: number[];
|
|
433
468
|
testId?: string;
|
|
434
469
|
displayMode?: 'toolbar' | 'inline';
|
|
470
|
+
/**
|
|
471
|
+
* Controlled search term. Omitted (every pre-existing consumer): the input
|
|
472
|
+
* owns its term through an internal state, unchanged. Provided: the input
|
|
473
|
+
* renders FROM this string and never mutates it — `onSearchTermChange`
|
|
474
|
+
* reports the would-be next term and the consumer decides, exactly as
|
|
475
|
+
* `checkboxSelection.selectedIds` does for selection.
|
|
476
|
+
*
|
|
477
|
+
* Independent of `onsearchchange`: that prop delegates FILTERING to the
|
|
478
|
+
* consumer, this one only delegates the visible term, so a controlled term
|
|
479
|
+
* still filters client-side unless `onsearchchange` is also supplied.
|
|
480
|
+
*/
|
|
481
|
+
searchTerm?: string;
|
|
482
|
+
/**
|
|
483
|
+
* Reports every would-be search term, in controlled and uncontrolled mode
|
|
484
|
+
* alike. A controlled consumer feeds it back through `searchTerm`; an
|
|
485
|
+
* uncontrolled one may use it purely as a notification.
|
|
486
|
+
*/
|
|
487
|
+
onSearchTermChange?: (searchTerm: string) => void;
|
|
488
|
+
};
|
|
489
|
+
/**
|
|
490
|
+
* Why the table is showing nothing, handed to the `empty` snippet.
|
|
491
|
+
*
|
|
492
|
+
* "No records yet" and "nothing matched your search" are different messages
|
|
493
|
+
* with different remedies, and only Table can tell them apart once the built-in
|
|
494
|
+
* search owns the term: a consumer reading `rows.length === 0` sees the same
|
|
495
|
+
* zero either way. `searchTerm` is passed alongside so the message can quote
|
|
496
|
+
* what was searched for.
|
|
497
|
+
*/
|
|
498
|
+
export type TableEmptyContext = {
|
|
499
|
+
reason: 'no-rows' | 'no-matches';
|
|
500
|
+
/** The term currently filtering the view; `''` when none is active. */
|
|
501
|
+
searchTerm: string;
|
|
435
502
|
};
|
|
436
503
|
export type TableProperties = OptionalTableProperties & TableEventProperties;
|
|
437
504
|
/**
|
|
@@ -487,6 +554,26 @@ export type OptionalTableProperties = {
|
|
|
487
554
|
* (e.g. via a server query).
|
|
488
555
|
*/
|
|
489
556
|
sortMode?: 'client' | 'server';
|
|
557
|
+
/**
|
|
558
|
+
* Controlled sort overlay, keyed by column ID (see `TableSortState`).
|
|
559
|
+
*
|
|
560
|
+
* Omitted (every pre-existing consumer): today's uncontrolled behavior —
|
|
561
|
+
* Table owns the sort state and a header click re-sorts it directly.
|
|
562
|
+
* Provided: the header indicators, `aria-sort`, and the client-side reorder
|
|
563
|
+
* all render FROM this value and Table never mutates it; a header click
|
|
564
|
+
* reports the would-be next state through `onsortchange` (and the
|
|
565
|
+
* unchanged `onsort`) and the consumer decides. This is what lets a sort
|
|
566
|
+
* held in a URL or on a server be restored into the built-in header.
|
|
567
|
+
*
|
|
568
|
+
* `null` is a controlled value meaning "no column is sorted" — pass it to
|
|
569
|
+
* clear a sort without handing sort ownership back to the table. A
|
|
570
|
+
* `columnId` that no currently rendered column carries (a hidden column)
|
|
571
|
+
* sorts nothing, rather than falling through to a neighboring field.
|
|
572
|
+
*
|
|
573
|
+
* Independent of `sortMode`: `'server'` still means Table does not reorder
|
|
574
|
+
* rows locally, controlled or not.
|
|
575
|
+
*/
|
|
576
|
+
sortState?: TableSortState | null;
|
|
490
577
|
/** Built-in paginator (see TablePaginationConfig). */
|
|
491
578
|
pagination?: TablePaginationConfig;
|
|
492
579
|
/**
|
|
@@ -526,6 +613,32 @@ export type OptionalTableProperties = {
|
|
|
526
613
|
* edge. Defaults to `false` (in-flow rendering, unchanged).
|
|
527
614
|
*/
|
|
528
615
|
usePortal?: boolean;
|
|
616
|
+
/**
|
|
617
|
+
* Opt-in stacked "record card" layout below a 640px viewport, in place
|
|
618
|
+
* of the default horizontal scroll. Off by default — flipping every existing
|
|
619
|
+
* consumer's table to a different layout at some width they never chose
|
|
620
|
+
* would be a breaking change to a published library, not a bug fix.
|
|
621
|
+
*
|
|
622
|
+
* CSS alone cannot build this for a Web Component consumer: the classic
|
|
623
|
+
* `td::before { content: attr(data-label) }` recipe needs a rule INSIDE the
|
|
624
|
+
* shadow tree, which a `<sui-table>` consumer's stylesheet can never reach.
|
|
625
|
+
* So Table carries the column→label mapping itself and renders it as markup
|
|
626
|
+
* (a real, `aria-hidden` element, not generated content — see the mobile
|
|
627
|
+
* label span in the template), which is compiled into the shadow root
|
|
628
|
+
* exactly like the rest of this file's styles and works identically through
|
|
629
|
+
* `<sui-table>` and the Svelte component.
|
|
630
|
+
*
|
|
631
|
+
* Stacking cells vertically means their CSS `display` can no longer be
|
|
632
|
+
* `table-cell` — which is also what silently drops the browser's implicit
|
|
633
|
+
* table/row/cell accessibility roles in several browser/AT combinations.
|
|
634
|
+
* This prop therefore also stamps explicit `role="table"/"rowgroup"/"row"/
|
|
635
|
+
* "columnheader"/"cell"` back onto the native elements (harmless and
|
|
636
|
+
* redundant at desktop width, load-bearing at the mobile breakpoint) — the
|
|
637
|
+
* documented fix for "CSS reformats a table but the AX tree must not lose
|
|
638
|
+
* it". See the `.table-mobile-cards` block in `<style>` for the layout and
|
|
639
|
+
* the accessibility decision recorded there for the visible-label choice.
|
|
640
|
+
*/
|
|
641
|
+
mobileCardLayout?: boolean;
|
|
529
642
|
sortable?: boolean;
|
|
530
643
|
sortableColumns?: number[];
|
|
531
644
|
stickyHeader?: boolean;
|
|
@@ -537,7 +650,14 @@ export type OptionalTableProperties = {
|
|
|
537
650
|
sortDescIcon?: Snippet;
|
|
538
651
|
sortDefaultIcon?: Snippet;
|
|
539
652
|
cell?: Snippet<[JSONValue, number, number]>;
|
|
540
|
-
|
|
653
|
+
/**
|
|
654
|
+
* Rendered in place of the rows when the view has none. Receives why
|
|
655
|
+
* (`TableEmptyContext`) so one snippet can serve both "no records yet" and
|
|
656
|
+
* "nothing matched", which are different messages with different remedies.
|
|
657
|
+
* Declaring it without parameters stays valid — every existing consumer is
|
|
658
|
+
* unaffected.
|
|
659
|
+
*/
|
|
660
|
+
empty?: Snippet<[TableEmptyContext]>;
|
|
541
661
|
classes?: string;
|
|
542
662
|
/** Snippet rendered in a footer region below the table (e.g. a paginator). */
|
|
543
663
|
paginatorSlot?: Snippet;
|
|
@@ -560,6 +680,17 @@ export type OptionalTableProperties = {
|
|
|
560
680
|
export type TableEventProperties = {
|
|
561
681
|
onrowclick?: (rowIndex: number, rowData: JSONValue[], originalIndex: number) => void;
|
|
562
682
|
onsort?: (columnIndex: number, direction: SortDirection) => void;
|
|
683
|
+
/**
|
|
684
|
+
* Fires with every `onsort`, carrying the same change keyed by column ID —
|
|
685
|
+
* the input half of the controlled contract (`sortState`) speaks IDs, so the
|
|
686
|
+
* report does too, and a consumer never has to resolve an index against a
|
|
687
|
+
* column list that may since have changed.
|
|
688
|
+
*
|
|
689
|
+
* Only ever reports a sorted state: a header click cycles ascending →
|
|
690
|
+
* descending → ascending, exactly as before. Clearing a sort is the
|
|
691
|
+
* consumer's own act of setting `sortState` to `null`.
|
|
692
|
+
*/
|
|
693
|
+
onsortchange?: (sortState: TableSortState) => void;
|
|
563
694
|
/**
|
|
564
695
|
* C2-3: When provided, the built-in client-side filtering is disabled and
|
|
565
696
|
* this callback is called on every search input change instead, letting the
|
package/dist/Tabs/Tabs.svelte
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import {
|
|
2
|
+
import { prefersReducedMotion } from '../utils';
|
|
3
|
+
import { getActiveElement } from '../_interaction/focus';
|
|
3
4
|
import type { TabItem, TabsProperties } from './properties';
|
|
4
5
|
import Img from '../Img/Img.svelte';
|
|
5
6
|
import chevronLeftSvg from '../assets/chevron-left.svg?raw';
|
|
@@ -13,6 +14,9 @@
|
|
|
13
14
|
activeKey,
|
|
14
15
|
disabled = false,
|
|
15
16
|
orientation = 'horizontal',
|
|
17
|
+
activationMode = 'automatic',
|
|
18
|
+
loop = true,
|
|
19
|
+
dir,
|
|
16
20
|
testId,
|
|
17
21
|
scrollLeftIcon,
|
|
18
22
|
scrollRightIcon,
|
|
@@ -34,19 +38,66 @@
|
|
|
34
38
|
return typeof item === 'string' ? item : item.label;
|
|
35
39
|
}
|
|
36
40
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
)
|
|
41
|
+
// What "the focused tab" means across a re-render. A keyed item keeps its identity
|
|
42
|
+
// when the list is reordered or trimmed; a plain string list has nothing stabler
|
|
43
|
+
// than its position.
|
|
44
|
+
function identityOf(item: string | TabItem, index: number): string {
|
|
45
|
+
return typeof item === 'object' ? item.key : String(index);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function isItemDisabled(index: number): boolean {
|
|
49
|
+
if (disabled) {
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
const item = items.at(index);
|
|
53
|
+
return typeof item === 'object' && item.disabled === true;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// -1 whenever nothing is selected: an out-of-range or non-integer `activeIndex`, an
|
|
57
|
+
// `activeKey` matching no item, or object items with no `activeKey` at all. The list
|
|
58
|
+
// then shows no selection rather than inventing one, and still offers a way in.
|
|
59
|
+
const selectedIndex = $derived.by(() => {
|
|
60
|
+
if (isObjectMode) {
|
|
61
|
+
return typeof activeKey === 'string'
|
|
62
|
+
? items.findIndex((item) => toTabItem(item)?.key === activeKey)
|
|
63
|
+
: -1;
|
|
64
|
+
}
|
|
65
|
+
return Number.isInteger(activeIndex) && activeIndex >= 0 && activeIndex < items.length
|
|
66
|
+
? activeIndex
|
|
67
|
+
: -1;
|
|
68
|
+
});
|
|
45
69
|
|
|
46
70
|
function isActiveItem(index: number): boolean {
|
|
47
|
-
return index ===
|
|
71
|
+
return index === selectedIndex;
|
|
48
72
|
}
|
|
49
73
|
|
|
74
|
+
let focusedKey: string | null = $state(null);
|
|
75
|
+
|
|
76
|
+
const focusedIndex = $derived(
|
|
77
|
+
focusedKey === null
|
|
78
|
+
? -1
|
|
79
|
+
: items.findIndex((item, index) => identityOf(item, index) === focusedKey)
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const firstEnabledIndex = $derived(items.findIndex((_, index) => !isItemDisabled(index)));
|
|
83
|
+
|
|
84
|
+
// A tablist is one tab stop. It is the focused tab while the user is inside the
|
|
85
|
+
// list, the selected tab otherwise, and the first enabled tab when nothing is
|
|
86
|
+
// selected — so a list with no selection, or one whose selection is disabled, can
|
|
87
|
+
// still be reached by Tab. A fully disabled list has no tab stop at all.
|
|
88
|
+
const tabStopIndex = $derived.by(() => {
|
|
89
|
+
if (disabled) {
|
|
90
|
+
return -1;
|
|
91
|
+
}
|
|
92
|
+
if (focusedIndex >= 0 && !isItemDisabled(focusedIndex)) {
|
|
93
|
+
return focusedIndex;
|
|
94
|
+
}
|
|
95
|
+
if (selectedIndex >= 0 && !isItemDisabled(selectedIndex)) {
|
|
96
|
+
return selectedIndex;
|
|
97
|
+
}
|
|
98
|
+
return firstEnabledIndex;
|
|
99
|
+
});
|
|
100
|
+
|
|
50
101
|
let scrollContainer: HTMLDivElement | null = null;
|
|
51
102
|
let canScrollLeft = $state(false);
|
|
52
103
|
let canScrollRight = $state(false);
|
|
@@ -63,6 +114,34 @@
|
|
|
63
114
|
const showStartArrow = $derived(isVertical ? canScrollUp : canScrollLeft);
|
|
64
115
|
const showEndArrow = $derived(isVertical ? canScrollDown : canScrollRight);
|
|
65
116
|
|
|
117
|
+
const explicitDir: 'ltr' | 'rtl' | null = $derived(dir === 'rtl' || dir === 'ltr' ? dir : null);
|
|
118
|
+
|
|
119
|
+
function tabElements(): HTMLElement[] {
|
|
120
|
+
return scrollContainer === null
|
|
121
|
+
? []
|
|
122
|
+
: Array.from(scrollContainer.querySelectorAll<HTMLElement>('[role="tab"]'));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Read at the moment a key is pressed rather than tracked as state: the direction
|
|
126
|
+
// that matters is the one in effect now, and it can come from an ancestor's `dir`
|
|
127
|
+
// attribute or from CSS, neither of which is reactive.
|
|
128
|
+
function isRightToLeft(): boolean {
|
|
129
|
+
if (explicitDir !== null) {
|
|
130
|
+
return explicitDir === 'rtl';
|
|
131
|
+
}
|
|
132
|
+
if (scrollContainer === null) {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
const declared = scrollContainer.closest('[dir]');
|
|
136
|
+
if (declared !== null) {
|
|
137
|
+
const value = declared.getAttribute('dir');
|
|
138
|
+
if (value === 'rtl' || value === 'ltr') {
|
|
139
|
+
return value === 'rtl';
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return getComputedStyle(scrollContainer).direction === 'rtl';
|
|
143
|
+
}
|
|
144
|
+
|
|
66
145
|
function updateOverflow(): void {
|
|
67
146
|
if (scrollContainer === null) {
|
|
68
147
|
return;
|
|
@@ -105,109 +184,215 @@
|
|
|
105
184
|
if (scrollContainer === null) {
|
|
106
185
|
return;
|
|
107
186
|
}
|
|
187
|
+
// This file already had a reduced-motion @media block, and it was counted as
|
|
188
|
+
// guarded on that alone -- but the block covers the indicator's CSS transition
|
|
189
|
+
// and cannot touch a scrollBy `behavior`, which is not a style. Right row, wrong
|
|
190
|
+
// reason: the JS scroll went on animating under the preference.
|
|
191
|
+
const behavior: ScrollBehavior = prefersReducedMotion() ? 'auto' : 'smooth';
|
|
108
192
|
if (isVertical) {
|
|
109
193
|
const amount = scrollContainer.clientHeight * 0.6;
|
|
110
194
|
scrollContainer.scrollBy({
|
|
111
195
|
top: edge === 'start' ? -amount : amount,
|
|
112
|
-
behavior
|
|
196
|
+
behavior
|
|
113
197
|
});
|
|
114
198
|
return;
|
|
115
199
|
}
|
|
116
200
|
const amount = scrollContainer.clientWidth * 0.6;
|
|
117
201
|
scrollContainer.scrollBy({
|
|
118
202
|
left: edge === 'start' ? -amount : amount,
|
|
119
|
-
behavior
|
|
203
|
+
behavior
|
|
120
204
|
});
|
|
121
205
|
}
|
|
122
206
|
|
|
123
|
-
function
|
|
124
|
-
if (
|
|
207
|
+
function activate(index: number): void {
|
|
208
|
+
if (isItemDisabled(index)) {
|
|
125
209
|
return;
|
|
126
210
|
}
|
|
211
|
+
const rawItem = items.at(index);
|
|
127
212
|
if (isObjectMode) {
|
|
128
|
-
const rawItem = items.at(index);
|
|
129
213
|
if (typeof rawItem !== 'object') {
|
|
130
214
|
return;
|
|
131
215
|
}
|
|
132
|
-
|
|
133
|
-
if (tabItem === null) {
|
|
216
|
+
if (rawItem.key === activeKey) {
|
|
134
217
|
return;
|
|
135
218
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
219
|
+
onkeychange?.(rawItem.key);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
if (typeof rawItem !== 'string' || index === activeIndex) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
activeIndex = index;
|
|
226
|
+
onchange?.(index, rawItem);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function handleTabClick(index: number): void {
|
|
230
|
+
activate(index);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function focusTabAt(index: number): void {
|
|
234
|
+
const item = items.at(index);
|
|
235
|
+
if (typeof item !== 'string' && typeof item !== 'object') {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
focusedKey = identityOf(item, index);
|
|
239
|
+
tabElements().at(index)?.focus();
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** Next enabled item in `step`'s direction, or -1 when there is none to move to. */
|
|
243
|
+
function nextEnabledIndex(from: number, step: number): number {
|
|
244
|
+
const count = items.length;
|
|
245
|
+
if (count === 0) {
|
|
246
|
+
return -1;
|
|
247
|
+
}
|
|
248
|
+
let index = from;
|
|
249
|
+
for (let moved = 0; moved < count; moved += 1) {
|
|
250
|
+
index += step;
|
|
251
|
+
if (index < 0 || index >= count) {
|
|
252
|
+
if (!loop) {
|
|
253
|
+
return -1;
|
|
254
|
+
}
|
|
255
|
+
index = ((index % count) + count) % count;
|
|
143
256
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
return;
|
|
257
|
+
if (!isItemDisabled(index)) {
|
|
258
|
+
return index;
|
|
147
259
|
}
|
|
148
|
-
activeIndex = index;
|
|
149
|
-
onchange?.(index, rawLabel);
|
|
150
260
|
}
|
|
261
|
+
return -1;
|
|
151
262
|
}
|
|
152
263
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
// tabindex="-1" item and the roving pattern breaks down.
|
|
158
|
-
function moveFocusToActiveTab(): void {
|
|
159
|
-
void tick().then(() => {
|
|
160
|
-
const activeTab = scrollContainer?.querySelector<HTMLElement>('[role="tab"][tabindex="0"]');
|
|
161
|
-
activeTab?.focus();
|
|
162
|
-
});
|
|
264
|
+
function edgeIndex(edge: 'start' | 'end'): number {
|
|
265
|
+
return edge === 'start'
|
|
266
|
+
? items.findIndex((_, index) => !isItemDisabled(index))
|
|
267
|
+
: items.reduce((found: number, _, index) => (isItemDisabled(index) ? found : index), -1);
|
|
163
268
|
}
|
|
164
269
|
|
|
165
|
-
// WAI-ARIA APG tablist keyboard contract
|
|
166
|
-
//
|
|
167
|
-
//
|
|
168
|
-
// tabindex makes every non-active tab keyboard-unreachable.
|
|
270
|
+
// WAI-ARIA APG tablist keyboard contract: orientation-aware arrow keys move through
|
|
271
|
+
// the enabled tabs, Home/End jump to the ends, and Enter/Space select. Whether
|
|
272
|
+
// moving also selects is `activationMode`'s decision.
|
|
169
273
|
function handleKeydown(event: KeyboardEvent, index: number): void {
|
|
274
|
+
// A tablist owns bare arrow keys, not the browser's and the OS's shortcuts built
|
|
275
|
+
// on them. Consuming Ctrl+Home or Shift+ArrowRight would take away scroll-to-top
|
|
276
|
+
// and text selection while the tablist happens to hold focus.
|
|
277
|
+
if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) {
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
if (disabled) {
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
170
283
|
if (event.key === 'Enter' || event.key === ' ') {
|
|
171
284
|
event.preventDefault();
|
|
172
|
-
|
|
285
|
+
activate(index);
|
|
173
286
|
return;
|
|
174
287
|
}
|
|
175
|
-
const
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
288
|
+
const rightToLeft = !isVertical && isRightToLeft();
|
|
289
|
+
const forwardKey = isVertical ? 'ArrowDown' : rightToLeft ? 'ArrowLeft' : 'ArrowRight';
|
|
290
|
+
const backwardKey = isVertical ? 'ArrowUp' : rightToLeft ? 'ArrowRight' : 'ArrowLeft';
|
|
291
|
+
let targetIndex: number;
|
|
292
|
+
if (event.key === forwardKey) {
|
|
293
|
+
targetIndex = nextEnabledIndex(index, 1);
|
|
294
|
+
} else if (event.key === backwardKey) {
|
|
295
|
+
targetIndex = nextEnabledIndex(index, -1);
|
|
182
296
|
} else if (event.key === 'Home') {
|
|
183
|
-
targetIndex =
|
|
297
|
+
targetIndex = edgeIndex('start');
|
|
184
298
|
} else if (event.key === 'End') {
|
|
185
|
-
targetIndex =
|
|
186
|
-
}
|
|
187
|
-
if (targetIndex === null) {
|
|
299
|
+
targetIndex = edgeIndex('end');
|
|
300
|
+
} else {
|
|
188
301
|
return;
|
|
189
302
|
}
|
|
190
303
|
event.preventDefault();
|
|
191
|
-
if (
|
|
304
|
+
if (targetIndex < 0 || targetIndex === index) {
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
focusTabAt(targetIndex);
|
|
308
|
+
if (activationMode === 'automatic') {
|
|
309
|
+
activate(targetIndex);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function handleFocusIn(event: FocusEvent): void {
|
|
314
|
+
const target = event.target;
|
|
315
|
+
if (!(target instanceof HTMLElement)) {
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
const index = tabElements().indexOf(target);
|
|
319
|
+
const item = items.at(index);
|
|
320
|
+
if (index < 0 || (typeof item !== 'string' && typeof item !== 'object')) {
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
focusedKey = identityOf(item, index);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function handleFocusOut(event: FocusEvent): void {
|
|
327
|
+
const next = event.relatedTarget;
|
|
328
|
+
if (next instanceof HTMLElement && tabElements().includes(next)) {
|
|
192
329
|
return;
|
|
193
330
|
}
|
|
194
|
-
|
|
195
|
-
|
|
331
|
+
focusedKey = null;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/** Identity of the selected item, for noticing that the selection itself changed. */
|
|
335
|
+
let lastSelectionKey: string | null = null;
|
|
336
|
+
|
|
337
|
+
// Keeps DOM focus and the roving tab stop honest after the list changes underneath
|
|
338
|
+
// them: the selection moved, the focused item was removed, reordered, or disabled.
|
|
339
|
+
// Runs from the same MutationObserver that already tracks overflow, so nothing here
|
|
340
|
+
// needs an effect. It never pulls focus into a list the user is not already in.
|
|
341
|
+
function reconcileFocus(): void {
|
|
342
|
+
const elements = tabElements();
|
|
343
|
+
const active = getActiveElement(scrollContainer);
|
|
344
|
+
const isInside = active instanceof HTMLElement && elements.includes(active);
|
|
345
|
+
const selectedItem = items.at(selectedIndex);
|
|
346
|
+
const selectionKey =
|
|
347
|
+
selectedIndex >= 0 && (typeof selectedItem === 'string' || typeof selectedItem === 'object')
|
|
348
|
+
? identityOf(selectedItem, selectedIndex)
|
|
349
|
+
: null;
|
|
350
|
+
const selectionChanged = selectionKey !== lastSelectionKey;
|
|
351
|
+
lastSelectionKey = selectionKey;
|
|
352
|
+
if (tabStopIndex < 0) {
|
|
353
|
+
if (isInside && active instanceof HTMLElement) {
|
|
354
|
+
active.blur();
|
|
355
|
+
}
|
|
356
|
+
focusedKey = null;
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
if (!isInside && focusedKey === null) {
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
// Focus follows a selection made elsewhere, so a parent switching tabs does not
|
|
363
|
+
// strand the user on a tab that is no longer the selected one.
|
|
364
|
+
const targetIndex =
|
|
365
|
+
selectionChanged && selectedIndex >= 0 && !isItemDisabled(selectedIndex)
|
|
366
|
+
? selectedIndex
|
|
367
|
+
: tabStopIndex;
|
|
368
|
+
const target = elements.at(targetIndex);
|
|
369
|
+
if (!(target instanceof HTMLElement)) {
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
const item = items.at(targetIndex);
|
|
373
|
+
if (typeof item === 'string' || typeof item === 'object') {
|
|
374
|
+
focusedKey = identityOf(item, targetIndex);
|
|
375
|
+
}
|
|
376
|
+
if (getActiveElement(target) !== target) {
|
|
377
|
+
target.focus();
|
|
378
|
+
}
|
|
196
379
|
}
|
|
197
380
|
|
|
198
381
|
function initOverflow(node: HTMLDivElement): () => void {
|
|
199
382
|
scrollContainer = node;
|
|
200
383
|
updateOverflow();
|
|
201
384
|
updateIndicator();
|
|
385
|
+
reconcileFocus();
|
|
202
386
|
const observer = new MutationObserver(() => {
|
|
203
387
|
updateOverflow();
|
|
204
388
|
updateIndicator();
|
|
389
|
+
reconcileFocus();
|
|
205
390
|
});
|
|
206
391
|
observer.observe(node, {
|
|
207
392
|
childList: true,
|
|
208
393
|
subtree: true,
|
|
209
394
|
attributes: true,
|
|
210
|
-
attributeFilter: ['class']
|
|
395
|
+
attributeFilter: ['class', 'tabindex', 'aria-selected', 'aria-disabled']
|
|
211
396
|
});
|
|
212
397
|
const resizeObserver = new ResizeObserver(() => {
|
|
213
398
|
updateOverflow();
|
|
@@ -226,7 +411,14 @@
|
|
|
226
411
|
);
|
|
227
412
|
</script>
|
|
228
413
|
|
|
229
|
-
<div
|
|
414
|
+
<div
|
|
415
|
+
class={rootClass}
|
|
416
|
+
class:disabled
|
|
417
|
+
class:vertical={isVertical}
|
|
418
|
+
{...explicitDir === null ? {} : { dir: explicitDir }}
|
|
419
|
+
data-pw={testId}
|
|
420
|
+
testID={testId}
|
|
421
|
+
>
|
|
230
422
|
{#if showStartArrow}
|
|
231
423
|
<button
|
|
232
424
|
class="tabs-arrow tabs-arrow-start"
|
|
@@ -249,8 +441,11 @@
|
|
|
249
441
|
class:fade-bottom={canScrollDown}
|
|
250
442
|
role="tablist"
|
|
251
443
|
aria-orientation={isVertical ? 'vertical' : null}
|
|
444
|
+
data-orientation={orientation}
|
|
252
445
|
{@attach initOverflow}
|
|
253
446
|
onscroll={updateOverflow}
|
|
447
|
+
onfocusin={handleFocusIn}
|
|
448
|
+
onfocusout={handleFocusOut}
|
|
254
449
|
>
|
|
255
450
|
{#each items as item, index (isObjectMode ? (toTabItem(item)?.key ?? index) : index)}
|
|
256
451
|
{@const tabItem = toTabItem(item)}
|
|
@@ -263,8 +458,11 @@
|
|
|
263
458
|
class:active={isActiveItem(index)}
|
|
264
459
|
role="tab"
|
|
265
460
|
aria-selected={isActiveItem(index)}
|
|
266
|
-
aria-disabled={
|
|
267
|
-
tabindex={
|
|
461
|
+
aria-disabled={isItemDisabled(index) ? 'true' : null}
|
|
462
|
+
tabindex={index === tabStopIndex ? 0 : -1}
|
|
463
|
+
data-state={isActiveItem(index) ? 'active' : 'inactive'}
|
|
464
|
+
data-disabled={isItemDisabled(index) ? '' : null}
|
|
465
|
+
data-orientation={orientation}
|
|
268
466
|
data-pw={tabItem?.testId}
|
|
269
467
|
testID={tabItem?.testId}
|
|
270
468
|
onclick={() => handleTabClick(index)}
|
|
@@ -412,7 +610,7 @@
|
|
|
412
610
|
}
|
|
413
611
|
|
|
414
612
|
.tabs-arrow:hover {
|
|
415
|
-
color: var(--tabs-arrow-hover-color, var(--tabs-active-color, #
|
|
613
|
+
color: var(--tabs-arrow-hover-color, var(--tabs-active-color, #0b57d0));
|
|
416
614
|
background: var(
|
|
417
615
|
--tabs-arrow-hover-background,
|
|
418
616
|
var(--tabs-arrow-background, var(--tabs-bar-background, #ffffff))
|
|
@@ -507,7 +705,7 @@
|
|
|
507
705
|
}
|
|
508
706
|
|
|
509
707
|
.tabs-item.active {
|
|
510
|
-
color: var(--tabs-active-color, #
|
|
708
|
+
color: var(--tabs-active-color, #0b57d0);
|
|
511
709
|
font-weight: var(--tabs-active-font-weight, 600);
|
|
512
710
|
background: var(--tabs-active-background, transparent);
|
|
513
711
|
}
|
|
@@ -608,7 +806,7 @@
|
|
|
608
806
|
font-weight: var(--tabs-section-label-font-weight, 700);
|
|
609
807
|
letter-spacing: var(--tabs-section-label-letter-spacing, 0.04em);
|
|
610
808
|
text-transform: var(--tabs-section-label-text-transform, uppercase);
|
|
611
|
-
color: var(--tabs-section-label-color, var(--tabs-item-color, #
|
|
809
|
+
color: var(--tabs-section-label-color, var(--tabs-item-color, #666666));
|
|
612
810
|
white-space: nowrap;
|
|
613
811
|
user-select: none;
|
|
614
812
|
}
|
|
@@ -23,6 +23,13 @@ export type TabItem = {
|
|
|
23
23
|
* `tab` snippet, since it sits outside the item row.
|
|
24
24
|
*/
|
|
25
25
|
sectionLabel?: string;
|
|
26
|
+
/**
|
|
27
|
+
* Blocks selection of this item. It keeps its place and its `aria-disabled`
|
|
28
|
+
* state, but arrow keys, `Home`/`End` and clicks skip past it, and it never
|
|
29
|
+
* becomes the list's tab stop. An item selected from outside stays selected
|
|
30
|
+
* even while disabled, matching how a controlled parent's state is honoured.
|
|
31
|
+
*/
|
|
32
|
+
disabled?: boolean;
|
|
26
33
|
};
|
|
27
34
|
export type TabsProperties = MandatoryTabsProperties & OptionalTabsProperties & TabsEventProperties;
|
|
28
35
|
export type MandatoryTabsProperties = {
|
|
@@ -38,6 +45,22 @@ export type OptionalTabsProperties = {
|
|
|
38
45
|
* rail) — indicator on the leading edge, scroll arrows point up/down.
|
|
39
46
|
*/
|
|
40
47
|
orientation?: 'horizontal' | 'vertical';
|
|
48
|
+
/**
|
|
49
|
+
* `'automatic'` (default) selects a tab as soon as arrow keys reach it, which is
|
|
50
|
+
* right when switching panels is instant. `'manual'` moves focus only, and waits
|
|
51
|
+
* for `Enter`, `Space` or a click — the WAI-ARIA APG recommendation when showing
|
|
52
|
+
* a panel is expensive, since arrowing past three tabs should not load three
|
|
53
|
+
* panels.
|
|
54
|
+
*/
|
|
55
|
+
activationMode?: 'automatic' | 'manual';
|
|
56
|
+
/** Whether arrow keys wrap around the ends. `true` by default. */
|
|
57
|
+
loop?: boolean;
|
|
58
|
+
/**
|
|
59
|
+
* Which way horizontal arrow keys read. Omitted, the inherited document direction
|
|
60
|
+
* decides, so an app that sets `dir` on a wrapper needs nothing here. Vertical
|
|
61
|
+
* lists are unaffected: `ArrowDown` always means "later in the list".
|
|
62
|
+
*/
|
|
63
|
+
dir?: 'ltr' | 'rtl';
|
|
41
64
|
testId?: string;
|
|
42
65
|
scrollLeftIcon?: Snippet;
|
|
43
66
|
scrollRightIcon?: Snippet;
|