@svgrid/grid 2.1.6 → 2.1.7
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/SvCarousel.svelte +35 -6
- package/dist/SvCarousel.svelte.d.ts +3 -0
- package/dist/SvColorInput.svelte +0 -1
- package/dist/SvContextMenu.svelte +26 -6
- package/dist/SvDrawer.svelte +5 -3
- package/dist/SvGrid.controller.svelte.d.ts +85 -2
- package/dist/SvGrid.controller.svelte.js +494 -1
- package/dist/SvGrid.css +142 -0
- package/dist/SvGrid.svelte +49 -27
- package/dist/SvGrid.types.d.ts +78 -0
- package/dist/SvGridBoard.svelte +71 -13
- package/dist/SvGridChart.svelte +41 -7
- package/dist/SvGridChart.svelte.d.ts +4 -0
- package/dist/SvGridChartPanel.svelte +485 -0
- package/dist/SvGridChartPanel.svelte.d.ts +28 -0
- package/dist/SvGridDropdown.svelte +18 -2
- package/dist/SvGroupCell.svelte +5 -5
- package/dist/SvMenu.svelte +25 -2
- package/dist/SvMenu.svelte.d.ts +3 -0
- package/dist/SvMenuList.svelte +31 -9
- package/dist/SvMenuList.svelte.d.ts +9 -1
- package/dist/SvNumberInput.svelte +1 -1
- package/dist/SvOtpInput.svelte +3 -2
- package/dist/SvPopover.svelte +13 -1
- package/dist/SvRowGroupPanel.svelte +23 -0
- package/dist/SvScrollArea.svelte +13 -2
- package/dist/SvScrollArea.svelte.d.ts +2 -0
- package/dist/SvTooltip.svelte +27 -2
- package/dist/SvTooltip.svelte.d.ts +3 -2
- package/dist/SvTour.svelte +30 -5
- package/dist/SvTour.svelte.d.ts +3 -0
- package/dist/SvTree.svelte +7 -3
- package/dist/SvTreeSelect.svelte +16 -9
- package/dist/build-api.js +51 -0
- package/dist/cdn/svgrid.js +17394 -16052
- package/dist/cdn/svgrid.svelte-external.js +16784 -15644
- package/dist/chart-export.d.ts +21 -0
- package/dist/chart-export.js +52 -11
- package/dist/chart.d.ts +30 -0
- package/dist/chart.js +53 -13
- package/dist/createAutocomplete.svelte.d.ts +6 -0
- package/dist/createAutocomplete.svelte.js +10 -0
- package/dist/createCombobox.svelte.d.ts +1 -0
- package/dist/createCombobox.svelte.js +1 -0
- package/dist/createCountryInput.svelte.d.ts +5 -0
- package/dist/createCountryInput.svelte.js +11 -0
- package/dist/createDropdownList.svelte.d.ts +4 -0
- package/dist/createDropdownList.svelte.js +7 -0
- package/dist/createPopoverSelect.svelte.d.ts +5 -3
- package/dist/createPopoverSelect.svelte.js +5 -3
- package/dist/createTree.svelte.d.ts +1 -0
- package/dist/createTree.svelte.js +15 -2
- package/dist/index.d.ts +4 -2
- package/dist/index.js +3 -2
- package/dist/svgrid-wrapper.types.d.ts +58 -0
- package/package.json +1 -1
- package/src/SvCarousel.svelte +35 -6
- package/src/SvColorInput.svelte +0 -1
- package/src/SvContextMenu.svelte +26 -6
- package/src/SvDrawer.svelte +5 -3
- package/src/SvGrid.controller.svelte.ts +481 -0
- package/src/SvGrid.css +142 -0
- package/src/SvGrid.svelte +49 -27
- package/src/SvGrid.types.ts +83 -0
- package/src/SvGridBoard.svelte +71 -13
- package/src/SvGridChart.svelte +41 -7
- package/src/SvGridChartPanel.svelte +485 -0
- package/src/SvGridDropdown.svelte +18 -2
- package/src/SvGroupCell.svelte +5 -5
- package/src/SvMenu.svelte +25 -2
- package/src/SvMenuList.svelte +31 -9
- package/src/SvNumberInput.svelte +1 -1
- package/src/SvOtpInput.svelte +3 -2
- package/src/SvPopover.svelte +13 -1
- package/src/SvRowGroupPanel.svelte +23 -0
- package/src/SvScrollArea.svelte +13 -2
- package/src/SvTooltip.svelte +27 -2
- package/src/SvTour.svelte +30 -5
- package/src/SvTree.svelte +7 -3
- package/src/SvTreeSelect.svelte +16 -9
- package/src/build-api.ts +49 -0
- package/src/chart-export.ts +57 -0
- package/src/chart.ts +70 -13
- package/src/createAutocomplete.svelte.ts +11 -0
- package/src/createCombobox.svelte.ts +1 -0
- package/src/createCountryInput.svelte.ts +12 -0
- package/src/createDropdownList.svelte.ts +8 -0
- package/src/createPopoverSelect.svelte.ts +5 -3
- package/src/createTree.svelte.ts +15 -2
- package/src/index.ts +6 -0
- package/src/overlays.test.ts +5 -0
- package/src/svgrid-wrapper.types.ts +62 -0
- package/src/svgrid.charting.test.ts +527 -0
- package/src/test-setup.ts +22 -0
package/src/chart.ts
CHANGED
|
@@ -106,6 +106,16 @@ export type ChartSpec = {
|
|
|
106
106
|
height?: number
|
|
107
107
|
/** Palette used when a series has no explicit `color`. */
|
|
108
108
|
palette?: string[]
|
|
109
|
+
/** Per-category color overrides (by category label) - for pie / donut slice
|
|
110
|
+
* recolouring, where colour follows the category, not a series. */
|
|
111
|
+
categoryColors?: Record<string, string>
|
|
112
|
+
/** Number format for the value axis, tooltips, data labels and reference
|
|
113
|
+
* lines. Unset = the default compact `1.2k` / `1.2M` style. */
|
|
114
|
+
valueFormat?: ChartValueFormat
|
|
115
|
+
/** Grouped (nested) category axis: a parent tier spanning consecutive leaf
|
|
116
|
+
* categories (spans must sum to `categories.length`). Vertical category
|
|
117
|
+
* charts only (ignored for time / horizontal / pie). */
|
|
118
|
+
categoryGroups?: Array<{ label: string; span: number }>
|
|
109
119
|
/** Stack bar / area series (per axis) instead of grouping them. */
|
|
110
120
|
stacked?: boolean
|
|
111
121
|
/** Stack to 100% (each category normalized to its total). Implies stacked. */
|
|
@@ -387,6 +397,9 @@ export type ChartGeometry = {
|
|
|
387
397
|
y2Ticks: ChartAxisTick[]
|
|
388
398
|
hasRightAxis: boolean
|
|
389
399
|
xTicks: ChartCategoryTick[]
|
|
400
|
+
/** Grouped category axis parent tier: label + span extent (pixels). Empty
|
|
401
|
+
* unless `spec.categoryGroups` is set on a vertical category chart. */
|
|
402
|
+
categoryGroupTicks: Array<{ label: string; xCenter: number; x0: number; x1: number }>
|
|
390
403
|
/** True when x labels are long/many and should be rotated. */
|
|
391
404
|
xLabelRotated: boolean
|
|
392
405
|
legend: ChartLegendItem[]
|
|
@@ -730,6 +743,25 @@ function fmtTick(n: number): string {
|
|
|
730
743
|
return String(Math.round(n * 100) / 100)
|
|
731
744
|
}
|
|
732
745
|
|
|
746
|
+
/** Value-axis / tooltip / label number format. */
|
|
747
|
+
export type ChartValueFormat = 'number' | 'currency' | 'percent' | 'compact'
|
|
748
|
+
|
|
749
|
+
/**
|
|
750
|
+
* Format a numeric value for display, honouring an optional `valueFormat`.
|
|
751
|
+
* Builds on the compact `1.2k` / `1.2M` base: currency prefixes `$` (sign
|
|
752
|
+
* outside), percent multiplies by 100 and suffixes `%`. Unset / `'number'` /
|
|
753
|
+
* `'compact'` = the plain compact form.
|
|
754
|
+
*/
|
|
755
|
+
export function formatChartValue(n: number, format?: ChartValueFormat): string {
|
|
756
|
+
if (!Number.isFinite(n)) return ''
|
|
757
|
+
if (format === 'currency') return `${n < 0 ? '-' : ''}$${fmtTick(Math.abs(n))}`
|
|
758
|
+
if (format === 'percent') {
|
|
759
|
+
const p = n * 100
|
|
760
|
+
return `${Math.round(p * 10) / 10}%`
|
|
761
|
+
}
|
|
762
|
+
return fmtTick(n)
|
|
763
|
+
}
|
|
764
|
+
|
|
733
765
|
const DAY = 86_400_000
|
|
734
766
|
/** Nice date-tick timestamps across [min, max]. */
|
|
735
767
|
function dateTicks(tMin: number, tMax: number): number[] {
|
|
@@ -833,6 +865,7 @@ export function buildChart(spec: ChartSpec, theme: 'light' | 'dark' = 'light'):
|
|
|
833
865
|
y2Ticks: [],
|
|
834
866
|
hasRightAxis: false,
|
|
835
867
|
xTicks: [],
|
|
868
|
+
categoryGroupTicks: [],
|
|
836
869
|
xLabelRotated: false,
|
|
837
870
|
legend,
|
|
838
871
|
donut: null,
|
|
@@ -945,7 +978,7 @@ export function buildChart(spec: ChartSpec, theme: 'light' | 'dark' = 'light'):
|
|
|
945
978
|
x: round(padL + slotW * i + slotW / 2),
|
|
946
979
|
}))
|
|
947
980
|
const yTicks: ChartAxisTick[] = dom.ticks.map((value) => ({
|
|
948
|
-
value, y: yOfW(value), label:
|
|
981
|
+
value, y: yOfW(value), label: formatChartValue(value, spec.valueFormat),
|
|
949
982
|
}))
|
|
950
983
|
return {
|
|
951
984
|
...empty,
|
|
@@ -1132,7 +1165,7 @@ export function buildChart(spec: ChartSpec, theme: 'light' | 'dark' = 'light'):
|
|
|
1132
1165
|
const legend = Array.from({ length: 5 }, (_, i) => {
|
|
1133
1166
|
const t = i / 4
|
|
1134
1167
|
const value = vMin + (vMax - vMin) * t
|
|
1135
|
-
return { value, color: colorAt(value), label:
|
|
1168
|
+
return { value, color: colorAt(value), label: formatChartValue(value, spec.valueFormat) }
|
|
1136
1169
|
})
|
|
1137
1170
|
return {
|
|
1138
1171
|
...empty,
|
|
@@ -1486,7 +1519,7 @@ export function buildChart(spec: ChartSpec, theme: 'light' | 'dark' = 'light'):
|
|
|
1486
1519
|
const heatmapLegend = Array.from({ length: 5 }, (_, i) => {
|
|
1487
1520
|
const t = i / 4
|
|
1488
1521
|
const value = vMin + (vMax - vMin) * t
|
|
1489
|
-
return { value, color: colorAt(value), label:
|
|
1522
|
+
return { value, color: colorAt(value), label: formatChartValue(value, spec.valueFormat) }
|
|
1490
1523
|
})
|
|
1491
1524
|
return {
|
|
1492
1525
|
...empty,
|
|
@@ -1537,10 +1570,11 @@ export function buildChart(spec: ChartSpec, theme: 'light' | 'dark' = 'light'):
|
|
|
1537
1570
|
} else {
|
|
1538
1571
|
path = `M${round(cx)},${round(cy)} L${round(ox0)},${round(oy0)} A${r},${r} 0 ${large} 1 ${round(ox1)},${round(oy1)} Z`
|
|
1539
1572
|
}
|
|
1573
|
+
const catLabel = spec.categories[i] ?? String(i)
|
|
1540
1574
|
return {
|
|
1541
1575
|
path,
|
|
1542
|
-
color: palette[i % palette.length]!,
|
|
1543
|
-
label:
|
|
1576
|
+
color: spec.categoryColors?.[catLabel] ?? palette[i % palette.length]!,
|
|
1577
|
+
label: catLabel,
|
|
1544
1578
|
value: v,
|
|
1545
1579
|
percent: frac * 100,
|
|
1546
1580
|
cx: round(cx + labelR * Math.cos(mid)),
|
|
@@ -1550,7 +1584,7 @@ export function buildChart(spec: ChartSpec, theme: 'light' | 'dark' = 'light'):
|
|
|
1550
1584
|
return {
|
|
1551
1585
|
...empty,
|
|
1552
1586
|
slices,
|
|
1553
|
-
legend: spec.categories.map((label, i) => ({ label, color: palette[i % palette.length]! })),
|
|
1587
|
+
legend: spec.categories.map((label, i) => ({ label, color: spec.categoryColors?.[label] ?? palette[i % palette.length]! })),
|
|
1554
1588
|
donut: ir > 0 ? { cx: round(cx), cy: round(cy), r: round(ir), total: s.values.reduce((a, b) => a + Math.max(0, b), 0) } : null,
|
|
1555
1589
|
}
|
|
1556
1590
|
}
|
|
@@ -1606,7 +1640,7 @@ export function buildChart(spec: ChartSpec, theme: 'light' | 'dark' = 'light'):
|
|
|
1606
1640
|
}
|
|
1607
1641
|
const referenceLines: ChartRefLineGeo[] = (spec.referenceLines ?? []).map((ref) => ({
|
|
1608
1642
|
y: yOf(ref.value),
|
|
1609
|
-
label: ref.label ??
|
|
1643
|
+
label: ref.label ?? formatChartValue(ref.value, spec.valueFormat),
|
|
1610
1644
|
color: ref.color ?? '#ef4444',
|
|
1611
1645
|
dashed: ref.dashed !== false,
|
|
1612
1646
|
}))
|
|
@@ -1615,7 +1649,7 @@ export function buildChart(spec: ChartSpec, theme: 'light' | 'dark' = 'light'):
|
|
|
1615
1649
|
plot,
|
|
1616
1650
|
scatterPoints,
|
|
1617
1651
|
referenceLines,
|
|
1618
|
-
yTicks: yDom.ticks.map((value) => ({ value, y: yOf(value), label:
|
|
1652
|
+
yTicks: yDom.ticks.map((value) => ({ value, y: yOf(value), label: formatChartValue(value, spec.valueFormat) })),
|
|
1619
1653
|
xTicks: xDom.ticks.map((value) => ({ label: fmtTick(value), x: xOf(value) })),
|
|
1620
1654
|
}
|
|
1621
1655
|
}
|
|
@@ -1702,7 +1736,7 @@ export function buildChart(spec: ChartSpec, theme: 'light' | 'dark' = 'light'):
|
|
|
1702
1736
|
}
|
|
1703
1737
|
|
|
1704
1738
|
const valueTicks: ChartCategoryTick[] = dom.ticks.map((value) => ({
|
|
1705
|
-
label: spec.stacked100 ? `${fmtTick(value)}%` :
|
|
1739
|
+
label: spec.stacked100 ? `${fmtTick(value)}%` : formatChartValue(value, spec.valueFormat),
|
|
1706
1740
|
x: xOf(value),
|
|
1707
1741
|
}))
|
|
1708
1742
|
const catTicks: ChartAxisTick[] = spec.categories.map((label, i) => ({
|
|
@@ -1712,7 +1746,7 @@ export function buildChart(spec: ChartSpec, theme: 'light' | 'dark' = 'light'):
|
|
|
1712
1746
|
}))
|
|
1713
1747
|
const referenceLinesV: ChartRefLineGeoV[] = (spec.referenceLines ?? []).map((ref) => ({
|
|
1714
1748
|
x: xOf(ref.value),
|
|
1715
|
-
label: ref.label ??
|
|
1749
|
+
label: ref.label ?? formatChartValue(ref.value, spec.valueFormat),
|
|
1716
1750
|
color: ref.color ?? '#ef4444',
|
|
1717
1751
|
dashed: ref.dashed !== false,
|
|
1718
1752
|
}))
|
|
@@ -1736,10 +1770,20 @@ export function buildChart(spec: ChartSpec, theme: 'light' | 'dark' = 'light'):
|
|
|
1736
1770
|
|
|
1737
1771
|
const maxLabel = spec.categories.reduce((m, c) => Math.max(m, c.length), 0)
|
|
1738
1772
|
const xLabelRotated = spec.categories.length > 8 || maxLabel > 9
|
|
1773
|
+
// Grouped (nested) category axis: valid only when the spans cover every leaf.
|
|
1774
|
+
const validGroups =
|
|
1775
|
+
spec.categoryGroups &&
|
|
1776
|
+
spec.categoryGroups.length > 0 &&
|
|
1777
|
+
spec.xType !== 'time' &&
|
|
1778
|
+
spec.orientation !== 'horizontal' &&
|
|
1779
|
+
spec.categoryGroups.reduce((a, g) => a + g.span, 0) === spec.categories.length
|
|
1780
|
+
? spec.categoryGroups
|
|
1781
|
+
: null
|
|
1782
|
+
const groupTierH = validGroups ? 18 : 0
|
|
1739
1783
|
const padL = 48 + (spec.yAxisTitle ? 16 : 0)
|
|
1740
1784
|
const padR = (hasRightAxis ? 48 : 12) + (spec.y2AxisTitle ? 16 : 0)
|
|
1741
1785
|
const padT = 10
|
|
1742
|
-
const padB = (xLabelRotated ? 54 : 28) + (spec.xAxisTitle ? 16 : 0)
|
|
1786
|
+
const padB = (xLabelRotated ? 54 : 28) + (spec.xAxisTitle ? 16 : 0) + groupTierH
|
|
1743
1787
|
const plotW = Math.max(1, width - padL - padR)
|
|
1744
1788
|
const plotH = Math.max(1, height - padT - padB)
|
|
1745
1789
|
const plot = { x: padL, y: padT, w: plotW, h: plotH }
|
|
@@ -1790,6 +1834,18 @@ export function buildChart(spec: ChartSpec, theme: 'light' | 'dark' = 'light'):
|
|
|
1790
1834
|
}))
|
|
1791
1835
|
: spec.categories.map((label, i) => ({ label, x: xCenter(i) }))
|
|
1792
1836
|
|
|
1837
|
+
// Parent-tier ticks for a grouped category axis: each spans its leaves.
|
|
1838
|
+
const categoryGroupTicks: ChartGeometry['categoryGroupTicks'] = []
|
|
1839
|
+
if (validGroups && !timeOk) {
|
|
1840
|
+
let start = 0
|
|
1841
|
+
for (const g of validGroups) {
|
|
1842
|
+
const x0 = round(padL + slot * start)
|
|
1843
|
+
const x1 = round(padL + slot * (start + g.span))
|
|
1844
|
+
categoryGroupTicks.push({ label: g.label, x0, x1, xCenter: round((x0 + x1) / 2) })
|
|
1845
|
+
start += g.span
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1793
1849
|
const barSeries = series.filter((s) => s.kind === 'bar')
|
|
1794
1850
|
const bars: ChartBar[] = []
|
|
1795
1851
|
if (barSeries.length) {
|
|
@@ -1992,7 +2048,7 @@ export function buildChart(spec: ChartSpec, theme: 'light' | 'dark' = 'light'):
|
|
|
1992
2048
|
}
|
|
1993
2049
|
|
|
1994
2050
|
const tickFor = (dom: NiceScale, log: boolean): ChartAxisTick[] =>
|
|
1995
|
-
dom.ticks.map((value) => ({ value, y: yOf(dom, value, log), label:
|
|
2051
|
+
dom.ticks.map((value) => ({ value, y: yOf(dom, value, log), label: formatChartValue(value, spec.valueFormat) }))
|
|
1996
2052
|
|
|
1997
2053
|
const referenceLines: ChartRefLineGeo[] = (spec.referenceLines ?? []).map((ref) => {
|
|
1998
2054
|
const onRight = ref.axis === 'right'
|
|
@@ -2000,7 +2056,7 @@ export function buildChart(spec: ChartSpec, theme: 'light' | 'dark' = 'light'):
|
|
|
2000
2056
|
const log = onRight ? rightLog : leftLog
|
|
2001
2057
|
return {
|
|
2002
2058
|
y: yOf(dom, ref.value, log),
|
|
2003
|
-
label: ref.label ??
|
|
2059
|
+
label: ref.label ?? formatChartValue(ref.value, spec.valueFormat),
|
|
2004
2060
|
color: ref.color ?? '#ef4444',
|
|
2005
2061
|
dashed: ref.dashed !== false,
|
|
2006
2062
|
}
|
|
@@ -2081,6 +2137,7 @@ export function buildChart(spec: ChartSpec, theme: 'light' | 'dark' = 'light'):
|
|
|
2081
2137
|
y2Ticks: rightDom ? tickFor(rightDom, rightLog) : [],
|
|
2082
2138
|
hasRightAxis,
|
|
2083
2139
|
xTicks,
|
|
2140
|
+
categoryGroupTicks,
|
|
2084
2141
|
xLabelRotated: timeOk ? false : xLabelRotated,
|
|
2085
2142
|
referenceLines,
|
|
2086
2143
|
overlays,
|
|
@@ -41,7 +41,12 @@ export type AutocompleteConfig = {
|
|
|
41
41
|
hint?: () => string | undefined
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
let uid = 0
|
|
45
|
+
|
|
44
46
|
export function createAutocomplete(config: AutocompleteConfig) {
|
|
47
|
+
const id = `sv-ac-${uid++}`
|
|
48
|
+
const listId = `${id}-list`
|
|
49
|
+
const optionId = (index: number) => `${listId}-opt-${index}`
|
|
45
50
|
const minChars = () => config.minChars?.() ?? 1
|
|
46
51
|
|
|
47
52
|
const ariaState = (): EditorAriaState => ({
|
|
@@ -82,6 +87,8 @@ export function createAutocomplete(config: AutocompleteConfig) {
|
|
|
82
87
|
get filtered() { return filtered },
|
|
83
88
|
/** Controlled text value. */
|
|
84
89
|
get value() { return config.value() },
|
|
90
|
+
/** id of the listbox element (matches inputProps `aria-controls`). */
|
|
91
|
+
listId,
|
|
85
92
|
isActive: (i: number) => i === active,
|
|
86
93
|
setActive: (i: number) => { active = i },
|
|
87
94
|
maybeOpen,
|
|
@@ -94,7 +101,9 @@ export function createAutocomplete(config: AutocompleteConfig) {
|
|
|
94
101
|
id: config.id?.(),
|
|
95
102
|
role: 'combobox' as const,
|
|
96
103
|
'aria-expanded': open,
|
|
104
|
+
'aria-controls': listId,
|
|
97
105
|
'aria-autocomplete': 'list' as const,
|
|
106
|
+
'aria-activedescendant': open && filtered[active] ? optionId(active) : undefined,
|
|
98
107
|
...editorAria(ariaState()),
|
|
99
108
|
value: config.value(),
|
|
100
109
|
disabled: config.disabled?.() ?? false,
|
|
@@ -104,10 +113,12 @@ export function createAutocomplete(config: AutocompleteConfig) {
|
|
|
104
113
|
}),
|
|
105
114
|
/** Spread onto the listbox container. */
|
|
106
115
|
listboxProps: () => ({
|
|
116
|
+
id: listId,
|
|
107
117
|
role: 'listbox' as const,
|
|
108
118
|
}),
|
|
109
119
|
/** Spread onto the suggestion at `index` (index into `filtered`). */
|
|
110
120
|
optionProps: (index: number) => ({
|
|
121
|
+
id: optionId(index),
|
|
111
122
|
role: 'option' as const,
|
|
112
123
|
tabindex: -1,
|
|
113
124
|
'aria-selected': index === active,
|
|
@@ -138,6 +138,7 @@ export function createCombobox(config: ComboboxConfig) {
|
|
|
138
138
|
'aria-expanded': open,
|
|
139
139
|
'aria-controls': listId,
|
|
140
140
|
'aria-autocomplete': 'list' as const,
|
|
141
|
+
'aria-activedescendant': open && filtered[active] ? optionId(active) : undefined,
|
|
141
142
|
...editorAria(ariaState()),
|
|
142
143
|
value: shownText,
|
|
143
144
|
disabled: disabled(),
|
|
@@ -41,7 +41,12 @@ export type CountryInputConfig = {
|
|
|
41
41
|
hint?: () => string | undefined
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
let uid = 0
|
|
45
|
+
|
|
44
46
|
export function createCountryInput(config: CountryInputConfig) {
|
|
47
|
+
const id = `sv-country-${uid++}`
|
|
48
|
+
const listId = `${id}-list`
|
|
49
|
+
const optionId = (i: number) => `${listId}-opt-${i}`
|
|
45
50
|
const disabled = () => config.disabled?.() ?? false
|
|
46
51
|
|
|
47
52
|
const ariaState = (): EditorAriaState => ({
|
|
@@ -75,6 +80,8 @@ export function createCountryInput(config: CountryInputConfig) {
|
|
|
75
80
|
function pick(code: string) { config.onChange?.(code); open = false; config.focusTrigger?.() }
|
|
76
81
|
function setQuery(v: string) { query = v; active = 0 }
|
|
77
82
|
function onSearchInput(e: Event) { setQuery((e.currentTarget as HTMLInputElement).value) }
|
|
83
|
+
/** The current active-descendant id (for the focused search input), or undefined. */
|
|
84
|
+
const activeDescendant = () => (open && filtered.length > 0 ? optionId(active) : undefined)
|
|
78
85
|
function onKeydown(e: KeyboardEvent) {
|
|
79
86
|
if (!open) return
|
|
80
87
|
if (e.key === 'ArrowDown') { e.preventDefault(); active = Math.min(active + 1, filtered.length - 1) }
|
|
@@ -111,6 +118,7 @@ export function createCountryInput(config: CountryInputConfig) {
|
|
|
111
118
|
type: 'button' as const,
|
|
112
119
|
'aria-haspopup': 'listbox' as const,
|
|
113
120
|
'aria-expanded': open,
|
|
121
|
+
'aria-controls': listId,
|
|
114
122
|
...editorAria(ariaState()),
|
|
115
123
|
disabled: disabled(),
|
|
116
124
|
onclick: toggle,
|
|
@@ -120,17 +128,21 @@ export function createCountryInput(config: CountryInputConfig) {
|
|
|
120
128
|
type: 'text' as const,
|
|
121
129
|
value: query,
|
|
122
130
|
'aria-label': config.searchLabel?.() ?? 'Search countries',
|
|
131
|
+
'aria-controls': listId,
|
|
132
|
+
'aria-activedescendant': activeDescendant(),
|
|
123
133
|
oninput: onSearchInput,
|
|
124
134
|
onkeydown: onKeydown,
|
|
125
135
|
}),
|
|
126
136
|
/** Spread onto the listbox container. */
|
|
127
137
|
listboxProps: () => ({
|
|
138
|
+
id: listId,
|
|
128
139
|
role: 'listbox' as const,
|
|
129
140
|
}),
|
|
130
141
|
/** Spread onto the country option at `index` (index into `filtered`). */
|
|
131
142
|
optionProps: (index: number) => {
|
|
132
143
|
const c = filtered[index]
|
|
133
144
|
return {
|
|
145
|
+
id: optionId(index),
|
|
134
146
|
role: 'option' as const,
|
|
135
147
|
tabindex: -1,
|
|
136
148
|
'aria-selected': c ? c.code === config.value() : false,
|
|
@@ -41,7 +41,11 @@ export type DropdownListConfig = {
|
|
|
41
41
|
hint?: () => string | undefined
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
let uid = 0
|
|
45
|
+
|
|
44
46
|
export function createDropdownList(config: DropdownListConfig) {
|
|
47
|
+
const listId = `sv-ddl-${uid++}`
|
|
48
|
+
const optionId = (index: number) => `${listId}-opt-${index}`
|
|
45
49
|
const opts = () => config.options()
|
|
46
50
|
const disabled = () => config.disabled?.() ?? false
|
|
47
51
|
|
|
@@ -125,6 +129,8 @@ export function createDropdownList(config: DropdownListConfig) {
|
|
|
125
129
|
type: 'button' as const,
|
|
126
130
|
'aria-haspopup': 'listbox' as const,
|
|
127
131
|
'aria-expanded': open,
|
|
132
|
+
'aria-controls': listId,
|
|
133
|
+
'aria-activedescendant': open && opts()[active] ? optionId(active) : undefined,
|
|
128
134
|
...editorAria(ariaState()),
|
|
129
135
|
disabled: disabled(),
|
|
130
136
|
onclick: toggle,
|
|
@@ -132,6 +138,7 @@ export function createDropdownList(config: DropdownListConfig) {
|
|
|
132
138
|
}),
|
|
133
139
|
/** Spread onto the listbox container. */
|
|
134
140
|
listboxProps: () => ({
|
|
141
|
+
id: listId,
|
|
135
142
|
role: 'listbox' as const,
|
|
136
143
|
tabindex: -1,
|
|
137
144
|
}),
|
|
@@ -140,6 +147,7 @@ export function createDropdownList(config: DropdownListConfig) {
|
|
|
140
147
|
const o = opts()[index]
|
|
141
148
|
return {
|
|
142
149
|
role: 'option' as const,
|
|
150
|
+
id: optionId(index),
|
|
143
151
|
tabindex: -1,
|
|
144
152
|
'aria-selected': o ? o.value === config.value() : false,
|
|
145
153
|
'aria-disabled': o?.disabled,
|
|
@@ -182,9 +182,11 @@ export function createPopoverSelect(config: PopoverSelectConfig) {
|
|
|
182
182
|
onPanelKeydown,
|
|
183
183
|
/** True when item `i` is the roving-active one. */
|
|
184
184
|
isActive: (i: number) => i === active,
|
|
185
|
-
/** Spread onto the trigger button.
|
|
186
|
-
|
|
187
|
-
|
|
185
|
+
/** Spread onto the trigger button. Pass the ARIA popup type the panel
|
|
186
|
+
* exposes (mirrors `focusOwnerProps(role)`); defaults to 'listbox' for
|
|
187
|
+
* the classic select panels. */
|
|
188
|
+
triggerProps: (haspopup: 'listbox' | 'tree' | 'grid' = 'listbox') => ({
|
|
189
|
+
'aria-haspopup': haspopup,
|
|
188
190
|
'aria-expanded': open,
|
|
189
191
|
'aria-controls': open ? panelId : undefined,
|
|
190
192
|
onkeydown: onTriggerKeydown,
|
package/src/createTree.svelte.ts
CHANGED
|
@@ -231,6 +231,18 @@ export function createTree(config: TreeConfig) {
|
|
|
231
231
|
config.onCheck?.([...next])
|
|
232
232
|
}
|
|
233
233
|
|
|
234
|
+
// Step from `from` towards `dir` (+1/-1), skipping disabled rows, so roving
|
|
235
|
+
// focus never lands on a node with zero semantic affordance for AT users.
|
|
236
|
+
// Falls back to `from` if every row in that direction is disabled.
|
|
237
|
+
function stepEnabled(from: number, dir: 1 | -1): number {
|
|
238
|
+
let i = from
|
|
239
|
+
while (i + dir >= 0 && i + dir <= rows.length - 1) {
|
|
240
|
+
i += dir
|
|
241
|
+
if (!rows[i]?.node.disabled) return i
|
|
242
|
+
}
|
|
243
|
+
return from
|
|
244
|
+
}
|
|
245
|
+
|
|
234
246
|
function onKeydown(e: KeyboardEvent) {
|
|
235
247
|
const item = rows[active]
|
|
236
248
|
if (!item) return
|
|
@@ -246,8 +258,8 @@ export function createTree(config: TreeConfig) {
|
|
|
246
258
|
}
|
|
247
259
|
const rtl = config.dir?.() === 'rtl'
|
|
248
260
|
switch (e.key) {
|
|
249
|
-
case 'ArrowDown': e.preventDefault(); setActive(
|
|
250
|
-
case 'ArrowUp': e.preventDefault(); setActive(
|
|
261
|
+
case 'ArrowDown': e.preventDefault(); setActive(stepEnabled(active, 1), true); break
|
|
262
|
+
case 'ArrowUp': e.preventDefault(); setActive(stepEnabled(active, -1), true); break
|
|
251
263
|
case 'ArrowRight': e.preventDefault(); (rtl ? collapse : expand)(); break
|
|
252
264
|
case 'ArrowLeft': e.preventDefault(); (rtl ? expand : collapse)(); break
|
|
253
265
|
case 'Enter': e.preventDefault(); select(item.node); break
|
|
@@ -294,6 +306,7 @@ export function createTree(config: TreeConfig) {
|
|
|
294
306
|
'aria-selected': row.node.id === selected(),
|
|
295
307
|
'aria-expanded': row.hasChildren ? row.open : undefined,
|
|
296
308
|
'aria-checked': ariaChecked,
|
|
309
|
+
'aria-disabled': row.node.disabled || undefined,
|
|
297
310
|
'data-row': row.index,
|
|
298
311
|
tabindex: row.index === active ? 0 : -1,
|
|
299
312
|
onclick: () => { setActive(row.index); select(row.node) },
|
package/src/index.ts
CHANGED
|
@@ -286,8 +286,11 @@ export {
|
|
|
286
286
|
export { default as FlexRender } from './FlexRender.svelte'
|
|
287
287
|
export { renderComponent, renderSnippet } from './render-component'
|
|
288
288
|
export { default as SvGridChart } from './SvGridChart.svelte'
|
|
289
|
+
export { default as SvGridChartPanel } from './SvGridChartPanel.svelte'
|
|
290
|
+
export type { ChartingConfig, ChartAggregateRequest, ChartAggregateBucket } from './SvGrid.types'
|
|
289
291
|
export {
|
|
290
292
|
buildChart,
|
|
293
|
+
formatChartValue,
|
|
291
294
|
rowsToChartSpec,
|
|
292
295
|
niceScale,
|
|
293
296
|
niceLogScale,
|
|
@@ -326,6 +329,7 @@ export {
|
|
|
326
329
|
type ChartGaugeLayout,
|
|
327
330
|
type ChartSankeyNode,
|
|
328
331
|
type ChartSankeyLink,
|
|
332
|
+
type ChartValueFormat,
|
|
329
333
|
type TreeNode,
|
|
330
334
|
} from './chart'
|
|
331
335
|
export {
|
|
@@ -333,6 +337,8 @@ export {
|
|
|
333
337
|
downloadChartSvg,
|
|
334
338
|
chartToPngBlob,
|
|
335
339
|
downloadChartPng,
|
|
340
|
+
chartSpecToCsv,
|
|
341
|
+
downloadChartCsv,
|
|
336
342
|
type ChartExportOptions,
|
|
337
343
|
} from './chart-export'
|
|
338
344
|
export {
|
package/src/overlays.test.ts
CHANGED
|
@@ -79,6 +79,11 @@ describe('SvTooltip', () => {
|
|
|
79
79
|
expect(tip).not.toBeNull()
|
|
80
80
|
expect(tip.getAttribute('role')).toBe('tooltip')
|
|
81
81
|
expect(anchor.getAttribute('aria-describedby')).toBe(tip.id)
|
|
82
|
+
// The wrapper span is never itself focusable, so AT users only ever land
|
|
83
|
+
// on the real interactive child below - it must carry the description
|
|
84
|
+
// too, or it's never announced on focus (see SvTooltip.svelte effect).
|
|
85
|
+
const focusEl = target.querySelector<HTMLElement>('.t')!
|
|
86
|
+
expect(focusEl.getAttribute('aria-describedby')).toBe(tip.id)
|
|
82
87
|
destroy()
|
|
83
88
|
} finally { vi.useRealTimers() }
|
|
84
89
|
})
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { CellFormatConfig, ColumnDef, RowData, SvGridOptions, TableFeatures } from './core'
|
|
2
2
|
import type { GridExportOptions, GridClipboardOptions } from './export-format'
|
|
3
|
+
import type { ChartSpec, ChartType } from './chart'
|
|
3
4
|
|
|
4
5
|
export type SvGridFilterOperator =
|
|
5
6
|
| 'contains'
|
|
@@ -33,6 +34,32 @@ export type SvGridViewState = {
|
|
|
33
34
|
>
|
|
34
35
|
/** Facet (Excel-style value checklist) selections, keyed by column id. */
|
|
35
36
|
facetFilters: Record<string, string[]>
|
|
37
|
+
/** Built-in chart panel state (present only when `charting` is on): the
|
|
38
|
+
* ACTIVE chart, for back-compat + spread-and-tweak. */
|
|
39
|
+
chart?: {
|
|
40
|
+
open: boolean
|
|
41
|
+
type: ChartType
|
|
42
|
+
dimension: string | null
|
|
43
|
+
series: string | null
|
|
44
|
+
measure: string | null
|
|
45
|
+
reduce: 'sum' | 'avg' | 'count'
|
|
46
|
+
stacked: boolean
|
|
47
|
+
}
|
|
48
|
+
/** All charts in the tab strip (multi-chart), plus the active index. */
|
|
49
|
+
charts?: Array<{
|
|
50
|
+
title: string
|
|
51
|
+
type: ChartType
|
|
52
|
+
dimension: string | null
|
|
53
|
+
series: string | null
|
|
54
|
+
measure: string | null
|
|
55
|
+
reduce: 'sum' | 'avg' | 'count'
|
|
56
|
+
stacked: boolean | null
|
|
57
|
+
dataLabels: boolean | null
|
|
58
|
+
logScale: boolean | null
|
|
59
|
+
timeAxis: boolean | null
|
|
60
|
+
valueFormat: 'number' | 'currency' | 'percent' | 'compact' | null
|
|
61
|
+
}>
|
|
62
|
+
chartActive?: number
|
|
36
63
|
}
|
|
37
64
|
|
|
38
65
|
/**
|
|
@@ -94,6 +121,41 @@ export type SvGridApi<
|
|
|
94
121
|
*/
|
|
95
122
|
getSelected(): Array<[number, number, number, number]>
|
|
96
123
|
|
|
124
|
+
// ----- Integrated charting (requires the `charting` prop) -----
|
|
125
|
+
/** Open the built-in chart panel. */
|
|
126
|
+
openChart(): void
|
|
127
|
+
/** Close the built-in chart panel. */
|
|
128
|
+
closeChart(): void
|
|
129
|
+
/** The live chart spec the panel is rendering, or `null`. */
|
|
130
|
+
getChartSpec(): ChartSpec | null
|
|
131
|
+
/** Select cell rectangles + open the chart panel (scopes to the range). */
|
|
132
|
+
chartRange(ranges?: ReadonlyArray<readonly [number, number, number, number]>): void
|
|
133
|
+
/**
|
|
134
|
+
* Configure the built-in chart panel's ACTIVE chart. Column references
|
|
135
|
+
* accept a column id OR field name. Opens the panel unless `open === false`.
|
|
136
|
+
*/
|
|
137
|
+
configureChart(config: {
|
|
138
|
+
open?: boolean
|
|
139
|
+
type?: ChartType
|
|
140
|
+
dimension?: string | null
|
|
141
|
+
series?: string | null
|
|
142
|
+
measure?: string | null
|
|
143
|
+
reduce?: 'sum' | 'avg' | 'count'
|
|
144
|
+
stacked?: boolean
|
|
145
|
+
dataLabels?: boolean
|
|
146
|
+
logScale?: boolean
|
|
147
|
+
timeAxis?: boolean
|
|
148
|
+
valueFormat?: 'number' | 'currency' | 'percent' | 'compact'
|
|
149
|
+
}): void
|
|
150
|
+
/**
|
|
151
|
+
* Register a natural-language "chart this" handler. When set, the chart
|
|
152
|
+
* panel shows an AI button. `@svgrid/enterprise`'s `enableAiCharting(api)`
|
|
153
|
+
* fills this. Pass `null` to remove it.
|
|
154
|
+
*/
|
|
155
|
+
setChartAiHandler(
|
|
156
|
+
handler: ((prompt: string) => Promise<Record<string, unknown> | null>) | null,
|
|
157
|
+
): void
|
|
158
|
+
|
|
97
159
|
// ----- Rows -----
|
|
98
160
|
/** Add one row. `position` defaults to `'bottom'`. */
|
|
99
161
|
addRow(row: TData, position?: 'top' | 'bottom' | number): void
|