@juspay/svelte-ui-components 4.50.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 +247 -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
|
@@ -550,6 +550,21 @@
|
|
|
550
550
|
min-width: 0;
|
|
551
551
|
}
|
|
552
552
|
|
|
553
|
+
/* Contrast contract (WCAG 1.4.3, 4.5:1 for this normal-size text): every colour the
|
|
554
|
+
live label can paint -- the resting ink AND the shimmer's highlight -- clears 4.5:1
|
|
555
|
+
on the surfaces it is documented for (white, #fafafa, #f4f4f5 and the library's
|
|
556
|
+
tinted greys in light; #0f0f17..#2f2f45 in the dark layer). The sweep therefore
|
|
557
|
+
travels from the resting ink toward a STRONGER ink (#6b6b6b -> #2b2b2b in light,
|
|
558
|
+
#9ca3af -> #e5e7eb in dark) rather than washing out toward the page, which is what
|
|
559
|
+
the previous #858585 -> #bebebe sweep did (1.8:1 at its peak on #fafafa). The swing
|
|
560
|
+
between the two stops is as large or larger (2.7:1 light, 2.1:1 dark, against the old
|
|
561
|
+
2.0:1), so the motion reads the same. Pinned in two places:
|
|
562
|
+
src/lib/ThinkingIndicator/thinking-indicator-contrast.test.ts (the token defaults)
|
|
563
|
+
and tests/thinking-indicator-contrast.spec.ts (rendered pixels, every shimmer phase).
|
|
564
|
+
|
|
565
|
+
The gradient is built from the same `--thinking-indicator-label-color` token the
|
|
566
|
+
static label uses, so overriding the colour retunes the shimmer's resting stops too;
|
|
567
|
+
`--thinking-indicator-shimmer-gradient` still replaces the whole thing. */
|
|
553
568
|
.status-label {
|
|
554
569
|
flex: 0 1 auto;
|
|
555
570
|
min-width: 0;
|
|
@@ -559,10 +574,15 @@
|
|
|
559
574
|
text-align: left;
|
|
560
575
|
font-size: var(--thinking-indicator-font-size, 0.75rem);
|
|
561
576
|
line-height: var(--thinking-indicator-line-height, 1.25rem);
|
|
562
|
-
color: var(--thinking-indicator-label-color, #
|
|
577
|
+
color: var(--thinking-indicator-label-color, #6b6b6b);
|
|
563
578
|
background: var(
|
|
564
579
|
--thinking-indicator-shimmer-gradient,
|
|
565
|
-
linear-gradient(
|
|
580
|
+
linear-gradient(
|
|
581
|
+
90deg,
|
|
582
|
+
var(--thinking-indicator-label-color, #6b6b6b) 0%,
|
|
583
|
+
var(--thinking-indicator-shimmer-highlight, #2b2b2b) 50%,
|
|
584
|
+
var(--thinking-indicator-label-color, #6b6b6b) 100%
|
|
585
|
+
)
|
|
566
586
|
);
|
|
567
587
|
background-size: 200% 100%;
|
|
568
588
|
background-clip: text;
|
|
@@ -571,10 +591,14 @@
|
|
|
571
591
|
infinite;
|
|
572
592
|
}
|
|
573
593
|
|
|
574
|
-
/* A settled label (no live busy phase) holds still instead of shimmering.
|
|
594
|
+
/* A settled label (no live busy phase) holds still instead of shimmering. It also drops
|
|
595
|
+
the gradient layer rather than just covering it: under an opaque text fill the layer
|
|
596
|
+
still shows at antialiased glyph edges, and now that its highlight is a dark ink, light
|
|
597
|
+
text on a dark recipe (the Chat tool-status example) would grow a dark fringe. */
|
|
575
598
|
.static-label {
|
|
576
599
|
animation: none;
|
|
577
|
-
-
|
|
600
|
+
background-image: none;
|
|
601
|
+
-webkit-text-fill-color: var(--thinking-indicator-label-color, #6b6b6b);
|
|
578
602
|
}
|
|
579
603
|
|
|
580
604
|
/* ---- chip variant: a self-contained floating pill (no ancestor supplies
|
|
@@ -619,9 +643,17 @@
|
|
|
619
643
|
font-size: var(--thinking-indicator-chip-font-size, 0.85rem);
|
|
620
644
|
font-weight: var(--thinking-indicator-chip-font-weight, 500);
|
|
621
645
|
color: var(--thinking-indicator-chip-color, #52525b);
|
|
646
|
+
/* Same contract as .status-label: resting ink and highlight both clear 4.5:1 on the
|
|
647
|
+
chip's own surface (#52525b / #18181b on #ffffff in light; #9ca3af / #f3f4f6 on
|
|
648
|
+
#1e1e2e in dark), and the resting stop follows the chip colour token. */
|
|
622
649
|
background: var(
|
|
623
650
|
--thinking-indicator-chip-shimmer-gradient,
|
|
624
|
-
linear-gradient(
|
|
651
|
+
linear-gradient(
|
|
652
|
+
90deg,
|
|
653
|
+
var(--thinking-indicator-chip-color, #52525b) 0%,
|
|
654
|
+
var(--thinking-indicator-chip-shimmer-highlight, #18181b) 50%,
|
|
655
|
+
var(--thinking-indicator-chip-color, #52525b) 100%
|
|
656
|
+
)
|
|
625
657
|
);
|
|
626
658
|
background-size: 200% 100%;
|
|
627
659
|
background-clip: text;
|
|
@@ -637,6 +669,7 @@
|
|
|
637
669
|
variant's. */
|
|
638
670
|
.chip-label.static-label {
|
|
639
671
|
animation: none;
|
|
672
|
+
background-image: none;
|
|
640
673
|
-webkit-text-fill-color: var(--thinking-indicator-chip-color, #52525b);
|
|
641
674
|
}
|
|
642
675
|
|
|
@@ -857,17 +890,21 @@
|
|
|
857
890
|
gap: 5px;
|
|
858
891
|
}
|
|
859
892
|
|
|
893
|
+
/* The +N / -N stats also sit on a selectable row's hover (#f4f4f4) and selected (#ececec)
|
|
894
|
+
backgrounds. #1f7a5f / #c93f38 were 4.4:1 / 4.2:1 on the selected one; these clear
|
|
895
|
+
4.5:1 on it and on white (thinking-indicator-contrast.test.ts pins both). */
|
|
860
896
|
.row-diffstat .added {
|
|
861
|
-
color: var(--thinking-indicator-trace-added-color, #
|
|
897
|
+
color: var(--thinking-indicator-trace-added-color, #1b7358);
|
|
862
898
|
}
|
|
863
899
|
|
|
864
900
|
.row-diffstat .removed {
|
|
865
|
-
color: var(--thinking-indicator-trace-removed-color, #
|
|
901
|
+
color: var(--thinking-indicator-trace-removed-color, #c2382f);
|
|
866
902
|
}
|
|
867
903
|
|
|
868
904
|
.trace-more {
|
|
869
905
|
font-size: var(--thinking-indicator-trace-more-font-size, 0.75rem);
|
|
870
|
-
|
|
906
|
+
/* Settled caption text at 0.75rem: #9a9a9a was 2.8:1 on white. */
|
|
907
|
+
color: var(--thinking-indicator-trace-more-color, #6b6b6b);
|
|
871
908
|
padding: 6px 0 0 22px;
|
|
872
909
|
animation: thinking-indicator-fade-in
|
|
873
910
|
var(--thinking-indicator-trace-more-animation-duration, var(--motion-duration, 300ms))
|
|
@@ -890,11 +927,13 @@
|
|
|
890
927
|
|
|
891
928
|
.status-label {
|
|
892
929
|
animation: none;
|
|
893
|
-
-
|
|
930
|
+
background-image: none;
|
|
931
|
+
-webkit-text-fill-color: var(--thinking-indicator-label-color, #6b6b6b);
|
|
894
932
|
}
|
|
895
933
|
|
|
896
934
|
.chip-label {
|
|
897
935
|
animation: none;
|
|
936
|
+
background-image: none;
|
|
898
937
|
-webkit-text-fill-color: var(--thinking-indicator-chip-color, #52525b);
|
|
899
938
|
}
|
|
900
939
|
}
|
package/dist/Toast/Toast.svelte
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { untrack } from 'svelte';
|
|
2
|
+
import { tick, untrack } from 'svelte';
|
|
3
3
|
import type { ToastDirection, ToastProperties } from './properties';
|
|
4
4
|
import Img from '../Img/Img.svelte';
|
|
5
5
|
|
|
@@ -26,6 +26,11 @@
|
|
|
26
26
|
}: ToastProperties = $props();
|
|
27
27
|
|
|
28
28
|
let showToast = $state(true);
|
|
29
|
+
let rootEl: HTMLDivElement | null = $state(null);
|
|
30
|
+
// One `ontoasthide` per hide, however it is reached (the exit's transitionend,
|
|
31
|
+
// or the no-exit path in hideToast) and however many times hideToast runs
|
|
32
|
+
// (the close button after the auto-hide timer already fired).
|
|
33
|
+
let hideNotified = false;
|
|
29
34
|
|
|
30
35
|
const rootClass = $derived(
|
|
31
36
|
['toast', type ?? '', classes ?? ''].filter((cls) => cls.length > 0).join(' ')
|
|
@@ -61,6 +66,13 @@
|
|
|
61
66
|
return distance == null ? {} : directionalOffset(resolvedDirection, distance);
|
|
62
67
|
});
|
|
63
68
|
|
|
69
|
+
// The offsets feed `translate(var(--toast-enter-x, 0px), var(--toast-enter-y, 0px))`
|
|
70
|
+
// in the stylesheet, where a unitless non-zero number is invalid at
|
|
71
|
+
// computed-value time: the whole `transform` then computes to `none` and the
|
|
72
|
+
// toast stops travelling at all. Inline custom properties carry no unit of
|
|
73
|
+
// their own, so the px has to be written here.
|
|
74
|
+
const toPx = (value?: number): string | null => (typeof value === 'number' ? `${value}px` : null);
|
|
75
|
+
|
|
64
76
|
// A `0`/`0s` CSS transition-duration never starts a transition, so it never
|
|
65
77
|
// fires `transitionend`, so `ontoasthide` (below) would never fire -- floor
|
|
66
78
|
// at 50ms so the transition always runs. Same reasoning as the reduced-motion
|
|
@@ -76,8 +88,30 @@
|
|
|
76
88
|
const openDurationStyle = $derived(toDurationStyle(inAnimationDuration));
|
|
77
89
|
const closeDurationStyle = $derived(toDurationStyle(outAnimationDuration));
|
|
78
90
|
|
|
91
|
+
function notifyHidden() {
|
|
92
|
+
if (!hideNotified) {
|
|
93
|
+
hideNotified = true;
|
|
94
|
+
ontoasthide?.();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
79
98
|
function hideToast() {
|
|
80
99
|
showToast = false;
|
|
100
|
+
// The closed rule's `display: none` only keeps the toast on screen for its
|
|
101
|
+
// exit through a discrete `display` transition (`allow-discrete`). An engine
|
|
102
|
+
// without one -- Firefox 150, as shipped in Playwright 1.60 -- applies
|
|
103
|
+
// `display: none` at once, which cancels the exit before it starts, so no
|
|
104
|
+
// `transitionend` is ever dispatched and the consumer's `ontoasthide` cleanup
|
|
105
|
+
// would never run.
|
|
106
|
+
// If the toast is already not displayed once the closed state has applied,
|
|
107
|
+
// there is no exit left to wait for: report the hide now. Where the
|
|
108
|
+
// transition does run the toast is still displayed here, and the
|
|
109
|
+
// `transitionend` below remains the only path.
|
|
110
|
+
void tick().then(() => {
|
|
111
|
+
if (!showToast && rootEl !== null && getComputedStyle(rootEl).display === 'none') {
|
|
112
|
+
notifyHidden();
|
|
113
|
+
}
|
|
114
|
+
});
|
|
81
115
|
}
|
|
82
116
|
|
|
83
117
|
function handleTransitionEnd(event: TransitionEvent) {
|
|
@@ -96,7 +130,7 @@
|
|
|
96
130
|
// correct under any reversal ordering, not just the common single-close
|
|
97
131
|
// case this guard was originally written for.
|
|
98
132
|
if (event.target === event.currentTarget && event.propertyName === 'opacity' && !showToast) {
|
|
99
|
-
|
|
133
|
+
notifyHidden();
|
|
100
134
|
}
|
|
101
135
|
}
|
|
102
136
|
|
|
@@ -111,6 +145,7 @@
|
|
|
111
145
|
|
|
112
146
|
untrack(() => {
|
|
113
147
|
showToast = true;
|
|
148
|
+
hideNotified = false;
|
|
114
149
|
});
|
|
115
150
|
|
|
116
151
|
const id = setTimeout(hideToast, duration);
|
|
@@ -133,6 +168,7 @@
|
|
|
133
168
|
aria-live off and back on around the reshow to force a re-announce,
|
|
134
169
|
not a change to the CSS transition mechanics above. -->
|
|
135
170
|
<div
|
|
171
|
+
bind:this={rootEl}
|
|
136
172
|
class={rootClass}
|
|
137
173
|
class:is-visible={showToast}
|
|
138
174
|
class:no-page-overlap={!overlapPage}
|
|
@@ -141,8 +177,8 @@
|
|
|
141
177
|
aria-live="assertive"
|
|
142
178
|
style:--toast-open-duration={openDurationStyle}
|
|
143
179
|
style:--toast-close-duration={closeDurationStyle}
|
|
144
|
-
style:--toast-enter-x={enterOffset.x}
|
|
145
|
-
style:--toast-enter-y={enterOffset.y}
|
|
180
|
+
style:--toast-enter-x={toPx(enterOffset.x)}
|
|
181
|
+
style:--toast-enter-y={toPx(enterOffset.y)}
|
|
146
182
|
ontransitionend={handleTransitionEnd}
|
|
147
183
|
data-pw={testId}
|
|
148
184
|
testID={testId}
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
classes,
|
|
11
11
|
id,
|
|
12
12
|
ariaLabel,
|
|
13
|
+
inputRole,
|
|
13
14
|
ariaLabelledby,
|
|
14
15
|
onclick,
|
|
15
16
|
name,
|
|
@@ -54,6 +55,7 @@
|
|
|
54
55
|
<label class="switch">
|
|
55
56
|
<input
|
|
56
57
|
id={inputId}
|
|
58
|
+
role={inputRole === 'switch' ? 'switch' : inputRole === 'checkbox' ? 'checkbox' : null}
|
|
57
59
|
class="input-checkbox"
|
|
58
60
|
type="checkbox"
|
|
59
61
|
aria-describedby={field.describedBy}
|
|
@@ -30,6 +30,8 @@ export type OptionalToggleProperties = {
|
|
|
30
30
|
id?: string;
|
|
31
31
|
/** Names the switch for assistive technology when it has no visible text. */
|
|
32
32
|
ariaLabel?: string;
|
|
33
|
+
/** Optional semantics on the native checkbox; default keeps its checkbox role. */
|
|
34
|
+
inputRole?: 'checkbox' | 'switch';
|
|
33
35
|
/**
|
|
34
36
|
* References a label in the same DOM root as the checkbox. `<sui-toggle>` exposes this as
|
|
35
37
|
* `inputAriaLabelledby` (`input-aria-labelledby`); it cannot reach the host document, so use
|
package/dist/_chart/Axis.svelte
CHANGED
|
@@ -42,6 +42,13 @@
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
const TICK_SIZE = 6;
|
|
45
|
+
|
|
46
|
+
// Tick labels are `aria-hidden`: they repeat what the chart's marks already
|
|
47
|
+
// say through their own accessible names ("Jan: 4.2K"), and a drawing that
|
|
48
|
+
// exposes interactive marks as a `group` (rather than the image it used to
|
|
49
|
+
// be, whose children were presentational) would otherwise read "0 2K 4K 6K
|
|
50
|
+
// Jan Feb Mar ..." ahead of the first mark. The axis *title* stays exposed:
|
|
51
|
+
// it is the one place a unit or dimension is stated.
|
|
45
52
|
</script>
|
|
46
53
|
|
|
47
54
|
<g
|
|
@@ -59,6 +66,7 @@
|
|
|
59
66
|
{#if rotateTicks && orientation === 'bottom'}
|
|
60
67
|
<text
|
|
61
68
|
class="tick-label"
|
|
69
|
+
aria-hidden="true"
|
|
62
70
|
transform="translate(0, {TICK_SIZE + 4}) rotate(-45)"
|
|
63
71
|
text-anchor="end"
|
|
64
72
|
dominant-baseline="auto"
|
|
@@ -70,6 +78,7 @@
|
|
|
70
78
|
{:else}
|
|
71
79
|
<text
|
|
72
80
|
class="tick-label"
|
|
81
|
+
aria-hidden="true"
|
|
73
82
|
y={orientation === 'bottom' ? TICK_SIZE + 4 : -(TICK_SIZE + 4)}
|
|
74
83
|
text-anchor="middle"
|
|
75
84
|
dominant-baseline={orientation === 'bottom' ? 'hanging' : 'auto'}
|
|
@@ -107,6 +116,7 @@
|
|
|
107
116
|
<line class="tick-mark" x2={orientation === 'left' ? -TICK_SIZE : TICK_SIZE} />
|
|
108
117
|
<text
|
|
109
118
|
class="tick-label"
|
|
119
|
+
aria-hidden="true"
|
|
110
120
|
x={orientation === 'left' ? -(TICK_SIZE + 4) : TICK_SIZE + 4}
|
|
111
121
|
text-anchor={orientation === 'left' ? 'end' : 'start'}
|
|
112
122
|
dominant-baseline="middle"
|
|
@@ -10,9 +10,25 @@
|
|
|
10
10
|
maxHeight = Infinity,
|
|
11
11
|
testId,
|
|
12
12
|
classes,
|
|
13
|
+
ariaLabel,
|
|
14
|
+
ariaDescription,
|
|
15
|
+
interactive = false,
|
|
13
16
|
children
|
|
14
17
|
}: ChartContainerProperties = $props();
|
|
15
18
|
|
|
19
|
+
// Instance-scoped, hydration-safe id for the <desc> element that
|
|
20
|
+
// `aria-describedby` points at. Two charts on one page (or in two shadow
|
|
21
|
+
// roots) must never resolve each other's description.
|
|
22
|
+
const instanceId = $props.id();
|
|
23
|
+
const descriptionId = `chart-desc-${instanceId}`;
|
|
24
|
+
|
|
25
|
+
// Whitespace-only strings are treated as absent so an empty prop never
|
|
26
|
+
// produces `aria-label=""`, which is a name of nothing rather than no name.
|
|
27
|
+
let name = $derived(typeof ariaLabel === 'string' && ariaLabel.trim() !== '' ? ariaLabel : null);
|
|
28
|
+
let description = $derived(
|
|
29
|
+
typeof ariaDescription === 'string' && ariaDescription.trim() !== '' ? ariaDescription : null
|
|
30
|
+
);
|
|
31
|
+
|
|
16
32
|
let containerEl: HTMLDivElement | null = $state(null);
|
|
17
33
|
let isMounted = false;
|
|
18
34
|
|
|
@@ -35,6 +51,7 @@
|
|
|
35
51
|
// changes, so the computed height stays current without waiting for a resize.
|
|
36
52
|
void aspectRatio;
|
|
37
53
|
void maxHeight;
|
|
54
|
+
void minHeight;
|
|
38
55
|
if (isMounted) {
|
|
39
56
|
measure();
|
|
40
57
|
}
|
|
@@ -72,13 +89,27 @@
|
|
|
72
89
|
testID={typeof testId === 'string' ? testId : null}
|
|
73
90
|
>
|
|
74
91
|
{#if width > 0 && height > 0}
|
|
92
|
+
<!--
|
|
93
|
+
`role="img"` makes its descendants presentational, so a drawing that holds
|
|
94
|
+
focusable marks must not use it: the marks are either dropped from the
|
|
95
|
+
accessibility tree or announced differently per browser, and axe reports
|
|
96
|
+
the nesting as `nested-interactive`. An interactive drawing is therefore a
|
|
97
|
+
named `group` whose children stay real controls; a static one stays a
|
|
98
|
+
named image. Either way the name sits on the <svg> itself -- a named
|
|
99
|
+
wrapper elsewhere does not name the image inside it.
|
|
100
|
+
-->
|
|
75
101
|
<svg
|
|
76
102
|
viewBox="0 0 {width} {height}"
|
|
77
103
|
preserveAspectRatio="xMidYMid meet"
|
|
78
|
-
role=
|
|
104
|
+
role={interactive ? 'group' : 'img'}
|
|
105
|
+
aria-label={name}
|
|
106
|
+
aria-describedby={description === null ? null : descriptionId}
|
|
79
107
|
{width}
|
|
80
108
|
{height}
|
|
81
109
|
>
|
|
110
|
+
{#if description !== null}
|
|
111
|
+
<desc id={descriptionId}>{description}</desc>
|
|
112
|
+
{/if}
|
|
82
113
|
{@render children()}
|
|
83
114
|
</svg>
|
|
84
115
|
{/if}
|
|
@@ -64,10 +64,24 @@
|
|
|
64
64
|
|
|
65
65
|
const pos = $derived.by(() => {
|
|
66
66
|
const tooltip = { width: tooltipWidth, height: tooltipHeight };
|
|
67
|
+
const rect = originEl?.getBoundingClientRect();
|
|
68
|
+
// Native rendered bounds include the label's loaded font and transforms.
|
|
69
|
+
// Only our opaque label plates participate; caller tooltip content stays
|
|
70
|
+
// intact and ordinary charts retain their existing placement.
|
|
71
|
+
const avoidRects = Array.from(
|
|
72
|
+
originEl?.querySelectorAll<SVGRectElement>('.chart-label-backdrop') ?? []
|
|
73
|
+
).map((plate) => {
|
|
74
|
+
const bounds = plate.getBoundingClientRect();
|
|
75
|
+
return {
|
|
76
|
+
left: bounds.left - (portal ? 0 : (rect?.left ?? 0)),
|
|
77
|
+
top: bounds.top - (portal ? 0 : (rect?.top ?? 0)),
|
|
78
|
+
width: bounds.width,
|
|
79
|
+
height: bounds.height
|
|
80
|
+
};
|
|
81
|
+
});
|
|
67
82
|
if (portal) {
|
|
68
83
|
void portalTick;
|
|
69
84
|
// Convert container coords to viewport coords and clamp to the viewport.
|
|
70
|
-
const rect = originEl?.getBoundingClientRect();
|
|
71
85
|
const dx = rect?.left ?? 0;
|
|
72
86
|
const dy = rect?.top ?? 0;
|
|
73
87
|
const container =
|
|
@@ -79,14 +93,15 @@
|
|
|
79
93
|
mouseY: mouseY + dy,
|
|
80
94
|
anchor: anchor === null ? null : { ...anchor, x: anchor.x + dx, y: anchor.y + dy },
|
|
81
95
|
tooltip,
|
|
82
|
-
container
|
|
96
|
+
container,
|
|
97
|
+
avoidRects
|
|
83
98
|
});
|
|
84
99
|
}
|
|
85
100
|
const container = {
|
|
86
101
|
width: tooltipEl?.offsetParent?.clientWidth ?? Number.POSITIVE_INFINITY,
|
|
87
102
|
height: tooltipEl?.offsetParent?.clientHeight ?? Number.POSITIVE_INFINITY
|
|
88
103
|
};
|
|
89
|
-
return computeTooltipPosition({ mouseX, mouseY, anchor, tooltip, container });
|
|
104
|
+
return computeTooltipPosition({ mouseX, mouseY, anchor, tooltip, container, avoidRects });
|
|
90
105
|
});
|
|
91
106
|
</script>
|
|
92
107
|
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount, type Snippet } from 'svelte';
|
|
3
|
+
|
|
4
|
+
let { children }: { children: Snippet } = $props();
|
|
5
|
+
let group: SVGGElement | null = $state(null);
|
|
6
|
+
let bounds: { x: number; y: number; width: number; height: number } | null = $state(null);
|
|
7
|
+
const PADDING = 3;
|
|
8
|
+
|
|
9
|
+
onMount(() => {
|
|
10
|
+
const text = group?.querySelector('text');
|
|
11
|
+
if (group === null || text === null || typeof text === 'undefined') {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
let mounted = true;
|
|
15
|
+
let frame = 0;
|
|
16
|
+
function measure(): void {
|
|
17
|
+
if (
|
|
18
|
+
!mounted ||
|
|
19
|
+
group === null ||
|
|
20
|
+
text === null ||
|
|
21
|
+
typeof text === 'undefined' ||
|
|
22
|
+
typeof text.getBBox !== 'function'
|
|
23
|
+
) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const box = text.getBBox();
|
|
27
|
+
if (box.width <= 0 || box.height <= 0) {
|
|
28
|
+
bounds = null;
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
// getBBox is in the text's own SVG coordinates. This also handles
|
|
32
|
+
// transformed children, rather than assuming page/parent rectangles.
|
|
33
|
+
const parent = group.getCTM?.();
|
|
34
|
+
const child = text.getCTM?.();
|
|
35
|
+
const matrix = parent && child ? parent.inverse().multiply(child) : null;
|
|
36
|
+
const corners = [
|
|
37
|
+
[box.x, box.y],
|
|
38
|
+
[box.x + box.width, box.y],
|
|
39
|
+
[box.x, box.y + box.height],
|
|
40
|
+
[box.x + box.width, box.y + box.height]
|
|
41
|
+
].map(([x, y]) =>
|
|
42
|
+
matrix === null
|
|
43
|
+
? { x, y }
|
|
44
|
+
: {
|
|
45
|
+
x: matrix.a * x + matrix.c * y + matrix.e,
|
|
46
|
+
y: matrix.b * x + matrix.d * y + matrix.f
|
|
47
|
+
}
|
|
48
|
+
);
|
|
49
|
+
const x = Math.min(...corners.map((point) => point.x)) - PADDING;
|
|
50
|
+
const y = Math.min(...corners.map((point) => point.y)) - PADDING;
|
|
51
|
+
bounds = {
|
|
52
|
+
x,
|
|
53
|
+
y,
|
|
54
|
+
width: Math.max(...corners.map((point) => point.x)) - x + PADDING,
|
|
55
|
+
height: Math.max(...corners.map((point) => point.y)) - y + PADDING
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
function schedule(): void {
|
|
59
|
+
cancelAnimationFrame(frame);
|
|
60
|
+
frame = requestAnimationFrame(measure);
|
|
61
|
+
}
|
|
62
|
+
measure();
|
|
63
|
+
const resize = new ResizeObserver(schedule);
|
|
64
|
+
resize.observe(text);
|
|
65
|
+
const mutation = new MutationObserver(schedule);
|
|
66
|
+
mutation.observe(text, {
|
|
67
|
+
subtree: true,
|
|
68
|
+
childList: true,
|
|
69
|
+
characterData: true,
|
|
70
|
+
attributes: true
|
|
71
|
+
});
|
|
72
|
+
// Font readiness can change glyph bounds without changing the label's
|
|
73
|
+
// content or CSS family. Measure the actual loaded font, not cached canvas
|
|
74
|
+
// estimates, and react to subsequent font loads as well.
|
|
75
|
+
document.fonts?.ready.then(() => {
|
|
76
|
+
if (mounted) {
|
|
77
|
+
schedule();
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
document.fonts?.addEventListener('loadingdone', schedule);
|
|
81
|
+
return () => {
|
|
82
|
+
mounted = false;
|
|
83
|
+
cancelAnimationFrame(frame);
|
|
84
|
+
resize.disconnect();
|
|
85
|
+
mutation.disconnect();
|
|
86
|
+
document.fonts?.removeEventListener('loadingdone', schedule);
|
|
87
|
+
};
|
|
88
|
+
});
|
|
89
|
+
</script>
|
|
90
|
+
|
|
91
|
+
<g class="chart-label-plate" bind:this={group}>
|
|
92
|
+
{#if bounds !== null}
|
|
93
|
+
<rect
|
|
94
|
+
class="chart-label-backdrop"
|
|
95
|
+
x={bounds.x}
|
|
96
|
+
y={bounds.y}
|
|
97
|
+
width={bounds.width}
|
|
98
|
+
height={bounds.height}
|
|
99
|
+
rx="2"
|
|
100
|
+
aria-hidden="true"
|
|
101
|
+
/>
|
|
102
|
+
{/if}
|
|
103
|
+
{@render children()}
|
|
104
|
+
</g>
|
|
105
|
+
|
|
106
|
+
<style>
|
|
107
|
+
.chart-label-plate {
|
|
108
|
+
pointer-events: none;
|
|
109
|
+
}
|
|
110
|
+
.chart-label-backdrop {
|
|
111
|
+
fill: var(--chart-label-background, light-dark(#fff, #111827));
|
|
112
|
+
stroke: none;
|
|
113
|
+
}
|
|
114
|
+
</style>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chart-level accessible naming shared by the seven chart families.
|
|
3
|
+
*
|
|
4
|
+
* A chart drawing needs a name of its own: a name on some wrapper elsewhere does
|
|
5
|
+
* not name the `<svg>` inside it (axe still reports `svg-img-alt` on the
|
|
6
|
+
* unnamed image), and a screen-reader user tabbing between data points has no
|
|
7
|
+
* other way to learn which chart a point belongs to. The consumer's
|
|
8
|
+
* `ariaLabel` always wins; this supplies the name only when none was given, so
|
|
9
|
+
* an unlabelled chart is at least identifiable by what it plots.
|
|
10
|
+
*/
|
|
11
|
+
export type ChartKind = 'line' | 'area' | 'bar' | 'pie' | 'sankey' | 'dual-axis' | 'funnel';
|
|
12
|
+
export type ChartNameParts = {
|
|
13
|
+
yAxisLabel?: string;
|
|
14
|
+
seriesNames?: readonly string[];
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Name used when the consumer passes no `ariaLabel`.
|
|
18
|
+
*
|
|
19
|
+
* `"{yAxisLabel} bar chart"` is the spelling BarChart has always put on its
|
|
20
|
+
* wrapping region, so a chart that already had a named region keeps the same
|
|
21
|
+
* words. Without a y-axis title, named series are listed (`"Line chart:
|
|
22
|
+
* Revenue, Cost"`); with neither, only the kind of chart is known.
|
|
23
|
+
*/
|
|
24
|
+
export declare function defaultChartName(kind: ChartKind, parts: ChartNameParts): string;
|
|
25
|
+
/**
|
|
26
|
+
* The consumer's non-blank `ariaLabel`, else the derived fallback. `unknown`
|
|
27
|
+
* because the prop is optional and an element attribute can hand over anything.
|
|
28
|
+
*/
|
|
29
|
+
export declare function resolveChartName(ariaLabel: unknown, fallback: string): string;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chart-level accessible naming shared by the seven chart families.
|
|
3
|
+
*
|
|
4
|
+
* A chart drawing needs a name of its own: a name on some wrapper elsewhere does
|
|
5
|
+
* not name the `<svg>` inside it (axe still reports `svg-img-alt` on the
|
|
6
|
+
* unnamed image), and a screen-reader user tabbing between data points has no
|
|
7
|
+
* other way to learn which chart a point belongs to. The consumer's
|
|
8
|
+
* `ariaLabel` always wins; this supplies the name only when none was given, so
|
|
9
|
+
* an unlabelled chart is at least identifiable by what it plots.
|
|
10
|
+
*/
|
|
11
|
+
const NOUN = {
|
|
12
|
+
line: 'line chart',
|
|
13
|
+
area: 'area chart',
|
|
14
|
+
bar: 'bar chart',
|
|
15
|
+
pie: 'pie chart',
|
|
16
|
+
sankey: 'flow diagram',
|
|
17
|
+
'dual-axis': 'dual-axis chart',
|
|
18
|
+
funnel: 'funnel chart'
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Name used when the consumer passes no `ariaLabel`.
|
|
22
|
+
*
|
|
23
|
+
* `"{yAxisLabel} bar chart"` is the spelling BarChart has always put on its
|
|
24
|
+
* wrapping region, so a chart that already had a named region keeps the same
|
|
25
|
+
* words. Without a y-axis title, named series are listed (`"Line chart:
|
|
26
|
+
* Revenue, Cost"`); with neither, only the kind of chart is known.
|
|
27
|
+
*/
|
|
28
|
+
export function defaultChartName(kind, parts) {
|
|
29
|
+
const noun = NOUN[kind];
|
|
30
|
+
const yAxisLabel = typeof parts.yAxisLabel === 'string' ? parts.yAxisLabel.trim() : '';
|
|
31
|
+
if (yAxisLabel !== '') {
|
|
32
|
+
return `${yAxisLabel} ${noun}`;
|
|
33
|
+
}
|
|
34
|
+
const names = (parts.seriesNames ?? []).map((name) => name.trim()).filter((name) => name !== '');
|
|
35
|
+
const title = noun.charAt(0).toUpperCase() + noun.slice(1);
|
|
36
|
+
return names.length > 0 ? `${title}: ${names.join(', ')}` : title;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The consumer's non-blank `ariaLabel`, else the derived fallback. `unknown`
|
|
40
|
+
* because the prop is optional and an element attribute can hand over anything.
|
|
41
|
+
*/
|
|
42
|
+
export function resolveChartName(ariaLabel, fallback) {
|
|
43
|
+
return typeof ariaLabel === 'string' && ariaLabel.trim() !== '' ? ariaLabel : fallback;
|
|
44
|
+
}
|
package/dist/_chart/measure.d.ts
CHANGED
|
@@ -8,5 +8,7 @@ export declare function measureText(text: string, font: FontSpec): {
|
|
|
8
8
|
width: number;
|
|
9
9
|
height: number;
|
|
10
10
|
};
|
|
11
|
+
/** A loaded font can replace its fallback without changing the font cache key. */
|
|
12
|
+
export declare function invalidateTextMeasurements(): void;
|
|
11
13
|
/** Reads a px-valued CSS custom property off an element, with an SSR-safe fallback. */
|
|
12
14
|
export declare function readCssVarPx(el: Element | null, name: string, fallback: number): number;
|
package/dist/_chart/measure.js
CHANGED
|
@@ -36,6 +36,10 @@ export function measureText(text, font) {
|
|
|
36
36
|
cache.set(key, width);
|
|
37
37
|
return { width, height };
|
|
38
38
|
}
|
|
39
|
+
/** A loaded font can replace its fallback without changing the font cache key. */
|
|
40
|
+
export function invalidateTextMeasurements() {
|
|
41
|
+
cache.clear();
|
|
42
|
+
}
|
|
39
43
|
/** Reads a px-valued CSS custom property off an element, with an SSR-safe fallback. */
|
|
40
44
|
export function readCssVarPx(el, name, fallback) {
|
|
41
45
|
if (typeof window === 'undefined' || el === null) {
|
|
@@ -3,6 +3,10 @@ type Size = {
|
|
|
3
3
|
width: number;
|
|
4
4
|
height: number;
|
|
5
5
|
};
|
|
6
|
+
type Rect = Size & {
|
|
7
|
+
left: number;
|
|
8
|
+
top: number;
|
|
9
|
+
};
|
|
6
10
|
/**
|
|
7
11
|
* Pure tooltip placement: anchor mode positions relative to a data point with
|
|
8
12
|
* side-flipping; cursor mode follows the pointer with flip-then-clamp on both
|
|
@@ -15,6 +19,8 @@ export declare function computeTooltipPosition(opts: {
|
|
|
15
19
|
tooltip: Size;
|
|
16
20
|
container: Size;
|
|
17
21
|
offset?: number;
|
|
22
|
+
/** Painted label plates in the same coordinate space as the anchor. */
|
|
23
|
+
avoidRects?: Rect[];
|
|
18
24
|
}): {
|
|
19
25
|
left: number;
|
|
20
26
|
top: number;
|