@juspay/svelte-ui-components 4.51.0 → 4.52.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Animations/tokenizedFly.js +9 -2
- package/dist/AreaChart/AreaChart.svelte +41 -13
- package/dist/AreaChart/properties.d.ts +12 -0
- package/dist/AttachmentChipRow/AttachmentChipRow.svelte +7 -6
- package/dist/BarChart/BarChart.svelte +54 -6
- package/dist/BarChart/properties.d.ts +12 -0
- package/dist/Calendar/Calendar.svelte +124 -51
- package/dist/Card/Card.svelte +5 -0
- package/dist/ChatSuggestions/ChatSuggestions.svelte +1 -0
- package/dist/CheckListItem/CheckListItem.svelte +27 -5
- package/dist/ColorPicker/ColorPicker.svelte +40 -9
- package/dist/ColorPicker/properties.d.ts +8 -0
- package/dist/Combobox/Combobox.svelte +34 -2
- package/dist/CommandMenu/CommandMenu.svelte +35 -8
- package/dist/CommandMenu/properties.d.ts +6 -0
- package/dist/DateRangePicker/DateRangePicker.svelte +5 -5
- package/dist/DualAxisBarChart/DualAxisBarChart.svelte +41 -7
- package/dist/DualAxisBarChart/properties.d.ts +4 -0
- package/dist/FileDropzoneTrigger/FileDropzoneTrigger.svelte +104 -10
- package/dist/FileDropzoneTrigger/properties.d.ts +14 -4
- package/dist/FileInput/FileInput.svelte +38 -9
- package/dist/FileInput/context.d.ts +20 -0
- package/dist/FileInput/context.js +8 -0
- package/dist/FileInput/properties.d.ts +25 -1
- package/dist/FunnelChart/FunnelChart.svelte +20 -1
- package/dist/FunnelChart/properties.d.ts +4 -0
- package/dist/Gallery/Gallery.svelte +72 -27
- package/dist/HITL/HITL.svelte +12 -3
- package/dist/LineChart/LineChart.svelte +105 -33
- package/dist/LineChart/properties.d.ts +12 -0
- package/dist/MediaPlayer/MediaPlayer.svelte +172 -2
- package/dist/MediaPlayer/MediaPlayer.svelte.d.ts +1 -1
- package/dist/MediaPlayer/properties.d.ts +31 -0
- package/dist/Menu/Menu.svelte +6 -4
- package/dist/Modal/Modal.svelte +15 -12
- package/dist/Modal/properties.d.ts +29 -1
- package/dist/Pagination/Pagination.svelte +1 -0
- package/dist/PieChart/PieChart.svelte +51 -29
- package/dist/PieChart/properties.d.ts +4 -0
- package/dist/Pill/Pill.svelte +5 -0
- package/dist/Radio/Radio.svelte +11 -2
- package/dist/Radio/properties.d.ts +7 -2
- package/dist/SankeyChart/SankeyChart.svelte +339 -211
- package/dist/SankeyChart/properties.d.ts +4 -0
- package/dist/Scroller/Scroller.svelte +236 -4
- package/dist/Scroller/properties.d.ts +8 -0
- package/dist/Select/Select.svelte +43 -5
- package/dist/Select/properties.d.ts +17 -1
- package/dist/Sheet/Sheet.svelte +31 -4
- package/dist/Sheet/properties.d.ts +6 -0
- package/dist/SplitInput/SplitInput.svelte +17 -0
- package/dist/SplitInput/field-name.d.ts +29 -0
- package/dist/SplitInput/field-name.js +50 -0
- package/dist/SplitInput/properties.d.ts +18 -4
- package/dist/Status/Status.svelte +24 -12
- package/dist/Status/properties.d.ts +11 -0
- package/dist/Table/BuiltinCell.svelte +13 -5
- package/dist/Table/BuiltinCell.svelte.d.ts +1 -0
- package/dist/Table/Table.svelte +78 -20
- package/dist/Table/a11y.d.ts +7 -0
- package/dist/Table/a11y.js +71 -0
- package/dist/Table/cellData.js +3 -0
- package/dist/Table/properties.d.ts +13 -5
- package/dist/ThemeSwitcher/ThemeSwitcher.svelte +4 -0
- package/dist/ThinkingIndicator/ThinkingIndicator.svelte +48 -9
- package/dist/Toast/Toast.svelte +40 -4
- package/dist/Toggle/Toggle.svelte +2 -0
- package/dist/Toggle/properties.d.ts +2 -0
- package/dist/_chart/Axis.svelte +10 -0
- package/dist/_chart/ChartContainer.svelte +32 -1
- package/dist/_chart/ChartTooltip.svelte +18 -3
- package/dist/_chart/LabelPlate.svelte +114 -0
- package/dist/_chart/LabelPlate.svelte.d.ts +7 -0
- package/dist/_chart/a11y.d.ts +29 -0
- package/dist/_chart/a11y.js +44 -0
- package/dist/_chart/measure.d.ts +2 -0
- package/dist/_chart/measure.js +4 -0
- package/dist/_chart/tooltipPosition.d.ts +6 -0
- package/dist/_chart/tooltipPosition.js +30 -2
- package/dist/_chart/types.d.ts +22 -0
- package/dist/_interaction/focusable.d.ts +47 -0
- package/dist/_interaction/focusable.js +139 -0
- package/dist/assets/captions-on.svg +3 -0
- package/dist/assets/captions.svg +5 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/styles/theme-dark.css +6 -1
- package/dist/types.d.ts +2 -0
- package/dist/utils.d.ts +18 -2
- package/dist/utils.js +73 -28
- package/dist-codemod/codemod/migrate-audit.js +9 -7
- package/dist-codemod/codemod/migration-assets/legacy-palette.css +9 -1
- package/dist-codemod/codemod/migration-assets-layout.js +61 -0
- package/dist-wc/index.js +5653 -4148
- package/package.json +4 -2
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
const nonEmpty = (text) => {
|
|
2
|
+
const trimmed = typeof text === 'string' ? text.trim() : '';
|
|
3
|
+
return trimmed === '' ? null : trimmed;
|
|
4
|
+
};
|
|
5
|
+
const capitalise = (text) => text.charAt(0).toUpperCase() + text.slice(1);
|
|
6
|
+
/**
|
|
7
|
+
* What one box holds, so the position reads as the user experiences it: the boxes
|
|
8
|
+
* of a PIN or one-time code are "digit 2 of 6", the boxes of a free-text code are
|
|
9
|
+
* "character 2 of 6", and anything wider than one character ("192", an area code)
|
|
10
|
+
* is just a "field" -- calling a three-digit octet a digit would be wrong.
|
|
11
|
+
*/
|
|
12
|
+
const unitFor = (config) => {
|
|
13
|
+
if (config.maxLength !== 1) {
|
|
14
|
+
return 'field';
|
|
15
|
+
}
|
|
16
|
+
return config.dataType === 'tel' || config.dataType === 'number' ? 'digit' : 'character';
|
|
17
|
+
};
|
|
18
|
+
/** Capitalised only when nothing precedes it: after "One-time code," it stays lower-case. */
|
|
19
|
+
const defaultPosition = (config, index, total, leading) => {
|
|
20
|
+
const text = `${unitFor(config)} ${index + 1} of ${total}`;
|
|
21
|
+
return leading ? capitalise(text) : text;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* The accessible name for one box of a SplitInput.
|
|
25
|
+
*
|
|
26
|
+
* A box on its own is meaningless -- "edit text, blank", six times -- so each one
|
|
27
|
+
* has to say which field it is. In order of how specific the caller was:
|
|
28
|
+
*
|
|
29
|
+
* 1. `config.ariaLabel`: the caller named this box by purpose ("Red", "Area
|
|
30
|
+
* code"). Used exactly as given; it already says which field it is.
|
|
31
|
+
* 2. `config.label`: the visible caption under the box ("R"), which is what the
|
|
32
|
+
* box is already called on screen -- the name has to contain it.
|
|
33
|
+
* 3. Otherwise the box's position: "digit 2 of 6".
|
|
34
|
+
*
|
|
35
|
+
* For 2 and 3 the group's name leads, because a screen reader's form-field list
|
|
36
|
+
* shows each control without its group: "One-time code, digit 2 of 6" is
|
|
37
|
+
* identifiable there, "digit 2 of 6" is not. With no group name the position is
|
|
38
|
+
* all there is, so the group should be named -- see `SplitInput`'s `ariaLabel`.
|
|
39
|
+
*/
|
|
40
|
+
export const resolveFieldName = ({ index, total, config, groupLabel, positionLabel }) => {
|
|
41
|
+
const explicit = nonEmpty(config.ariaLabel);
|
|
42
|
+
if (explicit !== null) {
|
|
43
|
+
return explicit;
|
|
44
|
+
}
|
|
45
|
+
const group = nonEmpty(groupLabel);
|
|
46
|
+
const own = nonEmpty(config.label) ??
|
|
47
|
+
nonEmpty(positionLabel?.(index + 1, total)) ??
|
|
48
|
+
defaultPosition(config, index, total, group === null);
|
|
49
|
+
return group === null ? own : `${group}, ${own}`;
|
|
50
|
+
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { OptionalInputProperties } from '../Input/properties';
|
|
2
|
-
export type FieldConfig = Pick<OptionalInputProperties, 'dataType' | 'maxLength' | 'min' | 'max' | 'placeholder' | 'validationPattern' | 'validators' | 'label' | 'autoComplete' | 'inputMode' | 'testId'>;
|
|
2
|
+
export type FieldConfig = Pick<OptionalInputProperties, 'dataType' | 'maxLength' | 'min' | 'max' | 'placeholder' | 'validationPattern' | 'validators' | 'label' | 'autoComplete' | 'inputMode' | 'testId' | 'ariaLabel'>;
|
|
3
3
|
export type SplitInputProperties = MandatorySplitInputProperties & OptionalSplitInputProperties & SplitInputEventProperties;
|
|
4
4
|
export type MandatorySplitInputProperties = {
|
|
5
5
|
values: string[];
|
|
@@ -13,11 +13,25 @@ export type OptionalSplitInputProperties = {
|
|
|
13
13
|
testId?: string;
|
|
14
14
|
classes?: string;
|
|
15
15
|
/**
|
|
16
|
-
* Names the whole group ("One-time code")
|
|
17
|
-
*
|
|
18
|
-
*
|
|
16
|
+
* Names the whole group ("One-time code"), and leads the name of every box in
|
|
17
|
+
* it ("One-time code, digit 2 of 6"), so each box is identifiable on its own --
|
|
18
|
+
* a screen reader's form-field list shows controls without their group. The
|
|
19
|
+
* boxes are individually meaningless, so naming the group is what makes them
|
|
20
|
+
* one control; leave it off and each box is named by its position alone
|
|
21
|
+
* ("Digit 2 of 6"), which says where it is but not what it is for.
|
|
22
|
+
*
|
|
23
|
+
* To name one box instead, set `ariaLabel` on its `fields` entry.
|
|
19
24
|
*/
|
|
20
25
|
ariaLabel?: string;
|
|
26
|
+
/**
|
|
27
|
+
* Builds the position part of a box's accessible name from its 1-based
|
|
28
|
+
* position and the number of boxes. Replaces the English default ("digit 2 of
|
|
29
|
+
* 6" for single-digit boxes, "character 2 of 6" for single-character ones,
|
|
30
|
+
* "field 2 of 4" for wider boxes), so it is the hook for another language or
|
|
31
|
+
* a better noun ("octet 2 of 4"). A box with its own `ariaLabel` or visible
|
|
32
|
+
* `label` is named by that instead and never asks for a position.
|
|
33
|
+
*/
|
|
34
|
+
positionLabel?: (position: number, total: number) => string;
|
|
21
35
|
/**
|
|
22
36
|
* Text shown, and announced, when the assembled value is in error ("That code
|
|
23
37
|
* is not right"). Referenced by `aria-describedby` on the group rather than on
|
|
@@ -4,18 +4,24 @@
|
|
|
4
4
|
import Img from '../Img/Img.svelte';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
7
|
+
* What `statusIcon` used to default to. It is a relative URL resolved against
|
|
8
|
+
* whatever page renders the component, and the library ships no such file, so
|
|
9
|
+
* it only ever pointed at something real for an app serving that exact path at
|
|
10
|
+
* its own root -- and resolved to `<route>/icons/order-success-icon.svg`
|
|
11
|
+
* anywhere deeper, where every card using the default made a request that
|
|
12
|
+
* 404ed before the fallback below rendered.
|
|
13
|
+
*
|
|
14
|
+
* It is no longer the default: an omitted `statusIcon` renders the built-in
|
|
15
|
+
* icon directly, with no request. The string is kept only so a consumer who
|
|
16
|
+
* spells it out themselves keeps the fallback they already had.
|
|
12
17
|
*/
|
|
13
18
|
const LEGACY_DEFAULT_STATUS_ICON = 'icons/order-success-icon.svg';
|
|
14
19
|
|
|
15
20
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
21
|
+
* The default icon, inlined so it needs no request and no packaged asset (a
|
|
22
|
+
* file would have to be resolved against the consumer's base path, the
|
|
23
|
+
* `svelte-package` output and the `<sui-*>` bundle alike). Also stands in when
|
|
24
|
+
* a consumer spells out the legacy default and it cannot be fetched.
|
|
19
25
|
*
|
|
20
26
|
* Carries no `role` or `aria-label` of its own. Both are in `Img`'s
|
|
21
27
|
* allowlist, so a root declaring them would be copied onto the live host and
|
|
@@ -26,7 +32,7 @@
|
|
|
26
32
|
"data:image/svg+xml,%3Csvg%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20viewBox%3D'0%200%2048%2048'%3E%3Ccircle%20cx%3D'24'%20cy%3D'24'%20r%3D'22'%20fill%3D'%23e6f6ec'%20stroke%3D'%232e994c'%20stroke-width%3D'2'%2F%3E%3Cpath%20d%3D'M15%2024.5l6.5%206.5L33%2019.5'%20fill%3D'none'%20stroke%3D'%232e994c'%20stroke-width%3D'3.5'%20stroke-linecap%3D'round'%20stroke-linejoin%3D'round'%2F%3E%3C%2Fsvg%3E";
|
|
27
33
|
|
|
28
34
|
let {
|
|
29
|
-
statusIcon
|
|
35
|
+
statusIcon,
|
|
30
36
|
statusIconAlt = 'status',
|
|
31
37
|
statusText = '',
|
|
32
38
|
statusTextTag = 'div',
|
|
@@ -40,8 +46,14 @@
|
|
|
40
46
|
testId
|
|
41
47
|
}: StatusProperties = $props();
|
|
42
48
|
|
|
43
|
-
//
|
|
44
|
-
//
|
|
49
|
+
// `??` rather than a parameter default: a `<sui-status>` whose `status-icon`
|
|
50
|
+
// attribute is removed hands the component `null`, which a default would pass
|
|
51
|
+
// straight to Img.
|
|
52
|
+
const resolvedStatusIcon = $derived(statusIcon ?? BUILTIN_STATUS_ICON);
|
|
53
|
+
|
|
54
|
+
// Scoped to the legacy default on purpose. Applying it to a caller's own icon
|
|
55
|
+
// would answer a failed failure-icon with a success checkmark. The built-in
|
|
56
|
+
// default needs none: it cannot fail to load.
|
|
45
57
|
const statusIconFallback = $derived(
|
|
46
58
|
statusIcon === LEGACY_DEFAULT_STATUS_ICON ? BUILTIN_STATUS_ICON : null
|
|
47
59
|
);
|
|
@@ -57,7 +69,7 @@
|
|
|
57
69
|
{#if icon}
|
|
58
70
|
{@render icon()}
|
|
59
71
|
{:else}
|
|
60
|
-
<Img inlineSvg src={
|
|
72
|
+
<Img inlineSvg src={resolvedStatusIcon} fallback={statusIconFallback} alt={statusIconAlt} />
|
|
61
73
|
{/if}
|
|
62
74
|
</div>
|
|
63
75
|
<svelte:element
|
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
2
|
import type { ButtonProperties } from '../Button/properties';
|
|
3
3
|
export type StatusProperties = StatusEventProperties & {
|
|
4
|
+
/**
|
|
5
|
+
* URL of the icon shown above the title. Omit it to get the built-in
|
|
6
|
+
* checkmark, which is inlined and costs no request.
|
|
7
|
+
*
|
|
8
|
+
* A URL you pass is used as given and never replaced when it fails to load:
|
|
9
|
+
* answering a failed failure-icon with a success checkmark would be worse
|
|
10
|
+
* than showing nothing. The one exception is the literal
|
|
11
|
+
* `'icons/order-success-icon.svg'` (the former default), which still falls
|
|
12
|
+
* back to the built-in checkmark so existing callers that spell it out are
|
|
13
|
+
* unchanged.
|
|
14
|
+
*/
|
|
4
15
|
statusIcon?: string;
|
|
5
16
|
/**
|
|
6
17
|
* Accessible name for the `statusIcon` image. Defaults to `'status'`, which
|
|
@@ -36,14 +36,19 @@
|
|
|
36
36
|
value,
|
|
37
37
|
rowIndex,
|
|
38
38
|
originalIndex,
|
|
39
|
+
ariaLabel,
|
|
39
40
|
usePortal = false
|
|
40
41
|
}: {
|
|
41
42
|
column: TableColumn;
|
|
42
43
|
value: TableCellValue;
|
|
43
44
|
rowIndex: number;
|
|
44
45
|
originalIndex: number;
|
|
46
|
+
ariaLabel?: string;
|
|
45
47
|
usePortal?: boolean;
|
|
46
48
|
} = $props();
|
|
49
|
+
const fieldLabel = $derived(
|
|
50
|
+
ariaLabel?.trim() || `${column.label || column.id} for row ${originalIndex + 1}`
|
|
51
|
+
);
|
|
47
52
|
|
|
48
53
|
// The td applies column.align as text-align, which flex containers ignore:
|
|
49
54
|
// column-flex builtins stretch their children and row-flex builtins pack to
|
|
@@ -301,20 +306,22 @@
|
|
|
301
306
|
{:else if column.type === 'toggle'}
|
|
302
307
|
{@const data = asJsonObject(value) ?? {}}
|
|
303
308
|
{@const isChecked = data.checked === true}
|
|
309
|
+
<!-- This is only an event boundary; the native Toggle input owns interaction. -->
|
|
310
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
304
311
|
<span
|
|
305
312
|
class="builtin-toggle"
|
|
306
|
-
role="switch"
|
|
307
|
-
aria-checked={isChecked ? 'true' : 'false'}
|
|
308
|
-
aria-label={typeof data.ariaLabel === 'string' ? data.ariaLabel : null}
|
|
309
313
|
data-pw={typeof data.testId === 'string' ? data.testId : null}
|
|
310
314
|
testID={typeof data.testId === 'string' ? data.testId : null}
|
|
311
315
|
onclick={stopClickPropagation}
|
|
312
316
|
onkeydown={stopKeydownPropagation}
|
|
313
|
-
tabindex={-1}
|
|
314
317
|
>
|
|
315
318
|
<Toggle
|
|
316
319
|
checked={isChecked}
|
|
317
320
|
text=""
|
|
321
|
+
inputRole="switch"
|
|
322
|
+
ariaLabel={typeof data.ariaLabel === 'string' && data.ariaLabel.trim()
|
|
323
|
+
? data.ariaLabel
|
|
324
|
+
: fieldLabel}
|
|
318
325
|
onclick={(newChecked) => column.onToggle?.(rowIndex, newChecked, originalIndex)}
|
|
319
326
|
/>
|
|
320
327
|
</span>
|
|
@@ -328,6 +335,7 @@
|
|
|
328
335
|
onkeydown={stopKeydownPropagation}
|
|
329
336
|
>
|
|
330
337
|
<Select
|
|
338
|
+
ariaLabel={selectData.ariaLabel?.trim() || fieldLabel}
|
|
331
339
|
items={selectData.options}
|
|
332
340
|
value={selectData.selectedId ? [selectData.selectedId] : []}
|
|
333
341
|
placeholder={selectData.placeholder ?? ''}
|
|
@@ -360,7 +368,7 @@
|
|
|
360
368
|
<Input
|
|
361
369
|
value={inputData.value ?? ''}
|
|
362
370
|
placeholder={inputData.placeholder ?? ''}
|
|
363
|
-
ariaLabel={inputData.ariaLabel
|
|
371
|
+
ariaLabel={inputData.ariaLabel?.trim() || fieldLabel}
|
|
364
372
|
disable={inputData.disabled ?? false}
|
|
365
373
|
testId={inputData.testId ?? ''}
|
|
366
374
|
dataType={asInputDataType(inputData.dataType ?? null)}
|
package/dist/Table/Table.svelte
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
SortDirection
|
|
7
7
|
} from './properties';
|
|
8
8
|
import { normalizeColumns } from './normalizeColumns';
|
|
9
|
+
import { rowContextLabels, selectionControlIds } from './a11y';
|
|
9
10
|
import BuiltinCell from './BuiltinCell.svelte';
|
|
10
11
|
import type { JSONValue } from 'type-decoder';
|
|
11
12
|
import { SvelteSet } from 'svelte/reactivity';
|
|
@@ -74,6 +75,9 @@
|
|
|
74
75
|
// (pre-sort/pre-filter). Row refs are preserved through sort/filter/paginate,
|
|
75
76
|
// so cell callbacks can hand consumers a sort-stable `originalIndex`.
|
|
76
77
|
let originalIndexByRow = $derived(new Map(effectiveData.map((row, index) => [row, index])));
|
|
78
|
+
const tableUid = $props.id();
|
|
79
|
+
const selectionId = selectionControlIds(tableUid);
|
|
80
|
+
let rowLabels = $derived(rowContextLabels(effectiveData, columns));
|
|
77
81
|
let effectiveSortableColumns = $derived(
|
|
78
82
|
normalized ? normalized.sortableColumns : sortableColumns
|
|
79
83
|
);
|
|
@@ -544,7 +548,7 @@
|
|
|
544
548
|
rowIndex: number,
|
|
545
549
|
originalIndex: number
|
|
546
550
|
): string => {
|
|
547
|
-
return cfg.getRowId ? cfg.getRowId(row, rowIndex, originalIndex) : String(
|
|
551
|
+
return cfg.getRowId ? cfg.getRowId(row, rowIndex, originalIndex) : String(originalIndex);
|
|
548
552
|
};
|
|
549
553
|
|
|
550
554
|
let internalSelectedIds = new SvelteSet<string>();
|
|
@@ -569,12 +573,9 @@
|
|
|
569
573
|
/**
|
|
570
574
|
* Stable string ID for every row in the currently visible (filtered) view.
|
|
571
575
|
*
|
|
572
|
-
*
|
|
573
|
-
*
|
|
574
|
-
*
|
|
575
|
-
* stable even when the visible set shrinks or expands as the search term
|
|
576
|
-
* changes — so a row that was "row 2" in the full set keeps ID `"2"` even
|
|
577
|
-
* when it becomes the only visible result.
|
|
576
|
+
* The callback retains its current pre-filter sorted index plus originalIndex.
|
|
577
|
+
* The default ID uses the source index, so sort/filter/page transforms do not
|
|
578
|
+
* silently transfer selection to a different record.
|
|
578
579
|
*
|
|
579
580
|
* The Map lookup uses object reference equality, so row objects in
|
|
580
581
|
* `filteredTableData` must be the same references as those in
|
|
@@ -594,7 +595,7 @@
|
|
|
594
595
|
originalIndexByRow.get(row) ?? stableIndex
|
|
595
596
|
);
|
|
596
597
|
}
|
|
597
|
-
return String(stableIndex);
|
|
598
|
+
return String(originalIndexByRow.get(row) ?? stableIndex);
|
|
598
599
|
});
|
|
599
600
|
});
|
|
600
601
|
|
|
@@ -718,13 +719,13 @@
|
|
|
718
719
|
};
|
|
719
720
|
|
|
720
721
|
/**
|
|
721
|
-
* Attributes for one
|
|
722
|
-
*
|
|
723
|
-
*
|
|
722
|
+
* Attributes for one checkbox (`rowIndex` -1 is the header). DOM identity is
|
|
723
|
+
* separate from the consumer's selection key. Preserve other caller attributes
|
|
724
|
+
* and valid unique custom IDs, while managing unsafe/colliding IDs ourselves.
|
|
724
725
|
*/
|
|
725
726
|
const checkboxAttributes = (rowId: string, rowIndex: number): Record<string, string> => {
|
|
726
727
|
const suffix = rowIndex === -1 ? 'select-all' : `row-checkbox-${rowId}`;
|
|
727
|
-
const own: Record<string, string> =
|
|
728
|
+
const own: Record<string, string> = {};
|
|
728
729
|
if (typeof testId === 'string') {
|
|
729
730
|
own['data-pw'] = `${testId}-${suffix}`;
|
|
730
731
|
own.testID = `${testId}-${suffix}`;
|
|
@@ -732,9 +733,36 @@
|
|
|
732
733
|
const consumer = checkboxSelection?.getRowAttributes
|
|
733
734
|
? checkboxSelection.getRowAttributes(rowId, rowIndex)
|
|
734
735
|
: {};
|
|
735
|
-
return {
|
|
736
|
+
return {
|
|
737
|
+
...own,
|
|
738
|
+
...consumer,
|
|
739
|
+
id: selectionId(rowId, rowIndex === -1, consumer.id)
|
|
740
|
+
};
|
|
736
741
|
};
|
|
737
742
|
|
|
743
|
+
let headerCheckboxAttributes = $derived(checkboxAttributes('__header__', -1));
|
|
744
|
+
// Evaluate caller attributes once for each rendered control. Both the DOM and
|
|
745
|
+
// its IDREF list consume the same result, including a caller-supplied ID.
|
|
746
|
+
let renderedCheckboxAttributes = $derived.by(() => {
|
|
747
|
+
if (!checkboxSelection || checkboxSelection.enabled === false) {
|
|
748
|
+
return new Map<JSONValue[], Record<string, string>>();
|
|
749
|
+
}
|
|
750
|
+
return new Map(
|
|
751
|
+
paginatedTableData.map((row, index) => {
|
|
752
|
+
const rowIndex = index + rowIndexOffset;
|
|
753
|
+
const rowId = rowIdByRow.get(row) ?? String(originalIndexByRow.get(row) ?? rowIndex);
|
|
754
|
+
return [row, checkboxAttributes(rowId, rowIndex)];
|
|
755
|
+
})
|
|
756
|
+
);
|
|
757
|
+
});
|
|
758
|
+
let controlledRowControlIds = $derived(
|
|
759
|
+
paginatedTableData.flatMap((row) => {
|
|
760
|
+
const rowId = rowIdByRow.get(row);
|
|
761
|
+
const id = renderedCheckboxAttributes.get(row)?.id;
|
|
762
|
+
return typeof rowId === 'string' && !isRowDisabled(rowId) && id ? [id] : [];
|
|
763
|
+
})
|
|
764
|
+
);
|
|
765
|
+
|
|
738
766
|
// ─── Empty-state reason ───────────────────────────────────────────────────
|
|
739
767
|
// "No records yet" and "nothing matched" need different messages and offer
|
|
740
768
|
// different remedies, and a consumer cannot tell them apart from `rows` alone
|
|
@@ -821,7 +849,14 @@
|
|
|
821
849
|
{#if caption}
|
|
822
850
|
<caption class="sr-only">{caption}</caption>
|
|
823
851
|
{/if}
|
|
824
|
-
<thead
|
|
852
|
+
<thead
|
|
853
|
+
role={mobileRole('rowgroup')}
|
|
854
|
+
class:mobile-controls={(isCheckboxMode && !isSingleSelect) ||
|
|
855
|
+
(hasSearchConfig && isInlineSearch) ||
|
|
856
|
+
effectiveHeaders.some(
|
|
857
|
+
(_header, index) => isColumnSortable(index) || Boolean(columns?.[index]?.tooltip)
|
|
858
|
+
)}
|
|
859
|
+
>
|
|
825
860
|
<tr role={mobileRole('row')}>
|
|
826
861
|
{#if isCheckboxMode && !isSingleSelect}
|
|
827
862
|
<th
|
|
@@ -837,10 +872,8 @@
|
|
|
837
872
|
controlled
|
|
838
873
|
checked={headerCheckboxState === 'all'}
|
|
839
874
|
indeterminate={headerCheckboxState === 'some'}
|
|
840
|
-
ariaControls={
|
|
841
|
-
|
|
842
|
-
.join(' ')}
|
|
843
|
-
attributes={checkboxAttributes('__header__', -1)}
|
|
875
|
+
ariaControls={controlledRowControlIds.join(' ')}
|
|
876
|
+
attributes={headerCheckboxAttributes}
|
|
844
877
|
onclick={toggleAllSelection}
|
|
845
878
|
/>
|
|
846
879
|
</th>
|
|
@@ -1096,7 +1129,7 @@
|
|
|
1096
1129
|
controlled
|
|
1097
1130
|
checked={rowSelected}
|
|
1098
1131
|
disabled={rowDisabled}
|
|
1099
|
-
attributes={
|
|
1132
|
+
attributes={renderedCheckboxAttributes.get(row)}
|
|
1100
1133
|
onclick={() => toggleRowSelection(rowId)}
|
|
1101
1134
|
/>
|
|
1102
1135
|
</td>
|
|
@@ -1148,9 +1181,16 @@
|
|
|
1148
1181
|
{rowIndex}
|
|
1149
1182
|
{originalIndex}
|
|
1150
1183
|
{usePortal}
|
|
1184
|
+
ariaLabel={labels
|
|
1185
|
+
?.cellEditor?.(
|
|
1186
|
+
cellLabel,
|
|
1187
|
+
rowLabels.get(row) ?? `row ${originalIndex + 1}`
|
|
1188
|
+
)
|
|
1189
|
+
?.trim() ||
|
|
1190
|
+
`${cellLabel} for ${rowLabels.get(row) ?? `row ${originalIndex + 1}`}`}
|
|
1151
1191
|
/>
|
|
1152
1192
|
{:else if typeof cell === 'function'}
|
|
1153
|
-
{@render cell(cellValue, rowIndex, colIndex)}
|
|
1193
|
+
{@render cell(cellValue, rowIndex, colIndex, originalIndex)}
|
|
1154
1194
|
{:else}
|
|
1155
1195
|
{cellValue}
|
|
1156
1196
|
{/if}
|
|
@@ -1790,6 +1830,7 @@
|
|
|
1790
1830
|
align-items: center;
|
|
1791
1831
|
gap: var(--table-paginator-gap, 12px);
|
|
1792
1832
|
flex-wrap: wrap;
|
|
1833
|
+
min-width: 0;
|
|
1793
1834
|
}
|
|
1794
1835
|
|
|
1795
1836
|
.table-paginator-controls :global(.pagination) {
|
|
@@ -1872,6 +1913,10 @@
|
|
|
1872
1913
|
.table-mobile-cards td {
|
|
1873
1914
|
display: block;
|
|
1874
1915
|
width: 100%;
|
|
1916
|
+
/* `width: 100%` is the card's outer width only if padding and border are
|
|
1917
|
+
inside it: the card row has both, so under content-box it was 26px wider
|
|
1918
|
+
than the table and its right edge ran past the column. */
|
|
1919
|
+
box-sizing: border-box;
|
|
1875
1920
|
}
|
|
1876
1921
|
|
|
1877
1922
|
/* Headers are not shown as a row anymore -- their text now repeats per
|
|
@@ -1891,6 +1936,19 @@
|
|
|
1891
1936
|
border-width: 0;
|
|
1892
1937
|
}
|
|
1893
1938
|
|
|
1939
|
+
/* A screen-reader-only header must not conceal operative sort or select-all
|
|
1940
|
+
controls. Keep those headers visible above the record cards, with the same
|
|
1941
|
+
controls, names and table relationships used at desktop width. */
|
|
1942
|
+
.table-mobile-cards thead.mobile-controls {
|
|
1943
|
+
position: static;
|
|
1944
|
+
width: 100%;
|
|
1945
|
+
height: auto;
|
|
1946
|
+
overflow: visible;
|
|
1947
|
+
clip: auto;
|
|
1948
|
+
clip-path: none;
|
|
1949
|
+
white-space: normal;
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1894
1952
|
/* The horizontal-scroll affordance this table uses at desktop width (see
|
|
1895
1953
|
.table-scroll / .table-scroll-shell above) has nothing to scroll once
|
|
1896
1954
|
every cell is a full-width block, and a lingering scrim would fade a
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { TableCellValue, TableColumn } from './properties';
|
|
2
|
+
/** Readable row context, independent of editable/selected control values. */
|
|
3
|
+
export declare function rowContextLabel(row: readonly TableCellValue[], originalIndex: number, columns?: readonly TableColumn[]): string;
|
|
4
|
+
/** Constructed once per data projection, then read without mutation by Table. */
|
|
5
|
+
export declare function rowContextLabels(rows: readonly TableCellValue[][], columns?: readonly TableColumn[]): ReadonlyMap<TableCellValue[], string>;
|
|
6
|
+
/** Opaque per-instance IDs: distinct raw keys never collapse through slugging. */
|
|
7
|
+
export declare function selectionControlIds(instanceId: string): (key: string, header: boolean, requestedId?: string) => string;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
const controlTypes = new Set(['input', 'select', 'toggle', 'button', 'action-group', 'popup-menu']);
|
|
2
|
+
/** Readable row context, independent of editable/selected control values. */
|
|
3
|
+
export function rowContextLabel(row, originalIndex, columns) {
|
|
4
|
+
for (const [index, value] of row.entries()) {
|
|
5
|
+
if (controlTypes.has(columns?.[index]?.type ?? '')) {
|
|
6
|
+
continue;
|
|
7
|
+
}
|
|
8
|
+
if (typeof value === 'string' && value.trim() !== '') {
|
|
9
|
+
return value;
|
|
10
|
+
}
|
|
11
|
+
if (typeof value === 'number') {
|
|
12
|
+
return String(value);
|
|
13
|
+
}
|
|
14
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
15
|
+
for (const key of ['text1', 'text', 'label', 'title', 'name']) {
|
|
16
|
+
const text = value[key];
|
|
17
|
+
if (typeof text === 'string' && text.trim() !== '') {
|
|
18
|
+
return text;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return `row ${originalIndex + 1}`;
|
|
24
|
+
}
|
|
25
|
+
/** Constructed once per data projection, then read without mutation by Table. */
|
|
26
|
+
export function rowContextLabels(rows, columns) {
|
|
27
|
+
const captions = rows.map((row, index) => rowContextLabel(row, index, columns));
|
|
28
|
+
const counts = new Map();
|
|
29
|
+
for (const label of captions) {
|
|
30
|
+
counts.set(label, (counts.get(label) ?? 0) + 1);
|
|
31
|
+
}
|
|
32
|
+
return new Map(rows.map((row, index) => [
|
|
33
|
+
row,
|
|
34
|
+
(counts.get(captions[index]) ?? 0) > 1
|
|
35
|
+
? `${captions[index]}, record ${index + 1}`
|
|
36
|
+
: captions[index]
|
|
37
|
+
]));
|
|
38
|
+
}
|
|
39
|
+
/** Opaque per-instance IDs: distinct raw keys never collapse through slugging. */
|
|
40
|
+
export function selectionControlIds(instanceId) {
|
|
41
|
+
const rows = new Map();
|
|
42
|
+
const owners = new Map();
|
|
43
|
+
const headerId = `table-${instanceId}-select-all`;
|
|
44
|
+
owners.set(headerId, 'header');
|
|
45
|
+
let sequence = 0;
|
|
46
|
+
const rowId = (key) => {
|
|
47
|
+
let id = rows.get(key);
|
|
48
|
+
if (typeof id !== 'string') {
|
|
49
|
+
do {
|
|
50
|
+
id = `table-${instanceId}-row-selection-${++sequence}`;
|
|
51
|
+
} while (owners.has(id));
|
|
52
|
+
rows.set(key, id);
|
|
53
|
+
owners.set(id, `row:${key}`);
|
|
54
|
+
}
|
|
55
|
+
return id;
|
|
56
|
+
};
|
|
57
|
+
return (key, header, requestedId) => {
|
|
58
|
+
const owner = header ? 'header' : `row:${key}`;
|
|
59
|
+
const generated = header ? headerId : rowId(key);
|
|
60
|
+
// Valid caller IDs remain usable for external labels/CSS. Invalid or conflicting
|
|
61
|
+
// IDs fall back to the managed ID; ownership persists across filter/page changes.
|
|
62
|
+
if (typeof requestedId === 'string' &&
|
|
63
|
+
requestedId !== '' &&
|
|
64
|
+
!/\s/.test(requestedId) &&
|
|
65
|
+
(!owners.has(requestedId) || owners.get(requestedId) === owner)) {
|
|
66
|
+
owners.set(requestedId, owner);
|
|
67
|
+
return requestedId;
|
|
68
|
+
}
|
|
69
|
+
return generated;
|
|
70
|
+
};
|
|
71
|
+
}
|
package/dist/Table/cellData.js
CHANGED
|
@@ -154,6 +154,9 @@ export const asSelectCellData = (value) => {
|
|
|
154
154
|
options.push({ id: optionRecord.id, label: optionRecord.label });
|
|
155
155
|
}
|
|
156
156
|
const selectData = { options };
|
|
157
|
+
if (typeof record.ariaLabel === 'string') {
|
|
158
|
+
selectData.ariaLabel = record.ariaLabel;
|
|
159
|
+
}
|
|
157
160
|
if (typeof record.selectedId === 'string') {
|
|
158
161
|
selectData.selectedId = record.selectedId;
|
|
159
162
|
}
|
|
@@ -128,6 +128,8 @@ export type TableSelectCellData = {
|
|
|
128
128
|
selectedId?: string;
|
|
129
129
|
placeholder?: string;
|
|
130
130
|
disabled?: boolean;
|
|
131
|
+
/** Overrides the generated column/row field name on the real combobox. */
|
|
132
|
+
ariaLabel?: string;
|
|
131
133
|
testId?: string;
|
|
132
134
|
/** Per-option test id prefix — each option emits `data-pw="{itemTestId}-{id}"`. */
|
|
133
135
|
itemTestId?: string;
|
|
@@ -349,7 +351,7 @@ export type TableColumn = {
|
|
|
349
351
|
* - `onSelectionChange` — called whenever the selection set changes, receives
|
|
350
352
|
* the new set of selected row IDs.
|
|
351
353
|
* - `getRowId` — derives a stable string ID from a row + its index. Defaults
|
|
352
|
-
* to
|
|
354
|
+
* to the original source index when omitted.
|
|
353
355
|
* - `disabledRowIds` — rows whose IDs appear in this set render a disabled,
|
|
354
356
|
* unchecked checkbox and cannot be selected.
|
|
355
357
|
*/
|
|
@@ -376,7 +378,9 @@ export type TableCheckboxSelectionConfig = {
|
|
|
376
378
|
/**
|
|
377
379
|
* Generic DOM-attribute spread onto each row checkbox (`rowIndex` is `-1`
|
|
378
380
|
* for the header select-all). An escape hatch for consumer-specific
|
|
379
|
-
* attributes (e.g. native test IDs) without the library learning them.
|
|
381
|
+
* attributes (e.g. native test IDs) without the library learning them. A custom
|
|
382
|
+
* `id` must be stable and unique within its DOM scope; blank, whitespace-bearing
|
|
383
|
+
* or conflicting IDs use the generated per-instance control ID instead.
|
|
380
384
|
*/
|
|
381
385
|
getRowAttributes?: (rowId: string, rowIndex: number) => Record<string, string>;
|
|
382
386
|
};
|
|
@@ -521,8 +525,8 @@ export type TableProperties = OptionalTableProperties & TableEventProperties;
|
|
|
521
525
|
* not ship an English UI.
|
|
522
526
|
*
|
|
523
527
|
* Every member is optional and falls back to an English default. All but
|
|
524
|
-
* `rowsPerPage` keep the string Table has always used;
|
|
525
|
-
* name to a control that previously had none.
|
|
528
|
+
* `rowsPerPage` and `cellEditor` keep the string Table has always used; those
|
|
529
|
+
* two add a name to a control that previously had none.
|
|
526
530
|
*
|
|
527
531
|
* The two that depend on a column, and the one that depends on a row, are
|
|
528
532
|
* functions rather than templates with a placeholder: a translator needs to
|
|
@@ -547,6 +551,8 @@ export type TableLabels = {
|
|
|
547
551
|
clearSearch?: string;
|
|
548
552
|
/** Button that dismisses the search box. Default: `Close search`. */
|
|
549
553
|
closeSearch?: string;
|
|
554
|
+
/** Built-in editor name. Default: `${columnLabel} for ${rowLabel}`. */
|
|
555
|
+
cellEditor?: (columnLabel: string, rowLabel: string) => string;
|
|
550
556
|
/** Page-size selector in the built-in paginator footer. Default: `Rows per page`. */
|
|
551
557
|
rowsPerPage?: string;
|
|
552
558
|
};
|
|
@@ -667,7 +673,9 @@ export type OptionalTableProperties = {
|
|
|
667
673
|
sortAscIcon?: Snippet;
|
|
668
674
|
sortDescIcon?: Snippet;
|
|
669
675
|
sortDefaultIcon?: Snippet;
|
|
670
|
-
|
|
676
|
+
/** Value, current row index, column index and stable original source index.
|
|
677
|
+
* Existing three-parameter snippets keep working and may ignore the added index. */
|
|
678
|
+
cell?: Snippet<[JSONValue, number, number, number]>;
|
|
671
679
|
/**
|
|
672
680
|
* Rendered in place of the rows when the view has none. Receives why
|
|
673
681
|
* (`TableEmptyContext`) so one snippet can serve both "no records yet" and
|
|
@@ -33,6 +33,10 @@
|
|
|
33
33
|
onchange
|
|
34
34
|
}: ThemeSwitcherProperties = $props();
|
|
35
35
|
|
|
36
|
+
// `value` is the INITIAL selection, not a controlled binding (docs/ThemeSwitcher.md): once
|
|
37
|
+
// mounted the switcher owns the selection -- clicks, the stored preference and onMount below
|
|
38
|
+
// move it -- so a later change to the prop is deliberately not followed.
|
|
39
|
+
// svelte-ignore state_referenced_locally
|
|
36
40
|
let currentValue: string = $state(value ?? 'system');
|
|
37
41
|
let systemPreference: string = $state('light');
|
|
38
42
|
|