@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
|
@@ -137,8 +137,15 @@ export function tokenizedFly(node, params) {
|
|
|
137
137
|
// above resolved to. 0.001 rather than a hard 0: still visually instant, but
|
|
138
138
|
// keeps this a real (non-zero-duration) transition for Svelte's own
|
|
139
139
|
// lifecycle/event handling instead of a degenerate edge case.
|
|
140
|
-
|
|
140
|
+
// MediaQueryList change events are asynchronous. A freshly opened transition
|
|
141
|
+
// must use the actual preference now, including a mounted preference flip.
|
|
142
|
+
const reduce = typeof window !== 'undefined' && typeof window.matchMedia === 'function'
|
|
143
|
+
? window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
|
144
|
+
: reducedMotion.current;
|
|
145
|
+
if (reduce) {
|
|
141
146
|
duration = 0.001;
|
|
147
|
+
x = 0;
|
|
148
|
+
y = 0;
|
|
142
149
|
}
|
|
143
150
|
const targetOpacity = +styles.opacity;
|
|
144
151
|
const transform = styles.transform === 'none' ? '' : styles.transform;
|
|
@@ -147,7 +154,7 @@ export function tokenizedFly(node, params) {
|
|
|
147
154
|
easing,
|
|
148
155
|
css: (t) => `
|
|
149
156
|
transform: ${transform} translate(${(1 - t) * x}px, ${(1 - t) * y}px);
|
|
150
|
-
opacity: ${targetOpacity * t};
|
|
157
|
+
opacity: ${reduce ? targetOpacity : targetOpacity * t};
|
|
151
158
|
`
|
|
152
159
|
};
|
|
153
160
|
}
|
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
AreaChartProperties,
|
|
5
5
|
AreaChartTooltipContext
|
|
6
6
|
} from './properties';
|
|
7
|
-
import { onMount } from 'svelte';
|
|
8
7
|
import ChartContainer from '../_chart/ChartContainer.svelte';
|
|
9
8
|
import Axis from '../_chart/Axis.svelte';
|
|
10
9
|
import ChartTooltip from '../_chart/ChartTooltip.svelte';
|
|
@@ -18,17 +17,12 @@
|
|
|
18
17
|
import { resolvePointLabels } from '../_chart/labels';
|
|
19
18
|
import { pointerPositionIn, dismissOnOutsidePointerDown } from '../_chart/interactions';
|
|
20
19
|
import { formatSeriesAggregate } from '../_chart/aggregate';
|
|
20
|
+
import { defaultChartName, resolveChartName } from '../_chart/a11y';
|
|
21
21
|
import type { LegendItem, Point, TooltipAnchor } from '../_chart/types';
|
|
22
22
|
import { DEFAULT_CHART_MAX_HEIGHT } from '../_chart/types';
|
|
23
23
|
|
|
24
24
|
// ── Props ──────────────────────────────────────────────────────
|
|
25
25
|
|
|
26
|
-
// Per-instance ID for SVG gradient <linearGradient id> references. Initialised
|
|
27
|
-
// inside onMount so the value is only ever generated on the client — avoids an
|
|
28
|
-
// SSR hydration mismatch that would occur if Math.random() ran on both server
|
|
29
|
-
// and client and produced different strings.
|
|
30
|
-
let uid = $state('');
|
|
31
|
-
|
|
32
26
|
let {
|
|
33
27
|
series,
|
|
34
28
|
curve = 'monotone',
|
|
@@ -58,10 +52,20 @@
|
|
|
58
52
|
tooltipPortal = false,
|
|
59
53
|
onpointhover,
|
|
60
54
|
onpointclick,
|
|
55
|
+
ariaLabel,
|
|
56
|
+
ariaDescription,
|
|
61
57
|
testId,
|
|
62
58
|
classes
|
|
63
59
|
}: AreaChartProperties = $props();
|
|
64
60
|
|
|
61
|
+
// Per-instance id for the SVG <linearGradient id> references. `$props.id()` is
|
|
62
|
+
// unique per component instance and identical on server and client, so two
|
|
63
|
+
// charts on one page never resolve each other's `url(#gradient)` and hydration
|
|
64
|
+
// never sees a different id than the server wrote. It replaces a Math.random()
|
|
65
|
+
// prefix that was only filled in at mount (until then every chart's gradient id
|
|
66
|
+
// collapsed to the same `area-grad--0`) and was collision-prone by construction.
|
|
67
|
+
const instanceId = $props.id();
|
|
68
|
+
|
|
65
69
|
// ── State ──────────────────────────────────────────────────────
|
|
66
70
|
|
|
67
71
|
let containerEl: HTMLDivElement | null = $state(null);
|
|
@@ -84,10 +88,6 @@
|
|
|
84
88
|
let mouseX = $state(0);
|
|
85
89
|
let mouseY = $state(0);
|
|
86
90
|
|
|
87
|
-
onMount(() => {
|
|
88
|
-
uid = Math.random().toString(36).slice(2, 9);
|
|
89
|
-
});
|
|
90
|
-
|
|
91
91
|
// ── Layout ─────────────────────────────────────────────────────
|
|
92
92
|
|
|
93
93
|
let isStacked = $derived(stacked || stackNormalize);
|
|
@@ -246,6 +246,27 @@
|
|
|
246
246
|
}))
|
|
247
247
|
);
|
|
248
248
|
|
|
249
|
+
// ── Chart-level accessible name ────────────────────────────────
|
|
250
|
+
|
|
251
|
+
// The drawing's own name: the consumer's `ariaLabel`, else one derived from
|
|
252
|
+
// what the chart plots (see `defaultChartName`).
|
|
253
|
+
let chartName = $derived(
|
|
254
|
+
resolveChartName(
|
|
255
|
+
ariaLabel,
|
|
256
|
+
defaultChartName('area', { yAxisLabel, seriesNames: series.map((s) => s.name) })
|
|
257
|
+
)
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
// Every finite point is a focusable role="button" mark (the `.focus-target`
|
|
261
|
+
// circles below), so a drawing with any is interactive and must not be exposed
|
|
262
|
+
// as an image. With no finite point there is nothing to operate and it is a
|
|
263
|
+
// plain named image.
|
|
264
|
+
let hasMarks = $derived(
|
|
265
|
+
areas.some((area) =>
|
|
266
|
+
area.points.some((point) => Number.isFinite(point.x) && Number.isFinite(point.y))
|
|
267
|
+
)
|
|
268
|
+
);
|
|
269
|
+
|
|
249
270
|
// ── Tooltip ────────────────────────────────────────────────────
|
|
250
271
|
|
|
251
272
|
// Precedence: keyboard focus wins over pointer hover -- see the comment on
|
|
@@ -594,6 +615,9 @@
|
|
|
594
615
|
{aspectRatio}
|
|
595
616
|
{minHeight}
|
|
596
617
|
{maxHeight}
|
|
618
|
+
ariaLabel={chartName}
|
|
619
|
+
{ariaDescription}
|
|
620
|
+
interactive={hasMarks}
|
|
597
621
|
>
|
|
598
622
|
{#if gradientFill}
|
|
599
623
|
<!-- <defs> must be a direct child of <svg> (SVG root), not inside a transformed <g>.
|
|
@@ -602,7 +626,7 @@
|
|
|
602
626
|
<defs>
|
|
603
627
|
{#each areas as area, si (si)}
|
|
604
628
|
<linearGradient
|
|
605
|
-
id="area-grad-{
|
|
629
|
+
id="area-grad-{instanceId}-{si}"
|
|
606
630
|
x1="0"
|
|
607
631
|
y1="0"
|
|
608
632
|
x2="0"
|
|
@@ -658,7 +682,7 @@
|
|
|
658
682
|
class="area-fill"
|
|
659
683
|
class:dimmed={pointerOrFocused !== null && pointerOrFocused.si !== si}
|
|
660
684
|
d={area.areaD}
|
|
661
|
-
fill={gradientFill ? `url(#area-grad-${
|
|
685
|
+
fill={gradientFill ? `url(#area-grad-${instanceId}-${si})` : area.color}
|
|
662
686
|
fill-opacity={gradientFill
|
|
663
687
|
? 1
|
|
664
688
|
: pointerOrFocused?.si === si
|
|
@@ -708,8 +732,12 @@
|
|
|
708
732
|
{#each area.points as _point, pi (pi)}
|
|
709
733
|
{@const pl = pointLabelPlacements[si][pi]}
|
|
710
734
|
{#if pl?.visible && Number.isFinite(pl.x) && Number.isFinite(pl.y)}
|
|
735
|
+
<!-- Repeats the point's own accessible name, which is what
|
|
736
|
+
assistive technology reads; exposing it again would announce
|
|
737
|
+
every value twice. -->
|
|
711
738
|
<text
|
|
712
739
|
class="point-value"
|
|
740
|
+
aria-hidden="true"
|
|
713
741
|
x={pl.x}
|
|
714
742
|
y={pl.y}
|
|
715
743
|
text-anchor="middle"
|
|
@@ -67,6 +67,18 @@ export type OptionalAreaChartProperties = {
|
|
|
67
67
|
empty?: Snippet;
|
|
68
68
|
/** Render the tooltip into document.body so scroll/overflow ancestors never clip it. */
|
|
69
69
|
tooltipPortal?: boolean;
|
|
70
|
+
/**
|
|
71
|
+
* Accessible name of the chart (`aria-label` on its `<svg>`). Say what the chart
|
|
72
|
+
* shows, e.g. `"Monthly revenue, January to June"`. When omitted a name is
|
|
73
|
+
* derived from the chart's own data (`"{yAxisLabel} area chart"`, else the series names, else `"Area chart"`), so an unlabelled chart is still
|
|
74
|
+
* identifiable -- but a name written for the page is almost always better.
|
|
75
|
+
*/
|
|
76
|
+
ariaLabel?: string;
|
|
77
|
+
/**
|
|
78
|
+
* Optional longer description (trend, range, the takeaway) announced after the
|
|
79
|
+
* name. Rendered as an SVG `<desc>` referenced by `aria-describedby`.
|
|
80
|
+
*/
|
|
81
|
+
ariaDescription?: string;
|
|
70
82
|
testId?: string;
|
|
71
83
|
classes?: string;
|
|
72
84
|
};
|
|
@@ -33,13 +33,14 @@
|
|
|
33
33
|
{#if images.length > 0 || videos.length > 0 || files.length > 0}
|
|
34
34
|
<Scroller
|
|
35
35
|
direction="horizontal"
|
|
36
|
+
ariaLabel="Pending attachments"
|
|
36
37
|
showArrows={false}
|
|
37
38
|
hideScrollbar={false}
|
|
38
39
|
showGradient={false}
|
|
39
40
|
classes={`attachment-chip-row ${classes ?? ''}`}
|
|
40
41
|
{testId}
|
|
41
42
|
>
|
|
42
|
-
{#each images as image (image.id)}
|
|
43
|
+
{#each images as image, index (image.id)}
|
|
43
44
|
<div
|
|
44
45
|
class="chip"
|
|
45
46
|
data-pw={typeof testId === 'string' ? `${testId}-image-${image.id}` : null}
|
|
@@ -67,7 +68,7 @@
|
|
|
67
68
|
<Button
|
|
68
69
|
variant="secondary"
|
|
69
70
|
iconOnly={true}
|
|
70
|
-
ariaLabel=
|
|
71
|
+
ariaLabel={`Remove ${image.filename?.trim() || `image ${index + 1} of ${images.length}`}`}
|
|
71
72
|
testId={testId && `${testId}-remove-image-${image.id}`}
|
|
72
73
|
onclick={() => onremoveimage(image.id)}
|
|
73
74
|
>
|
|
@@ -90,7 +91,7 @@
|
|
|
90
91
|
{/if}
|
|
91
92
|
</div>
|
|
92
93
|
{/each}
|
|
93
|
-
{#each videos as video (video.id)}
|
|
94
|
+
{#each videos as video, index (video.id)}
|
|
94
95
|
<div
|
|
95
96
|
class="chip"
|
|
96
97
|
data-pw={typeof testId === 'string' ? `${testId}-video-${video.id}` : null}
|
|
@@ -125,7 +126,7 @@
|
|
|
125
126
|
<Button
|
|
126
127
|
variant="secondary"
|
|
127
128
|
iconOnly={true}
|
|
128
|
-
ariaLabel=
|
|
129
|
+
ariaLabel={`Remove ${video.filename?.trim() || `video ${index + 1} of ${videos.length}`}`}
|
|
129
130
|
testId={testId && `${testId}-remove-video-${video.id}`}
|
|
130
131
|
onclick={() => onremovevideo(video.id)}
|
|
131
132
|
>
|
|
@@ -148,7 +149,7 @@
|
|
|
148
149
|
{/if}
|
|
149
150
|
</div>
|
|
150
151
|
{/each}
|
|
151
|
-
{#each files as file (file.id)}
|
|
152
|
+
{#each files as file, index (file.id)}
|
|
152
153
|
<div
|
|
153
154
|
class="chip"
|
|
154
155
|
data-pw={typeof testId === 'string' ? `${testId}-file-${file.id}` : null}
|
|
@@ -190,7 +191,7 @@
|
|
|
190
191
|
<Button
|
|
191
192
|
variant="secondary"
|
|
192
193
|
iconOnly={true}
|
|
193
|
-
ariaLabel=
|
|
194
|
+
ariaLabel={`Remove ${file.filename.trim() || `file ${index + 1} of ${files.length}`}`}
|
|
194
195
|
testId={testId && `${testId}-remove-file-${file.id}`}
|
|
195
196
|
onclick={() => onremovefile(file.id)}
|
|
196
197
|
>
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
dropOverlapping
|
|
31
31
|
} from '../_chart/labels';
|
|
32
32
|
import { formatSeriesAggregate } from '../_chart/aggregate';
|
|
33
|
+
import { defaultChartName, resolveChartName } from '../_chart/a11y';
|
|
33
34
|
import type { LegendItem, BarRect } from '../_chart/types';
|
|
34
35
|
import { DEFAULT_CHART_CORNER_RADIUS, DEFAULT_CHART_MAX_HEIGHT } from '../_chart/types';
|
|
35
36
|
import type { TooltipAnchor } from '../_chart/types';
|
|
@@ -37,10 +38,12 @@
|
|
|
37
38
|
import { SvelteMap, SvelteSet } from 'svelte/reactivity';
|
|
38
39
|
|
|
39
40
|
// ── Per-instance uid prefix for <defs> ids (A1-3) ─────────────
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
|
|
41
|
+
// `$props.id()` is unique per component instance and identical on server and
|
|
42
|
+
// client, so two BarCharts on one page can never resolve each other's
|
|
43
|
+
// `url(#pattern)` / `url(#gradient)` references, and hydration never sees a
|
|
44
|
+
// different id than the one the server wrote. (A `Math.random()` prefix was
|
|
45
|
+
// collision-prone by construction and differed between SSR and the client.)
|
|
46
|
+
const uid = $props.id();
|
|
44
47
|
|
|
45
48
|
/** Returns the plain CSS fallback color for any BarFill (used for legend, aria-label). */
|
|
46
49
|
function fallbackColor(fill: BarFill, index: number): string {
|
|
@@ -94,6 +97,8 @@
|
|
|
94
97
|
tooltipPortal = false,
|
|
95
98
|
onbarclick,
|
|
96
99
|
onbarhover,
|
|
100
|
+
ariaLabel,
|
|
101
|
+
ariaDescription,
|
|
97
102
|
testId,
|
|
98
103
|
classes
|
|
99
104
|
}: BarChartProperties = $props();
|
|
@@ -635,6 +640,33 @@
|
|
|
635
640
|
|
|
636
641
|
let isEmpty = $derived(resolvedSeries.every((s) => s.data.length === 0) || labels.length === 0);
|
|
637
642
|
|
|
643
|
+
// ── Chart-level accessible name ────────────────────────────────
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* The drawing's accessible name: the consumer's `ariaLabel`, else one derived
|
|
647
|
+
* from what the chart plots (see `defaultChartName`). It lives on the <svg>
|
|
648
|
+
* itself. It used to sit only on a wrapping `role="region"` that was present
|
|
649
|
+
* in every chart, leaving the image inside unnamed -- and giving every chart
|
|
650
|
+
* on a page the same landmark name.
|
|
651
|
+
*/
|
|
652
|
+
let chartName = $derived(
|
|
653
|
+
resolveChartName(
|
|
654
|
+
ariaLabel,
|
|
655
|
+
defaultChartName('bar', {
|
|
656
|
+
yAxisLabel,
|
|
657
|
+
seriesNames: isMulti ? rawSeries.map((s) => s.name) : []
|
|
658
|
+
})
|
|
659
|
+
)
|
|
660
|
+
);
|
|
661
|
+
|
|
662
|
+
/**
|
|
663
|
+
* Bars are focusable `role="button"` marks, so a chart that draws them is
|
|
664
|
+
* interactive and must not be exposed as an image. With `hideBarGraphics` (a
|
|
665
|
+
* legend/label-only mode) or no bars there is nothing to operate, and the
|
|
666
|
+
* drawing is a plain named image.
|
|
667
|
+
*/
|
|
668
|
+
let hasMarks = $derived(!hideBarGraphics && bars.length > 0);
|
|
669
|
+
|
|
638
670
|
// ── Scroll geometry ────────────────────────────────────────────
|
|
639
671
|
|
|
640
672
|
let minScrollWidth = $derived(
|
|
@@ -929,11 +961,20 @@
|
|
|
929
961
|
{/if}
|
|
930
962
|
|
|
931
963
|
<div class="chart-plot" bind:this={plotEl}>
|
|
964
|
+
<!--
|
|
965
|
+
A region landmark only when this wrapper really is a focusable scroll
|
|
966
|
+
container (axe: scrollable-region-focusable needs a focusable, named
|
|
967
|
+
container). It used to be a region in every chart, which put N identically
|
|
968
|
+
named landmarks on a page (axe: landmark-unique) and named the wrapper
|
|
969
|
+
while the chart image inside stayed unnamed. Its label deliberately
|
|
970
|
+
differs from the chart's own name so tabbing from the scroller into the
|
|
971
|
+
chart does not announce the same words twice.
|
|
972
|
+
-->
|
|
932
973
|
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
933
974
|
<div
|
|
934
975
|
class="chart-scroll-area"
|
|
935
|
-
role=
|
|
936
|
-
aria-label={
|
|
976
|
+
role={scrollable ? 'region' : null}
|
|
977
|
+
aria-label={scrollable ? `${chartName}, scrollable` : null}
|
|
937
978
|
tabindex={scrollable ? 0 : null}
|
|
938
979
|
style={scrollable
|
|
939
980
|
? `overflow-x: auto; -webkit-overflow-scrolling: touch; height: var(--barchart-scroll-area-height, auto);`
|
|
@@ -946,6 +987,9 @@
|
|
|
946
987
|
{aspectRatio}
|
|
947
988
|
{maxHeight}
|
|
948
989
|
{minHeight}
|
|
990
|
+
ariaLabel={chartName}
|
|
991
|
+
{ariaDescription}
|
|
992
|
+
interactive={hasMarks}
|
|
949
993
|
>
|
|
950
994
|
<!-- A1-2 / A1-3: SVG <defs> for pattern and gradient fills -->
|
|
951
995
|
{#if defsEntries.length > 0}
|
|
@@ -1112,8 +1156,12 @@
|
|
|
1112
1156
|
{/each}
|
|
1113
1157
|
{#each barLabels as bl, i (i)}
|
|
1114
1158
|
{#if bl.visible}
|
|
1159
|
+
<!-- Repeats the bar's own accessible name ("Jan: 4.2K"), which is
|
|
1160
|
+
what assistive technology reads; exposing it again would
|
|
1161
|
+
announce every value twice. -->
|
|
1115
1162
|
<text
|
|
1116
1163
|
class="bar-value"
|
|
1164
|
+
aria-hidden="true"
|
|
1117
1165
|
class:bar-value-inside={bl.p.placement === 'inside'}
|
|
1118
1166
|
data-inside={bl.p.placement === 'inside' ? 'true' : 'false'}
|
|
1119
1167
|
x={bl.p.x}
|
|
@@ -239,6 +239,18 @@ export type OptionalBarChartProperties = {
|
|
|
239
239
|
hideLegendBelow?: number;
|
|
240
240
|
/** Render the tooltip into document.body (position:fixed) so scroll/overflow ancestors never clip it. */
|
|
241
241
|
tooltipPortal?: boolean;
|
|
242
|
+
/**
|
|
243
|
+
* Accessible name of the chart (`aria-label` on its `<svg>`). Say what the chart
|
|
244
|
+
* shows, e.g. `"Monthly revenue, January to June"`. When omitted a name is
|
|
245
|
+
* derived from the chart's own data (`"{yAxisLabel} bar chart"`, else the series names, else `"Bar chart"`), so an unlabelled chart is still
|
|
246
|
+
* identifiable -- but a name written for the page is almost always better.
|
|
247
|
+
*/
|
|
248
|
+
ariaLabel?: string;
|
|
249
|
+
/**
|
|
250
|
+
* Optional longer description (trend, range, the takeaway) announced after the
|
|
251
|
+
* name. Rendered as an SVG `<desc>` referenced by `aria-describedby`.
|
|
252
|
+
*/
|
|
253
|
+
ariaDescription?: string;
|
|
242
254
|
testId?: string;
|
|
243
255
|
classes?: string;
|
|
244
256
|
};
|
|
@@ -26,6 +26,25 @@
|
|
|
26
26
|
initialMonth = null
|
|
27
27
|
}: CalendarProperties = $props();
|
|
28
28
|
|
|
29
|
+
type DayInfo = {
|
|
30
|
+
date: Date;
|
|
31
|
+
day: number;
|
|
32
|
+
isCurrentMonth: boolean;
|
|
33
|
+
isToday: boolean;
|
|
34
|
+
isDisabled: boolean;
|
|
35
|
+
isSelected: boolean;
|
|
36
|
+
isRangeStart: boolean;
|
|
37
|
+
isRangeEnd: boolean;
|
|
38
|
+
isInRange: boolean;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// The grid is named by the visible month heading (aria-labelledby), and the heading
|
|
42
|
+
// announces month changes. Per-instance, because two calendars on one page (the
|
|
43
|
+
// DateRangePicker mounts two) would otherwise resolve to whichever matching id
|
|
44
|
+
// comes first in the root.
|
|
45
|
+
const uid = $props.id();
|
|
46
|
+
const headerId = `calendar-heading-${uid}`;
|
|
47
|
+
|
|
29
48
|
const now = new SvelteDate();
|
|
30
49
|
// Intentionally read initialMonth once (untracked) — it seeds the display month at mount
|
|
31
50
|
// and subsequent prop changes should not navigate the calendar (user navigates via the buttons).
|
|
@@ -48,15 +67,28 @@
|
|
|
48
67
|
}
|
|
49
68
|
|
|
50
69
|
const dayNameFormatter = $derived(new Intl.DateTimeFormat(locale, { weekday: 'short' }));
|
|
70
|
+
const fullDayNameFormatter = $derived(new Intl.DateTimeFormat(locale, { weekday: 'long' }));
|
|
71
|
+
|
|
72
|
+
// `toLocaleDateString()` alone gives "6/15/2024" -- ambiguous between day-first and
|
|
73
|
+
// month-first locales and with no weekday or month name for a screen reader to
|
|
74
|
+
// anchor on. The long form names the weekday, month, day and year.
|
|
75
|
+
const dateLabelFormatter = $derived(
|
|
76
|
+
new Intl.DateTimeFormat(locale, {
|
|
77
|
+
weekday: 'long',
|
|
78
|
+
year: 'numeric',
|
|
79
|
+
month: 'long',
|
|
80
|
+
day: 'numeric'
|
|
81
|
+
})
|
|
82
|
+
);
|
|
51
83
|
|
|
52
84
|
const monthYearFormatter = $derived(
|
|
53
85
|
new Intl.DateTimeFormat(locale, { month: 'long', year: 'numeric' })
|
|
54
86
|
);
|
|
55
87
|
|
|
56
|
-
let dayNames: string[] = $derived(
|
|
88
|
+
let dayNames: { short: string; full: string }[] = $derived(
|
|
57
89
|
Array.from({ length: 7 }, (_, i) => {
|
|
58
90
|
const d = new SvelteDate(2024, 0, 7 + weekStartsOn + i);
|
|
59
|
-
return dayNameFormatter.format(d);
|
|
91
|
+
return { short: dayNameFormatter.format(d), full: fullDayNameFormatter.format(d) };
|
|
60
92
|
})
|
|
61
93
|
);
|
|
62
94
|
|
|
@@ -80,18 +112,6 @@
|
|
|
80
112
|
const normStart = rangeStart !== null ? normalizeDate(rangeStart) : null;
|
|
81
113
|
const normEnd = rangeEnd !== null ? normalizeDate(rangeEnd) : null;
|
|
82
114
|
|
|
83
|
-
type DayInfo = {
|
|
84
|
-
date: Date;
|
|
85
|
-
day: number;
|
|
86
|
-
isCurrentMonth: boolean;
|
|
87
|
-
isToday: boolean;
|
|
88
|
-
isDisabled: boolean;
|
|
89
|
-
isSelected: boolean;
|
|
90
|
-
isRangeStart: boolean;
|
|
91
|
-
isRangeEnd: boolean;
|
|
92
|
-
isInRange: boolean;
|
|
93
|
-
};
|
|
94
|
-
|
|
95
115
|
function fillerDay(year: number, month: number, day: number): DayInfo {
|
|
96
116
|
const date = new SvelteDate(year, month, day);
|
|
97
117
|
date.setHours(0, 0, 0, 0);
|
|
@@ -148,6 +168,15 @@
|
|
|
148
168
|
return days;
|
|
149
169
|
});
|
|
150
170
|
|
|
171
|
+
// ARIA requires gridcells to sit in rows. Chunking the flat day list keeps the
|
|
172
|
+
// arithmetic above untouched; the leading and trailing filler days already pad the
|
|
173
|
+
// list to a multiple of seven, so every row is full.
|
|
174
|
+
const weeks: DayInfo[][] = $derived(
|
|
175
|
+
Array.from({ length: calendarDays.length / 7 }, (_, row) =>
|
|
176
|
+
calendarDays.slice(row * 7, row * 7 + 7)
|
|
177
|
+
)
|
|
178
|
+
);
|
|
179
|
+
|
|
151
180
|
// A calendar grid is one tab stop, the same roving-tabindex pattern Tabs uses: the
|
|
152
181
|
// day being actively navigated while inside the grid, else the selected day, else
|
|
153
182
|
// today if it's in view and enabled, else the first enabled day -- so a month with
|
|
@@ -362,7 +391,7 @@
|
|
|
362
391
|
{/if}
|
|
363
392
|
</Button>
|
|
364
393
|
</div>
|
|
365
|
-
<span class="header-label">{headerLabel}</span>
|
|
394
|
+
<span class="header-label" id={headerId} aria-live="polite">{headerLabel}</span>
|
|
366
395
|
<div class="nav-button nav-next">
|
|
367
396
|
<Button
|
|
368
397
|
onclick={() => navigateMonth(1)}
|
|
@@ -379,41 +408,75 @@
|
|
|
379
408
|
</div>
|
|
380
409
|
</div>
|
|
381
410
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
411
|
+
<!--
|
|
412
|
+
A valid ARIA grid is grid > row > (columnheader | gridcell). The weekday names live
|
|
413
|
+
inside the grid as the first row, and every date sits in a gridcell inside a week row.
|
|
414
|
+
The rows are real boxes (display: grid), never display: contents, which browsers have
|
|
415
|
+
dropped from the accessibility tree before. Each gridcell wraps the date button
|
|
416
|
+
rather than replacing it: a native <button> keeps its disabled and click behaviour,
|
|
417
|
+
and aria-selected / aria-disabled belong on the cell because a button supports
|
|
418
|
+
neither aria-selected nor a cell-level disabled state.
|
|
419
|
+
-->
|
|
388
420
|
<div
|
|
389
421
|
class="grid"
|
|
390
422
|
bind:this={gridRef}
|
|
391
423
|
tabindex={tabStopDay === null ? 0 : -1}
|
|
392
424
|
role="grid"
|
|
425
|
+
aria-labelledby={headerId}
|
|
426
|
+
aria-multiselectable={mode === 'range' ? 'true' : null}
|
|
393
427
|
onkeydown={handleKeyDown}
|
|
394
428
|
>
|
|
395
|
-
|
|
396
|
-
{#
|
|
397
|
-
<
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
429
|
+
<div class="row day-names" role="row">
|
|
430
|
+
{#each dayNames as name, i (i)}
|
|
431
|
+
<div class="day-name" role="columnheader" aria-label={name.full}>{name.short}</div>
|
|
432
|
+
{/each}
|
|
433
|
+
</div>
|
|
434
|
+
{#each weeks as week (week[0].date.getTime())}
|
|
435
|
+
<div class="row week" role="row">
|
|
436
|
+
{#each week as dayInfo (dayInfo.date.getTime())}
|
|
437
|
+
{#if dayInfo.isCurrentMonth}
|
|
438
|
+
<div
|
|
439
|
+
class="cell-slot"
|
|
440
|
+
role="gridcell"
|
|
441
|
+
aria-selected={dayInfo.isSelected ||
|
|
442
|
+
dayInfo.isRangeStart ||
|
|
443
|
+
dayInfo.isRangeEnd ||
|
|
444
|
+
dayInfo.isInRange
|
|
445
|
+
? 'true'
|
|
446
|
+
: 'false'}
|
|
447
|
+
aria-disabled={dayInfo.isDisabled ? 'true' : null}
|
|
448
|
+
>
|
|
449
|
+
<button
|
|
450
|
+
type="button"
|
|
451
|
+
class="cell"
|
|
452
|
+
class:today={dayInfo.isToday}
|
|
453
|
+
class:selected={dayInfo.isSelected}
|
|
454
|
+
class:range-start={dayInfo.isRangeStart}
|
|
455
|
+
class:range-end={dayInfo.isRangeEnd}
|
|
456
|
+
class:in-range={dayInfo.isInRange}
|
|
457
|
+
class:disabled={dayInfo.isDisabled}
|
|
458
|
+
data-day={dayInfo.day}
|
|
459
|
+
disabled={dayInfo.isDisabled}
|
|
460
|
+
tabindex={tabStopDay === dayInfo.day ? 0 : -1}
|
|
461
|
+
aria-label={dateLabelFormatter.format(dayInfo.date)}
|
|
462
|
+
aria-current={dayInfo.isToday ? 'date' : null}
|
|
463
|
+
onclick={() => selectDate(dayInfo.date)}
|
|
464
|
+
>
|
|
465
|
+
{dayInfo.day}
|
|
466
|
+
</button>
|
|
467
|
+
</div>
|
|
468
|
+
{:else}
|
|
469
|
+
<div
|
|
470
|
+
class="cell-slot"
|
|
471
|
+
role="gridcell"
|
|
472
|
+
aria-disabled="true"
|
|
473
|
+
aria-label={dateLabelFormatter.format(dayInfo.date)}
|
|
474
|
+
>
|
|
475
|
+
<span class="cell outside-month">{dayInfo.day}</span>
|
|
476
|
+
</div>
|
|
477
|
+
{/if}
|
|
478
|
+
{/each}
|
|
479
|
+
</div>
|
|
417
480
|
{/each}
|
|
418
481
|
</div>
|
|
419
482
|
</div>
|
|
@@ -463,17 +526,27 @@
|
|
|
463
526
|
/* Size the columns to the cell, not 1fr, and centre the whole grid. With 1fr
|
|
464
527
|
columns the fixed-width cells sat centred inside wider tracks, leaving
|
|
465
528
|
horizontal gaps between days — which broke the range highlight into
|
|
466
|
-
disconnected boxes instead of one continuous pill. The
|
|
467
|
-
|
|
529
|
+
disconnected boxes instead of one continuous pill. The weekday-name row uses
|
|
530
|
+
the same track sizing as the week rows so it stays aligned with the days below.
|
|
531
|
+
Each row is its own grid (a real box, so the role="row" semantics survive); the
|
|
532
|
+
.grid wrapper just stacks them.
|
|
468
533
|
|
|
469
534
|
A host that wants the grid to fill a container sets both
|
|
470
535
|
--calendar-grid-columns and --calendar-cell-width: fluid columns with the
|
|
471
536
|
default fixed-width cells bring those gaps back, so the cells must be told
|
|
472
537
|
to fill their tracks too. */
|
|
473
|
-
.
|
|
538
|
+
.grid {
|
|
539
|
+
display: flex;
|
|
540
|
+
flex-direction: column;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
.row {
|
|
474
544
|
display: grid;
|
|
475
545
|
grid-template-columns: var(--calendar-grid-columns, repeat(7, var(--calendar-cell-size, 36px)));
|
|
476
546
|
justify-content: center;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
.day-names {
|
|
477
550
|
text-align: center;
|
|
478
551
|
}
|
|
479
552
|
|
|
@@ -484,11 +557,11 @@
|
|
|
484
557
|
padding: var(--calendar-day-name-padding, 4px 0);
|
|
485
558
|
}
|
|
486
559
|
|
|
487
|
-
.grid
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
560
|
+
/* The gap between week rows. It was `row-gap: 2px` on the single day grid; with one
|
|
561
|
+
element per row it is a margin between consecutive weeks, which leaves the
|
|
562
|
+
weekday-name row flush against the first week exactly as before. */
|
|
563
|
+
.week + .week {
|
|
564
|
+
margin-top: 2px;
|
|
492
565
|
}
|
|
493
566
|
|
|
494
567
|
.cell {
|
package/dist/Card/Card.svelte
CHANGED
|
@@ -38,6 +38,11 @@
|
|
|
38
38
|
* once per mounted instance is what a consumer needs to act on, and it costs
|
|
39
39
|
* nothing on subsequent updates.
|
|
40
40
|
*/
|
|
41
|
+
// Reading `attrs` once is the point of this block: it is a dev notice about what the consumer
|
|
42
|
+
// passed at mount, not state. Rendering reads `attrs` live in the spread on the root element
|
|
43
|
+
// below, so an `attrs` that changes afterwards is still applied; only the notice is not
|
|
44
|
+
// repeated. The ignore is on this one block, whose only reference is that read.
|
|
45
|
+
// svelte-ignore state_referenced_locally
|
|
41
46
|
if (import.meta.env?.DEV === true) {
|
|
42
47
|
for (const message of discardedAttrWarnings('Card', CARD_MANAGED_ATTRS, attrs)) {
|
|
43
48
|
console.warn(message);
|