@svgrid/grid 3.0.5 → 3.0.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/README.md +28 -7
- package/dist/GridMenus.svelte +34 -4
- package/dist/SvGrid.controller.svelte.d.ts +23 -3
- package/dist/SvGrid.controller.svelte.js +235 -54
- package/dist/SvGrid.css +143 -7
- package/dist/SvGrid.helpers.js +2 -4
- package/dist/SvGrid.svelte +223 -38
- package/dist/SvGrid.types.d.ts +77 -4
- package/dist/SvGridDropdown.svelte +20 -0
- package/dist/SvGridDropdown.svelte.d.ts +12 -0
- package/dist/SvModal.svelte +22 -0
- package/dist/SvModal.svelte.d.ts +7 -0
- package/dist/a11y/dismissable.d.ts +10 -0
- package/dist/a11y/dismissable.js +24 -2
- package/dist/build-api.js +81 -31
- package/dist/cdn/{GridMenus-CID62rlI.js → GridMenus-BgUS-W3o.js} +151 -142
- package/dist/cdn/GridMenus-CcwU8I4t.js +644 -0
- package/dist/cdn/{SvChartMenu-FBSMINA6.js → SvChartMenu-BFSTpvB-.js} +1 -1
- package/dist/cdn/{SvChartMenu-Bl6PBbkT.js → SvChartMenu-BZ1Iwv0y.js} +1 -1
- package/dist/cdn/{SvDateTimePicker-DQwt4UAs.js → SvDateTimePicker-D9u_cOf8.js} +1 -1
- package/dist/cdn/{SvDateTimePicker-vNU6bZ-q.js → SvDateTimePicker-s_6IUpI7.js} +1 -1
- package/dist/cdn/{SvGridCellEditor-B1p-vCK5.js → SvGridCellEditor-9yz-SbdZ.js} +2 -2
- package/dist/cdn/{SvGridCellEditor-D_0q4xAu.js → SvGridCellEditor-D7-ql1Hd.js} +2 -2
- package/dist/cdn/{SvGridChart-C3EWAZaM.js → SvGridChart-DMUOFxOe.js} +4 -4
- package/dist/cdn/{SvGridChart-DzLkSwCH.js → SvGridChart-N7ik0oWy.js} +4 -4
- package/dist/cdn/{SvGridChartBuilder-CfII62sZ.js → SvGridChartBuilder-CB_Jnm2k.js} +4 -4
- package/dist/cdn/{SvGridChartBuilder-BE2T1ykB.js → SvGridChartBuilder-Ca7gF8ZK.js} +4 -4
- package/dist/cdn/{SvGridChartPanel-xf4sWVTo.js → SvGridChartPanel-9GOJRHP7.js} +5 -5
- package/dist/cdn/{SvGridChartPanel-CCX5_Wgd.js → SvGridChartPanel-Dl-FujN8.js} +5 -5
- package/dist/cdn/{SvGridChartView-CfuXmY5I.js → SvGridChartView-B1LFgoPP.js} +2 -2
- package/dist/cdn/{SvGridChartView-DYwabQWj.js → SvGridChartView-By3n7DGZ.js} +2 -2
- package/dist/cdn/{SvGridDropdown-D0VdjeR8.js → SvGridDropdown-BWeM3lMe.js} +46 -46
- package/dist/cdn/{SvGridDropdown-D13MtJ2j.js → SvGridDropdown-CJ6QqTxu.js} +67 -67
- package/dist/cdn/{SvModal-CIZWCcad.js → SvModal-ClxGmpAc.js} +187 -186
- package/dist/cdn/{SvModal-BAE-pjZX.js → SvModal-DhB_VMFR.js} +148 -147
- package/dist/cdn/{cell-formatting-C2Hf5gqd.js → cell-formatting-Mnv84zoE.js} +28 -6
- package/dist/cdn/{chart-Dz7SqMXH.js → chart-By7h2Guv.js} +1 -1
- package/dist/cdn/{chart-panel-messages-CUbf2R4i.js → chart-panel-messages-BC-tOgFY.js} +1 -1
- package/dist/cdn/{chart-panel-messages-CmNrMdsr.js → chart-panel-messages-DUaXr-sb.js} +1 -1
- package/dist/cdn/{chart-summary-BJW_tg_X.js → chart-summary-BDtrNEiA.js} +1 -1
- package/dist/cdn/{chart-trend-CaN9mDEV.js → chart-trend-C1aOAPBZ.js} +1 -1
- package/dist/cdn/{dismissable-DAHetSNk.js → dismissable-Brc4EZU1.js} +5 -1
- package/dist/cdn/{editor-registry-BhgE3S84.js → editor-registry-BVeHvlZR.js} +49 -50
- package/dist/cdn/{export-format-CUDLy2yn.js → export-format-y1wdyCcv.js} +93 -97
- package/dist/cdn/{row-resize-niQCp040.js → row-resize-DtOS1KG6.js} +41 -32
- package/dist/cdn/{server-block-cache-CGoWz-87.js → server-block-cache-DIqb3VZn.js} +4 -0
- package/dist/cdn/{src-jcFMkBnN.js → src-25JmJ_7g.js} +4768 -4149
- package/dist/cdn/{src-Bklofsad.js → src-kE9B98r6.js} +4796 -4177
- package/dist/cdn/svgrid.js +18 -18
- package/dist/cdn/svgrid.svelte-external.js +18 -18
- package/dist/cdn/{validate-C7Rn_Bns.js → validate-AZoD8BoR.js} +6 -2
- package/dist/cell-formatting.d.ts +9 -0
- package/dist/cell-formatting.js +52 -1
- package/dist/cell-render.js +25 -6
- package/dist/clipboard.d.ts +2 -2
- package/dist/clipboard.js +72 -23
- package/dist/columns.js +7 -2
- package/dist/conditional-formatting.d.ts +2 -0
- package/dist/conditional-formatting.js +2 -0
- package/dist/core.d.ts +22 -6
- package/dist/core.js +478 -40
- package/dist/editing.js +24 -0
- package/dist/export-format.d.ts +8 -0
- package/dist/export-format.js +23 -16
- package/dist/grid-messages.d.ts +4 -0
- package/dist/grid-messages.js +4 -0
- package/dist/headless.d.ts +5 -0
- package/dist/headless.js +12 -0
- package/dist/history.d.ts +21 -0
- package/dist/history.js +34 -4
- package/dist/index.d.ts +1 -1
- package/dist/keyboard-handlers.js +24 -1
- package/dist/keyboard.js +4 -0
- package/dist/row-drag-touch.d.ts +1 -1
- package/dist/row-drag.js +35 -1
- package/dist/row-model.d.ts +6 -0
- package/dist/row-resize.js +22 -1
- package/dist/selection.js +1 -1
- package/dist/server-block-cache.d.ts +5 -0
- package/dist/server-block-cache.js +8 -0
- package/dist/server-data-source.d.ts +35 -3
- package/dist/server-data-source.js +11 -2
- package/dist/server.d.ts +1 -1
- package/dist/sparkline.js +7 -5
- package/dist/svgrid-wrapper.types.d.ts +33 -0
- package/dist/validate.js +9 -1
- package/dist/virtualization/column-virtualizer.d.ts +1 -0
- package/dist/virtualization/svelte-virtualizer.svelte.d.ts +1 -0
- package/dist/virtualization/svelte-virtualizer.svelte.js +1 -0
- package/dist/virtualization/virtualizer.d.ts +3 -0
- package/dist/virtualization/virtualizer.js +14 -1
- package/package.json +6 -1
- package/src/GridMenus.svelte +34 -4
- package/src/SvGrid.controller.svelte.ts +236 -57
- package/src/SvGrid.css +143 -7
- package/src/SvGrid.helpers.ts +2 -4
- package/src/SvGrid.svelte +223 -38
- package/src/SvGrid.types.ts +77 -4
- package/src/SvGridDropdown.svelte +20 -0
- package/src/SvModal.svelte +22 -0
- package/src/SvModal.test.ts +30 -1
- package/src/a11y/dismissable.test.ts +10 -1
- package/src/a11y/dismissable.ts +22 -2
- package/src/build-api.ts +85 -27
- package/src/cell-formatting.test.ts +17 -0
- package/src/cell-formatting.ts +49 -1
- package/src/cell-render.test.ts +24 -2
- package/src/cell-render.ts +24 -8
- package/src/clipboard.test.ts +107 -0
- package/src/clipboard.ts +62 -23
- package/src/columns.test.ts +4 -4
- package/src/columns.ts +7 -2
- package/src/conditional-formatting.test.ts +8 -0
- package/src/conditional-formatting.ts +3 -0
- package/src/core.sort.test.ts +86 -0
- package/src/core.tick-repair.test.ts +287 -0
- package/src/core.ts +478 -40
- package/src/editing.test.ts +52 -0
- package/src/editing.ts +25 -0
- package/src/export-dates.test.ts +114 -0
- package/src/export-format.ts +26 -14
- package/src/grid-messages.test.ts +7 -0
- package/src/grid-messages.ts +10 -0
- package/src/headless.ts +24 -0
- package/src/history.test.ts +49 -1
- package/src/history.ts +48 -2
- package/src/index.ts +2 -0
- package/src/keyboard-handlers.coverage.test.ts +65 -0
- package/src/keyboard-handlers.ts +26 -1
- package/src/keyboard.ts +3 -0
- package/src/row-drag-touch.ts +2 -2
- package/src/row-drag.test.ts +51 -0
- package/src/row-drag.ts +35 -2
- package/src/row-model.ts +6 -0
- package/src/row-resize.ts +24 -1
- package/src/selection.ts +1 -1
- package/src/server-block-cache.test.ts +27 -0
- package/src/server-block-cache.ts +13 -0
- package/src/server-data-source.infinite.test.ts +62 -0
- package/src/server-data-source.test.ts +19 -0
- package/src/server-data-source.ts +48 -4
- package/src/server.ts +2 -0
- package/src/sparkline.test.ts +7 -0
- package/src/sparkline.ts +6 -4
- package/src/svgrid-wrapper.types.ts +32 -0
- package/src/svgrid.api.test.ts +21 -0
- package/src/svgrid.charting.test.ts +12 -4
- package/src/svgrid.context-menu.test.ts +12 -0
- package/src/svgrid.data-push.svelte.test.ts +94 -0
- package/src/svgrid.detail-rows.svelte.test.ts +187 -0
- package/src/svgrid.filter-menu-scroll.test.ts +9 -0
- package/src/svgrid.gantt-seam.test.ts +1 -1
- package/src/svgrid.live-update-paths.svelte.test.ts +186 -0
- package/src/svgrid.menu-scroll-close.test.ts +10 -0
- package/src/svgrid.row-model-seam.svelte.test.ts +16 -0
- package/src/svgrid.row-pinning.test.ts +15 -1
- package/src/svgrid.selection-bar-seam.test.ts +1 -1
- package/src/svgrid.sticky-groups.svelte.test.ts +145 -0
- package/src/svgrid.upsell-license.test.ts +1 -1
- package/src/test-setup.ts +12 -0
- package/src/transaction.test.ts +58 -0
- package/src/validate.ts +7 -1
- package/src/virtualization/svelte-virtualizer.svelte.ts +1 -0
- package/src/virtualization/virtualizer.test.ts +44 -0
- package/src/virtualization/virtualizer.ts +13 -1
- package/dist/cdn/GridMenus-DLL-qZeu.js +0 -635
|
@@ -754,7 +754,15 @@ describe('SvGrid built-in charting', () => {
|
|
|
754
754
|
})
|
|
755
755
|
|
|
756
756
|
|
|
757
|
-
|
|
757
|
+
// The builder mounts a live SvGridChart per type, thirty of them, in one
|
|
758
|
+
// synchronous flush. Alone that is well under a second; under `--coverage`
|
|
759
|
+
// (instrumented chart code, every file of the suite in parallel workers)
|
|
760
|
+
// it took 22 s on a loaded machine and the four tests that open the
|
|
761
|
+
// builder timed out at 20 s, the last of them then reading a builder a
|
|
762
|
+
// timed-out neighbour left behind. The budget is for instrumentation,
|
|
763
|
+
// not for the product: `pnpm test` runs with coverage and is the gate.
|
|
764
|
+
const BUILDER_TIMEOUT = 90_000
|
|
765
|
+
it('the builder opens from the panel with a live thumbnail per type, and a card switches the chart', { timeout: BUILDER_TIMEOUT }, async () => {
|
|
758
766
|
const { api, target, destroy } = await mountGrid({ charting: { defaultOpen: true } })
|
|
759
767
|
try {
|
|
760
768
|
await tick()
|
|
@@ -805,7 +813,7 @@ describe('SvGrid built-in charting', () => {
|
|
|
805
813
|
}
|
|
806
814
|
})
|
|
807
815
|
|
|
808
|
-
it('the Format tab writes series labels, crosshair pills, a compact rule, a stack group and a style through configureChart', { timeout:
|
|
816
|
+
it('the Format tab writes series labels, crosshair pills, a compact rule, a stack group and a style through configureChart', { timeout: BUILDER_TIMEOUT }, async () => {
|
|
809
817
|
const { api, target, destroy } = await mountGrid({ charting: { defaultOpen: true } })
|
|
810
818
|
try {
|
|
811
819
|
await tick()
|
|
@@ -848,7 +856,7 @@ describe('SvGrid built-in charting', () => {
|
|
|
848
856
|
}
|
|
849
857
|
})
|
|
850
858
|
|
|
851
|
-
it('the builder Data tab renders the panel pickers and both write the same tab', { timeout:
|
|
859
|
+
it('the builder Data tab renders the panel pickers and both write the same tab', { timeout: BUILDER_TIMEOUT }, async () => {
|
|
852
860
|
const { api, target, destroy } = await mountGrid({ charting: { defaultOpen: true } })
|
|
853
861
|
try {
|
|
854
862
|
await tick()
|
|
@@ -883,7 +891,7 @@ describe('SvGrid built-in charting', () => {
|
|
|
883
891
|
}
|
|
884
892
|
})
|
|
885
893
|
|
|
886
|
-
it('localization.text relabels the panel and the builder; unset keys stay English', { timeout:
|
|
894
|
+
it('localization.text relabels the panel and the builder; unset keys stay English', { timeout: BUILDER_TIMEOUT }, async () => {
|
|
887
895
|
const { target, destroy } = await mountGrid({
|
|
888
896
|
charting: { defaultOpen: true },
|
|
889
897
|
localization: { text: { chartPanelTitle: 'Diagramm', chartGroupBy: 'Gruppieren nach', chartTypeBar: 'Balken', chartAdd: 'Diagramm hinzufügen', chartBuilderTitle: 'Diagramm-Editor', chartBuilderTabData: 'Daten', noRows: 'Keine Zeilen' } },
|
|
@@ -137,6 +137,18 @@ describe('SvGrid context menu', () => {
|
|
|
137
137
|
destroy()
|
|
138
138
|
})
|
|
139
139
|
|
|
140
|
+
it('closes on Escape, so a keyboard user is not trapped under it', async () => {
|
|
141
|
+
const { target, destroy } = await mountGrid(true)
|
|
142
|
+
await tick()
|
|
143
|
+
await openContextMenu(target)
|
|
144
|
+
expect(target.querySelector('.sv-grid-context-menu')).not.toBeNull()
|
|
145
|
+
window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }))
|
|
146
|
+
await vi.waitFor(() => {
|
|
147
|
+
expect(target.querySelector('.sv-grid-context-menu')).toBeNull()
|
|
148
|
+
})
|
|
149
|
+
destroy()
|
|
150
|
+
})
|
|
151
|
+
|
|
140
152
|
it('does not open when contextMenu is omitted (native menu shows)', async () => {
|
|
141
153
|
const { target, destroy } = await mountGrid(undefined)
|
|
142
154
|
await tick()
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pushing onto a `$state` array passed as `data`.
|
|
3
|
+
*
|
|
4
|
+
* docs/getting-started/3-data-and-columns.md teaches exactly this: declare
|
|
5
|
+
* `let rows = $state([...])`, hand it to `data`, and an "Add row" button that
|
|
6
|
+
* does nothing but `rows.push(...)`. The grid is supposed to follow.
|
|
7
|
+
*
|
|
8
|
+
* `svgrid.live-update-paths.svelte.test.ts` covers the two MUTATION paths
|
|
9
|
+
* (field write, element replacement) against a sorted grid. This file covers
|
|
10
|
+
* the LENGTH path on the plainest possible grid, because a row appearing is a
|
|
11
|
+
* different question from a cell changing: nothing in the template is reading
|
|
12
|
+
* the new row yet, so the row model has to notice the array grew.
|
|
13
|
+
*
|
|
14
|
+
* Lives in a `.svelte.test.ts` so `$state` is available.
|
|
15
|
+
*/
|
|
16
|
+
import { describe, expect, it } from 'vitest'
|
|
17
|
+
import { mount, unmount, flushSync } from 'svelte'
|
|
18
|
+
import SvGrid from './SvGrid.svelte'
|
|
19
|
+
|
|
20
|
+
type Staff = { id: string; firstName: string; age: number }
|
|
21
|
+
|
|
22
|
+
const staffColumns = [
|
|
23
|
+
{ field: 'firstName', header: 'First name', width: 180 },
|
|
24
|
+
{ field: 'age', header: 'Age', width: 90 },
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
/** The props object is the `$state` proxy itself; a spread would snapshot it. */
|
|
28
|
+
function pushProps(rows: Staff[]) {
|
|
29
|
+
const props = $state({
|
|
30
|
+
data: rows,
|
|
31
|
+
columns: staffColumns,
|
|
32
|
+
containerHeight: 400,
|
|
33
|
+
virtualization: false,
|
|
34
|
+
})
|
|
35
|
+
return props
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function mountGrid(props: ReturnType<typeof pushProps>) {
|
|
39
|
+
const target = document.createElement('div')
|
|
40
|
+
document.body.appendChild(target)
|
|
41
|
+
const app = mount(SvGrid, { target, props: props as never })
|
|
42
|
+
flushSync()
|
|
43
|
+
const names = () =>
|
|
44
|
+
Array.from(
|
|
45
|
+
target.querySelectorAll('tbody.sv-grid-body tr.sv-grid-row:not(.sv-grid-row-spacer)'),
|
|
46
|
+
).map((tr) => tr.querySelector('.sv-grid-cell:not(.sv-grid-selection-cell)')?.textContent?.trim())
|
|
47
|
+
return {
|
|
48
|
+
names,
|
|
49
|
+
done() {
|
|
50
|
+
unmount(app)
|
|
51
|
+
target.remove()
|
|
52
|
+
},
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
describe('data array length changes', () => {
|
|
57
|
+
it('renders the rows it was given', () => {
|
|
58
|
+
const rows = $state<Staff[]>([
|
|
59
|
+
{ id: '1', firstName: 'Ada', age: 36 },
|
|
60
|
+
{ id: '2', firstName: 'Linus', age: 54 },
|
|
61
|
+
])
|
|
62
|
+
const g = mountGrid(pushProps(rows))
|
|
63
|
+
expect(g.names()).toEqual(['Ada', 'Linus'])
|
|
64
|
+
g.done()
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('push onto the $state array adds the row (the docs "Add row" button)', () => {
|
|
68
|
+
const rows = $state<Staff[]>([
|
|
69
|
+
{ id: '1', firstName: 'Ada', age: 36 },
|
|
70
|
+
{ id: '2', firstName: 'Linus', age: 54 },
|
|
71
|
+
])
|
|
72
|
+
const g = mountGrid(pushProps(rows))
|
|
73
|
+
|
|
74
|
+
rows.push({ id: '3', firstName: 'New', age: 0 })
|
|
75
|
+
flushSync()
|
|
76
|
+
|
|
77
|
+
expect(g.names()).toEqual(['Ada', 'Linus', 'New'])
|
|
78
|
+
g.done()
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('splice off the $state array removes the row', () => {
|
|
82
|
+
const rows = $state<Staff[]>([
|
|
83
|
+
{ id: '1', firstName: 'Ada', age: 36 },
|
|
84
|
+
{ id: '2', firstName: 'Linus', age: 54 },
|
|
85
|
+
])
|
|
86
|
+
const g = mountGrid(pushProps(rows))
|
|
87
|
+
|
|
88
|
+
rows.splice(0, 1)
|
|
89
|
+
flushSync()
|
|
90
|
+
|
|
91
|
+
expect(g.names()).toEqual(['Linus'])
|
|
92
|
+
g.done()
|
|
93
|
+
})
|
|
94
|
+
})
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detail rows under virtualization: `detailRowHeight` sizes a detail row for
|
|
3
|
+
* the virtualizer, so a master-detail grid no longer needs
|
|
4
|
+
* `virtualization={false}`.
|
|
5
|
+
*/
|
|
6
|
+
import { describe, expect, it } from 'vitest'
|
|
7
|
+
import { mount, unmount } from 'svelte'
|
|
8
|
+
import SvGrid from './SvGrid.svelte'
|
|
9
|
+
import { createCoreRowModel, tableFeatures } from './index'
|
|
10
|
+
import type { ColumnDef, SvGridApi } from './index'
|
|
11
|
+
|
|
12
|
+
type Row = { id: string; name: string; kind: 'data' | 'detail' }
|
|
13
|
+
const features = tableFeatures({})
|
|
14
|
+
const cols: ColumnDef<typeof features, Row>[] = [{ field: 'name', header: 'Name', width: 200 }]
|
|
15
|
+
const tick = () => new Promise<void>((r) => setTimeout(r))
|
|
16
|
+
|
|
17
|
+
function rows(): Row[] {
|
|
18
|
+
const out: Row[] = []
|
|
19
|
+
for (let i = 0; i < 12; i += 1) {
|
|
20
|
+
out.push({ id: `r${i}`, name: `row ${i}`, kind: 'data' })
|
|
21
|
+
if (i === 1 || i === 4) out.push({ id: `d${i}`, name: `detail of ${i}`, kind: 'detail' })
|
|
22
|
+
}
|
|
23
|
+
return out
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function mountGrid(extra: Record<string, unknown>) {
|
|
27
|
+
return new Promise<{ api: SvGridApi<typeof features, Row>; target: HTMLElement; destroy: () => void }>((res, rej) => {
|
|
28
|
+
const target = document.createElement('div')
|
|
29
|
+
document.body.appendChild(target)
|
|
30
|
+
const app = mount(SvGrid, {
|
|
31
|
+
target,
|
|
32
|
+
props: {
|
|
33
|
+
data: rows(),
|
|
34
|
+
columns: cols,
|
|
35
|
+
features,
|
|
36
|
+
_rowModels: { coreRowModel: createCoreRowModel() },
|
|
37
|
+
getRowId: (r: Row) => r.id,
|
|
38
|
+
rowHeight: 30,
|
|
39
|
+
containerHeight: 300,
|
|
40
|
+
virtualization: true,
|
|
41
|
+
isDetailRow: (r: Row) => r.kind === 'detail',
|
|
42
|
+
onApiReady(api: SvGridApi<typeof features, Row>) {
|
|
43
|
+
res({ api, target, destroy: () => { unmount(app); target.remove() } })
|
|
44
|
+
},
|
|
45
|
+
...extra,
|
|
46
|
+
} as any,
|
|
47
|
+
})
|
|
48
|
+
queueMicrotask(() => { if (!target.querySelector('[role="grid"]')) rej(new Error('no grid')) })
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
describe('detailRowHeight', () => {
|
|
53
|
+
it('sizes detail rows for the virtualizer and leaves the others at rowHeight', async () => {
|
|
54
|
+
const { api, target, destroy } = await mountGrid({ detailRowHeight: 120 })
|
|
55
|
+
try {
|
|
56
|
+
await tick()
|
|
57
|
+
const detail = target.querySelector<HTMLElement>('tr.sv-grid-detail-row')!
|
|
58
|
+
expect(detail.classList.contains('sv-grid-detail-row-sized')).toBe(true)
|
|
59
|
+
expect(detail.style.height).toBe('120px')
|
|
60
|
+
// Index 2 is the detail under row 1; 3 is a plain row.
|
|
61
|
+
expect(api.getRowHeight(2)).toBe(120)
|
|
62
|
+
expect(api.getRowHeight(3)).toBe(30)
|
|
63
|
+
} finally {
|
|
64
|
+
destroy()
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('takes a function of the row', async () => {
|
|
69
|
+
const { api, destroy } = await mountGrid({ detailRowHeight: (r: Row) => (r.id === 'd1' ? 80 : 200) })
|
|
70
|
+
try {
|
|
71
|
+
await tick()
|
|
72
|
+
expect(api.getRowHeight(2)).toBe(80)
|
|
73
|
+
expect(api.getRowHeight(6)).toBe(200)
|
|
74
|
+
} finally {
|
|
75
|
+
destroy()
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('without one a detail row is auto height, as before', async () => {
|
|
80
|
+
const { target, destroy } = await mountGrid({ virtualization: false })
|
|
81
|
+
try {
|
|
82
|
+
await tick()
|
|
83
|
+
const detail = target.querySelector<HTMLElement>('tr.sv-grid-detail-row')!
|
|
84
|
+
expect(detail.classList.contains('sv-grid-detail-row-sized')).toBe(false)
|
|
85
|
+
expect(detail.style.height).toBe('')
|
|
86
|
+
} finally {
|
|
87
|
+
destroy()
|
|
88
|
+
}
|
|
89
|
+
})
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
describe('showDetailToggle', () => {
|
|
93
|
+
it('is a row-header column: sticky, no menu, no resize handle, a chevron per data row and none on a detail row', async () => {
|
|
94
|
+
const open = new Set<string>(['r1', 'r4'])
|
|
95
|
+
const toggled: string[] = []
|
|
96
|
+
const { target, destroy } = await mountGrid({
|
|
97
|
+
showDetailToggle: true,
|
|
98
|
+
columnResize: true,
|
|
99
|
+
columnMenu: true,
|
|
100
|
+
onDetailToggle: (r: Row) => toggled.push(r.id),
|
|
101
|
+
isDetailOpen: (r: Row) => open.has(r.id),
|
|
102
|
+
detailRowHeight: 60,
|
|
103
|
+
})
|
|
104
|
+
try {
|
|
105
|
+
await tick()
|
|
106
|
+
const head = target.querySelector<HTMLElement>('thead th.sv-grid-detail-toggle-column')!
|
|
107
|
+
expect(head).not.toBeNull()
|
|
108
|
+
expect(head.getAttribute('aria-hidden')).toBe('true')
|
|
109
|
+
expect(head.querySelector('.sv-grid-col-menu-btn, .sv-grid-column-resizer, [data-resize-handle]')).toBeNull()
|
|
110
|
+
expect(head.style.position || getComputedStyle(head).position).not.toBe('')
|
|
111
|
+
const first = target.querySelector<HTMLElement>('tbody tr.sv-grid-row:not(.sv-grid-detail-row)')!
|
|
112
|
+
const cell = first.querySelector<HTMLElement>('td.sv-grid-detail-toggle-cell')!
|
|
113
|
+
expect(cell).not.toBeNull()
|
|
114
|
+
// The gutter is not a data cell: nothing the keyboard or the selection can land on.
|
|
115
|
+
expect(cell.hasAttribute('data-svgrid-col')).toBe(false)
|
|
116
|
+
const chev = cell.querySelector<HTMLButtonElement>('button.sv-grid-detail-toggle')!
|
|
117
|
+
expect(chev.getAttribute('aria-expanded')).toBe('false')
|
|
118
|
+
expect(chev.getAttribute('aria-label')).toBe('Open details')
|
|
119
|
+
chev.click()
|
|
120
|
+
expect(toggled).toEqual(['r0'])
|
|
121
|
+
// Row 1 is open: its chevron says so; the detail row under it has a blank gutter cell.
|
|
122
|
+
const rowsAll = [...target.querySelectorAll<HTMLElement>('tbody tr.sv-grid-row')]
|
|
123
|
+
const r1 = rowsAll.find((tr) => tr.textContent?.includes('row 1'))!
|
|
124
|
+
expect(r1.querySelector('button.sv-grid-detail-toggle')!.getAttribute('aria-expanded')).toBe('true')
|
|
125
|
+
expect(r1.querySelector('button.sv-grid-detail-toggle')!.getAttribute('aria-label')).toBe('Close details')
|
|
126
|
+
const detail = target.querySelector<HTMLElement>('tr.sv-grid-detail-row')!
|
|
127
|
+
expect(detail.querySelector('button.sv-grid-detail-toggle')).toBeNull()
|
|
128
|
+
// The full-width cell spans the gutter too.
|
|
129
|
+
expect(detail.querySelector<HTMLTableCellElement>('td.sv-grid-detail-cell')!.colSpan).toBe(first.children.length)
|
|
130
|
+
} finally {
|
|
131
|
+
destroy()
|
|
132
|
+
}
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('leaves the chevron out where hasDetail says no, and sticks after the row numbers and the checkbox', async () => {
|
|
136
|
+
const { target, destroy } = await mountGrid({
|
|
137
|
+
showDetailToggle: true,
|
|
138
|
+
showRowNumbers: true,
|
|
139
|
+
showRowSelection: true,
|
|
140
|
+
onDetailToggle: () => {},
|
|
141
|
+
hasDetail: (r: Row) => r.id !== 'r0',
|
|
142
|
+
})
|
|
143
|
+
try {
|
|
144
|
+
await tick()
|
|
145
|
+
const rowsAll = [...target.querySelectorAll<HTMLElement>('tbody tr.sv-grid-row:not(.sv-grid-detail-row)')]
|
|
146
|
+
expect(rowsAll[0]!.querySelector('td.sv-grid-detail-toggle-cell button')).toBeNull()
|
|
147
|
+
expect(rowsAll[1]!.querySelector('td.sv-grid-detail-toggle-cell button')).not.toBeNull()
|
|
148
|
+
// Row number (56) + selection (44) sit before it.
|
|
149
|
+
const cell = rowsAll[1]!.querySelector<HTMLElement>('td.sv-grid-detail-toggle-cell')!
|
|
150
|
+
expect(cell.style.left).toBe('100px')
|
|
151
|
+
expect(cell.style.width).toBe('36px')
|
|
152
|
+
// The system cells come in that order, then the data.
|
|
153
|
+
const classes = [...rowsAll[1]!.children].map((td) => td.className.split(' ')[1])
|
|
154
|
+
expect(classes.slice(0, 3)).toEqual(['sv-grid-row-number-cell', 'sv-grid-selection-cell', 'sv-grid-detail-toggle-cell'])
|
|
155
|
+
} finally {
|
|
156
|
+
destroy()
|
|
157
|
+
}
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
it('on a server row model the seam decides: groups get no chevron, a leaf reads detailOpen and calls toggleDetail', async () => {
|
|
161
|
+
const calls: string[] = []
|
|
162
|
+
const { target, destroy } = await mountGrid({
|
|
163
|
+
showDetailToggle: true,
|
|
164
|
+
serverGroup: {
|
|
165
|
+
isGroup: (r: Row) => r.id === 'r0',
|
|
166
|
+
level: () => 0,
|
|
167
|
+
onToggle: () => {},
|
|
168
|
+
toggleDetail: (r: Row) => calls.push(r.id),
|
|
169
|
+
detailOpen: (r: Row) => r.id === 'r1',
|
|
170
|
+
hasDetail: (r: Row) => r.kind === 'data',
|
|
171
|
+
},
|
|
172
|
+
})
|
|
173
|
+
try {
|
|
174
|
+
await tick()
|
|
175
|
+
const rowsAll = [...target.querySelectorAll<HTMLElement>('tbody tr.sv-grid-row')]
|
|
176
|
+
const r0 = rowsAll.find((tr) => tr.textContent?.includes('row 0'))!
|
|
177
|
+
const r1 = rowsAll.find((tr) => tr.textContent?.includes('row 1'))!
|
|
178
|
+
expect(r0.querySelector('button.sv-grid-detail-toggle')).toBeNull()
|
|
179
|
+
const chev = r1.querySelector<HTMLButtonElement>('button.sv-grid-detail-toggle')!
|
|
180
|
+
expect(chev.getAttribute('aria-expanded')).toBe('true')
|
|
181
|
+
chev.click()
|
|
182
|
+
expect(calls).toEqual(['r1'])
|
|
183
|
+
} finally {
|
|
184
|
+
destroy()
|
|
185
|
+
}
|
|
186
|
+
})
|
|
187
|
+
})
|
|
@@ -60,6 +60,14 @@ function mountGrid() {
|
|
|
60
60
|
})
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
/**
|
|
64
|
+
* The close-on-scroll listener arms one animation frame after a popover
|
|
65
|
+
* mounts, so a scroll queued before the opening click cannot dismiss what
|
|
66
|
+
* that click opened. Every open helper waits that frame out before a case
|
|
67
|
+
* dispatches its scroll.
|
|
68
|
+
*/
|
|
69
|
+
const nextFrame = () => new Promise<void>((resolve) => requestAnimationFrame(() => resolve()))
|
|
70
|
+
|
|
63
71
|
async function openFilterMenu(target: HTMLElement) {
|
|
64
72
|
const btn = target.querySelector('.sv-grid-col-filter-btn') as HTMLButtonElement
|
|
65
73
|
expect(btn).not.toBeNull()
|
|
@@ -74,6 +82,7 @@ async function openFilterMenu(target: HTMLElement) {
|
|
|
74
82
|
expect(menu).not.toBeNull()
|
|
75
83
|
expect(menu!.querySelector('.sv-listbox')).not.toBeNull()
|
|
76
84
|
})
|
|
85
|
+
await nextFrame()
|
|
77
86
|
return target.querySelector('.sv-grid-filter-menu') as HTMLElement
|
|
78
87
|
}
|
|
79
88
|
|
|
@@ -104,7 +104,7 @@ describe('gantt - without the Enterprise renderer', () => {
|
|
|
104
104
|
it('says a license is needed, and links to it', async () => {
|
|
105
105
|
// Naming the package is what unblocks a developer trying it; saying a key
|
|
106
106
|
// is needed is what they have to know before shipping. Enterprise is
|
|
107
|
-
//
|
|
107
|
+
// runs without a key, so the wording must not imply the feature is inert without
|
|
108
108
|
// one - it says "watermark", which is what actually happens.
|
|
109
109
|
const { target, destroy } = await mountGrid()
|
|
110
110
|
await tick()
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two ways a live feed can change a row, and what each one reaches.
|
|
3
|
+
*
|
|
4
|
+
* A `$state` array passed as `data` has two update paths for a row's VALUES,
|
|
5
|
+
* and they are not interchangeable, which is the whole design problem of a
|
|
6
|
+
* ticking grid. (Changing the row COUNT is a third path, tracked on its own;
|
|
7
|
+
* it lives in `svgrid.data-push.svelte.test.ts`.)
|
|
8
|
+
*
|
|
9
|
+
* 1. Mutate a field through the proxy (`rows[i].price = x`). The visible
|
|
10
|
+
* cell re-renders because its template reads the field through the
|
|
11
|
+
* proxy. The row model does NOT run - it caches on the array reference
|
|
12
|
+
* and the reference did not change - so an active sort or filter is
|
|
13
|
+
* stale for that row until something replaces the array.
|
|
14
|
+
* 2. Replace the array (`rows = next`, or `api.applyTransaction`). The row
|
|
15
|
+
* model rebuilds and re-runs the pipeline over every row; sort and filter
|
|
16
|
+
* are current again.
|
|
17
|
+
*
|
|
18
|
+
* Replacing one ELEMENT through the proxy (`rows[i] = {...}`) is the case a
|
|
19
|
+
* React-trained hand reaches for, and it is the one that does neither: the
|
|
20
|
+
* base row keeps its old `original`, so the cell keeps its old value.
|
|
21
|
+
*
|
|
22
|
+
* These are the facts the real-time docs and the blog describe; if one of
|
|
23
|
+
* them changes, this file is where it shows up first.
|
|
24
|
+
*
|
|
25
|
+
* Lives in a `.svelte.test.ts` so `$state` is available.
|
|
26
|
+
*/
|
|
27
|
+
import { describe, expect, it } from 'vitest'
|
|
28
|
+
import { mount, unmount, flushSync } from 'svelte'
|
|
29
|
+
import SvGrid from './SvGrid.svelte'
|
|
30
|
+
import {
|
|
31
|
+
createCoreRowModel,
|
|
32
|
+
createFilteredRowModel,
|
|
33
|
+
createSortedRowModel,
|
|
34
|
+
rowSortingFeature,
|
|
35
|
+
sortFns,
|
|
36
|
+
tableFeatures,
|
|
37
|
+
} from './index'
|
|
38
|
+
import type { SvGridApi } from './index'
|
|
39
|
+
|
|
40
|
+
type Quote = { id: string; symbol: string; price: number }
|
|
41
|
+
|
|
42
|
+
const features = tableFeatures({ rowSortingFeature })
|
|
43
|
+
const columns = [
|
|
44
|
+
{ field: 'symbol', header: 'Symbol', width: 120 },
|
|
45
|
+
{ field: 'price', header: 'Price', width: 120, editorType: 'number' as const },
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
function seed(): Quote[] {
|
|
49
|
+
return [
|
|
50
|
+
{ id: 'a', symbol: 'AAA', price: 10 },
|
|
51
|
+
{ id: 'b', symbol: 'BBB', price: 20 },
|
|
52
|
+
{ id: 'c', symbol: 'CCC', price: 30 },
|
|
53
|
+
]
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The props object must be the `$state` proxy itself, not a copy of it: a
|
|
58
|
+
* spread would snapshot `data` at mount and the reassignment tests would be
|
|
59
|
+
* testing nothing.
|
|
60
|
+
*/
|
|
61
|
+
function liveProps(data: Quote[]) {
|
|
62
|
+
const props = $state({
|
|
63
|
+
data,
|
|
64
|
+
columns,
|
|
65
|
+
features,
|
|
66
|
+
_rowModels: {
|
|
67
|
+
coreRowModel: createCoreRowModel(),
|
|
68
|
+
filteredRowModel: createFilteredRowModel(),
|
|
69
|
+
sortedRowModel: createSortedRowModel(sortFns),
|
|
70
|
+
},
|
|
71
|
+
getRowId: (r: Quote) => r.id,
|
|
72
|
+
// Price descending: CCC, BBB, AAA. A tick that lifts AAA above CCC is
|
|
73
|
+
// the observable difference between "cell updated" and "re-sorted".
|
|
74
|
+
initialSorting: [{ id: 'price', desc: true }],
|
|
75
|
+
containerHeight: 400,
|
|
76
|
+
virtualization: false,
|
|
77
|
+
onApiReady: undefined as ((next: SvGridApi<typeof features, Quote>) => void) | undefined,
|
|
78
|
+
})
|
|
79
|
+
return props
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function mountGrid(props: ReturnType<typeof liveProps>) {
|
|
83
|
+
const target = document.createElement('div')
|
|
84
|
+
document.body.appendChild(target)
|
|
85
|
+
let api: SvGridApi<typeof features, Quote> | null = null
|
|
86
|
+
props.onApiReady = (next) => (api = next)
|
|
87
|
+
const app = mount(SvGrid, { target, props: props as never })
|
|
88
|
+
flushSync()
|
|
89
|
+
const dataRows = () =>
|
|
90
|
+
Array.from(target.querySelectorAll('tbody.sv-grid-body tr.sv-grid-row:not(.sv-grid-row-spacer)'))
|
|
91
|
+
const cellsOf = (tr: Element) =>
|
|
92
|
+
Array.from(tr.querySelectorAll('.sv-grid-cell:not(.sv-grid-selection-cell)')).map((td) =>
|
|
93
|
+
td.textContent?.trim(),
|
|
94
|
+
)
|
|
95
|
+
const symbolsInOrder = () => dataRows().map((tr) => cellsOf(tr)[0])
|
|
96
|
+
const priceOf = (symbol: string) => {
|
|
97
|
+
const tr = dataRows().find((row) => cellsOf(row)[0] === symbol)
|
|
98
|
+
return tr ? cellsOf(tr)[1] : undefined
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
app,
|
|
102
|
+
target,
|
|
103
|
+
api: () => api!,
|
|
104
|
+
symbolsInOrder,
|
|
105
|
+
priceOf,
|
|
106
|
+
done() {
|
|
107
|
+
unmount(app)
|
|
108
|
+
target.remove()
|
|
109
|
+
},
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
describe('live update paths', () => {
|
|
114
|
+
it('starts sorted by price descending', () => {
|
|
115
|
+
const props = liveProps(seed())
|
|
116
|
+
const g = mountGrid(props)
|
|
117
|
+
expect(g.symbolsInOrder()).toEqual(['CCC', 'BBB', 'AAA'])
|
|
118
|
+
g.done()
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it('proxy field write: the cell updates, the sort does not re-run', () => {
|
|
122
|
+
const props = liveProps(seed())
|
|
123
|
+
const g = mountGrid(props)
|
|
124
|
+
|
|
125
|
+
props.data[0]!.price = 99 // AAA now the highest price
|
|
126
|
+
flushSync()
|
|
127
|
+
|
|
128
|
+
expect(g.priceOf('AAA')).toBe('99')
|
|
129
|
+
// Still where it was: the row model never saw the write.
|
|
130
|
+
expect(g.symbolsInOrder()).toEqual(['CCC', 'BBB', 'AAA'])
|
|
131
|
+
g.done()
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('proxy element replacement: neither the cell nor the sort updates', () => {
|
|
135
|
+
const props = liveProps(seed())
|
|
136
|
+
const g = mountGrid(props)
|
|
137
|
+
|
|
138
|
+
props.data[0] = { ...props.data[0]!, price: 99 }
|
|
139
|
+
flushSync()
|
|
140
|
+
|
|
141
|
+
// The base row still points at the object it was built from.
|
|
142
|
+
expect(g.priceOf('AAA')).toBe('10')
|
|
143
|
+
expect(g.symbolsInOrder()).toEqual(['CCC', 'BBB', 'AAA'])
|
|
144
|
+
g.done()
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it('array reassignment: the cell updates and the sort re-runs', () => {
|
|
148
|
+
const props = liveProps(seed())
|
|
149
|
+
const g = mountGrid(props)
|
|
150
|
+
|
|
151
|
+
props.data = props.data.map((r) => (r.id === 'a' ? { ...r, price: 99 } : r))
|
|
152
|
+
flushSync()
|
|
153
|
+
|
|
154
|
+
expect(g.priceOf('AAA')).toBe('99')
|
|
155
|
+
expect(g.symbolsInOrder()).toEqual(['AAA', 'CCC', 'BBB'])
|
|
156
|
+
g.done()
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it('applyTransaction: the cell updates and the sort re-runs', () => {
|
|
160
|
+
const props = liveProps(seed())
|
|
161
|
+
const g = mountGrid(props)
|
|
162
|
+
|
|
163
|
+
g.api().applyTransaction({ update: [{ id: 'a', symbol: 'AAA', price: 99 }] })
|
|
164
|
+
flushSync()
|
|
165
|
+
|
|
166
|
+
expect(g.priceOf('AAA')).toBe('99')
|
|
167
|
+
expect(g.symbolsInOrder()).toEqual(['AAA', 'CCC', 'BBB'])
|
|
168
|
+
g.done()
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it('a proxy field write is caught up by the next array replacement', () => {
|
|
172
|
+
const props = liveProps(seed())
|
|
173
|
+
const g = mountGrid(props)
|
|
174
|
+
|
|
175
|
+
props.data[0]!.price = 99
|
|
176
|
+
flushSync()
|
|
177
|
+
expect(g.symbolsInOrder()).toEqual(['CCC', 'BBB', 'AAA'])
|
|
178
|
+
|
|
179
|
+
// Any structural change re-runs the pipeline over the current values,
|
|
180
|
+
// including the one the proxy write left behind.
|
|
181
|
+
props.data = [...props.data]
|
|
182
|
+
flushSync()
|
|
183
|
+
expect(g.symbolsInOrder()).toEqual(['AAA', 'CCC', 'BBB'])
|
|
184
|
+
g.done()
|
|
185
|
+
})
|
|
186
|
+
})
|
|
@@ -75,6 +75,14 @@ function mountGrid(extra: Record<string, unknown> = {}) {
|
|
|
75
75
|
)
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
/**
|
|
79
|
+
* The close-on-scroll listener arms one animation frame after a popover
|
|
80
|
+
* mounts, so a scroll queued before the opening click cannot dismiss what
|
|
81
|
+
* that click opened. Every open helper waits that frame out before a case
|
|
82
|
+
* dispatches its scroll.
|
|
83
|
+
*/
|
|
84
|
+
const nextFrame = () => new Promise<void>((resolve) => requestAnimationFrame(() => resolve()))
|
|
85
|
+
|
|
78
86
|
async function openContextMenu(target: HTMLElement) {
|
|
79
87
|
const cell = target.querySelector('.sv-grid-cell[data-svgrid-row]') as HTMLElement
|
|
80
88
|
const r = cell.getBoundingClientRect()
|
|
@@ -86,6 +94,7 @@ async function openContextMenu(target: HTMLElement) {
|
|
|
86
94
|
expect(menu).not.toBeNull()
|
|
87
95
|
expect(menu!.querySelectorAll('.sv-grid-menu-item').length).toBeGreaterThan(0)
|
|
88
96
|
})
|
|
97
|
+
await nextFrame()
|
|
89
98
|
return target.querySelector('.sv-grid-context-menu') as HTMLElement
|
|
90
99
|
}
|
|
91
100
|
|
|
@@ -100,6 +109,7 @@ async function openCommentEditor(target: HTMLElement) {
|
|
|
100
109
|
await vi.waitFor(() => {
|
|
101
110
|
expect(target.querySelector('.sv-grid-comment-editor')).not.toBeNull()
|
|
102
111
|
})
|
|
112
|
+
await nextFrame()
|
|
103
113
|
return target.querySelector('.sv-grid-comment-editor') as HTMLElement
|
|
104
114
|
}
|
|
105
115
|
|
|
@@ -177,6 +177,22 @@ describe('rowPlaceholder', () => {
|
|
|
177
177
|
destroy()
|
|
178
178
|
})
|
|
179
179
|
|
|
180
|
+
it('styles footer and grand-total rows a row model flags on the data', async () => {
|
|
181
|
+
// A server-side model cannot reach the row objects the grid builds, so
|
|
182
|
+
// it puts the client model's flags on the data instead; same chrome.
|
|
183
|
+
const data = [
|
|
184
|
+
{ id: 1, name: 'Row 1' },
|
|
185
|
+
{ id: 2, name: 'Total', __groupFooter: true },
|
|
186
|
+
{ id: 3, name: 'Grand total', __groupFooter: true, __grandTotal: true },
|
|
187
|
+
] as Row[]
|
|
188
|
+
const { target, destroy } = await mountGrid({}, data)
|
|
189
|
+
await tick()
|
|
190
|
+
const rows = [...target.querySelectorAll('tbody .sv-grid-row')]
|
|
191
|
+
expect(rows.map((r) => r.classList.contains('sv-grid-group-footer-row'))).toEqual([false, true, true])
|
|
192
|
+
expect(rows.map((r) => r.classList.contains('sv-grid-grand-total-row'))).toEqual([false, false, true])
|
|
193
|
+
destroy()
|
|
194
|
+
})
|
|
195
|
+
|
|
180
196
|
it('keeps placeholder rows out of select-all', async () => {
|
|
181
197
|
const { api, destroy } = await mountGrid({
|
|
182
198
|
showRowSelection: true,
|
|
@@ -40,7 +40,7 @@ const rows: Row[] = [
|
|
|
40
40
|
const cols: ColumnDef<typeof features, Row>[] = [
|
|
41
41
|
{ field: 'id', header: 'ID', width: 100, editable: false },
|
|
42
42
|
{ field: 'account', header: 'Account', width: 200, editable: false },
|
|
43
|
-
{ field: 'arr', header: 'ARR', width: 130, editable: false, align: 'right',
|
|
43
|
+
{ field: 'arr', header: 'ARR', width: 130, editable: false, align: 'right', cellClass: 'money',
|
|
44
44
|
format: { type: 'number', options: { style: 'currency', currency: 'USD', maximumFractionDigits: 0 } } },
|
|
45
45
|
{ field: 'seats', header: 'Seats', width: 90, editable: false, align: 'right' },
|
|
46
46
|
]
|
|
@@ -167,6 +167,20 @@ describe('SvGrid - pinnedTopRows render', () => {
|
|
|
167
167
|
} finally { destroy() }
|
|
168
168
|
})
|
|
169
169
|
|
|
170
|
+
it('aligns and classes the cells the way the column says (a total sat left under a right-aligned column)', async () => {
|
|
171
|
+
const { target, destroy } = await mountGrid({ pinnedTopRows: [TOTALS] })
|
|
172
|
+
try {
|
|
173
|
+
await tick()
|
|
174
|
+
const tds = target.querySelectorAll('tr.sv-grid-pinned-row-top td')
|
|
175
|
+
expect(tds[0]!.getAttribute('data-align')).toBe('left')
|
|
176
|
+
expect(tds[2]!.getAttribute('data-align')).toBe('right')
|
|
177
|
+
expect(tds[3]!.getAttribute('data-align')).toBe('right')
|
|
178
|
+
// A static cellClass reaches the pinned cell; only a function did before.
|
|
179
|
+
expect(tds[2]!.classList.contains('money')).toBe(true)
|
|
180
|
+
expect(tds[0]!.classList.contains('money')).toBe(false)
|
|
181
|
+
} finally { destroy() }
|
|
182
|
+
})
|
|
183
|
+
|
|
170
184
|
it('stacks multiple pinned-top rows with data-pinned-index', async () => {
|
|
171
185
|
const { target, destroy } = await mountGrid({
|
|
172
186
|
pinnedTopRows: [TOTALS, BENCHMARK],
|
|
@@ -87,7 +87,7 @@ describe('selectionBar - without the Enterprise renderer', () => {
|
|
|
87
87
|
it('says a license is needed, and links to it', async () => {
|
|
88
88
|
// Naming the package is what unblocks a developer trying it; saying a key
|
|
89
89
|
// is needed is what they have to know before shipping. Enterprise is
|
|
90
|
-
//
|
|
90
|
+
// runs without a key, so the wording must not imply the feature is inert without
|
|
91
91
|
// one - it says "watermark", which is what actually happens.
|
|
92
92
|
const { api, target, destroy } = await mountGrid(true)
|
|
93
93
|
await tick()
|