@svgrid/grid 3.0.1 → 3.0.2
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/GridFooter.svelte +14 -5
- package/dist/GridFooter.svelte.d.ts +8 -0
- package/dist/GridMenus.svelte +18 -10
- package/dist/GridMenus.svelte.d.ts +2 -1
- package/dist/SvGrid.controller.svelte.d.ts +17 -0
- package/dist/SvGrid.controller.svelte.js +128 -14
- package/dist/SvGrid.css +79 -10
- package/dist/SvGrid.svelte +191 -186
- package/dist/SvGrid.types.d.ts +42 -1
- package/dist/SvGridChart.svelte +571 -41
- package/dist/SvGridChart.svelte.d.ts +63 -1
- package/dist/SvGridChartPanel.svelte +169 -21
- package/dist/SvGridChartView.svelte +9 -0
- package/dist/SvGridChartView.svelte.d.ts +7 -0
- package/dist/SvGroupCell.svelte +28 -3
- package/dist/SvGroupCell.svelte.d.ts +8 -0
- package/dist/SvRowGroupPanel.svelte +20 -4
- package/dist/SvRowGroupPanel.svelte.d.ts +8 -0
- package/dist/ai.d.ts +11 -2
- package/dist/ai.js +51 -7
- package/dist/cdn/GridMenus-DyFA-QxA.js +602 -0
- package/dist/cdn/{GridMenus-DFB6iwF8.js → GridMenus-M2XP9t0_.js} +152 -150
- package/dist/cdn/SvGridChart-DAb9RbRs.js +1480 -0
- package/dist/cdn/SvGridChart-Yl-IXZFO.js +1481 -0
- package/dist/cdn/SvGridChartPanel-DayQpDsR.js +834 -0
- package/dist/cdn/SvGridChartPanel-aqf8n-vJ.js +810 -0
- package/dist/cdn/{SvGridChartView-BhUEJ5ki.js → SvGridChartView-CxfakxR0.js} +5 -3
- package/dist/cdn/{SvGridChartView-00LPUHL1.js → SvGridChartView-Dm_YEx9L.js} +5 -3
- package/dist/cdn/cell-formatting-C2Hf5gqd.js +86 -0
- package/dist/cdn/chart-CKJRCPOk.js +1652 -0
- package/dist/cdn/{export-format-Cv1Dll6k.js → export-format-CUDLy2yn.js} +84 -168
- package/dist/cdn/{src-C8QjSaq-.js → src-BJ2opd0j.js} +3608 -3414
- package/dist/cdn/{src-D0hO2eYN.js → src-DNbz94Ct.js} +8617 -8423
- package/dist/cdn/svgrid.js +16 -15
- package/dist/cdn/svgrid.svelte-external.js +16 -15
- package/dist/cell-formatting.d.ts +7 -0
- package/dist/cell-formatting.js +7 -1
- package/dist/chart-export.d.ts +23 -2
- package/dist/chart-export.js +80 -6
- package/dist/chart.d.ts +410 -18
- package/dist/chart.js +824 -24
- package/dist/grid-icons.d.ts +49 -0
- package/dist/grid-icons.js +86 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -2
- package/package.json +11 -11
- package/src/GridFooter.svelte +14 -5
- package/src/GridMenus.svelte +18 -10
- package/src/SvGrid.controller.svelte.ts +146 -13
- package/src/SvGrid.css +79 -10
- package/src/SvGrid.svelte +191 -186
- package/src/SvGrid.types.ts +42 -1
- package/src/SvGridChart.svelte +571 -41
- package/src/SvGridChart.test.ts +243 -1
- package/src/SvGridChartPanel.svelte +169 -21
- package/src/SvGridChartView.svelte +9 -0
- package/src/SvGroupCell.svelte +28 -3
- package/src/SvRowGroupPanel.svelte +20 -4
- package/src/ai.test.ts +60 -1
- package/src/ai.ts +59 -6
- package/src/cell-formatting.ts +7 -1
- package/src/chart-export.test.ts +112 -0
- package/src/chart-export.ts +76 -8
- package/src/chart.test.ts +830 -1
- package/src/chart.ts +1053 -32
- package/src/grid-icons.test.ts +46 -0
- package/src/grid-icons.ts +184 -0
- package/src/icon-overrides.grid.test.ts +188 -0
- package/src/icon-seam.test.ts +100 -0
- package/src/index.ts +19 -0
- package/src/svgrid.charting.test.ts +120 -0
- package/dist/cdn/GridMenus-CYzOvmNQ.js +0 -600
- package/dist/cdn/SvGridChart-BEJmNNx9.js +0 -1203
- package/dist/cdn/SvGridChart-BwVos976.js +0 -1202
- package/dist/cdn/SvGridChartPanel-BOknOkrP.js +0 -561
- package/dist/cdn/SvGridChartPanel-D2PjII4O.js +0 -585
- package/dist/cdn/chart-i4XcXyHJ.js +0 -1214
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The icon catalogue as data: the name list and the glyph table have to agree
|
|
3
|
+
* with each other and with the filter-operator catalogue.
|
|
4
|
+
*
|
|
5
|
+
* The third test here is the one that earns its keep. Operator icons are
|
|
6
|
+
* data-driven (`filterOperatorOptions[].iconName`), and an operator naming an
|
|
7
|
+
* icon the grid has no case for renders an empty box rather than failing -
|
|
8
|
+
* which is exactly how `op-between` shipped blank. `GridIconName` makes that a
|
|
9
|
+
* compile error now, and this makes it a test failure too, so it also catches
|
|
10
|
+
* an operator added through a path that skips type-checking.
|
|
11
|
+
*/
|
|
12
|
+
import { describe, expect, it } from 'vitest'
|
|
13
|
+
import { GRID_ICON_GLYPHS, GRID_ICON_NAMES, type GridIconName } from './grid-icons'
|
|
14
|
+
import { filterOperatorOptions } from './filter-operators'
|
|
15
|
+
|
|
16
|
+
describe('grid icon catalogue', () => {
|
|
17
|
+
it('lists every name exactly once', () => {
|
|
18
|
+
const dupes = GRID_ICON_NAMES.filter((n, i) => GRID_ICON_NAMES.indexOf(n) !== i)
|
|
19
|
+
expect(dupes, `duplicated icon names: ${dupes.join(', ')}`).toEqual([])
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('only maps glyphs for names in the catalogue', () => {
|
|
23
|
+
const orphans = Object.keys(GRID_ICON_GLYPHS).filter(
|
|
24
|
+
(k) => !GRID_ICON_NAMES.includes(k as GridIconName),
|
|
25
|
+
)
|
|
26
|
+
expect(orphans, `glyphs for unknown icons: ${orphans.join(', ')}`).toEqual([])
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('has a name for every filter operator icon', () => {
|
|
30
|
+
const missing = filterOperatorOptions
|
|
31
|
+
.map((o) => o.iconName)
|
|
32
|
+
.filter((n) => !GRID_ICON_NAMES.includes(n))
|
|
33
|
+
expect(missing, `operators naming an icon the grid cannot draw: ${missing.join(', ')}`).toEqual(
|
|
34
|
+
[],
|
|
35
|
+
)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('keeps the pin icons separate from the operator icons they look like', () => {
|
|
39
|
+
// Pin used to render `op-startsWith` / `op-greaterThan` / `x` directly, so
|
|
40
|
+
// overriding a filter operator silently repainted the pin menu. Same
|
|
41
|
+
// default glyphs, different names.
|
|
42
|
+
for (const name of ['pin-left', 'pin-right', 'unpin'] as const) {
|
|
43
|
+
expect(GRID_ICON_NAMES).toContain(name)
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
})
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Grid chrome icons. Every glyph `<SvGrid>` draws for its own UI - sort
|
|
3
|
+
* indicators, the filter funnel, expanders, the pager arrows, tool-panel
|
|
4
|
+
* buttons - has a name here, and the `icons` prop replaces any subset of them.
|
|
5
|
+
*
|
|
6
|
+
* This mirrors `grid-messages.ts`: a name-keyed map with built-in defaults,
|
|
7
|
+
* where an unset name keeps the default. A partial map is all a consumer ever
|
|
8
|
+
* needs, so `icons` is safe to pass without knowing the full catalogue. That
|
|
9
|
+
* matters more than it looks - a single catch-all snippet would make the
|
|
10
|
+
* consumer responsible for every name the grid can ask for, and missing one
|
|
11
|
+
* renders an empty box. The `op-between` comment in SvGrid.svelte records what
|
|
12
|
+
* that looks like when it ships.
|
|
13
|
+
*
|
|
14
|
+
* Deliberately NOT in here:
|
|
15
|
+
* - the scrollbar stepper arrows. `sv-grid-scrollbar.ts` is a custom element
|
|
16
|
+
* that builds its SVG with `createElementNS` inside a shadow root, on
|
|
17
|
+
* purpose, so it stays compatible with strict CSP / Trusted Types. A Svelte
|
|
18
|
+
* snippet cannot render there. Theme it with the `--sg-scrollbar-*` tokens.
|
|
19
|
+
* - the checkbox tick and its indeterminate dash. Both are `::after`
|
|
20
|
+
* pseudo-elements drawn from borders (SvGrid.css), a form-control
|
|
21
|
+
* affordance rather than an icon, and they render once per visible row.
|
|
22
|
+
* - conditional formatting's `iconSet`. Those are value-driven data glyphs a
|
|
23
|
+
* consumer already chooses; a different concept that shares the word.
|
|
24
|
+
*/
|
|
25
|
+
import type { Snippet } from 'svelte'
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Every icon `<SvGrid>` draws for its own chrome. Pass any subset of these to
|
|
29
|
+
* the `icons` prop to swap the built-in glyph for your own markup.
|
|
30
|
+
*/
|
|
31
|
+
export type GridIconName =
|
|
32
|
+
// ---- Header + column menu ----
|
|
33
|
+
| 'sort'
|
|
34
|
+
| 'sort-asc'
|
|
35
|
+
| 'sort-desc'
|
|
36
|
+
| 'filter'
|
|
37
|
+
| 'menu'
|
|
38
|
+
| 'group'
|
|
39
|
+
| 'x'
|
|
40
|
+
| 'chevron-down'
|
|
41
|
+
| 'chevron-right'
|
|
42
|
+
| 'autosize'
|
|
43
|
+
| 'columns'
|
|
44
|
+
| 'reset'
|
|
45
|
+
// ---- Filter operators (mirror `iconName` in filter-operators.ts) ----
|
|
46
|
+
| 'op-contains'
|
|
47
|
+
| 'op-notContains'
|
|
48
|
+
| 'op-equals'
|
|
49
|
+
| 'op-notEquals'
|
|
50
|
+
| 'op-startsWith'
|
|
51
|
+
| 'op-endsWith'
|
|
52
|
+
| 'op-regex'
|
|
53
|
+
| 'op-in'
|
|
54
|
+
| 'op-notIn'
|
|
55
|
+
| 'op-greaterThan'
|
|
56
|
+
| 'op-lessThan'
|
|
57
|
+
| 'op-between'
|
|
58
|
+
| 'op-isBlank'
|
|
59
|
+
| 'op-isNotBlank'
|
|
60
|
+
// ---- Pinning. Their defaults borrow the operator glyphs above, but they get
|
|
61
|
+
// their own names so overriding a filter operator does not silently repaint
|
|
62
|
+
// the pin menu. ----
|
|
63
|
+
| 'pin-left'
|
|
64
|
+
| 'pin-right'
|
|
65
|
+
| 'unpin'
|
|
66
|
+
// ---- Toolbar + overlays ----
|
|
67
|
+
| 'search'
|
|
68
|
+
| 'tool-panel'
|
|
69
|
+
| 'chart'
|
|
70
|
+
| 'advanced-filter'
|
|
71
|
+
| 'column-group-caret'
|
|
72
|
+
// ---- Glyph icons: the built-in is a character, not a path ----
|
|
73
|
+
| 'close'
|
|
74
|
+
| 'clear'
|
|
75
|
+
| 'remove'
|
|
76
|
+
| 'row-number'
|
|
77
|
+
| 'pinned-row-top'
|
|
78
|
+
| 'pinned-row-bottom'
|
|
79
|
+
| 'find-prev'
|
|
80
|
+
| 'find-next'
|
|
81
|
+
| 'move-up'
|
|
82
|
+
| 'move-down'
|
|
83
|
+
| 'group-add'
|
|
84
|
+
| 'page-first'
|
|
85
|
+
| 'page-prev'
|
|
86
|
+
| 'page-next'
|
|
87
|
+
| 'page-last'
|
|
88
|
+
| 'page-size-caret'
|
|
89
|
+
| 'drag-handle'
|
|
90
|
+
| 'breadcrumb-separator'
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Consumer icon overrides, keyed by name. Any subset; names you leave out keep
|
|
94
|
+
* their built-in glyph, so `icons` never has to be exhaustive.
|
|
95
|
+
*/
|
|
96
|
+
export type GridIcons = Partial<Record<GridIconName, Snippet>>
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Icons whose built-in form is a character rather than an SVG path. Kept as
|
|
100
|
+
* data so `<SvGrid>`, its footer and the standalone `SvRowGroupPanel` render
|
|
101
|
+
* the same defaults instead of three copies drifting apart.
|
|
102
|
+
*
|
|
103
|
+
* Each is the exact character that shipped inline before, so a grid that sets
|
|
104
|
+
* no `icons` renders byte-for-byte what it always did.
|
|
105
|
+
*/
|
|
106
|
+
export const GRID_ICON_GLYPHS: Partial<Record<GridIconName, string>> = {
|
|
107
|
+
close: '✕', // multiplication X
|
|
108
|
+
clear: '✕',
|
|
109
|
+
remove: '×', // multiplication sign
|
|
110
|
+
'row-number': '#',
|
|
111
|
+
'pinned-row-top': '↑', // upwards arrow
|
|
112
|
+
'pinned-row-bottom': '↓', // downwards arrow
|
|
113
|
+
'find-prev': '↑',
|
|
114
|
+
'find-next': '↓',
|
|
115
|
+
'move-up': '↑',
|
|
116
|
+
'move-down': '↓',
|
|
117
|
+
'group-add': '⊞', // squared plus
|
|
118
|
+
'page-first': '⇤', // leftwards arrow to bar
|
|
119
|
+
'page-prev': '‹', // single left-pointing angle quote
|
|
120
|
+
'page-next': '›', // single right-pointing angle quote
|
|
121
|
+
'page-last': '⇥', // rightwards arrow to bar
|
|
122
|
+
'page-size-caret': '▾', // black down-pointing small triangle
|
|
123
|
+
'drag-handle': '⠿', // braille pattern dots-123456, the grip
|
|
124
|
+
'breadcrumb-separator': '›',
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Every icon name, in catalogue order. Exported for tests and docs generation;
|
|
129
|
+
* the grid itself never reads it, so it costs a consumer's bundle nothing.
|
|
130
|
+
*/
|
|
131
|
+
export const GRID_ICON_NAMES: ReadonlyArray<GridIconName> = [
|
|
132
|
+
'sort',
|
|
133
|
+
'sort-asc',
|
|
134
|
+
'sort-desc',
|
|
135
|
+
'filter',
|
|
136
|
+
'menu',
|
|
137
|
+
'group',
|
|
138
|
+
'x',
|
|
139
|
+
'chevron-down',
|
|
140
|
+
'chevron-right',
|
|
141
|
+
'autosize',
|
|
142
|
+
'columns',
|
|
143
|
+
'reset',
|
|
144
|
+
'op-contains',
|
|
145
|
+
'op-notContains',
|
|
146
|
+
'op-equals',
|
|
147
|
+
'op-notEquals',
|
|
148
|
+
'op-startsWith',
|
|
149
|
+
'op-endsWith',
|
|
150
|
+
'op-regex',
|
|
151
|
+
'op-in',
|
|
152
|
+
'op-notIn',
|
|
153
|
+
'op-greaterThan',
|
|
154
|
+
'op-lessThan',
|
|
155
|
+
'op-between',
|
|
156
|
+
'op-isBlank',
|
|
157
|
+
'op-isNotBlank',
|
|
158
|
+
'pin-left',
|
|
159
|
+
'pin-right',
|
|
160
|
+
'unpin',
|
|
161
|
+
'search',
|
|
162
|
+
'tool-panel',
|
|
163
|
+
'chart',
|
|
164
|
+
'advanced-filter',
|
|
165
|
+
'column-group-caret',
|
|
166
|
+
'close',
|
|
167
|
+
'clear',
|
|
168
|
+
'remove',
|
|
169
|
+
'row-number',
|
|
170
|
+
'pinned-row-top',
|
|
171
|
+
'pinned-row-bottom',
|
|
172
|
+
'find-prev',
|
|
173
|
+
'find-next',
|
|
174
|
+
'move-up',
|
|
175
|
+
'move-down',
|
|
176
|
+
'group-add',
|
|
177
|
+
'page-first',
|
|
178
|
+
'page-prev',
|
|
179
|
+
'page-next',
|
|
180
|
+
'page-last',
|
|
181
|
+
'page-size-caret',
|
|
182
|
+
'drag-handle',
|
|
183
|
+
'breadcrumb-separator',
|
|
184
|
+
]
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DOM: the `icons` prop replaces the grid's own glyphs, one name at a time.
|
|
3
|
+
*
|
|
4
|
+
* The contract these lock in:
|
|
5
|
+
* - per-name fallback. Overriding one icon must not blank the others; that
|
|
6
|
+
* cliff is the whole reason `icons` is a map and not a single snippet.
|
|
7
|
+
* - an override reaches the lazily-imported column menu, not just the parts
|
|
8
|
+
* of the grid that render eagerly.
|
|
9
|
+
* - overriding a filter operator does NOT repaint the pin menu, which shared
|
|
10
|
+
* its glyphs before this feature.
|
|
11
|
+
* - defaults are untouched when `icons` is absent.
|
|
12
|
+
*
|
|
13
|
+
* jsdom does not compute `transform`, so the rotation contract is asserted as
|
|
14
|
+
* the class hook the CSS keys off (`.sv-grid-icon` on the wrapper, as a direct
|
|
15
|
+
* child of the toggle) rather than as a computed style. The rotation itself is
|
|
16
|
+
* browser-only verification.
|
|
17
|
+
*/
|
|
18
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
19
|
+
import { createRawSnippet, flushSync, mount, unmount } from 'svelte'
|
|
20
|
+
import SvGrid from './SvGrid.svelte'
|
|
21
|
+
import {
|
|
22
|
+
createCoreRowModel,
|
|
23
|
+
createFilteredRowModel,
|
|
24
|
+
createPaginatedRowModel,
|
|
25
|
+
createSortedRowModel,
|
|
26
|
+
sortFns,
|
|
27
|
+
tableFeatures,
|
|
28
|
+
rowSortingFeature,
|
|
29
|
+
} from './index'
|
|
30
|
+
import type { ColumnDef } from './index'
|
|
31
|
+
|
|
32
|
+
type Row = { id: number; name: string }
|
|
33
|
+
const features = tableFeatures({ rowSortingFeature })
|
|
34
|
+
const cols: ColumnDef<typeof features, Row>[] = [{ field: 'name', header: 'Name', width: 160 }]
|
|
35
|
+
const data: Row[] = Array.from({ length: 12 }, (_, i) => ({ id: i, name: `n${i}` }))
|
|
36
|
+
|
|
37
|
+
const tick = () => new Promise<void>((r) => queueMicrotask(r))
|
|
38
|
+
|
|
39
|
+
/** A marker icon that is trivially findable in the DOM. */
|
|
40
|
+
const mark = (id: string) =>
|
|
41
|
+
createRawSnippet(() => ({
|
|
42
|
+
render: () => `<svg data-icon="${id}"><path d="M0 0" /></svg>`,
|
|
43
|
+
}))
|
|
44
|
+
|
|
45
|
+
function mountGrid(props: Record<string, unknown>) {
|
|
46
|
+
const target = document.createElement('div')
|
|
47
|
+
document.body.appendChild(target)
|
|
48
|
+
const app = mount(SvGrid, {
|
|
49
|
+
target,
|
|
50
|
+
props: {
|
|
51
|
+
data,
|
|
52
|
+
columns: cols,
|
|
53
|
+
features,
|
|
54
|
+
_rowModels: {
|
|
55
|
+
coreRowModel: createCoreRowModel(),
|
|
56
|
+
filteredRowModel: createFilteredRowModel(),
|
|
57
|
+
sortedRowModel: createSortedRowModel(sortFns),
|
|
58
|
+
paginatedRowModel: createPaginatedRowModel(),
|
|
59
|
+
},
|
|
60
|
+
containerHeight: 240,
|
|
61
|
+
virtualization: false,
|
|
62
|
+
...props,
|
|
63
|
+
} as never,
|
|
64
|
+
})
|
|
65
|
+
return { target, app, destroy: () => { unmount(app); target.remove() } }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let cleanup: (() => void) | null = null
|
|
69
|
+
afterEach(() => { cleanup?.(); cleanup = null })
|
|
70
|
+
|
|
71
|
+
describe('icons prop', () => {
|
|
72
|
+
it('renders the built-in glyphs when no icons are passed', async () => {
|
|
73
|
+
const { target, destroy } = mountGrid({ showColumnMenu: true })
|
|
74
|
+
cleanup = destroy
|
|
75
|
+
await tick()
|
|
76
|
+
// The header menu button draws a real <svg> from the built-in catalogue.
|
|
77
|
+
const menuBtn = target.querySelector('.sv-grid-col-menu-btn')
|
|
78
|
+
expect(menuBtn).not.toBeNull()
|
|
79
|
+
expect(menuBtn!.querySelector('svg.sv-grid-icon')).not.toBeNull()
|
|
80
|
+
expect(target.querySelector('[data-icon]')).toBeNull()
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('replaces one icon and leaves the rest alone', async () => {
|
|
84
|
+
const { target, destroy } = mountGrid({
|
|
85
|
+
showColumnMenu: true,
|
|
86
|
+
showFilterRow: true,
|
|
87
|
+
icons: { menu: mark('m') },
|
|
88
|
+
})
|
|
89
|
+
cleanup = destroy
|
|
90
|
+
await tick()
|
|
91
|
+
|
|
92
|
+
const menuBtn = target.querySelector('.sv-grid-col-menu-btn')!
|
|
93
|
+
// The override is there...
|
|
94
|
+
expect(menuBtn.querySelector('[data-icon="m"]')).not.toBeNull()
|
|
95
|
+
// ...and the built-in it replaced is not drawn alongside it.
|
|
96
|
+
expect(menuBtn.querySelector('svg.sv-grid-icon')).toBeNull()
|
|
97
|
+
// An unset name still renders its built-in. This is the assertion that a
|
|
98
|
+
// single catch-all snippet could not satisfy.
|
|
99
|
+
expect(target.querySelectorAll('svg.sv-grid-icon').length).toBeGreaterThan(0)
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('wraps an override in the icon box the CSS keys off', async () => {
|
|
103
|
+
const { target, destroy } = mountGrid({ showColumnMenu: true, icons: { menu: mark('m') } })
|
|
104
|
+
cleanup = destroy
|
|
105
|
+
await tick()
|
|
106
|
+
const wrapper = target.querySelector('[data-icon="m"]')!.parentElement!
|
|
107
|
+
expect(wrapper.classList.contains('sv-grid-icon')).toBe(true)
|
|
108
|
+
expect(wrapper.classList.contains('sv-grid-icon-custom')).toBe(true)
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('reaches the lazily-imported column menu', async () => {
|
|
112
|
+
const { target, destroy } = mountGrid({
|
|
113
|
+
showColumnMenu: true,
|
|
114
|
+
icons: { 'sort-asc': mark('sa') },
|
|
115
|
+
})
|
|
116
|
+
cleanup = destroy
|
|
117
|
+
await tick()
|
|
118
|
+
;(target.querySelector('.sv-grid-col-menu-btn') as HTMLElement).click()
|
|
119
|
+
// GridMenus arrives through a dynamic import(), so poll rather than
|
|
120
|
+
// guessing a tick count.
|
|
121
|
+
await vi.waitFor(() => {
|
|
122
|
+
expect(target.querySelector('[data-icon="sa"]')).not.toBeNull()
|
|
123
|
+
})
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('does not repaint the pin menu when a filter operator is overridden', async () => {
|
|
127
|
+
const { target, destroy } = mountGrid({
|
|
128
|
+
showColumnMenu: true,
|
|
129
|
+
// The pin section is hidden while columns are virtualized, and that
|
|
130
|
+
// defaults to on. This is COLUMN virtualization, not the row
|
|
131
|
+
// `virtualization: false` the shared helper sets.
|
|
132
|
+
columnVirtualization: false,
|
|
133
|
+
icons: { 'op-startsWith': mark('sw') },
|
|
134
|
+
})
|
|
135
|
+
cleanup = destroy
|
|
136
|
+
await tick()
|
|
137
|
+
;(target.querySelector('.sv-grid-col-menu-btn') as HTMLElement).click()
|
|
138
|
+
|
|
139
|
+
// Assert the item is really there before asserting about its icon -
|
|
140
|
+
// a conditional check here would pass just as happily if the pin menu
|
|
141
|
+
// stopped rendering at all.
|
|
142
|
+
const pinLeft = await vi.waitFor(() => {
|
|
143
|
+
const el = [...target.querySelectorAll('.sv-grid-menu-item')].find((e) =>
|
|
144
|
+
e.textContent?.includes('Pin to left'),
|
|
145
|
+
)
|
|
146
|
+
expect(el).toBeTruthy()
|
|
147
|
+
return el!
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
// Pin borrowed `op-startsWith` before this feature; it must not follow
|
|
151
|
+
// that override now.
|
|
152
|
+
expect(pinLeft.querySelector('[data-icon="sw"]')).toBeNull()
|
|
153
|
+
expect(pinLeft.querySelector('svg.sv-grid-icon')).not.toBeNull()
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
it('overrides the pager arrows through GridFooter', async () => {
|
|
157
|
+
const { target, destroy } = mountGrid({
|
|
158
|
+
showPagination: true,
|
|
159
|
+
pageSize: 5,
|
|
160
|
+
icons: { 'page-next': mark('pn') },
|
|
161
|
+
})
|
|
162
|
+
cleanup = destroy
|
|
163
|
+
await tick()
|
|
164
|
+
const hit = target.querySelector('[data-icon="pn"]')
|
|
165
|
+
expect(hit).not.toBeNull()
|
|
166
|
+
// Still a real pager button, and its label is untouched: icons and
|
|
167
|
+
// localeText are independent axes.
|
|
168
|
+
const btn = hit!.closest('.sv-grid-pagination-btn')
|
|
169
|
+
expect(btn).not.toBeNull()
|
|
170
|
+
expect(btn!.getAttribute('aria-label')).toBe('Next page')
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
it('picks up icons set after mount through api.setOption', async () => {
|
|
174
|
+
let api: { setOption: (k: string, v: unknown) => void } | undefined
|
|
175
|
+
const { target, destroy } = mountGrid({
|
|
176
|
+
showColumnMenu: true,
|
|
177
|
+
onApiReady: (a: typeof api) => (api = a),
|
|
178
|
+
})
|
|
179
|
+
cleanup = destroy
|
|
180
|
+
await tick()
|
|
181
|
+
expect(target.querySelector('[data-icon="rt"]')).toBeNull()
|
|
182
|
+
|
|
183
|
+
api!.setOption('icons', { menu: mark('rt') })
|
|
184
|
+
flushSync()
|
|
185
|
+
await tick()
|
|
186
|
+
expect(target.querySelector('[data-icon="rt"]')).not.toBeNull()
|
|
187
|
+
})
|
|
188
|
+
})
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Source scan: every chrome icon goes through the `icon` snippet.
|
|
3
|
+
*
|
|
4
|
+
* The `icons` prop is only worth having if it covers the chrome, and coverage
|
|
5
|
+
* is the kind of thing that rots one commit at a time - somebody adds a toolbar
|
|
6
|
+
* button with an inline `<svg>`, it works, nobody notices it ignores `icons`
|
|
7
|
+
* until a consumer reports a stray glyph in a re-skinned grid. This is the
|
|
8
|
+
* check that turns that into a red test instead.
|
|
9
|
+
*
|
|
10
|
+
* It reads source rather than DOM on purpose: a rendering test can only cover
|
|
11
|
+
* the states it happens to mount, and the icons most likely to be missed are
|
|
12
|
+
* the ones behind a rarely-exercised branch.
|
|
13
|
+
*/
|
|
14
|
+
import { readFileSync } from 'node:fs'
|
|
15
|
+
import { join } from 'node:path'
|
|
16
|
+
import { describe, expect, it } from 'vitest'
|
|
17
|
+
import { GRID_ICON_GLYPHS, GRID_ICON_NAMES, type GridIconName } from './grid-icons'
|
|
18
|
+
|
|
19
|
+
const SRC = join(process.cwd(), 'src')
|
|
20
|
+
const read = (f: string) => readFileSync(join(SRC, f), 'utf8')
|
|
21
|
+
|
|
22
|
+
/** Files that render grid chrome and therefore must go through the seam. */
|
|
23
|
+
const CHROME = ['SvGrid.svelte', 'GridMenus.svelte', 'GridFooter.svelte', 'SvRowGroupPanel.svelte']
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Drop the regions allowed to contain raw icon markup: the `icon` snippet's own
|
|
27
|
+
* body (which IS the built-in set), the `ic` snippet in SvRowGroupPanel, and
|
|
28
|
+
* every `<script>` block.
|
|
29
|
+
*/
|
|
30
|
+
function stripAllowedRegions(src: string): string {
|
|
31
|
+
let out = src.replace(/<script[\s\S]*?<\/script>/g, '')
|
|
32
|
+
// The built-in catalogue: from the snippet header to its closing tag.
|
|
33
|
+
out = out.replace(/\{#snippet icon\(name: GridIconName\)\}[\s\S]*?\n\{\/snippet\}/g, '')
|
|
34
|
+
out = out.replace(/\{#snippet ic\(name: GridIconName\)\}[\s\S]*?\{\/snippet\}/g, '')
|
|
35
|
+
return out
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe('icon seam', () => {
|
|
39
|
+
it('leaves no inline <svg> outside the built-in catalogue', () => {
|
|
40
|
+
// Data visualisation is not chrome: these draw values, not affordances, and
|
|
41
|
+
// there is nothing for a consumer's icon set to say about them.
|
|
42
|
+
const ALLOWED = ['sv-grid-sparkline']
|
|
43
|
+
|
|
44
|
+
const offenders: string[] = []
|
|
45
|
+
for (const file of CHROME) {
|
|
46
|
+
const body = stripAllowedRegions(read(file))
|
|
47
|
+
for (const [i, line] of body.split('\n').entries()) {
|
|
48
|
+
if (!line.includes('<svg')) continue
|
|
49
|
+
if (ALLOWED.some((a) => body.split('\n').slice(i, i + 3).join(' ').includes(a))) continue
|
|
50
|
+
offenders.push(`${file}:${i + 1} ${line.trim().slice(0, 80)}`)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
expect(
|
|
54
|
+
offenders,
|
|
55
|
+
`Inline <svg> outside the icon snippet - route it through {@render icon("...")} ` +
|
|
56
|
+
`and add a name to grid-icons.ts:\n ${offenders.join('\n ')}`,
|
|
57
|
+
).toEqual([])
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('leaves no glyph character used as an icon', () => {
|
|
61
|
+
// Every character the catalogue owns. If one of these appears as an
|
|
62
|
+
// element's text content outside the snippet, it is an icon that bypassed
|
|
63
|
+
// the seam.
|
|
64
|
+
const glyphs = [...new Set(Object.values(GRID_ICON_GLYPHS))].filter((g) => g !== '#')
|
|
65
|
+
const offenders: string[] = []
|
|
66
|
+
for (const file of CHROME) {
|
|
67
|
+
const body = stripAllowedRegions(read(file))
|
|
68
|
+
for (const [i, line] of body.split('\n').entries()) {
|
|
69
|
+
for (const g of glyphs) {
|
|
70
|
+
// `>GLYPH<` is the shape of a glyph rendered as element content.
|
|
71
|
+
if (new RegExp(`>\\s*${escapeRe(g)}\\s*<`).test(line)) {
|
|
72
|
+
offenders.push(`${file}:${i + 1} ${JSON.stringify(g)} in ${line.trim().slice(0, 70)}`)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
expect(
|
|
78
|
+
offenders,
|
|
79
|
+
`Glyph rendered directly instead of through the icon seam:\n ${offenders.join('\n ')}`,
|
|
80
|
+
).toEqual([])
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('only renders names the catalogue defines', () => {
|
|
84
|
+
const unknown: string[] = []
|
|
85
|
+
for (const file of CHROME) {
|
|
86
|
+
const src = read(file)
|
|
87
|
+
for (const m of src.matchAll(/\{@render ic(?:on)?\(\s*["']([a-zA-Z-]+)["']\s*\)\}/g)) {
|
|
88
|
+
const name = m[1] as GridIconName
|
|
89
|
+
if (!GRID_ICON_NAMES.includes(name)) unknown.push(`${file}: ${name}`)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
expect(unknown, `Icon names with no entry in grid-icons.ts:\n ${unknown.join('\n ')}`).toEqual(
|
|
93
|
+
[],
|
|
94
|
+
)
|
|
95
|
+
})
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
function escapeRe(s: string): string {
|
|
99
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
100
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -536,6 +536,12 @@ export {
|
|
|
536
536
|
buildChart,
|
|
537
537
|
formatChartValue,
|
|
538
538
|
rowsToChartSpec,
|
|
539
|
+
rowsToBoxSpec,
|
|
540
|
+
rowsToDirectSpec,
|
|
541
|
+
boxStats,
|
|
542
|
+
sliceChartWindow,
|
|
543
|
+
chartScales,
|
|
544
|
+
ordinalDateTicks,
|
|
539
545
|
niceScale,
|
|
540
546
|
niceLogScale,
|
|
541
547
|
linearTrend,
|
|
@@ -550,7 +556,13 @@ export {
|
|
|
550
556
|
type ChartSpec,
|
|
551
557
|
type ChartSeries,
|
|
552
558
|
type ChartGeometry,
|
|
559
|
+
type ChartScales,
|
|
553
560
|
type ChartBar,
|
|
561
|
+
type ChartCandle,
|
|
562
|
+
type ChartBox,
|
|
563
|
+
type ChartErrorBar,
|
|
564
|
+
type BoxStats,
|
|
565
|
+
type OhlcBar,
|
|
554
566
|
type ChartLine,
|
|
555
567
|
type ChartLinePoint,
|
|
556
568
|
type ChartPieSlice,
|
|
@@ -581,6 +593,7 @@ export {
|
|
|
581
593
|
downloadChartSvg,
|
|
582
594
|
chartToPngBlob,
|
|
583
595
|
downloadChartPng,
|
|
596
|
+
chartCsvExportable,
|
|
584
597
|
chartSpecToCsv,
|
|
585
598
|
downloadChartCsv,
|
|
586
599
|
type ChartExportOptions,
|
|
@@ -693,6 +706,12 @@ export {
|
|
|
693
706
|
resolveGridMessages,
|
|
694
707
|
type GridMessages,
|
|
695
708
|
} from './grid-messages'
|
|
709
|
+
export {
|
|
710
|
+
GRID_ICON_GLYPHS,
|
|
711
|
+
GRID_ICON_NAMES,
|
|
712
|
+
type GridIconName,
|
|
713
|
+
type GridIcons,
|
|
714
|
+
} from './grid-icons'
|
|
696
715
|
export {
|
|
697
716
|
insertGroupFooters,
|
|
698
717
|
buildAutoGroupColumns,
|