@svgrid/grid 1.1.0 → 1.1.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/FlexRender.svelte +96 -96
- package/dist/GridMenus.svelte +205 -127
- package/dist/SvGrid.controller.svelte.d.ts +44 -1
- package/dist/SvGrid.controller.svelte.js +240 -44
- package/dist/SvGrid.css +325 -22
- package/dist/SvGrid.helpers.test.d.ts +1 -0
- package/dist/SvGrid.helpers.test.js +298 -0
- package/dist/SvGrid.svelte +729 -203
- package/dist/SvGrid.types.d.ts +91 -4
- package/dist/aligned-grids.d.ts +6 -0
- package/dist/aligned-grids.js +84 -0
- package/dist/aligned-grids.test.d.ts +1 -0
- package/dist/aligned-grids.test.js +75 -0
- package/dist/build-api.coverage.test.d.ts +20 -0
- package/dist/build-api.coverage.test.js +505 -0
- package/dist/build-api.js +61 -31
- package/dist/cell-render.test.d.ts +1 -0
- package/dist/cell-render.test.js +338 -0
- package/dist/cell-values.d.ts +1 -1
- package/dist/cell-values.js +7 -7
- package/dist/chart-export.test.d.ts +1 -0
- package/dist/chart-export.test.js +302 -0
- package/dist/chart.coverage.test.d.ts +1 -0
- package/dist/chart.coverage.test.js +748 -0
- package/dist/clipboard.js +88 -24
- package/dist/clipboard.test.d.ts +1 -0
- package/dist/clipboard.test.js +700 -0
- package/dist/collaboration.coverage.test.d.ts +1 -0
- package/dist/collaboration.coverage.test.js +200 -0
- package/dist/column-groups.d.ts +19 -0
- package/dist/column-groups.js +62 -0
- package/dist/column-groups.test.d.ts +1 -0
- package/dist/column-groups.test.js +56 -0
- package/dist/column-types.d.ts +10 -0
- package/dist/column-types.js +63 -0
- package/dist/column-types.test.d.ts +1 -0
- package/dist/column-types.test.js +62 -0
- package/dist/columns.test.d.ts +1 -0
- package/dist/columns.test.js +625 -0
- package/dist/core.d.ts +86 -1
- package/dist/core.js +2 -2
- package/dist/editing.d.ts +7 -0
- package/dist/editing.js +191 -5
- package/dist/editing.test.d.ts +1 -0
- package/dist/editing.test.js +732 -0
- package/dist/editors/cell-editors.coverage.test.d.ts +1 -0
- package/dist/editors/cell-editors.coverage.test.js +139 -0
- package/dist/facet-buckets.test.d.ts +1 -0
- package/dist/facet-buckets.test.js +296 -0
- package/dist/filter-operators.test.d.ts +1 -0
- package/dist/filter-operators.test.js +135 -0
- package/dist/hyperformula-adapter.test.d.ts +1 -0
- package/dist/hyperformula-adapter.test.js +205 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.js +4 -1
- package/dist/keyboard-handlers.coverage.test.d.ts +1 -0
- package/dist/keyboard-handlers.coverage.test.js +495 -0
- package/dist/keyboard-handlers.js +7 -2
- package/dist/menus.js +1 -0
- package/dist/menus.test.d.ts +1 -0
- package/dist/menus.test.js +560 -0
- package/dist/named-views.coverage.test.d.ts +1 -0
- package/dist/named-views.coverage.test.js +180 -0
- package/dist/row-drag.d.ts +49 -0
- package/dist/row-drag.js +221 -0
- package/dist/row-drag.test.d.ts +1 -0
- package/dist/row-drag.test.js +142 -0
- package/dist/row-resize.test.d.ts +1 -0
- package/dist/row-resize.test.js +329 -0
- package/dist/scroll-sync.js +3 -0
- package/dist/scroll-sync.test.d.ts +1 -0
- package/dist/scroll-sync.test.js +290 -0
- package/dist/selection.d.ts +7 -1
- package/dist/selection.js +76 -36
- package/dist/selection.multi-range.test.d.ts +1 -0
- package/dist/selection.multi-range.test.js +55 -0
- package/dist/selection.test.d.ts +1 -0
- package/dist/selection.test.js +647 -0
- package/dist/server-data-source.coverage.test.d.ts +1 -0
- package/dist/server-data-source.coverage.test.js +154 -0
- package/dist/spreadsheet.d.ts +30 -0
- package/dist/spreadsheet.js +48 -0
- package/dist/spreadsheet.test.d.ts +1 -0
- package/dist/spreadsheet.test.js +446 -0
- package/dist/summaries.js +4 -4
- package/dist/sv-grid-scrollbar.js +13 -1
- package/dist/svgrid-wrapper.types.d.ts +19 -0
- package/dist/svgrid.behavior.test.js +20 -0
- package/dist/svgrid.interaction.test.js +31 -0
- package/dist/svgrid.new-features.wrapper.test.js +34 -2
- package/dist/test-setup.js +9 -3
- package/dist/virtualization/scroll-scaling.d.ts +17 -0
- package/dist/virtualization/scroll-scaling.js +35 -0
- package/dist/virtualization/scroll-scaling.test.js +42 -1
- package/package.json +2 -1
- package/src/FlexRender.svelte +96 -96
- package/src/GridMenus.svelte +205 -127
- package/src/SvGrid.controller.svelte.ts +204 -36
- package/src/SvGrid.css +277 -12
- package/src/SvGrid.svelte +727 -205
- package/src/SvGrid.types.ts +85 -4
- package/src/a11y.contract.test.ts +49 -49
- package/src/a11y.test.ts +59 -59
- package/src/a11y.ts +59 -59
- package/src/aligned-grids.test.ts +80 -0
- package/src/aligned-grids.ts +87 -0
- package/src/build-api.ts +43 -23
- package/src/cell-formatting.ts +169 -169
- package/src/cell-values.ts +4 -4
- package/src/clipboard.test.ts +49 -0
- package/src/clipboard.ts +51 -23
- package/src/column-groups.test.ts +59 -0
- package/src/column-groups.ts +80 -0
- package/src/column-types.test.ts +68 -0
- package/src/column-types.ts +82 -0
- package/src/core.performance.test.ts +30 -30
- package/src/core.ts +1077 -999
- package/src/createGrid.svelte.ts +42 -42
- package/src/createGrid.test.ts +10 -10
- package/src/createGridState.svelte.ts +17 -17
- package/src/editing.ts +161 -5
- package/src/flex-render.ts +3 -3
- package/src/index.ts +208 -196
- package/src/keyboard.test.ts +59 -59
- package/src/keyboard.ts +97 -97
- package/src/menus.ts +1 -0
- package/src/merge-objects.ts +48 -48
- package/src/render-component.ts +28 -28
- package/src/row-drag.test.ts +168 -0
- package/src/row-drag.ts +255 -0
- package/src/scroll-sync.ts +2 -0
- package/src/selection.multi-range.test.ts +61 -0
- package/src/selection.ts +71 -37
- package/src/spreadsheet.test.ts +46 -2
- package/src/spreadsheet.ts +58 -0
- package/src/static-functions.ts +11 -11
- package/src/subscribe.ts +38 -38
- package/src/summaries.ts +4 -4
- package/src/svgrid-wrapper.types.ts +412 -393
- package/src/svgrid.features.test.ts +157 -157
- package/src/svgrid.new-features.wrapper.test.ts +2 -2
- package/src/svgrid.wrapper.test.ts +40 -40
- package/src/virtualization/column-virtualizer.test.ts +27 -27
- package/src/virtualization/column-virtualizer.ts +30 -30
- package/src/virtualization/svelte-virtualizer.svelte.ts +26 -26
- package/src/virtualization/types.ts +30 -30
- package/src/virtualization/virtualizer.test.ts +47 -47
- package/src/virtualization/virtualizer.ts +296 -296
package/src/SvGrid.types.ts
CHANGED
|
@@ -150,7 +150,32 @@ export type Props<TFeatures extends TableFeatures = TableFeatures, TData extends
|
|
|
150
150
|
showFilterMenu?: boolean;
|
|
151
151
|
showFilterRow?: boolean;
|
|
152
152
|
enableCellSelection?: boolean;
|
|
153
|
+
/**
|
|
154
|
+
* Prepend a header row (the column labels) to copied cell ranges, so pasting
|
|
155
|
+
* into Excel / Sheets includes the headers. Applies per copied range.
|
|
156
|
+
*/
|
|
157
|
+
copyHeadersToClipboard?: boolean;
|
|
158
|
+
/**
|
|
159
|
+
* Transform each cell value on its way to the clipboard - e.g. strip
|
|
160
|
+
* currency symbols, expand codes to labels, or redact. Receives the display
|
|
161
|
+
* value plus the row/column context; return the string (or value) to copy.
|
|
162
|
+
*/
|
|
163
|
+
processCellForClipboard?: (params: {
|
|
164
|
+
value: unknown;
|
|
165
|
+
column: unknown;
|
|
166
|
+
row: TData;
|
|
167
|
+
rowIndex: number;
|
|
168
|
+
columnId: string;
|
|
169
|
+
}) => unknown;
|
|
153
170
|
enableInlineEditing?: boolean;
|
|
171
|
+
/**
|
|
172
|
+
* Full-row editing. When `true`, starting an edit puts the WHOLE row into
|
|
173
|
+
* edit mode - every editable cell shows an inline editor at once - and a
|
|
174
|
+
* single Enter (or focus leaving the row) commits all of them; Esc cancels
|
|
175
|
+
* the whole row. Requires `enableInlineEditing`. The full-row editor covers
|
|
176
|
+
* text / number / date / datetime / checkbox / list-select editor types.
|
|
177
|
+
*/
|
|
178
|
+
fullRowEditing?: boolean;
|
|
154
179
|
enableRowSummaries?: boolean;
|
|
155
180
|
/**
|
|
156
181
|
* Excel-style status bar under the grid showing live aggregates of the
|
|
@@ -166,12 +191,21 @@ export type Props<TFeatures extends TableFeatures = TableFeatures, TData extends
|
|
|
166
191
|
>;
|
|
167
192
|
};
|
|
168
193
|
/**
|
|
169
|
-
* Show
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
* on the right edge.
|
|
194
|
+
* Show the docked tool panel - the enterprise sidebar with Columns and
|
|
195
|
+
* Filters tabs. A "Columns & Filters" button appears in a toolbar above the
|
|
196
|
+
* grid; the panel docks on the right edge.
|
|
173
197
|
*/
|
|
174
198
|
toolPanel?: boolean;
|
|
199
|
+
/**
|
|
200
|
+
* Render the header column menu (⋮) as a tabbed popover - **General**,
|
|
201
|
+
* **Filter**, and **Columns** tabs (the AG-Grid layout). Defaults to `false`,
|
|
202
|
+
* which keeps the flat menu (actions list + "Choose columns" submenu).
|
|
203
|
+
*/
|
|
204
|
+
columnMenuTabs?: boolean;
|
|
205
|
+
/** Open the tool panel on first render (instead of collapsed). */
|
|
206
|
+
toolPanelDefaultOpen?: boolean;
|
|
207
|
+
/** Which tab the tool panel starts on. Defaults to `'columns'`. */
|
|
208
|
+
toolPanelDefaultTab?: "columns" | "filters";
|
|
175
209
|
/**
|
|
176
210
|
* Quick way to pick which selection surfaces are active. `'row'` shows the
|
|
177
211
|
* selection checkbox column only, `'cell'` allows rectangle/range cell
|
|
@@ -419,6 +453,53 @@ export type Props<TFeatures extends TableFeatures = TableFeatures, TData extends
|
|
|
419
453
|
columnOrder?: ReadonlyArray<string>;
|
|
420
454
|
/** Fires every time the column order changes (drag or `api.setColumnOrder`). */
|
|
421
455
|
onColumnOrderChange?: (order: ReadonlyArray<string>) => void;
|
|
456
|
+
/**
|
|
457
|
+
* Infer each column's data type (number / boolean / date / ISO date-string /
|
|
458
|
+
* text) from the first data row, for columns that declare neither an
|
|
459
|
+
* explicit `editorType` nor a `cellDataType`. Sets the matching editor,
|
|
460
|
+
* alignment, date format, and filter operators automatically. Explicit
|
|
461
|
+
* column config always wins. Defaults to `false`.
|
|
462
|
+
*/
|
|
463
|
+
inferColumnTypes?: boolean;
|
|
464
|
+
/**
|
|
465
|
+
* Enables managed row dragging. When `true`, every row becomes a drag
|
|
466
|
+
* source (grab cursor + a grip in the row-number cell) and a drop
|
|
467
|
+
* indicator paints between rows during a drag. On drop the grid mutates
|
|
468
|
+
* its own internal data - reordering within the grid, or moving the row
|
|
469
|
+
* across grids that share the same {@link rowDragGroup}. Defaults to
|
|
470
|
+
* `false`.
|
|
471
|
+
*/
|
|
472
|
+
rowDragManaged?: boolean;
|
|
473
|
+
/**
|
|
474
|
+
* Connection group for cross-grid row dragging. Grids that share the same
|
|
475
|
+
* non-empty `rowDragGroup` string (and have `rowDragManaged` on) can
|
|
476
|
+
* exchange rows: dragging a row out of one and dropping it into another
|
|
477
|
+
* removes it from the source and inserts it into the target. Omit to keep
|
|
478
|
+
* dragging confined to reordering within a single grid.
|
|
479
|
+
*/
|
|
480
|
+
rowDragGroup?: string;
|
|
481
|
+
/**
|
|
482
|
+
* Fires on the TARGET grid after a managed row drag settles, with the
|
|
483
|
+
* moved row, its landing index, whether it stayed in the same grid, and
|
|
484
|
+
* the source / target grid ids. Use it to mirror the move into your own
|
|
485
|
+
* state (persistence, server sync). The grid has already applied the
|
|
486
|
+
* change to its internal data by the time this fires.
|
|
487
|
+
*/
|
|
488
|
+
onRowDragEnd?: (event: {
|
|
489
|
+
row: TData;
|
|
490
|
+
toIndex: number;
|
|
491
|
+
sameGrid: boolean;
|
|
492
|
+
fromGridId: number;
|
|
493
|
+
toGridId: number;
|
|
494
|
+
}) => void;
|
|
495
|
+
/**
|
|
496
|
+
* Align this grid with others that share the same non-empty
|
|
497
|
+
* `alignedGridGroup` string: horizontal scroll and column-resize widths are
|
|
498
|
+
* kept in lockstep across every grid in the group. Use for a totals/header
|
|
499
|
+
* grid above a body grid, or side-by-side comparison grids that must line up.
|
|
500
|
+
* The grids should declare the same columns (matched by id) for widths to map.
|
|
501
|
+
*/
|
|
502
|
+
alignedGridGroup?: string;
|
|
422
503
|
/**
|
|
423
504
|
* BCP-47 locale tag (or array of fallbacks) used for accent- and
|
|
424
505
|
* case-insensitive text filtering / sorting / search. Powered by
|
|
@@ -1,49 +1,49 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest'
|
|
2
|
-
import {
|
|
3
|
-
getGridCellA11yProps,
|
|
4
|
-
getGridHeaderA11yProps,
|
|
5
|
-
getGridRootA11yProps,
|
|
6
|
-
getGridRowA11yProps,
|
|
7
|
-
} from './a11y'
|
|
8
|
-
|
|
9
|
-
function attrsToHtml(attrs: Record<string, string | number | boolean>) {
|
|
10
|
-
return Object.entries(attrs)
|
|
11
|
-
.map(([key, value]) => `${key}="${String(value)}"`)
|
|
12
|
-
.join(' ')
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
describe('a11y contract rendering', () => {
|
|
16
|
-
it('builds expected headless markup attributes', () => {
|
|
17
|
-
const root = getGridRootA11yProps()
|
|
18
|
-
const header = getGridHeaderA11yProps({
|
|
19
|
-
sortable: true,
|
|
20
|
-
sortDirection: 'descending',
|
|
21
|
-
})
|
|
22
|
-
const row = getGridRowA11yProps(1)
|
|
23
|
-
const cell = getGridCellA11yProps({
|
|
24
|
-
rowIndex: 1,
|
|
25
|
-
colIndex: 2,
|
|
26
|
-
selected: true,
|
|
27
|
-
})
|
|
28
|
-
|
|
29
|
-
const html = [
|
|
30
|
-
`<table ${attrsToHtml(root)}><thead>`,
|
|
31
|
-
`<tr ${attrsToHtml(row)}>`,
|
|
32
|
-
`<th ${attrsToHtml(header)}>Age</th>`,
|
|
33
|
-
`</tr></thead><tbody>`,
|
|
34
|
-
`<tr ${attrsToHtml(row)}><td ${attrsToHtml(cell)}>42</td></tr>`,
|
|
35
|
-
`</tbody></table>`,
|
|
36
|
-
].join('')
|
|
37
|
-
|
|
38
|
-
expect(html).toContain('role="grid"')
|
|
39
|
-
expect(html).toContain('role="columnheader"')
|
|
40
|
-
expect(html).toContain('aria-sort="descending"')
|
|
41
|
-
expect(html).toContain('aria-selected="true"')
|
|
42
|
-
expect(html).toContain('aria-colindex="2"')
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
it('creates stable active descendant ids', async () => {
|
|
46
|
-
const { getGridCellDomId } = await import('./a11y')
|
|
47
|
-
expect(getGridCellDomId('svgrid', 3, 4)).toBe('svgrid_cell_3_4')
|
|
48
|
-
})
|
|
49
|
-
})
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
getGridCellA11yProps,
|
|
4
|
+
getGridHeaderA11yProps,
|
|
5
|
+
getGridRootA11yProps,
|
|
6
|
+
getGridRowA11yProps,
|
|
7
|
+
} from './a11y'
|
|
8
|
+
|
|
9
|
+
function attrsToHtml(attrs: Record<string, string | number | boolean>) {
|
|
10
|
+
return Object.entries(attrs)
|
|
11
|
+
.map(([key, value]) => `${key}="${String(value)}"`)
|
|
12
|
+
.join(' ')
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe('a11y contract rendering', () => {
|
|
16
|
+
it('builds expected headless markup attributes', () => {
|
|
17
|
+
const root = getGridRootA11yProps()
|
|
18
|
+
const header = getGridHeaderA11yProps({
|
|
19
|
+
sortable: true,
|
|
20
|
+
sortDirection: 'descending',
|
|
21
|
+
})
|
|
22
|
+
const row = getGridRowA11yProps(1)
|
|
23
|
+
const cell = getGridCellA11yProps({
|
|
24
|
+
rowIndex: 1,
|
|
25
|
+
colIndex: 2,
|
|
26
|
+
selected: true,
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
const html = [
|
|
30
|
+
`<table ${attrsToHtml(root)}><thead>`,
|
|
31
|
+
`<tr ${attrsToHtml(row)}>`,
|
|
32
|
+
`<th ${attrsToHtml(header)}>Age</th>`,
|
|
33
|
+
`</tr></thead><tbody>`,
|
|
34
|
+
`<tr ${attrsToHtml(row)}><td ${attrsToHtml(cell)}>42</td></tr>`,
|
|
35
|
+
`</tbody></table>`,
|
|
36
|
+
].join('')
|
|
37
|
+
|
|
38
|
+
expect(html).toContain('role="grid"')
|
|
39
|
+
expect(html).toContain('role="columnheader"')
|
|
40
|
+
expect(html).toContain('aria-sort="descending"')
|
|
41
|
+
expect(html).toContain('aria-selected="true"')
|
|
42
|
+
expect(html).toContain('aria-colindex="2"')
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('creates stable active descendant ids', async () => {
|
|
46
|
+
const { getGridCellDomId } = await import('./a11y')
|
|
47
|
+
expect(getGridCellDomId('svgrid', 3, 4)).toBe('svgrid_cell_3_4')
|
|
48
|
+
})
|
|
49
|
+
})
|
package/src/a11y.test.ts
CHANGED
|
@@ -1,59 +1,59 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest'
|
|
2
|
-
import {
|
|
3
|
-
getGridCellA11yProps,
|
|
4
|
-
getGridHeaderA11yProps,
|
|
5
|
-
getGridRootA11yProps,
|
|
6
|
-
getGridRowA11yProps,
|
|
7
|
-
} from './a11y'
|
|
8
|
-
|
|
9
|
-
describe('a11y helpers', () => {
|
|
10
|
-
it('returns grid role props', () => {
|
|
11
|
-
expect(
|
|
12
|
-
getGridRootA11yProps({
|
|
13
|
-
activeDescendantId: 'svgrid_cell_0_0',
|
|
14
|
-
rowCount: 10,
|
|
15
|
-
colCount: 5,
|
|
16
|
-
}),
|
|
17
|
-
).toEqual({
|
|
18
|
-
role: 'grid',
|
|
19
|
-
tabindex: 0,
|
|
20
|
-
'aria-activedescendant': 'svgrid_cell_0_0',
|
|
21
|
-
'aria-rowcount': 10,
|
|
22
|
-
'aria-colcount': 5,
|
|
23
|
-
})
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
it('returns header sort semantics', () => {
|
|
27
|
-
expect(
|
|
28
|
-
getGridHeaderA11yProps({
|
|
29
|
-
sortable: true,
|
|
30
|
-
sortDirection: 'ascending',
|
|
31
|
-
}),
|
|
32
|
-
).toEqual({
|
|
33
|
-
role: 'columnheader',
|
|
34
|
-
'aria-sort': 'ascending',
|
|
35
|
-
})
|
|
36
|
-
})
|
|
37
|
-
|
|
38
|
-
it('returns row and cell indices for accessibility metadata', () => {
|
|
39
|
-
expect(getGridRowA11yProps(2)).toEqual({
|
|
40
|
-
role: 'row',
|
|
41
|
-
'aria-rowindex': 2,
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
expect(
|
|
45
|
-
getGridCellA11yProps({
|
|
46
|
-
id: 'svgrid_cell_2_3',
|
|
47
|
-
rowIndex: 2,
|
|
48
|
-
colIndex: 3,
|
|
49
|
-
selected: true,
|
|
50
|
-
}),
|
|
51
|
-
).toEqual({
|
|
52
|
-
role: 'gridcell',
|
|
53
|
-
id: 'svgrid_cell_2_3',
|
|
54
|
-
'aria-colindex': 3,
|
|
55
|
-
'aria-rowindex': 2,
|
|
56
|
-
'aria-selected': true,
|
|
57
|
-
})
|
|
58
|
-
})
|
|
59
|
-
})
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
getGridCellA11yProps,
|
|
4
|
+
getGridHeaderA11yProps,
|
|
5
|
+
getGridRootA11yProps,
|
|
6
|
+
getGridRowA11yProps,
|
|
7
|
+
} from './a11y'
|
|
8
|
+
|
|
9
|
+
describe('a11y helpers', () => {
|
|
10
|
+
it('returns grid role props', () => {
|
|
11
|
+
expect(
|
|
12
|
+
getGridRootA11yProps({
|
|
13
|
+
activeDescendantId: 'svgrid_cell_0_0',
|
|
14
|
+
rowCount: 10,
|
|
15
|
+
colCount: 5,
|
|
16
|
+
}),
|
|
17
|
+
).toEqual({
|
|
18
|
+
role: 'grid',
|
|
19
|
+
tabindex: 0,
|
|
20
|
+
'aria-activedescendant': 'svgrid_cell_0_0',
|
|
21
|
+
'aria-rowcount': 10,
|
|
22
|
+
'aria-colcount': 5,
|
|
23
|
+
})
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('returns header sort semantics', () => {
|
|
27
|
+
expect(
|
|
28
|
+
getGridHeaderA11yProps({
|
|
29
|
+
sortable: true,
|
|
30
|
+
sortDirection: 'ascending',
|
|
31
|
+
}),
|
|
32
|
+
).toEqual({
|
|
33
|
+
role: 'columnheader',
|
|
34
|
+
'aria-sort': 'ascending',
|
|
35
|
+
})
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('returns row and cell indices for accessibility metadata', () => {
|
|
39
|
+
expect(getGridRowA11yProps(2)).toEqual({
|
|
40
|
+
role: 'row',
|
|
41
|
+
'aria-rowindex': 2,
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
expect(
|
|
45
|
+
getGridCellA11yProps({
|
|
46
|
+
id: 'svgrid_cell_2_3',
|
|
47
|
+
rowIndex: 2,
|
|
48
|
+
colIndex: 3,
|
|
49
|
+
selected: true,
|
|
50
|
+
}),
|
|
51
|
+
).toEqual({
|
|
52
|
+
role: 'gridcell',
|
|
53
|
+
id: 'svgrid_cell_2_3',
|
|
54
|
+
'aria-colindex': 3,
|
|
55
|
+
'aria-rowindex': 2,
|
|
56
|
+
'aria-selected': true,
|
|
57
|
+
})
|
|
58
|
+
})
|
|
59
|
+
})
|
package/src/a11y.ts
CHANGED
|
@@ -1,59 +1,59 @@
|
|
|
1
|
-
export type GridSortDirection = 'ascending' | 'descending' | 'none' | 'other'
|
|
2
|
-
|
|
3
|
-
export type GridColumnA11yInput = {
|
|
4
|
-
sortable?: boolean
|
|
5
|
-
sortDirection?: GridSortDirection
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export type GridCellA11yInput = {
|
|
9
|
-
colIndex?: number
|
|
10
|
-
rowIndex?: number
|
|
11
|
-
selected?: boolean
|
|
12
|
-
id?: string
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function getGridRootA11yProps(input: {
|
|
16
|
-
activeDescendantId?: string | null
|
|
17
|
-
rowCount?: number
|
|
18
|
-
colCount?: number
|
|
19
|
-
} = {}) {
|
|
20
|
-
return {
|
|
21
|
-
role: 'grid',
|
|
22
|
-
tabindex: 0,
|
|
23
|
-
...(input.activeDescendantId
|
|
24
|
-
? { 'aria-activedescendant': input.activeDescendantId }
|
|
25
|
-
: {}),
|
|
26
|
-
...(input.rowCount !== undefined ? { 'aria-rowcount': input.rowCount } : {}),
|
|
27
|
-
...(input.colCount !== undefined ? { 'aria-colcount': input.colCount } : {}),
|
|
28
|
-
} as const
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function getGridHeaderA11yProps(input: GridColumnA11yInput = {}) {
|
|
32
|
-
const { sortable = false, sortDirection = 'none' } = input
|
|
33
|
-
return {
|
|
34
|
-
role: 'columnheader',
|
|
35
|
-
...(sortable ? { 'aria-sort': sortDirection } : {}),
|
|
36
|
-
} as const
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function getGridRowA11yProps(rowIndex?: number) {
|
|
40
|
-
return {
|
|
41
|
-
role: 'row',
|
|
42
|
-
...(rowIndex !== undefined ? { 'aria-rowindex': rowIndex } : {}),
|
|
43
|
-
} as const
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export function getGridCellA11yProps(input: GridCellA11yInput = {}) {
|
|
47
|
-
const { colIndex, rowIndex, selected = false, id } = input
|
|
48
|
-
return {
|
|
49
|
-
role: 'gridcell',
|
|
50
|
-
...(id ? { id } : {}),
|
|
51
|
-
...(colIndex !== undefined ? { 'aria-colindex': colIndex } : {}),
|
|
52
|
-
...(rowIndex !== undefined ? { 'aria-rowindex': rowIndex } : {}),
|
|
53
|
-
...(selected ? { 'aria-selected': true } : {}),
|
|
54
|
-
} as const
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export function getGridCellDomId(baseId: string, rowIndex: number, colIndex: number) {
|
|
58
|
-
return `${baseId}_cell_${rowIndex}_${colIndex}`
|
|
59
|
-
}
|
|
1
|
+
export type GridSortDirection = 'ascending' | 'descending' | 'none' | 'other'
|
|
2
|
+
|
|
3
|
+
export type GridColumnA11yInput = {
|
|
4
|
+
sortable?: boolean
|
|
5
|
+
sortDirection?: GridSortDirection
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type GridCellA11yInput = {
|
|
9
|
+
colIndex?: number
|
|
10
|
+
rowIndex?: number
|
|
11
|
+
selected?: boolean
|
|
12
|
+
id?: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function getGridRootA11yProps(input: {
|
|
16
|
+
activeDescendantId?: string | null
|
|
17
|
+
rowCount?: number
|
|
18
|
+
colCount?: number
|
|
19
|
+
} = {}) {
|
|
20
|
+
return {
|
|
21
|
+
role: 'grid',
|
|
22
|
+
tabindex: 0,
|
|
23
|
+
...(input.activeDescendantId
|
|
24
|
+
? { 'aria-activedescendant': input.activeDescendantId }
|
|
25
|
+
: {}),
|
|
26
|
+
...(input.rowCount !== undefined ? { 'aria-rowcount': input.rowCount } : {}),
|
|
27
|
+
...(input.colCount !== undefined ? { 'aria-colcount': input.colCount } : {}),
|
|
28
|
+
} as const
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function getGridHeaderA11yProps(input: GridColumnA11yInput = {}) {
|
|
32
|
+
const { sortable = false, sortDirection = 'none' } = input
|
|
33
|
+
return {
|
|
34
|
+
role: 'columnheader',
|
|
35
|
+
...(sortable ? { 'aria-sort': sortDirection } : {}),
|
|
36
|
+
} as const
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function getGridRowA11yProps(rowIndex?: number) {
|
|
40
|
+
return {
|
|
41
|
+
role: 'row',
|
|
42
|
+
...(rowIndex !== undefined ? { 'aria-rowindex': rowIndex } : {}),
|
|
43
|
+
} as const
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function getGridCellA11yProps(input: GridCellA11yInput = {}) {
|
|
47
|
+
const { colIndex, rowIndex, selected = false, id } = input
|
|
48
|
+
return {
|
|
49
|
+
role: 'gridcell',
|
|
50
|
+
...(id ? { id } : {}),
|
|
51
|
+
...(colIndex !== undefined ? { 'aria-colindex': colIndex } : {}),
|
|
52
|
+
...(rowIndex !== undefined ? { 'aria-rowindex': rowIndex } : {}),
|
|
53
|
+
...(selected ? { 'aria-selected': true } : {}),
|
|
54
|
+
} as const
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function getGridCellDomId(baseId: string, rowIndex: number, colIndex: number) {
|
|
58
|
+
return `${baseId}_cell_${rowIndex}_${colIndex}`
|
|
59
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for aligned grids: two members sharing a group mirror horizontal
|
|
3
|
+
* scroll and column-resize widths to each other. Driven with minimal fake ctxs.
|
|
4
|
+
*/
|
|
5
|
+
import { describe, expect, it } from 'vitest'
|
|
6
|
+
import { createAlignedGrids } from './aligned-grids'
|
|
7
|
+
|
|
8
|
+
function makeCtx(group: string | undefined) {
|
|
9
|
+
return {
|
|
10
|
+
props: { alignedGridGroup: group },
|
|
11
|
+
scrollContainer: { scrollLeft: 0 } as { scrollLeft: number },
|
|
12
|
+
columnWidths: {} as Record<string, number>,
|
|
13
|
+
} as Record<string, any>
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe('aligned grids', () => {
|
|
17
|
+
it('mirrors horizontal scroll to peers in the same group', () => {
|
|
18
|
+
const a = makeCtx('g')
|
|
19
|
+
const b = makeCtx('g')
|
|
20
|
+
const ha = createAlignedGrids(a)
|
|
21
|
+
const hb = createAlignedGrids(b)
|
|
22
|
+
const un1 = ha.register()
|
|
23
|
+
const un2 = hb.register()
|
|
24
|
+
|
|
25
|
+
ha.broadcastScroll(240)
|
|
26
|
+
expect(b.scrollContainer.scrollLeft).toBe(240)
|
|
27
|
+
// and the other way
|
|
28
|
+
hb.broadcastScroll(80)
|
|
29
|
+
expect(a.scrollContainer.scrollLeft).toBe(80)
|
|
30
|
+
un1(); un2()
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('mirrors column-resize widths to peers (matched by column id)', () => {
|
|
34
|
+
const a = makeCtx('g')
|
|
35
|
+
const b = makeCtx('g')
|
|
36
|
+
const ha = createAlignedGrids(a)
|
|
37
|
+
const hb = createAlignedGrids(b)
|
|
38
|
+
ha.register(); hb.register()
|
|
39
|
+
|
|
40
|
+
a.columnWidths = { Feb: 180 }
|
|
41
|
+
ha.broadcastWidths()
|
|
42
|
+
expect(b.columnWidths.Feb).toBe(180)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('does NOT mirror across different groups', () => {
|
|
46
|
+
const a = makeCtx('one')
|
|
47
|
+
const b = makeCtx('two')
|
|
48
|
+
const ha = createAlignedGrids(a)
|
|
49
|
+
const hb = createAlignedGrids(b)
|
|
50
|
+
ha.register(); hb.register()
|
|
51
|
+
|
|
52
|
+
ha.broadcastScroll(200)
|
|
53
|
+
expect(b.scrollContainer.scrollLeft).toBe(0)
|
|
54
|
+
a.columnWidths = { Feb: 999 }
|
|
55
|
+
ha.broadcastWidths()
|
|
56
|
+
expect(b.columnWidths.Feb).toBeUndefined()
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('is inert with no group set, and after unregister', () => {
|
|
60
|
+
const a = makeCtx(undefined)
|
|
61
|
+
const b = makeCtx(undefined)
|
|
62
|
+
const ha = createAlignedGrids(a)
|
|
63
|
+
const hb = createAlignedGrids(b)
|
|
64
|
+
ha.register(); hb.register()
|
|
65
|
+
ha.broadcastScroll(120)
|
|
66
|
+
expect(b.scrollContainer.scrollLeft).toBe(0)
|
|
67
|
+
|
|
68
|
+
// Grouped, then unregistered -> no longer receives updates.
|
|
69
|
+
const c = makeCtx('gg')
|
|
70
|
+
const d = makeCtx('gg')
|
|
71
|
+
const hc = createAlignedGrids(c)
|
|
72
|
+
const hd = createAlignedGrids(d)
|
|
73
|
+
const unc = hc.register()
|
|
74
|
+
const und = hd.register()
|
|
75
|
+
und() // d leaves the group
|
|
76
|
+
hc.broadcastScroll(55)
|
|
77
|
+
expect(d.scrollContainer.scrollLeft).toBe(0)
|
|
78
|
+
unc()
|
|
79
|
+
})
|
|
80
|
+
})
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// Aligned grids - keep two or more independent grids in lockstep on horizontal
|
|
2
|
+
// scroll and column widths. Grids that share the same `alignedGridGroup` string
|
|
3
|
+
// register with a module-level registry (same pattern as managed row dragging);
|
|
4
|
+
// when one scrolls horizontally or a column is resized, the others follow.
|
|
5
|
+
//
|
|
6
|
+
// Loop-safety without flags: scroll sync no-ops when the target already has the
|
|
7
|
+
// value (so the echo scroll event never fires); width sync compares a serialized
|
|
8
|
+
// snapshot and pre-seeds it on apply, so the reactive effect that fires after
|
|
9
|
+
// applying a peer's widths sees "no change" and doesn't echo.
|
|
10
|
+
|
|
11
|
+
type Member = {
|
|
12
|
+
id: number;
|
|
13
|
+
applyScroll: (left: number) => void;
|
|
14
|
+
applyWidths: (widths: Record<string, number>) => void;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// Shared across every grid in the bundle.
|
|
18
|
+
const groups = new Map<string, Map<number, Member>>();
|
|
19
|
+
let seq = 0;
|
|
20
|
+
|
|
21
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
22
|
+
export function createAlignedGrids<TFeatures, TData>(ctx: any) {
|
|
23
|
+
const id = ++seq;
|
|
24
|
+
let lastWidthsSerial = "";
|
|
25
|
+
|
|
26
|
+
const group = (): string | null => ctx.props.alignedGridGroup ?? null;
|
|
27
|
+
|
|
28
|
+
function peers(): Member[] {
|
|
29
|
+
const g = group();
|
|
30
|
+
if (!g) return [];
|
|
31
|
+
const m = groups.get(g);
|
|
32
|
+
if (!m) return [];
|
|
33
|
+
const out: Member[] = [];
|
|
34
|
+
for (const member of m.values()) if (member.id !== id) out.push(member);
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const self: Member = {
|
|
39
|
+
id,
|
|
40
|
+
applyScroll(left: number) {
|
|
41
|
+
const el = ctx.scrollContainer as HTMLElement | null;
|
|
42
|
+
// Only set when it differs, so no echo scroll event fires -> no loop.
|
|
43
|
+
if (el && el.scrollLeft !== left) el.scrollLeft = left;
|
|
44
|
+
},
|
|
45
|
+
applyWidths(widths: Record<string, number>) {
|
|
46
|
+
const merged = { ...(ctx.columnWidths as Record<string, number>), ...widths };
|
|
47
|
+
// Pre-seed the serial so the columnWidths effect that fires after this
|
|
48
|
+
// set sees an unchanged snapshot and skips its own broadcast.
|
|
49
|
+
lastWidthsSerial = JSON.stringify(merged);
|
|
50
|
+
ctx.columnWidths = merged;
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// Register on mount; returns the unregister for the effect's cleanup.
|
|
55
|
+
function register(): () => void {
|
|
56
|
+
const g = group();
|
|
57
|
+
if (!g) return () => {};
|
|
58
|
+
let m = groups.get(g);
|
|
59
|
+
if (!m) {
|
|
60
|
+
m = new Map();
|
|
61
|
+
groups.set(g, m);
|
|
62
|
+
}
|
|
63
|
+
m.set(id, self);
|
|
64
|
+
return () => {
|
|
65
|
+
const mm = groups.get(g);
|
|
66
|
+
if (!mm) return;
|
|
67
|
+
mm.delete(id);
|
|
68
|
+
if (!mm.size) groups.delete(g);
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function broadcastScroll(left: number) {
|
|
73
|
+
const p = peers();
|
|
74
|
+
for (const member of p) member.applyScroll(left);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function broadcastWidths() {
|
|
78
|
+
if (!group()) return;
|
|
79
|
+
const serial = JSON.stringify((ctx.columnWidths as Record<string, number>) ?? {});
|
|
80
|
+
if (serial === lastWidthsSerial) return; // nothing new (or echo of an apply)
|
|
81
|
+
lastWidthsSerial = serial;
|
|
82
|
+
const w = ctx.columnWidths as Record<string, number>;
|
|
83
|
+
for (const member of peers()) member.applyWidths(w);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return { register, broadcastScroll, broadcastWidths, alignedGridId: id };
|
|
87
|
+
}
|