@juspay/svelte-ui-components 4.20.0 → 4.22.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/AreaChart/AreaChart.svelte +10 -1
- package/dist/AreaChart/properties.d.ts +15 -0
- package/dist/BarChart/BarChart.svelte +15 -2
- package/dist/BarChart/properties.d.ts +16 -0
- package/dist/ChatMessage/ChatMessage.svelte +39 -0
- package/dist/DualAxisBarChart/DualAxisBarChart.svelte +12 -1
- package/dist/DualAxisBarChart/properties.d.ts +15 -0
- package/dist/LineChart/LineChart.svelte +7 -1
- package/dist/LineChart/properties.d.ts +14 -0
- package/dist/MarkdownText/MarkdownText.svelte +4 -1
- package/dist/MarkdownText/markdown.js +24 -4
- package/dist/MarkdownText/properties.d.ts +11 -0
- package/dist/Table/Table.svelte +12 -7
- package/dist/Table/properties.d.ts +32 -0
- package/dist/_chart/Legend.svelte +41 -13
- package/dist/_chart/aggregate.d.ts +35 -0
- package/dist/_chart/aggregate.js +68 -0
- package/dist/_chart/narrationHighlighter.d.ts +83 -0
- package/dist/_chart/narrationHighlighter.js +113 -0
- package/dist/_chart/types.d.ts +8 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist-wc/index.js +359 -302
- package/package.json +1 -1
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import { measureText } from '../_chart/measure';
|
|
14
14
|
import { resolvePointLabels } from '../_chart/labels';
|
|
15
15
|
import { pointerPositionIn, dismissOnOutsidePointerDown } from '../_chart/interactions';
|
|
16
|
+
import { formatSeriesAggregate } from '../_chart/aggregate';
|
|
16
17
|
import type { LegendItem, Point } from '../_chart/types';
|
|
17
18
|
import { DEFAULT_CHART_MAX_HEIGHT } from '../_chart/types';
|
|
18
19
|
|
|
@@ -171,7 +172,15 @@
|
|
|
171
172
|
});
|
|
172
173
|
|
|
173
174
|
let legendItems = $derived<LegendItem[]>(
|
|
174
|
-
series.map((s, i) => ({
|
|
175
|
+
series.map((s, i) => ({
|
|
176
|
+
label: s.name,
|
|
177
|
+
color: s.color ?? getColor(i),
|
|
178
|
+
aggregateLabel: formatSeriesAggregate(
|
|
179
|
+
s.data.map((d) => d.y),
|
|
180
|
+
s.aggregate ?? 'none',
|
|
181
|
+
s.aggregateFormat ?? yTickFormat ?? formatNumber
|
|
182
|
+
)
|
|
183
|
+
}))
|
|
175
184
|
);
|
|
176
185
|
|
|
177
186
|
// ── Tooltip ────────────────────────────────────────────────────
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
2
|
import type { CurveType } from '../_chart/types';
|
|
3
|
+
import type { SeriesAggregate } from '../_chart/aggregate';
|
|
3
4
|
export type AreaChartDataPoint = {
|
|
4
5
|
x: number;
|
|
5
6
|
y: number;
|
|
@@ -9,6 +10,20 @@ export type AreaChartSeries = {
|
|
|
9
10
|
name: string;
|
|
10
11
|
data: AreaChartDataPoint[];
|
|
11
12
|
color?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Shows an aggregate of this series' `y` values beside its legend label
|
|
15
|
+
* (e.g. "Revenue $128K"). Aggregates that series' own values only -- when
|
|
16
|
+
* `stacked` is true this is never the per-category stack total, which is a
|
|
17
|
+
* separate, existing computation. Omit (or `'none'`, the default) to show
|
|
18
|
+
* no aggregate; every chart renders unchanged without it.
|
|
19
|
+
*/
|
|
20
|
+
aggregate?: SeriesAggregate;
|
|
21
|
+
/**
|
|
22
|
+
* Formatter for this series' legend aggregate. Defaults to the chart's own
|
|
23
|
+
* `yTickFormat` (or the built-in number formatter) -- set this only when
|
|
24
|
+
* the aggregate needs a different unit than the axis.
|
|
25
|
+
*/
|
|
26
|
+
aggregateFormat?: (value: number) => string;
|
|
12
27
|
};
|
|
13
28
|
export type AreaChartTooltipContext = {
|
|
14
29
|
x: number;
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
placedLabelRect,
|
|
30
30
|
dropOverlapping
|
|
31
31
|
} from '../_chart/labels';
|
|
32
|
+
import { formatSeriesAggregate } from '../_chart/aggregate';
|
|
32
33
|
import type { LegendItem, BarRect } from '../_chart/types';
|
|
33
34
|
import { DEFAULT_CHART_CORNER_RADIUS, DEFAULT_CHART_MAX_HEIGHT } from '../_chart/types';
|
|
34
35
|
import type { TooltipAnchor } from '../_chart/types';
|
|
@@ -583,10 +584,22 @@
|
|
|
583
584
|
|
|
584
585
|
let legendItems = $derived<LegendItem[]>(
|
|
585
586
|
isMulti
|
|
586
|
-
?
|
|
587
|
+
? rawSeries.map((s, i) => ({
|
|
587
588
|
label: s.name,
|
|
588
589
|
color: fallbackColor(s.color ?? getColor(i), i),
|
|
589
|
-
hidden: hiddenSeries.has(i)
|
|
590
|
+
hidden: hiddenSeries.has(i),
|
|
591
|
+
// Aggregates this series' own raw `value`s, from `rawSeries` rather
|
|
592
|
+
// than `resolvedSeries`: the latter may hold normaliseToFirstPoint
|
|
593
|
+
// percentages or topN-clipped/overflow values, neither of which is
|
|
594
|
+
// the series' real data (never the cross-series stack total either,
|
|
595
|
+
// which is an unrelated existing computation). Defaults to the
|
|
596
|
+
// chart's own `valueFormat`/number formatter, matching the "reuse
|
|
597
|
+
// the axis/tooltip formatting mechanism" rule.
|
|
598
|
+
aggregateLabel: formatSeriesAggregate(
|
|
599
|
+
s.data.map((d) => d.value),
|
|
600
|
+
s.aggregate ?? 'none',
|
|
601
|
+
s.aggregateFormat ?? format
|
|
602
|
+
)
|
|
590
603
|
}))
|
|
591
604
|
: []
|
|
592
605
|
);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
2
|
import type { ChartHighlightAPI } from '../_chart/highlight';
|
|
3
|
+
import type { SeriesAggregate } from '../_chart/aggregate';
|
|
3
4
|
export type BarFillPattern = {
|
|
4
5
|
pattern: {
|
|
5
6
|
/** SVG pattern element type: 'lines' | 'dots' | 'crosshatch' */
|
|
@@ -52,6 +53,21 @@ export type BarChartSeries = {
|
|
|
52
53
|
data: BarChartDataPoint[];
|
|
53
54
|
/** Series-level fill: plain color, pattern, or gradient. */
|
|
54
55
|
color?: BarFill;
|
|
56
|
+
/**
|
|
57
|
+
* Shows an aggregate of this series' `value`s beside its legend label (e.g.
|
|
58
|
+
* "Revenue $128K"). Aggregates that series' own values only -- in
|
|
59
|
+
* `groupMode="stacked"` this is never the per-category stack total, which
|
|
60
|
+
* is a separate, existing computation. Omit (or `'none'`, the default) to
|
|
61
|
+
* show no aggregate; every chart renders unchanged without it.
|
|
62
|
+
*/
|
|
63
|
+
aggregate?: SeriesAggregate;
|
|
64
|
+
/**
|
|
65
|
+
* Formatter for this series' legend aggregate. Defaults to the chart's own
|
|
66
|
+
* `valueFormat` (or the built-in number formatter) -- set this only when
|
|
67
|
+
* the aggregate needs a different unit than the axis, e.g. a percentage
|
|
68
|
+
* average alongside a currency-formatted axis.
|
|
69
|
+
*/
|
|
70
|
+
aggregateFormat?: (value: number) => string;
|
|
55
71
|
};
|
|
56
72
|
/**
|
|
57
73
|
* Geometry for a single rendered bar, in inner (post-margin) coordinates — the
|
|
@@ -570,6 +570,11 @@
|
|
|
570
570
|
opacity: var(--chat-message-blockquote-opacity, 0.85);
|
|
571
571
|
}
|
|
572
572
|
|
|
573
|
+
/* Fallback for a bare <table> in caller-supplied `html`, which has no scroll
|
|
574
|
+
container of its own: it keeps the block treatment so a wide table scrolls
|
|
575
|
+
rather than overflowing the message. A table that came from `markdown` (or
|
|
576
|
+
from `renderMarkdown`) arrives inside `.markdown-table-wrapper`, and the
|
|
577
|
+
rules below take it back out of block layout. */
|
|
573
578
|
.body :global(table) {
|
|
574
579
|
display: block;
|
|
575
580
|
max-width: 100%;
|
|
@@ -578,6 +583,40 @@
|
|
|
578
583
|
margin: 0.5em 0;
|
|
579
584
|
}
|
|
580
585
|
|
|
586
|
+
/* The wrapper scrolls, not the table. `display: block` on a <table> is what
|
|
587
|
+
makes `overflow-x` work on the element itself, but it strips the table's
|
|
588
|
+
semantics for assistive technology -- a screen reader loses row and column
|
|
589
|
+
navigation. MarkdownText separates the scroll container from the table for
|
|
590
|
+
exactly that reason; this component rendered the same wrapper markup and
|
|
591
|
+
styled the table instead, so a chat table was scrollable and no longer a
|
|
592
|
+
table. Chat is where markdown tables overwhelmingly land. */
|
|
593
|
+
.body :global(.markdown-table-wrapper) {
|
|
594
|
+
display: block;
|
|
595
|
+
max-width: 100%;
|
|
596
|
+
overflow-x: auto;
|
|
597
|
+
margin: 0.5em 0;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/* The wrapper is a tab stop (the renderer emits tabindex="0" so the scroll is
|
|
601
|
+
reachable by keyboard). The browser's default ring already applies; this
|
|
602
|
+
replaces it with the same 2px treatment the rest of the library uses, since
|
|
603
|
+
the 1px UA default is easy to miss against a message bubble. */
|
|
604
|
+
.body :global(.markdown-table-wrapper:focus-visible) {
|
|
605
|
+
outline: 2px solid var(--chat-message-table-focus-outline-color, #3b82f6);
|
|
606
|
+
outline-offset: 2px;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/* Without an intrinsic width the table is exactly its container's width and a
|
|
610
|
+
wide table compresses instead of scrolling. `max-content` supplies it;
|
|
611
|
+
`min-width` keeps a narrow table filling the bubble. */
|
|
612
|
+
.body :global(.markdown-table-wrapper table) {
|
|
613
|
+
display: table;
|
|
614
|
+
width: max-content;
|
|
615
|
+
min-width: 100%;
|
|
616
|
+
overflow-x: visible;
|
|
617
|
+
margin: 0;
|
|
618
|
+
}
|
|
619
|
+
|
|
581
620
|
.body :global(th),
|
|
582
621
|
.body :global(td) {
|
|
583
622
|
padding: 5px 10px;
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
import { getColor } from '../_chart/colors';
|
|
19
19
|
import { formatNumber } from '../_chart/format';
|
|
20
20
|
import { roundedRectPath, linePath } from '../_chart/paths';
|
|
21
|
+
import { formatSeriesAggregate } from '../_chart/aggregate';
|
|
21
22
|
import type {
|
|
22
23
|
LegendItem,
|
|
23
24
|
TooltipData,
|
|
@@ -280,7 +281,17 @@
|
|
|
280
281
|
series.map((s, si) => ({
|
|
281
282
|
label: s.name,
|
|
282
283
|
color: resolvedColor(s, si),
|
|
283
|
-
hidden: hiddenSeries.has(si)
|
|
284
|
+
hidden: hiddenSeries.has(si),
|
|
285
|
+
// Defaults to the same per-axis formatter (leftFormat/rightFormat, used
|
|
286
|
+
// for ticks/tooltip above) as whichever axis this series is plotted
|
|
287
|
+
// against, so a left-axis currency series and a right-axis percentage
|
|
288
|
+
// series each get a sensible default without either specifying
|
|
289
|
+
// `aggregateFormat`.
|
|
290
|
+
aggregateLabel: formatSeriesAggregate(
|
|
291
|
+
s.data,
|
|
292
|
+
s.aggregate ?? 'none',
|
|
293
|
+
s.aggregateFormat ?? (s.yAxisIndex === 0 ? leftFormat : rightFormat)
|
|
294
|
+
)
|
|
284
295
|
}))
|
|
285
296
|
);
|
|
286
297
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { SeriesAggregate } from '../_chart/aggregate';
|
|
2
3
|
/**
|
|
3
4
|
* Configuration for one of the two independent Y-axes.
|
|
4
5
|
*/
|
|
@@ -37,6 +38,20 @@ export type DualAxisSeries = {
|
|
|
37
38
|
* `'line'` — line with optional dots drawn over the column layer.
|
|
38
39
|
*/
|
|
39
40
|
type?: 'column' | 'line';
|
|
41
|
+
/**
|
|
42
|
+
* Shows an aggregate of this series' values beside its legend label (e.g.
|
|
43
|
+
* "Revenue $128K"). Omit (or `'none'`, the default) to show no aggregate;
|
|
44
|
+
* every chart renders unchanged without it.
|
|
45
|
+
*/
|
|
46
|
+
aggregate?: SeriesAggregate;
|
|
47
|
+
/**
|
|
48
|
+
* Formatter for this series' legend aggregate. Defaults to the `valueFormat`
|
|
49
|
+
* of whichever axis (`leftAxis`/`rightAxis`) this series' `yAxisIndex`
|
|
50
|
+
* points at -- reusing the same per-axis formatting the chart already has,
|
|
51
|
+
* rather than a parallel mechanism. Set this only when the aggregate needs
|
|
52
|
+
* a different unit than its axis.
|
|
53
|
+
*/
|
|
54
|
+
aggregateFormat?: (value: number) => string;
|
|
40
55
|
};
|
|
41
56
|
/**
|
|
42
57
|
* Data passed to `tooltipSnippet` when the user hovers a category.
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import { formatNumber, defaultTickFormat } from '../_chart/format';
|
|
15
15
|
import { measureText } from '../_chart/measure';
|
|
16
16
|
import { resolvePointLabels } from '../_chart/labels';
|
|
17
|
+
import { formatSeriesAggregate } from '../_chart/aggregate';
|
|
17
18
|
import type { LegendItem, Point, TooltipAnchor } from '../_chart/types';
|
|
18
19
|
import { pointerPositionIn, dismissOnOutsidePointerDown } from '../_chart/interactions';
|
|
19
20
|
import { SvelteSet } from 'svelte/reactivity';
|
|
@@ -236,7 +237,12 @@
|
|
|
236
237
|
series.map((s, i) => ({
|
|
237
238
|
label: s.name,
|
|
238
239
|
color: s.color ?? getColor(i),
|
|
239
|
-
hidden: hiddenSeries.has(i)
|
|
240
|
+
hidden: hiddenSeries.has(i),
|
|
241
|
+
aggregateLabel: formatSeriesAggregate(
|
|
242
|
+
s.data.map((d) => d.y),
|
|
243
|
+
s.aggregate ?? 'none',
|
|
244
|
+
s.aggregateFormat ?? yTickFormat ?? formatNumber
|
|
245
|
+
)
|
|
240
246
|
}))
|
|
241
247
|
);
|
|
242
248
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
2
|
import type { CurveType } from '../_chart/types';
|
|
3
3
|
import type { ChartHighlightAPI } from '../_chart/highlight';
|
|
4
|
+
import type { SeriesAggregate } from '../_chart/aggregate';
|
|
4
5
|
export type LineChartDataPoint = {
|
|
5
6
|
x: number;
|
|
6
7
|
y: number;
|
|
@@ -16,6 +17,19 @@ export type LineChartSeries = {
|
|
|
16
17
|
* custom SVG `stroke-dasharray`. Omit for a solid line.
|
|
17
18
|
*/
|
|
18
19
|
dash?: boolean | string;
|
|
20
|
+
/**
|
|
21
|
+
* Shows an aggregate of this series' `y` values beside its legend label
|
|
22
|
+
* (e.g. "Revenue $128K"). Omit (or `'none'`, the default) to show no
|
|
23
|
+
* aggregate; every chart renders unchanged without it.
|
|
24
|
+
*/
|
|
25
|
+
aggregate?: SeriesAggregate;
|
|
26
|
+
/**
|
|
27
|
+
* Formatter for this series' legend aggregate. Defaults to the chart's own
|
|
28
|
+
* `yTickFormat` (or the built-in number formatter) -- set this only when
|
|
29
|
+
* the aggregate needs a different unit than the axis, e.g. a percentage
|
|
30
|
+
* average alongside a count-formatted axis.
|
|
31
|
+
*/
|
|
32
|
+
aggregateFormat?: (value: number) => string;
|
|
19
33
|
};
|
|
20
34
|
export type LineChartTooltipContext = {
|
|
21
35
|
x: number;
|
|
@@ -8,10 +8,13 @@
|
|
|
8
8
|
testId,
|
|
9
9
|
classes,
|
|
10
10
|
tableLabel,
|
|
11
|
+
tableWrapperClass,
|
|
11
12
|
sanitize
|
|
12
13
|
}: MarkdownTextProperties = $props();
|
|
13
14
|
|
|
14
|
-
let html = $derived(
|
|
15
|
+
let html = $derived(
|
|
16
|
+
renderMarkdown(markdown, { breaks, tableLabel, tableWrapperClass, sanitize })
|
|
17
|
+
);
|
|
15
18
|
</script>
|
|
16
19
|
|
|
17
20
|
<div
|
|
@@ -245,14 +245,32 @@ function annotateExternalLinks(html) {
|
|
|
245
245
|
scrollable region with a tabindex. */
|
|
246
246
|
const TABLE_OPEN = /<table>/g;
|
|
247
247
|
const TABLE_CLOSE = /<\/table>/g;
|
|
248
|
-
|
|
248
|
+
const DEFAULT_TABLE_WRAPPER_CLASS = 'markdown-table-wrapper';
|
|
249
|
+
function wrapTables(html, label, wrapperClass) {
|
|
249
250
|
/* `role="region"` without an accessible name announces a landmark the user
|
|
250
251
|
cannot identify, which is worse than no landmark, so the role appears only
|
|
251
252
|
when the caller supplies a name. The tabindex does not depend on it:
|
|
252
253
|
keyboard scrolling should work either way. */
|
|
254
|
+
/* A caller's class is ADDED, never substituted. The wrapper's own styling --
|
|
255
|
+
`overflow-x: auto`, the `:focus-visible` ring, and the table's max-content
|
|
256
|
+
width -- is scoped to `.markdown-table-wrapper` in MarkdownText.svelte, and
|
|
257
|
+
a Svelte scoped selector cannot be templated to a name supplied at runtime.
|
|
258
|
+
Substituting the class would leave a `tabindex="0"` element that cannot
|
|
259
|
+
scroll and shows no focus ring, undoing the fix that made a wide table
|
|
260
|
+
keyboard-scrollable in the first place. Adding costs a consumer nothing:
|
|
261
|
+
their own selector still matches, and they can still override by
|
|
262
|
+
specificity. */
|
|
263
|
+
const custom = typeof wrapperClass === 'string' ? wrapperClass.trim() : '';
|
|
264
|
+
// The caller's value lands inside a double-quoted HTML attribute, same as
|
|
265
|
+
// `label` below -- escaping it the same way is what stops a value like
|
|
266
|
+
// `" onmouseover="alert(1)` from closing the attribute early and injecting a
|
|
267
|
+
// new one. The built-in name is a literal and needs no escaping.
|
|
268
|
+
const classAttr = custom.length > 0
|
|
269
|
+
? `${DEFAULT_TABLE_WRAPPER_CLASS} ${escapeHtml(custom)}`
|
|
270
|
+
: DEFAULT_TABLE_WRAPPER_CLASS;
|
|
253
271
|
const open = typeof label === 'string' && label.length > 0
|
|
254
|
-
? `<div class="
|
|
255
|
-
:
|
|
272
|
+
? `<div class="${classAttr}" tabindex="0" role="region" aria-label="${escapeHtml(label)}">`
|
|
273
|
+
: `<div class="${classAttr}" tabindex="0">`;
|
|
256
274
|
return html.replace(TABLE_OPEN, `${open}<table>`).replace(TABLE_CLOSE, '</table></div>');
|
|
257
275
|
}
|
|
258
276
|
function instanceFor(options) {
|
|
@@ -292,5 +310,7 @@ export function renderMarkdown(markdown, options = {}) {
|
|
|
292
310
|
}
|
|
293
311
|
/* Inline parsing produces no block elements, so there is no table to wrap. */
|
|
294
312
|
const linked = annotateExternalLinks(output);
|
|
295
|
-
return options.inline === true
|
|
313
|
+
return options.inline === true
|
|
314
|
+
? linked
|
|
315
|
+
: wrapTables(linked, options.tableLabel, options.tableWrapperClass);
|
|
296
316
|
}
|
|
@@ -20,6 +20,15 @@ export type OptionalMarkdownTextProperties = {
|
|
|
20
20
|
* worse than none.
|
|
21
21
|
*/
|
|
22
22
|
tableLabel?: string;
|
|
23
|
+
/**
|
|
24
|
+
* Extra class name added to the scrollable region wrapping each table,
|
|
25
|
+
* alongside the built-in `markdown-table-wrapper` rather than replacing it
|
|
26
|
+
* (the wrapper's scroll/focus behaviour is scoped to that class and cannot
|
|
27
|
+
* be retargeted). Lets a consumer whose own stylesheet targets a different
|
|
28
|
+
* class keep that styling too. Omitting it, or passing an empty or
|
|
29
|
+
* whitespace-only string, keeps only `markdown-table-wrapper`.
|
|
30
|
+
*/
|
|
31
|
+
tableWrapperClass?: string;
|
|
23
32
|
/**
|
|
24
33
|
* Narrow which URL protocols survive on rendered links/images. See
|
|
25
34
|
* `MarkdownSanitizeOptions`.
|
|
@@ -104,6 +113,8 @@ export type RenderMarkdownOptions = {
|
|
|
104
113
|
inline?: boolean;
|
|
105
114
|
/** Accessible name for the scroll region wrapping each table. See `MarkdownTextProperties.tableLabel`. */
|
|
106
115
|
tableLabel?: string;
|
|
116
|
+
/** Class name for the scroll region wrapping each table. See `MarkdownTextProperties.tableWrapperClass`. */
|
|
117
|
+
tableWrapperClass?: string;
|
|
107
118
|
/** Narrow the protocol allow-list. See `MarkdownSanitizeOptions`. */
|
|
108
119
|
sanitize?: MarkdownSanitizeOptions;
|
|
109
120
|
};
|
package/dist/Table/Table.svelte
CHANGED
|
@@ -51,7 +51,8 @@
|
|
|
51
51
|
summaryRowIndex = null,
|
|
52
52
|
headerTooltipIcon,
|
|
53
53
|
headerTooltipPosition,
|
|
54
|
-
usePortal = false
|
|
54
|
+
usePortal = false,
|
|
55
|
+
labels
|
|
55
56
|
}: TableProperties = $props();
|
|
56
57
|
|
|
57
58
|
// ─── Keyed column model → positional projection ─────────────────────────────
|
|
@@ -646,7 +647,7 @@
|
|
|
646
647
|
class="table-search-clear"
|
|
647
648
|
type="button"
|
|
648
649
|
onclick={clearSearch}
|
|
649
|
-
aria-label=
|
|
650
|
+
aria-label={labels?.clearSearch ?? 'Clear search'}
|
|
650
651
|
>
|
|
651
652
|
<!-- eslint-disable svelte/no-at-html-tags -->
|
|
652
653
|
{@html closeSvg}
|
|
@@ -688,7 +689,7 @@
|
|
|
688
689
|
so the tri-state is computed here and never flipped locally. -->
|
|
689
690
|
<Checkbox
|
|
690
691
|
text=""
|
|
691
|
-
ariaLabel=
|
|
692
|
+
ariaLabel={labels?.selectAllRows ?? 'Select all rows'}
|
|
692
693
|
controlled
|
|
693
694
|
checked={headerCheckboxState === 'all'}
|
|
694
695
|
indeterminate={headerCheckboxState === 'some'}
|
|
@@ -773,7 +774,7 @@
|
|
|
773
774
|
>
|
|
774
775
|
<Button
|
|
775
776
|
{...triggerProps}
|
|
776
|
-
ariaLabel=
|
|
777
|
+
ariaLabel={labels?.filterBy?.(header) ?? `Filter by ${header}`}
|
|
777
778
|
testId={headerColumn.testId &&
|
|
778
779
|
`${headerColumn.testId}-filter-trigger`}
|
|
779
780
|
>
|
|
@@ -791,7 +792,10 @@
|
|
|
791
792
|
{/if}
|
|
792
793
|
{#if isColumnSortable(colIndex)}
|
|
793
794
|
<div class="sort-button">
|
|
794
|
-
<Button
|
|
795
|
+
<Button
|
|
796
|
+
onclick={() => handleSort(colIndex)}
|
|
797
|
+
ariaLabel={labels?.sortBy?.(header) ?? `Sort by ${header}`}
|
|
798
|
+
>
|
|
795
799
|
{#if sortColumn === colIndex && sortDirection === 'asc'}
|
|
796
800
|
{#if typeof sortAscIcon === 'function'}
|
|
797
801
|
{@render sortAscIcon()}
|
|
@@ -840,7 +844,7 @@
|
|
|
840
844
|
<Button
|
|
841
845
|
variant="ghost"
|
|
842
846
|
iconOnly={true}
|
|
843
|
-
ariaLabel=
|
|
847
|
+
ariaLabel={labels?.closeSearch ?? 'Close search'}
|
|
844
848
|
onclick={clearSearch}
|
|
845
849
|
>
|
|
846
850
|
{#snippet icon()}
|
|
@@ -921,7 +925,8 @@
|
|
|
921
925
|
>
|
|
922
926
|
<Checkbox
|
|
923
927
|
text=""
|
|
924
|
-
ariaLabel={
|
|
928
|
+
ariaLabel={labels?.selectRow?.(rowId) ??
|
|
929
|
+
`Select row ${rowId || 'non-selectable'}`}
|
|
925
930
|
controlled
|
|
926
931
|
checked={rowSelected}
|
|
927
932
|
disabled={rowDisabled}
|
|
@@ -434,7 +434,39 @@ export type TableSearchConfig = {
|
|
|
434
434
|
displayMode?: 'toolbar' | 'inline';
|
|
435
435
|
};
|
|
436
436
|
export type TableProperties = OptionalTableProperties & TableEventProperties;
|
|
437
|
+
/**
|
|
438
|
+
* Accessible names Table generates for its own controls, for callers who do
|
|
439
|
+
* not ship an English UI.
|
|
440
|
+
*
|
|
441
|
+
* Every member is optional and falls back to the English string Table has
|
|
442
|
+
* always used, so adding this prop changes nothing for existing consumers.
|
|
443
|
+
*
|
|
444
|
+
* The two that depend on a column, and the one that depends on a row, are
|
|
445
|
+
* functions rather than templates with a placeholder: a translator needs to
|
|
446
|
+
* put the header where their grammar wants it, not where an English sentence
|
|
447
|
+
* happens to leave a slot.
|
|
448
|
+
*
|
|
449
|
+
* `Search` is deliberately absent -- `searchConfig.placeholder` already names
|
|
450
|
+
* the search input and has since search shipped.
|
|
451
|
+
*/
|
|
452
|
+
export type TableLabels = {
|
|
453
|
+
/** Sort button on a column header. Default: `Sort by ${header}`. */
|
|
454
|
+
sortBy?: (header: string) => string;
|
|
455
|
+
/** Filter trigger on a column header. Default: `Filter by ${header}`. */
|
|
456
|
+
filterBy?: (header: string) => string;
|
|
457
|
+
/** Per-row selection checkbox. Receives the row id, or `''` when the row
|
|
458
|
+
* cannot be selected -- the default spells that case out rather than
|
|
459
|
+
* naming an empty row. Default: `Select row ${rowId || 'non-selectable'}`. */
|
|
460
|
+
selectRow?: (rowId: string) => string;
|
|
461
|
+
/** Header checkbox that selects every row on the page. Default: `Select all rows`. */
|
|
462
|
+
selectAllRows?: string;
|
|
463
|
+
/** Button that empties the search box. Default: `Clear search`. */
|
|
464
|
+
clearSearch?: string;
|
|
465
|
+
/** Button that dismisses the search box. Default: `Close search`. */
|
|
466
|
+
closeSearch?: string;
|
|
467
|
+
};
|
|
437
468
|
export type OptionalTableProperties = {
|
|
469
|
+
labels?: TableLabels;
|
|
438
470
|
tableTitle?: string | null;
|
|
439
471
|
tableHeaders?: string[];
|
|
440
472
|
tableData?: Array<JSONValue[]>;
|
|
@@ -11,22 +11,38 @@
|
|
|
11
11
|
{:else}
|
|
12
12
|
{#each items as item, i (i)}
|
|
13
13
|
{#if typeof onToggle === 'function'}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
<
|
|
25
|
-
|
|
14
|
+
<!-- The aggregate sits OUTSIDE the button deliberately. Every bit of
|
|
15
|
+
text inside a button becomes part of its accessible name, so an
|
|
16
|
+
aggregate inside it would make the control's name change whenever
|
|
17
|
+
the data does -- "Revenue $6,500" today, something else tomorrow.
|
|
18
|
+
A name that moves breaks voice-control targeting and any consumer
|
|
19
|
+
test matching it exactly, and announces a figure as the control's
|
|
20
|
+
identity rather than as information about the series. It also
|
|
21
|
+
stops a number being a click target for toggling a series, which
|
|
22
|
+
it never should have been. -->
|
|
23
|
+
<div class="legend-item" class:legend-hidden={item.hidden}>
|
|
24
|
+
<button
|
|
25
|
+
type="button"
|
|
26
|
+
class="legend-toggle"
|
|
27
|
+
aria-pressed={!item.hidden}
|
|
28
|
+
onclick={() => onToggle(i)}
|
|
29
|
+
data-pw={`legend-toggle-${i}`}
|
|
30
|
+
testID={`legend-toggle-${i}`}
|
|
31
|
+
>
|
|
32
|
+
<span class="legend-swatch" style="background: {item.color}"></span>
|
|
33
|
+
<span class="legend-label">{item.label}</span>
|
|
34
|
+
</button>
|
|
35
|
+
{#if item.aggregateLabel}
|
|
36
|
+
<span class="legend-aggregate">{item.aggregateLabel}</span>
|
|
37
|
+
{/if}
|
|
38
|
+
</div>
|
|
26
39
|
{:else}
|
|
27
40
|
<div class="legend-item">
|
|
28
41
|
<span class="legend-swatch" style="background: {item.color}"></span>
|
|
29
42
|
<span class="legend-label">{item.label}</span>
|
|
43
|
+
{#if item.aggregateLabel}
|
|
44
|
+
<span class="legend-aggregate">{item.aggregateLabel}</span>
|
|
45
|
+
{/if}
|
|
30
46
|
</div>
|
|
31
47
|
{/if}
|
|
32
48
|
{/each}
|
|
@@ -72,7 +88,18 @@
|
|
|
72
88
|
color: var(--chart-legend-color, light-dark(#333, #e5e7eb));
|
|
73
89
|
}
|
|
74
90
|
|
|
91
|
+
.legend-aggregate {
|
|
92
|
+
font-size: var(--chart-legend-font-size, 12px);
|
|
93
|
+
font-weight: 600;
|
|
94
|
+
color: var(--chart-legend-aggregate-color, light-dark(#111, #f5f5f5));
|
|
95
|
+
}
|
|
96
|
+
|
|
75
97
|
.legend-toggle {
|
|
98
|
+
/* The button no longer IS the row -- it holds the swatch and label, and the
|
|
99
|
+
aggregate is its sibling -- so it carries its own flex layout. */
|
|
100
|
+
display: flex;
|
|
101
|
+
align-items: center;
|
|
102
|
+
gap: 6px;
|
|
76
103
|
background: none;
|
|
77
104
|
border: none;
|
|
78
105
|
padding: 0;
|
|
@@ -85,7 +112,8 @@
|
|
|
85
112
|
opacity: 0.25;
|
|
86
113
|
}
|
|
87
114
|
|
|
88
|
-
.legend-hidden .legend-label
|
|
115
|
+
.legend-hidden .legend-label,
|
|
116
|
+
.legend-hidden .legend-aggregate {
|
|
89
117
|
color: var(--chart-legend-hidden-color, light-dark(#bbb, #555));
|
|
90
118
|
}
|
|
91
119
|
</style>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How a chart legend summarises a series' values into one number shown
|
|
3
|
+
* beside its label. `'none'` (the default for every series that doesn't set
|
|
4
|
+
* this) shows nothing -- the feature is purely additive.
|
|
5
|
+
*/
|
|
6
|
+
export type SeriesAggregate = 'sum' | 'average' | 'min' | 'max' | 'none';
|
|
7
|
+
/**
|
|
8
|
+
* Reduces a series' values to a single number for its legend entry.
|
|
9
|
+
*
|
|
10
|
+
* Null and non-finite entries (a missing point, or `NaN`/`Infinity` slipping
|
|
11
|
+
* through past the `number` type at runtime) are treated as ABSENT rather
|
|
12
|
+
* than as zero -- they are skipped entirely, not summed/counted as 0. This
|
|
13
|
+
* only changes the result for `average` (skipping shrinks the denominator)
|
|
14
|
+
* and `min`/`max` (an absent point can never set an extreme); `sum` is
|
|
15
|
+
* identical either way since 0 is the additive identity.
|
|
16
|
+
*
|
|
17
|
+
* Zero remaining values after skipping -- an empty series, or one whose
|
|
18
|
+
* points are all absent -- resolves to `null` for EVERY kind, `sum`
|
|
19
|
+
* included: a "total" of 0 for a series with no real data would be a
|
|
20
|
+
* fabricated number, not an observed one, so the legend renders nothing
|
|
21
|
+
* rather than a misleading 0.
|
|
22
|
+
*
|
|
23
|
+
* Aggregation is always over one series' own values, never across a stack --
|
|
24
|
+
* a stacked bar/area chart's per-category stack total is an unrelated,
|
|
25
|
+
* existing computation (the y-domain sum) that this function does not touch.
|
|
26
|
+
*/
|
|
27
|
+
export declare function computeSeriesAggregate(values: ReadonlyArray<number | null>, aggregate: SeriesAggregate): number | null;
|
|
28
|
+
/**
|
|
29
|
+
* Computes and formats a series aggregate for legend display in one step,
|
|
30
|
+
* reusing the chart's own value formatter (the same one driving its
|
|
31
|
+
* axis/tooltip) rather than a parallel formatting path. Returns `null` --
|
|
32
|
+
* render nothing -- whenever there is no aggregate to show, so the formatter
|
|
33
|
+
* is never handed `NaN` and a caller never needs its own null guard.
|
|
34
|
+
*/
|
|
35
|
+
export declare function formatSeriesAggregate(values: ReadonlyArray<number | null>, aggregate: SeriesAggregate, format: (value: number) => string): string | null;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reduces a series' values to a single number for its legend entry.
|
|
3
|
+
*
|
|
4
|
+
* Null and non-finite entries (a missing point, or `NaN`/`Infinity` slipping
|
|
5
|
+
* through past the `number` type at runtime) are treated as ABSENT rather
|
|
6
|
+
* than as zero -- they are skipped entirely, not summed/counted as 0. This
|
|
7
|
+
* only changes the result for `average` (skipping shrinks the denominator)
|
|
8
|
+
* and `min`/`max` (an absent point can never set an extreme); `sum` is
|
|
9
|
+
* identical either way since 0 is the additive identity.
|
|
10
|
+
*
|
|
11
|
+
* Zero remaining values after skipping -- an empty series, or one whose
|
|
12
|
+
* points are all absent -- resolves to `null` for EVERY kind, `sum`
|
|
13
|
+
* included: a "total" of 0 for a series with no real data would be a
|
|
14
|
+
* fabricated number, not an observed one, so the legend renders nothing
|
|
15
|
+
* rather than a misleading 0.
|
|
16
|
+
*
|
|
17
|
+
* Aggregation is always over one series' own values, never across a stack --
|
|
18
|
+
* a stacked bar/area chart's per-category stack total is an unrelated,
|
|
19
|
+
* existing computation (the y-domain sum) that this function does not touch.
|
|
20
|
+
*/
|
|
21
|
+
export function computeSeriesAggregate(values, aggregate) {
|
|
22
|
+
if (aggregate === 'none') {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
let sum = 0;
|
|
26
|
+
let count = 0;
|
|
27
|
+
let min = Infinity;
|
|
28
|
+
let max = -Infinity;
|
|
29
|
+
for (const value of values) {
|
|
30
|
+
if (value === null || !Number.isFinite(value)) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
sum += value;
|
|
34
|
+
count += 1;
|
|
35
|
+
if (value < min) {
|
|
36
|
+
min = value;
|
|
37
|
+
}
|
|
38
|
+
if (value > max) {
|
|
39
|
+
max = value;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (count === 0) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
switch (aggregate) {
|
|
46
|
+
case 'sum':
|
|
47
|
+
return sum;
|
|
48
|
+
case 'average':
|
|
49
|
+
return sum / count;
|
|
50
|
+
case 'min':
|
|
51
|
+
return min;
|
|
52
|
+
case 'max':
|
|
53
|
+
return max;
|
|
54
|
+
default:
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Computes and formats a series aggregate for legend display in one step,
|
|
60
|
+
* reusing the chart's own value formatter (the same one driving its
|
|
61
|
+
* axis/tooltip) rather than a parallel formatting path. Returns `null` --
|
|
62
|
+
* render nothing -- whenever there is no aggregate to show, so the formatter
|
|
63
|
+
* is never handed `NaN` and a caller never needs its own null guard.
|
|
64
|
+
*/
|
|
65
|
+
export function formatSeriesAggregate(values, aggregate, format) {
|
|
66
|
+
const value = computeSeriesAggregate(values, aggregate);
|
|
67
|
+
return value === null ? null : format(value);
|
|
68
|
+
}
|