@svgrid/grid 2.6.22 → 2.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +88 -88
- package/README.md +199 -199
- package/dist/FlexRender.svelte +96 -96
- package/dist/GridFooter.svelte +181 -181
- package/dist/GridMenus.svelte +184 -6
- package/dist/SvAutoComplete.svelte +169 -165
- package/dist/SvAvatar.svelte +75 -75
- package/dist/SvCalendar.svelte +503 -503
- package/dist/SvCarousel.svelte +141 -141
- package/dist/SvCheckBox.svelte +102 -102
- package/dist/SvCircularProgress.svelte +109 -109
- package/dist/SvColorInput.svelte +181 -181
- package/dist/SvComboBox.svelte +279 -275
- package/dist/SvContextMenu.svelte +116 -116
- package/dist/SvCountryInput.svelte +163 -163
- package/dist/SvDateRangeInput.svelte +25 -3
- package/dist/SvDrawer.svelte +254 -254
- package/dist/SvDropDownList.svelte +378 -374
- package/dist/SvDurationInput.svelte +126 -126
- package/dist/SvField.svelte +293 -293
- package/dist/SvForm.svelte +437 -437
- package/dist/SvGrid.controller.svelte.d.ts +48 -1
- package/dist/SvGrid.controller.svelte.js +214 -52
- package/dist/SvGrid.css +221 -0
- package/dist/SvGrid.helpers.d.ts +55 -0
- package/dist/SvGrid.helpers.js +83 -0
- package/dist/SvGrid.svelte +145 -0
- package/dist/SvGrid.types.d.ts +105 -0
- package/dist/SvGridCellEditor.svelte +744 -662
- package/dist/SvGridChart.svelte +1716 -1716
- package/dist/SvGridChartPanel.svelte +485 -485
- package/dist/SvGridChartView.svelte +70 -70
- package/dist/SvGridDropdown.svelte +728 -728
- package/dist/SvGridSelect.svelte +270 -266
- package/dist/SvGridSelect.svelte.d.ts +1 -1
- package/dist/SvGroupCell.svelte +116 -116
- package/dist/SvListBox.svelte +334 -326
- package/dist/SvMaskedInput.svelte +122 -122
- package/dist/SvMenu.svelte +124 -124
- package/dist/SvMenuList.svelte +146 -146
- package/dist/SvMultiSelect.svelte +293 -289
- package/dist/SvMultiSelect.svelte.d.ts +1 -1
- package/dist/SvNumberInput.svelte +172 -168
- package/dist/SvOtpInput.svelte +158 -158
- package/dist/SvPasswordInput.svelte +151 -151
- package/dist/SvPhoneInput.svelte +133 -133
- package/dist/SvPopover.svelte +197 -197
- package/dist/SvProgress.svelte +116 -116
- package/dist/SvRadioGroup.svelte +107 -107
- package/dist/SvRating.svelte +112 -112
- package/dist/SvResult.svelte +73 -73
- package/dist/SvRichCell.svelte +125 -0
- package/dist/SvRichCell.svelte.d.ts +15 -0
- package/dist/SvRichText.svelte +211 -211
- package/dist/SvRowGroupPanel.svelte +170 -170
- package/dist/SvScrollArea.svelte +61 -61
- package/dist/SvSlider.svelte +203 -203
- package/dist/SvSwitchButton.svelte +108 -108
- package/dist/SvTagsInput.svelte +115 -115
- package/dist/SvTextArea.svelte +5 -1
- package/dist/SvTextInput.svelte +147 -147
- package/dist/SvTimePicker.svelte +245 -245
- package/dist/SvToaster.svelte +159 -159
- package/dist/SvToggleButton.svelte +85 -85
- package/dist/SvTooltip.svelte +161 -161
- package/dist/SvTour.svelte +208 -208
- package/dist/SvTree.svelte +444 -444
- package/dist/SvTreeSelect.svelte +239 -235
- package/dist/SvTreeSelect.svelte.d.ts +1 -1
- package/dist/builtin-editors.d.ts +2 -2
- package/dist/builtin-editors.js +19 -3
- package/dist/cdn/GridMenus-C4oLsJyj.js +600 -0
- package/dist/cdn/GridMenus-yEivjNwl.js +601 -0
- package/dist/cdn/SvDateRangeInput-Bh0A0JkF.js +199 -0
- package/dist/cdn/SvDateRangeInput-DflbiP7N.js +198 -0
- package/dist/cdn/SvDateTimePicker-BWwpfB_o.js +812 -0
- package/dist/cdn/SvDateTimePicker-Bivn8dAP.js +816 -0
- package/dist/cdn/SvGridCellEditor-AXL8cHGO.js +550 -0
- package/dist/cdn/SvGridCellEditor-Ba7rY3eu.js +549 -0
- package/dist/cdn/{SvGridChart-BwVos976.js → SvGridChart-Bs2GIR2Q.js} +1 -1
- package/dist/cdn/{SvGridChart-BEJmNNx9.js → SvGridChart-CwhFz7GV.js} +1 -1
- package/dist/cdn/{SvGridChartPanel-BOknOkrP.js → SvGridChartPanel-1WXuStzM.js} +1 -1
- package/dist/cdn/{SvGridChartPanel-D2PjII4O.js → SvGridChartPanel-e5_nqNP9.js} +1 -1
- package/dist/cdn/{SvGridChartView-00LPUHL1.js → SvGridChartView-SmPW10dI.js} +1 -1
- package/dist/cdn/{SvGridChartView-BhUEJ5ki.js → SvGridChartView-eSPuJE6g.js} +1 -1
- package/dist/cdn/{SvGridDropdown-D0VdjeR8.js → SvGridDropdown-B1ZcLpgs.js} +1 -1
- package/dist/cdn/{SvGridDropdown-D13MtJ2j.js → SvGridDropdown-CtdbHcIS.js} +1 -1
- package/dist/cdn/date-format-CcP1tafP.js +1395 -0
- package/dist/cdn/date-format-DZT7T1wf.js +1393 -0
- package/dist/cdn/{editor-registry-CiI0xlKg.js → editor-registry-BhgE3S84.js} +51 -25
- package/dist/cdn/{src-C9Hihx1W.js → src-COC0vnpg.js} +7233 -6701
- package/dist/cdn/{src-D1lXwq1l.js → src-pYGV2rMR.js} +5747 -5215
- package/dist/cdn/svgrid.js +15 -13
- package/dist/cdn/svgrid.svelte-external.js +15 -13
- package/dist/cell-values.d.ts +16 -0
- package/dist/cell-values.js +28 -0
- package/dist/chart-export.js +8 -8
- package/dist/clipboard.d.ts +22 -0
- package/dist/clipboard.js +405 -27
- package/dist/editing.js +12 -7
- package/dist/editors/cell-editors.d.ts +11 -0
- package/dist/editors/cell-editors.js +18 -1
- package/dist/grid-messages.d.ts +15 -0
- package/dist/grid-messages.js +15 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/list-option.d.ts +6 -0
- package/dist/menus.js +40 -7
- package/dist/sanitize-html.d.ts +37 -0
- package/dist/sanitize-html.js +234 -0
- package/dist/selection-bar-view.svelte.d.ts +25 -0
- package/dist/selection-bar-view.svelte.js +13 -0
- package/dist/selection.d.ts +2 -1
- package/dist/selection.js +129 -13
- package/package.json +11 -11
- package/src/FlexRender.svelte +96 -96
- package/src/GridFooter.svelte +181 -181
- package/src/GridMenus.svelte +184 -6
- package/src/SvAutoComplete.svelte +169 -165
- package/src/SvAvatar.svelte +75 -75
- package/src/SvCalendar.svelte +503 -503
- package/src/SvCalendar.test.ts +226 -226
- package/src/SvCarousel.svelte +141 -141
- package/src/SvCheckBox.svelte +102 -102
- package/src/SvCircularProgress.svelte +109 -109
- package/src/SvColorInput.svelte +181 -181
- package/src/SvComboBox.svelte +279 -275
- package/src/SvContextMenu.svelte +116 -116
- package/src/SvCountryInput.svelte +163 -163
- package/src/SvDateRangeInput.svelte +25 -3
- package/src/SvDrawer.svelte +254 -254
- package/src/SvDropDownList.svelte +378 -374
- package/src/SvDurationInput.svelte +126 -126
- package/src/SvField.svelte +293 -293
- package/src/SvForm.svelte +437 -437
- package/src/SvForm.test.ts +411 -411
- package/src/SvGrid.controller.svelte.ts +251 -61
- package/src/SvGrid.css +221 -0
- package/src/SvGrid.helpers.ts +87 -0
- package/src/SvGrid.svelte +145 -0
- package/src/SvGrid.types.ts +2092 -1980
- package/src/SvGridCellEditor.svelte +744 -662
- package/src/SvGridChart.svelte +1716 -1716
- package/src/SvGridChartPanel.svelte +485 -485
- package/src/SvGridChartView.svelte +70 -70
- package/src/SvGridDropdown.svelte +728 -728
- package/src/SvGridSelect.svelte +270 -266
- package/src/SvGroupCell.svelte +116 -116
- package/src/SvListBox.svelte +334 -326
- package/src/SvMaskedInput.svelte +122 -122
- package/src/SvMenu.svelte +124 -124
- package/src/SvMenu.test.ts +97 -97
- package/src/SvMenuList.svelte +146 -146
- package/src/SvMultiSelect.svelte +293 -289
- package/src/SvNumberInput.svelte +172 -168
- package/src/SvOtpInput.svelte +158 -158
- package/src/SvPasswordInput.svelte +151 -151
- package/src/SvPhoneInput.svelte +133 -133
- package/src/SvPopover.svelte +197 -197
- package/src/SvProgress.svelte +116 -116
- package/src/SvRadioGroup.svelte +107 -107
- package/src/SvRating.svelte +112 -112
- package/src/SvResult.svelte +73 -73
- package/src/SvRichCell.svelte +125 -0
- package/src/SvRichText.svelte +211 -211
- package/src/SvRowGroupPanel.svelte +170 -170
- package/src/SvScrollArea.svelte +61 -61
- package/src/SvSlider.svelte +203 -203
- package/src/SvSwitchButton.svelte +108 -108
- package/src/SvTagsInput.svelte +115 -115
- package/src/SvTextArea.svelte +5 -1
- package/src/SvTextInput.svelte +147 -147
- package/src/SvTimePicker.svelte +245 -245
- package/src/SvToaster.svelte +159 -159
- package/src/SvToaster.test.ts +95 -95
- package/src/SvToggleButton.svelte +85 -85
- package/src/SvTooltip.svelte +161 -161
- package/src/SvTour.svelte +208 -208
- package/src/SvTree.svelte +444 -444
- package/src/SvTreeSelect.svelte +239 -235
- package/src/a11y/dismissable.test.ts +119 -119
- package/src/a11y/dismissable.ts +114 -114
- package/src/a11y.contract.test.ts +49 -49
- package/src/a11y.test.ts +59 -59
- package/src/a11y.ts +61 -61
- package/src/ai.test.ts +502 -502
- package/src/ai.ts +1419 -1419
- package/src/build-api.coverage.test.ts +633 -633
- package/src/build-api.ts +846 -846
- package/src/builtin-editors.grid.test.ts +83 -83
- package/src/builtin-editors.test.ts +18 -1
- package/src/builtin-editors.ts +19 -3
- package/src/cell-formatting.ts +171 -171
- package/src/cell-render.test.ts +513 -513
- package/src/cell-render.ts +496 -496
- package/src/cell-values.ts +148 -118
- package/src/chart-export.ts +202 -202
- package/src/chart-view.svelte.ts +36 -36
- package/src/chart.ts +2321 -2321
- package/src/clipboard.test.ts +93 -20
- package/src/clipboard.ts +433 -33
- package/src/collaboration.test.ts +104 -104
- package/src/collaboration.ts +167 -167
- package/src/column-groups.ts +78 -78
- package/src/core.performance.test.ts +30 -30
- package/src/core.ts +1865 -1865
- package/src/createAutocomplete.svelte.ts +132 -132
- package/src/createCombobox.svelte.ts +191 -191
- package/src/createCountryInput.svelte.ts +157 -157
- package/src/createDropdownList.svelte.ts +168 -168
- package/src/createForm.svelte.ts +386 -386
- package/src/createGrid.svelte.ts +42 -42
- package/src/createGrid.test.ts +10 -10
- package/src/createGridState.svelte.ts +17 -17
- package/src/createListbox.svelte.ts +250 -250
- package/src/createMenu.svelte.ts +224 -224
- package/src/createPopoverSelect.svelte.ts +213 -213
- package/src/createSlider.svelte.ts +191 -191
- package/src/createTooltip.svelte.ts +144 -144
- package/src/createTree.svelte.ts +322 -322
- package/src/date-string-column.test.ts +87 -0
- package/src/datetime/date-core.ts +206 -206
- package/src/datetime/date-restrict.ts +61 -61
- package/src/datetime/timezone.ts +135 -135
- package/src/dock-manager-model.ts +596 -596
- package/src/dock-model.ts +374 -374
- package/src/editing.test.ts +974 -949
- package/src/editing.ts +609 -606
- package/src/editor-block-prop.test.ts +132 -0
- package/src/editor-contract.ts +171 -171
- package/src/editor-registry.grid.test.ts +144 -144
- package/src/editor-registry.ts +122 -122
- package/src/editors/cell-editors.ts +27 -1
- package/src/export-data-api.test.ts +126 -126
- package/src/export-format.test.ts +107 -107
- package/src/export-format.ts +601 -601
- package/src/filter-operators.ts +160 -160
- package/src/filtering/excel-filters.ts +325 -325
- package/src/flex-render.ts +3 -3
- package/src/form-field.ts +127 -127
- package/src/grid-messages.ts +34 -0
- package/src/group-display.test.ts +167 -167
- package/src/group-display.ts +200 -200
- package/src/headless.ts +87 -87
- package/src/index.ts +13 -0
- package/src/js-scroller.svelte.ts +173 -173
- package/src/keyboard-handlers.ts +270 -270
- package/src/keyboard.test.ts +59 -59
- package/src/keyboard.ts +97 -97
- package/src/list-nav.test.ts +49 -49
- package/src/list-nav.ts +29 -29
- package/src/list-option.test.ts +56 -56
- package/src/list-option.ts +179 -173
- package/src/menus.test.ts +43 -0
- package/src/menus.ts +597 -567
- package/src/merge-objects.ts +48 -48
- package/src/move-cells.test.ts +850 -0
- package/src/number-editor-literal.test.ts +84 -0
- package/src/number-editor.grid.test.ts +160 -0
- package/src/overlays.test.ts +90 -90
- package/src/positioning.ts +268 -268
- package/src/render-component.ts +28 -28
- package/src/row-drag-touch.ts +211 -211
- package/src/row-drag.test.ts +353 -353
- package/src/row-drag.ts +415 -415
- package/src/row-resize.test.ts +524 -524
- package/src/row-resize.ts +228 -228
- package/src/sanitize-html.test.ts +182 -0
- package/src/sanitize-html.ts +235 -0
- package/src/scheduler-ical.ts +181 -181
- package/src/scheduler-model.test.ts +562 -562
- package/src/scheduler-model.ts +873 -873
- package/src/selection-bar-view.svelte.ts +36 -0
- package/src/selection.test.ts +900 -743
- package/src/selection.ts +131 -13
- package/src/server-data-source.test.ts +383 -383
- package/src/server-data-source.ts +469 -469
- package/src/sparkline.test.ts +68 -68
- package/src/sparkline.ts +169 -169
- package/src/spreadsheet.test.ts +488 -488
- package/src/spreadsheet.ts +312 -312
- package/src/static-functions.ts +11 -11
- package/src/subscribe.ts +38 -38
- package/src/summaries.ts +113 -113
- package/src/svgrid-wrapper.types.ts +563 -563
- package/src/svgrid.async-editor-options.test.ts +273 -273
- package/src/svgrid.auto-row-height.test.ts +204 -204
- package/src/svgrid.behavior.test.ts +910 -910
- package/src/svgrid.charting.test.ts +534 -534
- package/src/svgrid.comments-autocomplete.test.ts +127 -127
- package/src/svgrid.context-menu.test.ts +147 -147
- package/src/svgrid.features.test.ts +157 -157
- package/src/svgrid.filter-depth.test.ts +163 -163
- package/src/svgrid.filter-menu-listbox.svelte.test.ts +377 -331
- package/src/svgrid.filter-menu-scroll.test.ts +112 -112
- package/src/svgrid.grand-total.test.ts +188 -188
- package/src/svgrid.group-display-mode.test.ts +171 -171
- package/src/svgrid.group-footers.test.ts +121 -121
- package/src/svgrid.group-pagination.test.ts +153 -153
- package/src/svgrid.in-suggest-snapshot.svelte.test.ts +91 -0
- package/src/svgrid.menu-scroll-close.test.ts +204 -0
- package/src/svgrid.new-features.wrapper.test.ts +251 -251
- package/src/svgrid.selection-bar-seam.test.ts +185 -0
- package/src/svgrid.tree-data.test.ts +186 -186
- package/src/svgrid.upsell-license.test.ts +117 -0
- package/src/svgrid.wrapper.test.ts +63 -63
- package/src/svgriddropdown.async-panel.svelte.test.ts +195 -195
- package/src/test-setup.ts +62 -62
- package/src/themes/index.ts +288 -288
- package/src/toast-store.svelte.ts +250 -250
- package/src/toast-store.test.ts +147 -147
- package/src/tree-row-model.test.ts +168 -168
- package/src/ui-buttons.test.ts +144 -144
- package/src/ui-inputs.test.ts +118 -118
- package/src/ui-localization.test.ts +113 -113
- package/src/ui-range.test.ts +70 -70
- package/src/ui-selection.test.ts +155 -155
- package/src/ui-tier1.test.ts +142 -142
- package/src/virtual.test.ts +88 -88
- 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 +322 -322
- package/dist/cdn/GridMenus-BuoBPqxx.js +0 -493
- package/dist/cdn/GridMenus-n4llxoOI.js +0 -494
- package/dist/cdn/SvDateTimePicker-CHnUkWcH.js +0 -2206
- package/dist/cdn/SvDateTimePicker-CRQH_U7E.js +0 -2201
- package/dist/cdn/SvGridCellEditor-BGUCzuPB.js +0 -523
- package/dist/cdn/SvGridCellEditor-G8IMWIws.js +0 -522
|
@@ -1,153 +1,153 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* DOM: with grouping on, `pageSize` budgets DATA rows, not group banners (#73).
|
|
3
|
-
*
|
|
4
|
-
* The row model hands the view one flat list with group banners interleaved
|
|
5
|
-
* between their children. Slicing that list by `pageSize` spent the budget on
|
|
6
|
-
* banners: ten groups of two at `pageSize: 10` showed roughly three data rows,
|
|
7
|
-
* and with enough small groups a page could show almost none.
|
|
8
|
-
*
|
|
9
|
-
* Now the page window is measured in data rows, and each page reprints the
|
|
10
|
-
* banners its rows sit under - so a group split across a page boundary is
|
|
11
|
-
* labelled on both pages.
|
|
12
|
-
*/
|
|
13
|
-
import { afterEach, describe, expect, it } from 'vitest'
|
|
14
|
-
import { mount, unmount } from 'svelte'
|
|
15
|
-
import SvGrid from './SvGrid.svelte'
|
|
16
|
-
import {
|
|
17
|
-
createCoreRowModel,
|
|
18
|
-
createExpandedRowModel,
|
|
19
|
-
createFilteredRowModel,
|
|
20
|
-
createGroupedRowModel,
|
|
21
|
-
createPaginatedRowModel,
|
|
22
|
-
createSortedRowModel,
|
|
23
|
-
sortFns,
|
|
24
|
-
tableFeatures,
|
|
25
|
-
rowSortingFeature,
|
|
26
|
-
columnGroupingFeature,
|
|
27
|
-
} from './index'
|
|
28
|
-
|
|
29
|
-
type Row = { id: number; region: string; amount: number }
|
|
30
|
-
|
|
31
|
-
// Ten groups of two rows - the shape from the report.
|
|
32
|
-
const data: Row[] = []
|
|
33
|
-
for (let g = 0; g < 10; g += 1) {
|
|
34
|
-
data.push({ id: g * 2, region: `R${g}`, amount: 1 })
|
|
35
|
-
data.push({ id: g * 2 + 1, region: `R${g}`, amount: 2 })
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const columns = [
|
|
39
|
-
{ field: 'region', header: 'Region', width: 120 },
|
|
40
|
-
{ field: 'amount', header: 'Amount', width: 100 },
|
|
41
|
-
]
|
|
42
|
-
|
|
43
|
-
const features = tableFeatures({ rowSortingFeature, columnGroupingFeature })
|
|
44
|
-
const tick = () => new Promise<void>((r) => queueMicrotask(r))
|
|
45
|
-
|
|
46
|
-
function mountGrid(pageSize: number, groupFooters = false) {
|
|
47
|
-
const target = document.createElement('div')
|
|
48
|
-
document.body.appendChild(target)
|
|
49
|
-
// Grouping state starts empty and is driven through the api - there is no
|
|
50
|
-
// initial-grouping prop - so group by region as soon as the api lands.
|
|
51
|
-
let api: { setGroupBy: (ids: string[]) => void } | null = null
|
|
52
|
-
const app = mount(SvGrid, {
|
|
53
|
-
target,
|
|
54
|
-
props: {
|
|
55
|
-
data,
|
|
56
|
-
columns,
|
|
57
|
-
features,
|
|
58
|
-
groupable: true,
|
|
59
|
-
groupFooters,
|
|
60
|
-
onApiReady: (a: never) => { api = a as never },
|
|
61
|
-
pageable: true,
|
|
62
|
-
pageSize,
|
|
63
|
-
_rowModels: {
|
|
64
|
-
coreRowModel: createCoreRowModel(),
|
|
65
|
-
filteredRowModel: createFilteredRowModel(),
|
|
66
|
-
sortedRowModel: createSortedRowModel(sortFns),
|
|
67
|
-
groupedRowModel: createGroupedRowModel(),
|
|
68
|
-
expandedRowModel: createExpandedRowModel(),
|
|
69
|
-
paginatedRowModel: createPaginatedRowModel(),
|
|
70
|
-
},
|
|
71
|
-
containerHeight: 900,
|
|
72
|
-
virtualization: false,
|
|
73
|
-
} as never,
|
|
74
|
-
})
|
|
75
|
-
return {
|
|
76
|
-
target,
|
|
77
|
-
groupByRegion: () => api?.setGroupBy(['region']),
|
|
78
|
-
/** Groups render collapsed by default; click every banner to open them. */
|
|
79
|
-
expandAll: () => {
|
|
80
|
-
for (const btn of target.querySelectorAll<HTMLButtonElement>('.sv-grid-group-toggle')) {
|
|
81
|
-
if (btn.getAttribute('aria-expanded') !== 'true') btn.click()
|
|
82
|
-
}
|
|
83
|
-
},
|
|
84
|
-
destroy: () => { unmount(app); target.remove() },
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
let cleanup: (() => void) | null = null
|
|
89
|
-
afterEach(() => { cleanup?.(); cleanup = null })
|
|
90
|
-
|
|
91
|
-
const dataRows = (t: HTMLElement) =>
|
|
92
|
-
[...t.querySelectorAll('tbody tr.sv-grid-row')].filter(
|
|
93
|
-
(r) => !r.classList.contains('sv-grid-group-row') && !r.classList.contains('sv-grid-row-spacer'),
|
|
94
|
-
)
|
|
95
|
-
const groupRows = (t: HTMLElement) => [...t.querySelectorAll('tbody tr.sv-grid-group-row')]
|
|
96
|
-
|
|
97
|
-
async function grouped(pageSize: number, expand = true, groupFooters = false) {
|
|
98
|
-
const g = mountGrid(pageSize, groupFooters)
|
|
99
|
-
cleanup = g.destroy
|
|
100
|
-
await tick()
|
|
101
|
-
g.groupByRegion()
|
|
102
|
-
await tick()
|
|
103
|
-
if (expand) {
|
|
104
|
-
g.expandAll()
|
|
105
|
-
await tick()
|
|
106
|
-
}
|
|
107
|
-
return g
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
describe('grouped pagination (#73)', () => {
|
|
111
|
-
it('fills the page with pageSize DATA rows, not banners', async () => {
|
|
112
|
-
const { target } = await grouped(10)
|
|
113
|
-
// Before the fix the flat slice returned 10 TOTAL rows - banners included -
|
|
114
|
-
// so only a handful of them were data.
|
|
115
|
-
expect(dataRows(target)).toHaveLength(10)
|
|
116
|
-
// Those 10 data rows span 5 groups, so 5 banners come along for free.
|
|
117
|
-
expect(groupRows(target)).toHaveLength(5)
|
|
118
|
-
})
|
|
119
|
-
|
|
120
|
-
it('reports the data-row count in the pager, not banners + data', async () => {
|
|
121
|
-
const { target } = await grouped(10)
|
|
122
|
-
// 20 data rows at pageSize 10 = 2 pages. Counting banners would say 30.
|
|
123
|
-
expect(target.textContent).toMatch(/of\s+20\b/)
|
|
124
|
-
})
|
|
125
|
-
|
|
126
|
-
it('reprints the banner for a group straddling a page boundary', async () => {
|
|
127
|
-
const { target } = await grouped(3)
|
|
128
|
-
// Page 0 holds R0's two rows plus R1's first, so both banners show.
|
|
129
|
-
const labels = groupRows(target).map((r) => r.textContent ?? '')
|
|
130
|
-
expect(labels.some((t) => t.includes('R0'))).toBe(true)
|
|
131
|
-
expect(labels.some((t) => t.includes('R1'))).toBe(true)
|
|
132
|
-
expect(dataRows(target)).toHaveLength(3)
|
|
133
|
-
})
|
|
134
|
-
|
|
135
|
-
it('does not spend page budget on group footers', async () => {
|
|
136
|
-
// Footers are inserted after paging, so turning them on must not push data
|
|
137
|
-
// rows onto the next page.
|
|
138
|
-
const { target } = await grouped(10, true, true)
|
|
139
|
-
expect(dataRows(target).filter((r) => !r.classList.contains('sv-grid-group-footer-row')))
|
|
140
|
-
.toHaveLength(10)
|
|
141
|
-
expect(target.textContent).toMatch(/of\s+20\b/)
|
|
142
|
-
})
|
|
143
|
-
|
|
144
|
-
it('still fills a page when every group is collapsed', async () => {
|
|
145
|
-
// Collapsed groups are the visible unit. Treating them as free headers
|
|
146
|
-
// emptied the page entirely - caught here, not by the pure helper.
|
|
147
|
-
const { target } = await grouped(4, false)
|
|
148
|
-
expect(groupRows(target)).toHaveLength(4)
|
|
149
|
-
expect(dataRows(target)).toHaveLength(0)
|
|
150
|
-
// 10 collapsed groups over pageSize 4.
|
|
151
|
-
expect(target.textContent).toMatch(/of\s+10\b/)
|
|
152
|
-
})
|
|
153
|
-
})
|
|
1
|
+
/**
|
|
2
|
+
* DOM: with grouping on, `pageSize` budgets DATA rows, not group banners (#73).
|
|
3
|
+
*
|
|
4
|
+
* The row model hands the view one flat list with group banners interleaved
|
|
5
|
+
* between their children. Slicing that list by `pageSize` spent the budget on
|
|
6
|
+
* banners: ten groups of two at `pageSize: 10` showed roughly three data rows,
|
|
7
|
+
* and with enough small groups a page could show almost none.
|
|
8
|
+
*
|
|
9
|
+
* Now the page window is measured in data rows, and each page reprints the
|
|
10
|
+
* banners its rows sit under - so a group split across a page boundary is
|
|
11
|
+
* labelled on both pages.
|
|
12
|
+
*/
|
|
13
|
+
import { afterEach, describe, expect, it } from 'vitest'
|
|
14
|
+
import { mount, unmount } from 'svelte'
|
|
15
|
+
import SvGrid from './SvGrid.svelte'
|
|
16
|
+
import {
|
|
17
|
+
createCoreRowModel,
|
|
18
|
+
createExpandedRowModel,
|
|
19
|
+
createFilteredRowModel,
|
|
20
|
+
createGroupedRowModel,
|
|
21
|
+
createPaginatedRowModel,
|
|
22
|
+
createSortedRowModel,
|
|
23
|
+
sortFns,
|
|
24
|
+
tableFeatures,
|
|
25
|
+
rowSortingFeature,
|
|
26
|
+
columnGroupingFeature,
|
|
27
|
+
} from './index'
|
|
28
|
+
|
|
29
|
+
type Row = { id: number; region: string; amount: number }
|
|
30
|
+
|
|
31
|
+
// Ten groups of two rows - the shape from the report.
|
|
32
|
+
const data: Row[] = []
|
|
33
|
+
for (let g = 0; g < 10; g += 1) {
|
|
34
|
+
data.push({ id: g * 2, region: `R${g}`, amount: 1 })
|
|
35
|
+
data.push({ id: g * 2 + 1, region: `R${g}`, amount: 2 })
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const columns = [
|
|
39
|
+
{ field: 'region', header: 'Region', width: 120 },
|
|
40
|
+
{ field: 'amount', header: 'Amount', width: 100 },
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
const features = tableFeatures({ rowSortingFeature, columnGroupingFeature })
|
|
44
|
+
const tick = () => new Promise<void>((r) => queueMicrotask(r))
|
|
45
|
+
|
|
46
|
+
function mountGrid(pageSize: number, groupFooters = false) {
|
|
47
|
+
const target = document.createElement('div')
|
|
48
|
+
document.body.appendChild(target)
|
|
49
|
+
// Grouping state starts empty and is driven through the api - there is no
|
|
50
|
+
// initial-grouping prop - so group by region as soon as the api lands.
|
|
51
|
+
let api: { setGroupBy: (ids: string[]) => void } | null = null
|
|
52
|
+
const app = mount(SvGrid, {
|
|
53
|
+
target,
|
|
54
|
+
props: {
|
|
55
|
+
data,
|
|
56
|
+
columns,
|
|
57
|
+
features,
|
|
58
|
+
groupable: true,
|
|
59
|
+
groupFooters,
|
|
60
|
+
onApiReady: (a: never) => { api = a as never },
|
|
61
|
+
pageable: true,
|
|
62
|
+
pageSize,
|
|
63
|
+
_rowModels: {
|
|
64
|
+
coreRowModel: createCoreRowModel(),
|
|
65
|
+
filteredRowModel: createFilteredRowModel(),
|
|
66
|
+
sortedRowModel: createSortedRowModel(sortFns),
|
|
67
|
+
groupedRowModel: createGroupedRowModel(),
|
|
68
|
+
expandedRowModel: createExpandedRowModel(),
|
|
69
|
+
paginatedRowModel: createPaginatedRowModel(),
|
|
70
|
+
},
|
|
71
|
+
containerHeight: 900,
|
|
72
|
+
virtualization: false,
|
|
73
|
+
} as never,
|
|
74
|
+
})
|
|
75
|
+
return {
|
|
76
|
+
target,
|
|
77
|
+
groupByRegion: () => api?.setGroupBy(['region']),
|
|
78
|
+
/** Groups render collapsed by default; click every banner to open them. */
|
|
79
|
+
expandAll: () => {
|
|
80
|
+
for (const btn of target.querySelectorAll<HTMLButtonElement>('.sv-grid-group-toggle')) {
|
|
81
|
+
if (btn.getAttribute('aria-expanded') !== 'true') btn.click()
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
destroy: () => { unmount(app); target.remove() },
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
let cleanup: (() => void) | null = null
|
|
89
|
+
afterEach(() => { cleanup?.(); cleanup = null })
|
|
90
|
+
|
|
91
|
+
const dataRows = (t: HTMLElement) =>
|
|
92
|
+
[...t.querySelectorAll('tbody tr.sv-grid-row')].filter(
|
|
93
|
+
(r) => !r.classList.contains('sv-grid-group-row') && !r.classList.contains('sv-grid-row-spacer'),
|
|
94
|
+
)
|
|
95
|
+
const groupRows = (t: HTMLElement) => [...t.querySelectorAll('tbody tr.sv-grid-group-row')]
|
|
96
|
+
|
|
97
|
+
async function grouped(pageSize: number, expand = true, groupFooters = false) {
|
|
98
|
+
const g = mountGrid(pageSize, groupFooters)
|
|
99
|
+
cleanup = g.destroy
|
|
100
|
+
await tick()
|
|
101
|
+
g.groupByRegion()
|
|
102
|
+
await tick()
|
|
103
|
+
if (expand) {
|
|
104
|
+
g.expandAll()
|
|
105
|
+
await tick()
|
|
106
|
+
}
|
|
107
|
+
return g
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
describe('grouped pagination (#73)', () => {
|
|
111
|
+
it('fills the page with pageSize DATA rows, not banners', async () => {
|
|
112
|
+
const { target } = await grouped(10)
|
|
113
|
+
// Before the fix the flat slice returned 10 TOTAL rows - banners included -
|
|
114
|
+
// so only a handful of them were data.
|
|
115
|
+
expect(dataRows(target)).toHaveLength(10)
|
|
116
|
+
// Those 10 data rows span 5 groups, so 5 banners come along for free.
|
|
117
|
+
expect(groupRows(target)).toHaveLength(5)
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('reports the data-row count in the pager, not banners + data', async () => {
|
|
121
|
+
const { target } = await grouped(10)
|
|
122
|
+
// 20 data rows at pageSize 10 = 2 pages. Counting banners would say 30.
|
|
123
|
+
expect(target.textContent).toMatch(/of\s+20\b/)
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('reprints the banner for a group straddling a page boundary', async () => {
|
|
127
|
+
const { target } = await grouped(3)
|
|
128
|
+
// Page 0 holds R0's two rows plus R1's first, so both banners show.
|
|
129
|
+
const labels = groupRows(target).map((r) => r.textContent ?? '')
|
|
130
|
+
expect(labels.some((t) => t.includes('R0'))).toBe(true)
|
|
131
|
+
expect(labels.some((t) => t.includes('R1'))).toBe(true)
|
|
132
|
+
expect(dataRows(target)).toHaveLength(3)
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('does not spend page budget on group footers', async () => {
|
|
136
|
+
// Footers are inserted after paging, so turning them on must not push data
|
|
137
|
+
// rows onto the next page.
|
|
138
|
+
const { target } = await grouped(10, true, true)
|
|
139
|
+
expect(dataRows(target).filter((r) => !r.classList.contains('sv-grid-group-footer-row')))
|
|
140
|
+
.toHaveLength(10)
|
|
141
|
+
expect(target.textContent).toMatch(/of\s+20\b/)
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it('still fills a page when every group is collapsed', async () => {
|
|
145
|
+
// Collapsed groups are the visible unit. Treating them as free headers
|
|
146
|
+
// emptied the page entirely - caught here, not by the pure helper.
|
|
147
|
+
const { target } = await grouped(4, false)
|
|
148
|
+
expect(groupRows(target)).toHaveLength(4)
|
|
149
|
+
expect(dataRows(target)).toHaveLength(0)
|
|
150
|
+
// 10 collapsed groups over pageSize 4.
|
|
151
|
+
expect(target.textContent).toMatch(/of\s+10\b/)
|
|
152
|
+
})
|
|
153
|
+
})
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `in` / `notIn` suggestions dropdown shares the facet scan with the
|
|
3
|
+
* filter menu's checklist. That scan now also runs the filter pipeline, to
|
|
4
|
+
* offer only values still reachable under the other columns' filters - which
|
|
5
|
+
* put an O(rows) pass on the typing path of the chip input, because the
|
|
6
|
+
* suggestions re-derive on every keystroke.
|
|
7
|
+
*
|
|
8
|
+
* They are snapshotted per open instead, the same way the menu's checklist is.
|
|
9
|
+
* This pins that: typing must not re-scan.
|
|
10
|
+
*/
|
|
11
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
12
|
+
import { mount, unmount } from 'svelte'
|
|
13
|
+
import SvGrid from './SvGrid.svelte'
|
|
14
|
+
import {
|
|
15
|
+
columnFilteringFeature,
|
|
16
|
+
rowSortingFeature,
|
|
17
|
+
tableFeatures,
|
|
18
|
+
} from './index'
|
|
19
|
+
import type { ColumnDef } from './index'
|
|
20
|
+
|
|
21
|
+
type Row = { id: number; symbol: string }
|
|
22
|
+
const features = tableFeatures({ rowSortingFeature, columnFilteringFeature })
|
|
23
|
+
const tick = () => new Promise<void>((r) => setTimeout(r))
|
|
24
|
+
|
|
25
|
+
describe('in/notIn suggestions', () => {
|
|
26
|
+
it('scans once per open, not once per keystroke', async () => {
|
|
27
|
+
const data: Row[] = Array.from({ length: 40 }, (_, i) => ({
|
|
28
|
+
id: i + 1,
|
|
29
|
+
symbol: `SYM${String(i).padStart(3, '0')}`,
|
|
30
|
+
}))
|
|
31
|
+
|
|
32
|
+
// Count reads of the accessor the facet scan goes through. A per-keystroke
|
|
33
|
+
// re-scan shows up here as another 40 reads per character typed.
|
|
34
|
+
let reads = 0
|
|
35
|
+
const columns: ColumnDef<typeof features, Row>[] = [
|
|
36
|
+
{
|
|
37
|
+
id: 'symbol',
|
|
38
|
+
header: 'Symbol',
|
|
39
|
+
width: 200,
|
|
40
|
+
fieldFn: (row: Row) => {
|
|
41
|
+
reads += 1
|
|
42
|
+
return row.symbol
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
const target = document.createElement('div')
|
|
48
|
+
document.body.appendChild(target)
|
|
49
|
+
const app = mount(SvGrid<typeof features, Row>, {
|
|
50
|
+
target,
|
|
51
|
+
props: { data, columns, features, filterMode: 'row', height: 300 } as never,
|
|
52
|
+
})
|
|
53
|
+
await tick()
|
|
54
|
+
|
|
55
|
+
// Switch the column to `in`, which is what turns the filter-row box into a
|
|
56
|
+
// chip input with suggestions.
|
|
57
|
+
const operatorBtn = target.querySelector(
|
|
58
|
+
'.sv-grid-filter-operator-btn',
|
|
59
|
+
) as HTMLButtonElement
|
|
60
|
+
operatorBtn.click()
|
|
61
|
+
await vi.waitFor(() => expect(document.querySelector('.sv-grid-operator-menu')).not.toBeNull())
|
|
62
|
+
const inItem = Array.from(
|
|
63
|
+
document.querySelectorAll('.sv-grid-operator-menu .sv-grid-menu-item'),
|
|
64
|
+
).find((el) => el.textContent?.trim() === 'In') as HTMLElement
|
|
65
|
+
inItem.click()
|
|
66
|
+
await tick()
|
|
67
|
+
|
|
68
|
+
const input = target.querySelector(
|
|
69
|
+
'.sv-grid-filter-chip-input, .sv-grid-filter-value',
|
|
70
|
+
) as HTMLInputElement
|
|
71
|
+
expect(input).not.toBeNull()
|
|
72
|
+
input.focus()
|
|
73
|
+
input.dispatchEvent(new Event('focus', { bubbles: true }))
|
|
74
|
+
await vi.waitFor(() => expect(document.querySelector('.sv-grid-in-suggest')).not.toBeNull())
|
|
75
|
+
|
|
76
|
+
const afterOpen = reads
|
|
77
|
+
expect(afterOpen).toBeGreaterThan(0)
|
|
78
|
+
|
|
79
|
+
// Type into the box: the list narrows from the snapshot, no fresh scan.
|
|
80
|
+
for (const text of ['S', 'SY', 'SYM', 'SYM0', 'SYM00']) {
|
|
81
|
+
input.value = text
|
|
82
|
+
input.dispatchEvent(new Event('input', { bubbles: true }))
|
|
83
|
+
await tick()
|
|
84
|
+
}
|
|
85
|
+
expect(document.querySelector('.sv-grid-in-suggest')).not.toBeNull()
|
|
86
|
+
expect(reads).toBe(afterOpen)
|
|
87
|
+
|
|
88
|
+
unmount(app)
|
|
89
|
+
target.remove()
|
|
90
|
+
})
|
|
91
|
+
})
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The scroll-close gate for the CELL CONTEXT MENU and the COMMENT EDITOR.
|
|
3
|
+
*
|
|
4
|
+
* Both are positioned from a one-time cursor measurement and rendered
|
|
5
|
+
* `position: fixed`, exactly like the column / filter / operator menus - but
|
|
6
|
+
* both were missing from the effect that closes those on an outside scroll, so
|
|
7
|
+
* after any scroll they floated over unrelated cells.
|
|
8
|
+
*
|
|
9
|
+
* The comment editor is closed by SAVING, not discarding. Backdrop-click and
|
|
10
|
+
* Escape throw the draft away and that is right, because both are the user
|
|
11
|
+
* saying "away with this". A scroll is not a dismissal gesture, so it must not
|
|
12
|
+
* destroy text the user has typed.
|
|
13
|
+
*
|
|
14
|
+
* The sibling file `svgrid.filter-menu-scroll.test.ts` covers the four menus
|
|
15
|
+
* that were already gated, including the must-NOT-close cases.
|
|
16
|
+
*/
|
|
17
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
18
|
+
import { mount, unmount } from 'svelte'
|
|
19
|
+
import SvGrid from './SvGrid.svelte'
|
|
20
|
+
import {
|
|
21
|
+
createCoreRowModel,
|
|
22
|
+
createFilteredRowModel,
|
|
23
|
+
createSortedRowModel,
|
|
24
|
+
rowSortingFeature,
|
|
25
|
+
sortFns,
|
|
26
|
+
tableFeatures,
|
|
27
|
+
} from './index'
|
|
28
|
+
import type { ColumnDef, SvGridApi } from './index'
|
|
29
|
+
|
|
30
|
+
type Row = { id: number; name: string; team: string }
|
|
31
|
+
const features = tableFeatures({ rowSortingFeature })
|
|
32
|
+
const rows: Row[] = Array.from({ length: 6 }, (_, i) => ({
|
|
33
|
+
id: i + 1,
|
|
34
|
+
name: `Person ${i + 1}`,
|
|
35
|
+
team: ['A', 'B'][i % 2]!,
|
|
36
|
+
}))
|
|
37
|
+
const cols: ColumnDef<typeof features, Row>[] = [
|
|
38
|
+
{ field: 'name', header: 'Name', width: 200, editorType: 'text' },
|
|
39
|
+
{ field: 'team', header: 'Team', width: 160, editorType: 'text' },
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
/** Macrotask boundary - drains the overlay's lazy import() plus its flush. */
|
|
43
|
+
const tick = () => new Promise<void>((r) => setTimeout(r))
|
|
44
|
+
|
|
45
|
+
function mountGrid(extra: Record<string, unknown> = {}) {
|
|
46
|
+
return new Promise<{ api: SvGridApi<typeof features, Row>; target: HTMLElement; destroy: () => void }>(
|
|
47
|
+
(res, rej) => {
|
|
48
|
+
const target = document.createElement('div')
|
|
49
|
+
document.body.appendChild(target)
|
|
50
|
+
const app = mount(SvGrid, {
|
|
51
|
+
target,
|
|
52
|
+
props: {
|
|
53
|
+
data: rows,
|
|
54
|
+
columns: cols,
|
|
55
|
+
features,
|
|
56
|
+
_rowModels: {
|
|
57
|
+
coreRowModel: createCoreRowModel(),
|
|
58
|
+
filteredRowModel: createFilteredRowModel(),
|
|
59
|
+
sortedRowModel: createSortedRowModel(sortFns),
|
|
60
|
+
},
|
|
61
|
+
rowHeight: 32,
|
|
62
|
+
containerHeight: 240,
|
|
63
|
+
virtualization: false,
|
|
64
|
+
// `comment` is not one of the default items - ask for it by id so
|
|
65
|
+
// the comment editor is reachable at all.
|
|
66
|
+
contextMenu: ['copy', 'comment'],
|
|
67
|
+
onApiReady(api: SvGridApi<typeof features, Row>) {
|
|
68
|
+
res({ api, target, destroy: () => { unmount(app); target.remove() } })
|
|
69
|
+
},
|
|
70
|
+
...extra,
|
|
71
|
+
} as any,
|
|
72
|
+
})
|
|
73
|
+
queueMicrotask(() => { if (!target.querySelector('[role="grid"]')) rej(new Error('no grid')) })
|
|
74
|
+
},
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function openContextMenu(target: HTMLElement) {
|
|
79
|
+
const cell = target.querySelector('.sv-grid-cell[data-svgrid-row]') as HTMLElement
|
|
80
|
+
const r = cell.getBoundingClientRect()
|
|
81
|
+
cell.dispatchEvent(
|
|
82
|
+
new MouseEvent('contextmenu', { bubbles: true, cancelable: true, clientX: r.x + 4, clientY: r.y + 4 }),
|
|
83
|
+
)
|
|
84
|
+
await vi.waitFor(() => {
|
|
85
|
+
const menu = target.querySelector('.sv-grid-context-menu')
|
|
86
|
+
expect(menu).not.toBeNull()
|
|
87
|
+
expect(menu!.querySelectorAll('.sv-grid-menu-item').length).toBeGreaterThan(0)
|
|
88
|
+
})
|
|
89
|
+
return target.querySelector('.sv-grid-context-menu') as HTMLElement
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Right-click, then pick "Edit comment" to get the comment popover open. */
|
|
93
|
+
async function openCommentEditor(target: HTMLElement) {
|
|
94
|
+
const menu = await openContextMenu(target)
|
|
95
|
+
const item = [...menu.querySelectorAll<HTMLElement>('.sv-grid-menu-item')].find((b) =>
|
|
96
|
+
(b.textContent ?? '').includes('Edit comment'),
|
|
97
|
+
)
|
|
98
|
+
expect(item, 'the "Edit comment" item should be in the configured menu').toBeTruthy()
|
|
99
|
+
item!.click()
|
|
100
|
+
await vi.waitFor(() => {
|
|
101
|
+
expect(target.querySelector('.sv-grid-comment-editor')).not.toBeNull()
|
|
102
|
+
})
|
|
103
|
+
return target.querySelector('.sv-grid-comment-editor') as HTMLElement
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Scroll something that is NOT inside any menu. */
|
|
107
|
+
function scrollOutside(target: HTMLElement) {
|
|
108
|
+
const viewport = target.querySelector('.sv-grid-viewport') ?? target
|
|
109
|
+
viewport.dispatchEvent(new Event('scroll'))
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
describe('cell context menu - scroll', () => {
|
|
113
|
+
it('closes when something outside it scrolls', async () => {
|
|
114
|
+
const { target, destroy } = await mountGrid()
|
|
115
|
+
await tick()
|
|
116
|
+
await openContextMenu(target)
|
|
117
|
+
|
|
118
|
+
scrollOutside(target)
|
|
119
|
+
await vi.waitFor(() => expect(target.querySelector('.sv-grid-context-menu')).toBeNull())
|
|
120
|
+
|
|
121
|
+
destroy()
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('stays open while the menu itself scrolls', async () => {
|
|
125
|
+
const { target, destroy } = await mountGrid()
|
|
126
|
+
await tick()
|
|
127
|
+
const menu = await openContextMenu(target)
|
|
128
|
+
|
|
129
|
+
menu.dispatchEvent(new Event('scroll'))
|
|
130
|
+
await tick()
|
|
131
|
+
expect(target.querySelector('.sv-grid-context-menu')).not.toBeNull()
|
|
132
|
+
|
|
133
|
+
destroy()
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it('carries the grid id, so a second grid can tell whose menu it is', async () => {
|
|
137
|
+
const { target, destroy } = await mountGrid()
|
|
138
|
+
await tick()
|
|
139
|
+
const menu = await openContextMenu(target)
|
|
140
|
+
|
|
141
|
+
const id = menu.getAttribute('data-svgrid-menu')
|
|
142
|
+
expect(id).toBeTruthy()
|
|
143
|
+
// The same id the grid stamps on its cells, so the two are one grid.
|
|
144
|
+
const cellId = target.querySelector('.sv-grid-cell[id]')?.getAttribute('id')
|
|
145
|
+
expect(cellId).toContain(id!)
|
|
146
|
+
|
|
147
|
+
destroy()
|
|
148
|
+
})
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
describe('comment editor - scroll', () => {
|
|
152
|
+
it('closes when something outside it scrolls', async () => {
|
|
153
|
+
const { target, destroy } = await mountGrid()
|
|
154
|
+
await tick()
|
|
155
|
+
await openCommentEditor(target)
|
|
156
|
+
|
|
157
|
+
scrollOutside(target)
|
|
158
|
+
await vi.waitFor(() => expect(target.querySelector('.sv-grid-comment-editor')).toBeNull())
|
|
159
|
+
|
|
160
|
+
destroy()
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
it('SAVES the draft rather than throwing it away', async () => {
|
|
164
|
+
const changes: Array<{ note: string }> = []
|
|
165
|
+
const { target, destroy } = await mountGrid({
|
|
166
|
+
onNoteChange: (e: { note: string }) => changes.push(e),
|
|
167
|
+
})
|
|
168
|
+
await tick()
|
|
169
|
+
const editor = await openCommentEditor(target)
|
|
170
|
+
|
|
171
|
+
const textarea = editor.querySelector('textarea') as HTMLTextAreaElement
|
|
172
|
+
textarea.value = 'half-written thought'
|
|
173
|
+
textarea.dispatchEvent(new Event('input', { bubbles: true }))
|
|
174
|
+
await tick()
|
|
175
|
+
|
|
176
|
+
scrollOutside(target)
|
|
177
|
+
await vi.waitFor(() => expect(target.querySelector('.sv-grid-comment-editor')).toBeNull())
|
|
178
|
+
|
|
179
|
+
expect(changes.map((c) => c.note)).toContain('half-written thought')
|
|
180
|
+
|
|
181
|
+
destroy()
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
it('still DISCARDS on Escape, which is a real dismissal', async () => {
|
|
185
|
+
const changes: Array<{ note: string }> = []
|
|
186
|
+
const { target, destroy } = await mountGrid({
|
|
187
|
+
onNoteChange: (e: { note: string }) => changes.push(e),
|
|
188
|
+
})
|
|
189
|
+
await tick()
|
|
190
|
+
const editor = await openCommentEditor(target)
|
|
191
|
+
|
|
192
|
+
const textarea = editor.querySelector('textarea') as HTMLTextAreaElement
|
|
193
|
+
textarea.value = 'never mind'
|
|
194
|
+
textarea.dispatchEvent(new Event('input', { bubbles: true }))
|
|
195
|
+
await tick()
|
|
196
|
+
|
|
197
|
+
textarea.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }))
|
|
198
|
+
await vi.waitFor(() => expect(target.querySelector('.sv-grid-comment-editor')).toBeNull())
|
|
199
|
+
|
|
200
|
+
expect(changes).toHaveLength(0)
|
|
201
|
+
|
|
202
|
+
destroy()
|
|
203
|
+
})
|
|
204
|
+
})
|