@svgrid/grid 2.2.27 → 2.2.29
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 +199 -199
- package/dist/FlexRender.svelte +96 -96
- package/dist/GridFooter.svelte +179 -179
- package/dist/GridMenus.svelte +782 -734
- package/dist/SvAutoComplete.svelte +165 -165
- package/dist/SvCalendar.svelte +503 -503
- package/dist/SvCarousel.svelte +141 -141
- package/dist/SvCheckBox.svelte +101 -101
- package/dist/SvColorInput.svelte +181 -181
- package/dist/SvComboBox.svelte +275 -275
- package/dist/SvContextMenu.svelte +116 -116
- package/dist/SvCountryInput.svelte +160 -160
- package/dist/SvDrawer.svelte +254 -250
- package/dist/SvDropDownList.svelte +374 -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 +14 -1
- package/dist/SvGrid.controller.svelte.js +112 -28
- package/dist/SvGrid.css +2704 -2723
- package/dist/SvGrid.svelte +3361 -3333
- package/dist/SvGrid.types.d.ts +20 -5
- package/dist/SvGridChart.svelte +1724 -1724
- package/dist/SvGridChartPanel.svelte +485 -485
- package/dist/SvGridChartView.svelte +70 -70
- package/dist/SvGridDropdown.svelte +696 -696
- package/dist/SvGridSelect.svelte +259 -259
- package/dist/SvGroupCell.svelte +109 -109
- package/dist/SvListBox.svelte +326 -287
- package/dist/SvListBox.svelte.d.ts +10 -1
- package/dist/SvMaskedInput.svelte +122 -122
- package/dist/SvMenu.svelte +124 -124
- package/dist/SvMenuList.svelte +146 -146
- package/dist/SvMultiSelect.svelte +281 -281
- package/dist/SvNumberInput.svelte +168 -168
- package/dist/SvOtpInput.svelte +158 -158
- package/dist/SvPasswordInput.svelte +151 -151
- package/dist/SvPhoneInput.svelte +133 -131
- package/dist/SvPopover.svelte +197 -197
- package/dist/SvRadioGroup.svelte +107 -107
- package/dist/SvRowGroupPanel.svelte +170 -170
- package/dist/SvScrollArea.svelte +61 -61
- package/dist/SvSlider.svelte +200 -200
- package/dist/SvSwitchButton.svelte +107 -107
- package/dist/SvTagsInput.svelte +112 -112
- 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 +204 -204
- package/dist/SvTree.svelte +437 -437
- package/dist/SvTreeSelect.svelte +233 -233
- package/dist/cdn/GridMenus-Da_p7SKS.js +486 -0
- package/dist/cdn/GridMenus-Ds3OpzZT.js +490 -0
- package/dist/cdn/{SvDateTimePicker-B1sDc9Pz.js → SvDateTimePicker-B8GopjhC.js} +2 -2
- package/dist/cdn/{SvDateTimePicker-Bh0FTs1E.js → SvDateTimePicker-DB0aIEk8.js} +2 -2
- package/dist/cdn/{SvGridChart-Bgk4hDs8.js → SvGridChart-BcLCYvw9.js} +1 -1
- package/dist/cdn/{SvGridChart-BKQsO5fZ.js → SvGridChart-BhnFdLd0.js} +1 -1
- package/dist/cdn/{SvGridChartPanel-DkCc2Z7b.js → SvGridChartPanel-Cmx9DHAS.js} +1 -1
- package/dist/cdn/{SvGridChartPanel-DSkHIQDd.js → SvGridChartPanel-DvjwxLvK.js} +1 -1
- package/dist/cdn/{SvGridChartView-BrbVaELm.js → SvGridChartView-B40DTJSX.js} +1 -1
- package/dist/cdn/{SvGridChartView-i9rWvSKg.js → SvGridChartView-PwqI4s02.js} +1 -1
- package/dist/cdn/{SvGridDropdown-D08sVmAy.js → SvGridDropdown-DjALc_4f.js} +1 -1
- package/dist/cdn/{SvGridDropdown-CGxxopQ4.js → SvGridDropdown-hpnHvOGt.js} +1 -1
- package/dist/cdn/{src-cgs1wcUS.js → src-BLJfdyL0.js} +4034 -3889
- package/dist/cdn/{src-Cw8d9U8j.js → src-DYDCiC0D.js} +7053 -6908
- package/dist/cdn/svgrid.js +9 -9
- package/dist/cdn/svgrid.svelte-external.js +9 -9
- package/dist/chart-export.js +8 -8
- package/dist/column-groups.js +8 -10
- package/dist/column-id.d.ts +9 -0
- package/dist/column-id.js +25 -0
- package/dist/core.js +2 -1
- package/dist/createListbox.svelte.d.ts +15 -2
- package/dist/createListbox.svelte.js +27 -6
- package/dist/editing.d.ts +1 -0
- package/dist/editing.js +45 -10
- package/dist/filtering/excel-filters.d.ts +30 -10
- package/dist/filtering/excel-filters.js +96 -15
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/menus.d.ts +2 -0
- package/dist/menus.js +25 -0
- package/dist/row-resize.js +40 -0
- package/dist/selection.js +1 -1
- package/package.json +10 -10
- package/src/FlexRender.svelte +96 -96
- package/src/GridFooter.svelte +179 -179
- package/src/GridMenus.svelte +782 -734
- package/src/SvAutoComplete.svelte +165 -165
- package/src/SvCalendar.svelte +503 -503
- package/src/SvCalendar.test.ts +226 -226
- package/src/SvCarousel.svelte +141 -141
- package/src/SvCheckBox.svelte +101 -101
- package/src/SvColorInput.svelte +181 -181
- package/src/SvComboBox.svelte +275 -275
- package/src/SvContextMenu.svelte +116 -116
- package/src/SvCountryInput.svelte +160 -160
- package/src/SvDrawer.svelte +254 -250
- package/src/SvDrawer.test.ts +18 -1
- package/src/SvDropDownList.svelte +374 -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 +3335 -3239
- package/src/SvGrid.css +2704 -2723
- package/src/SvGrid.svelte +3361 -3333
- package/src/SvGrid.types.ts +1628 -1613
- package/src/SvGridChart.svelte +1724 -1724
- package/src/SvGridChartPanel.svelte +485 -485
- package/src/SvGridChartView.svelte +70 -70
- package/src/SvGridDropdown.svelte +696 -696
- package/src/SvGridSelect.svelte +259 -259
- package/src/SvGroupCell.svelte +109 -109
- package/src/SvListBox.svelte +326 -287
- 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 +281 -281
- package/src/SvNavPane.test.ts +15 -4
- package/src/SvNumberInput.svelte +168 -168
- package/src/SvOtpInput.svelte +158 -158
- package/src/SvPasswordInput.svelte +151 -151
- package/src/SvPhoneInput.svelte +133 -131
- package/src/SvPopover.svelte +197 -197
- package/src/SvRadioGroup.svelte +107 -107
- package/src/SvRowGroupPanel.svelte +170 -170
- package/src/SvScrollArea.svelte +61 -61
- package/src/SvSlider.svelte +200 -200
- package/src/SvSwitchButton.svelte +107 -107
- package/src/SvTagsInput.svelte +112 -112
- 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 +204 -204
- package/src/SvTree.svelte +437 -437
- package/src/SvTreeSelect.svelte +233 -233
- 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 +1391 -1391
- package/src/build-api.coverage.test.ts +633 -633
- package/src/build-api.ts +895 -895
- package/src/builtin-editors.grid.test.ts +85 -85
- package/src/cell-formatting.ts +169 -169
- package/src/cell-render.ts +469 -469
- package/src/chart-export.ts +201 -201
- package/src/chart-view.svelte.ts +36 -36
- package/src/chart.ts +2296 -2296
- package/src/collaboration.test.ts +104 -104
- package/src/collaboration.ts +167 -167
- package/src/column-groups.test.ts +48 -0
- package/src/column-groups.ts +8 -10
- package/src/column-id.ts +30 -0
- package/src/core.performance.test.ts +30 -30
- package/src/core.ts +1112 -1111
- 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 +39 -7
- package/src/createMenu.svelte.ts +225 -225
- 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 +319 -319
- package/src/editing.test.ts +949 -859
- package/src/editing.ts +709 -675
- package/src/editor-contract.ts +157 -157
- package/src/export-data-api.test.ts +126 -126
- package/src/export-format.test.ts +107 -107
- package/src/export-format.ts +598 -598
- package/src/filter-operators.ts +157 -157
- package/src/filtering/excel-filters.test.ts +44 -1
- package/src/filtering/excel-filters.ts +91 -14
- package/src/flex-render.ts +3 -3
- package/src/form-field.ts +127 -127
- package/src/index.ts +715 -713
- package/src/js-scroller.svelte.ts +173 -173
- 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 +172 -172
- package/src/menus.test.ts +736 -700
- package/src/menus.ts +679 -652
- package/src/merge-objects.ts +48 -48
- package/src/overlays.test.ts +90 -90
- package/src/render-component.ts +28 -28
- package/src/row-resize.test.ts +100 -0
- package/src/row-resize.ts +36 -0
- package/src/scheduler-model.test.ts +562 -562
- package/src/scheduler-model.ts +872 -872
- package/src/selection.test.ts +754 -754
- package/src/selection.ts +600 -600
- package/src/server-data-source.test.ts +289 -289
- package/src/server-data-source.ts +413 -413
- package/src/sparkline.test.ts +68 -68
- package/src/sparkline.ts +169 -169
- package/src/spreadsheet.test.ts +489 -489
- package/src/spreadsheet.ts +312 -312
- package/src/static-functions.ts +11 -11
- package/src/subscribe.ts +38 -38
- package/src/svgrid-wrapper.types.ts +524 -524
- package/src/svgrid.api-extensions.test.ts +8 -3
- package/src/svgrid.behavior.test.ts +706 -706
- package/src/svgrid.cell-dom-ids.test.ts +96 -0
- package/src/svgrid.charting.test.ts +534 -534
- package/src/svgrid.conditional-stat-scope.test.ts +111 -0
- package/src/svgrid.features.test.ts +157 -157
- package/src/svgrid.filter-menu-listbox.svelte.test.ts +308 -0
- package/src/svgrid.filter-menu-scroll.test.ts +3 -1
- package/src/svgrid.group-header-ids.test.ts +121 -0
- package/src/svgrid.new-features.wrapper.test.ts +251 -251
- package/src/svgrid.wrapper.test.ts +40 -40
- package/src/test-setup.ts +62 -62
- package/src/themes/index.ts +215 -215
- package/src/toast-store.svelte.ts +246 -246
- package/src/toast-store.test.ts +147 -147
- package/src/ui-buttons.test.ts +144 -144
- package/src/ui-inputs.test.ts +118 -118
- 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 +296 -296
- package/themes/ember.css +71 -71
- package/dist/cdn/GridMenus-BzxEYNtW.js +0 -421
- package/dist/cdn/GridMenus-Dr-nGEXn.js +0 -417
package/src/merge-objects.ts
CHANGED
|
@@ -1,48 +1,48 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Merges objects together while keeping their getters alive.
|
|
3
|
-
* Taken from SolidJS:
|
|
4
|
-
* https://github.com/solidjs/solid/blob/24abc825c0996fd2bc8c1de1491efe9a7e743aff/packages/solid/src/server/rendering.ts#L82-L115
|
|
5
|
-
*/
|
|
6
|
-
export function mergeObjects<T>(source: T): T
|
|
7
|
-
export function mergeObjects<T, U>(source: T, source1: U): T & U
|
|
8
|
-
export function mergeObjects<T, U, V>(
|
|
9
|
-
source: T,
|
|
10
|
-
source1: U,
|
|
11
|
-
source2: V,
|
|
12
|
-
): T & U & V
|
|
13
|
-
export function mergeObjects<T, U, V, W>(
|
|
14
|
-
source: T,
|
|
15
|
-
source1: U,
|
|
16
|
-
source2: V,
|
|
17
|
-
source3: W,
|
|
18
|
-
): T & U & V & W
|
|
19
|
-
export function mergeObjects(...sources: any): any {
|
|
20
|
-
const target = {}
|
|
21
|
-
|
|
22
|
-
for (let source of sources) {
|
|
23
|
-
if (typeof source === 'function') source = source()
|
|
24
|
-
|
|
25
|
-
if (source) {
|
|
26
|
-
const descriptors = Object.getOwnPropertyDescriptors(source)
|
|
27
|
-
|
|
28
|
-
for (const key in descriptors) {
|
|
29
|
-
if (key in target) continue
|
|
30
|
-
|
|
31
|
-
Object.defineProperty(target, key, {
|
|
32
|
-
enumerable: true,
|
|
33
|
-
get() {
|
|
34
|
-
for (let i = sources.length - 1; i >= 0; i--) {
|
|
35
|
-
let scopedSource = sources[i]
|
|
36
|
-
if (typeof scopedSource === 'function') scopedSource = scopedSource()
|
|
37
|
-
|
|
38
|
-
const value = (scopedSource || {})[key]
|
|
39
|
-
if (value !== undefined) return value
|
|
40
|
-
}
|
|
41
|
-
},
|
|
42
|
-
})
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
return target
|
|
48
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Merges objects together while keeping their getters alive.
|
|
3
|
+
* Taken from SolidJS:
|
|
4
|
+
* https://github.com/solidjs/solid/blob/24abc825c0996fd2bc8c1de1491efe9a7e743aff/packages/solid/src/server/rendering.ts#L82-L115
|
|
5
|
+
*/
|
|
6
|
+
export function mergeObjects<T>(source: T): T
|
|
7
|
+
export function mergeObjects<T, U>(source: T, source1: U): T & U
|
|
8
|
+
export function mergeObjects<T, U, V>(
|
|
9
|
+
source: T,
|
|
10
|
+
source1: U,
|
|
11
|
+
source2: V,
|
|
12
|
+
): T & U & V
|
|
13
|
+
export function mergeObjects<T, U, V, W>(
|
|
14
|
+
source: T,
|
|
15
|
+
source1: U,
|
|
16
|
+
source2: V,
|
|
17
|
+
source3: W,
|
|
18
|
+
): T & U & V & W
|
|
19
|
+
export function mergeObjects(...sources: any): any {
|
|
20
|
+
const target = {}
|
|
21
|
+
|
|
22
|
+
for (let source of sources) {
|
|
23
|
+
if (typeof source === 'function') source = source()
|
|
24
|
+
|
|
25
|
+
if (source) {
|
|
26
|
+
const descriptors = Object.getOwnPropertyDescriptors(source)
|
|
27
|
+
|
|
28
|
+
for (const key in descriptors) {
|
|
29
|
+
if (key in target) continue
|
|
30
|
+
|
|
31
|
+
Object.defineProperty(target, key, {
|
|
32
|
+
enumerable: true,
|
|
33
|
+
get() {
|
|
34
|
+
for (let i = sources.length - 1; i >= 0; i--) {
|
|
35
|
+
let scopedSource = sources[i]
|
|
36
|
+
if (typeof scopedSource === 'function') scopedSource = scopedSource()
|
|
37
|
+
|
|
38
|
+
const value = (scopedSource || {})[key]
|
|
39
|
+
if (value !== undefined) return value
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return target
|
|
48
|
+
}
|
package/src/overlays.test.ts
CHANGED
|
@@ -1,90 +1,90 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Component tests for the overlay trio: SvModal (dialog semantics, Escape /
|
|
3
|
-
* backdrop close), SvPopover (click open + outside close) and SvTooltip
|
|
4
|
-
* (focus-triggered, aria-describedby wiring).
|
|
5
|
-
*/
|
|
6
|
-
import { describe, expect, it, vi, afterEach } from 'vitest'
|
|
7
|
-
import { mount, unmount, flushSync, createRawSnippet } from 'svelte'
|
|
8
|
-
import SvModal from './SvModal.svelte'
|
|
9
|
-
import SvPopover from './SvPopover.svelte'
|
|
10
|
-
import SvTooltip from './SvTooltip.svelte'
|
|
11
|
-
|
|
12
|
-
const body = createRawSnippet(() => ({ render: () => `<p class="body">hello</p>` }))
|
|
13
|
-
const anchorBtn = createRawSnippet(() => ({ render: () => `<button class="anchor">open</button>` }))
|
|
14
|
-
|
|
15
|
-
function mountC(Comp: any, props: Record<string, unknown>) {
|
|
16
|
-
const target = document.createElement('div')
|
|
17
|
-
document.body.appendChild(target)
|
|
18
|
-
const app = mount(Comp, { target, props: props as any })
|
|
19
|
-
flushSync()
|
|
20
|
-
return { target, destroy: () => { unmount(app); target.remove() } }
|
|
21
|
-
}
|
|
22
|
-
afterEach(() => {
|
|
23
|
-
document.querySelectorAll('.sv-modal__backdrop, .sv-pop__panel, .sv-tip').forEach((n) => n.remove())
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
describe('SvModal', () => {
|
|
27
|
-
it('renders a labelled modal dialog when open', () => {
|
|
28
|
-
const { destroy } = mountC(SvModal, { open: true, title: 'Edit', children: body })
|
|
29
|
-
try {
|
|
30
|
-
const dialog = document.querySelector<HTMLElement>('.sv-modal')!
|
|
31
|
-
expect(dialog.getAttribute('role')).toBe('dialog')
|
|
32
|
-
expect(dialog.getAttribute('aria-modal')).toBe('true')
|
|
33
|
-
const titleId = dialog.getAttribute('aria-labelledby')!
|
|
34
|
-
expect(document.getElementById(titleId)!.textContent).toBe('Edit')
|
|
35
|
-
expect(document.querySelector('.sv-modal .body')!.textContent).toBe('hello')
|
|
36
|
-
} finally { destroy() }
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
it('Escape and the close button both fire onClose', () => {
|
|
40
|
-
let closed = 0
|
|
41
|
-
const { destroy } = mountC(SvModal, { open: true, title: 'X', onClose: () => (closed++), children: body })
|
|
42
|
-
try {
|
|
43
|
-
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }))
|
|
44
|
-
flushSync()
|
|
45
|
-
expect(closed).toBe(1)
|
|
46
|
-
} finally { destroy() }
|
|
47
|
-
})
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
describe('SvPopover', () => {
|
|
51
|
-
it('opens the panel on anchor click and closes on outside pointerdown', () => {
|
|
52
|
-
const { target, destroy } = mountC(SvPopover, { anchor: anchorBtn, children: body })
|
|
53
|
-
try {
|
|
54
|
-
expect(document.querySelector('.sv-pop__panel')).toBeNull()
|
|
55
|
-
target.querySelector<HTMLElement>('.anchor')!.click()
|
|
56
|
-
flushSync()
|
|
57
|
-
const panel = document.querySelector<HTMLElement>('.sv-pop__panel')!
|
|
58
|
-
expect(panel).not.toBeNull()
|
|
59
|
-
expect(panel.getAttribute('role')).toBe('dialog')
|
|
60
|
-
// Outside click closes.
|
|
61
|
-
document.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true }))
|
|
62
|
-
flushSync()
|
|
63
|
-
expect(document.querySelector('.sv-pop__panel')).toBeNull()
|
|
64
|
-
} finally { destroy() }
|
|
65
|
-
})
|
|
66
|
-
})
|
|
67
|
-
|
|
68
|
-
describe('SvTooltip', () => {
|
|
69
|
-
it('shows after the delay on focus and wires aria-describedby', () => {
|
|
70
|
-
vi.useFakeTimers()
|
|
71
|
-
try {
|
|
72
|
-
const child = createRawSnippet(() => ({ render: () => `<button class="t">i</button>` }))
|
|
73
|
-
const { target, destroy } = mountC(SvTooltip, { text: 'Tip text', delay: 200, children: child })
|
|
74
|
-
const anchor = target.querySelector<HTMLElement>('.sv-tip__anchor')!
|
|
75
|
-
anchor.dispatchEvent(new FocusEvent('focusin', { bubbles: true }))
|
|
76
|
-
vi.advanceTimersByTime(210)
|
|
77
|
-
flushSync()
|
|
78
|
-
const tip = document.querySelector<HTMLElement>('.sv-tip')!
|
|
79
|
-
expect(tip).not.toBeNull()
|
|
80
|
-
expect(tip.getAttribute('role')).toBe('tooltip')
|
|
81
|
-
expect(anchor.getAttribute('aria-describedby')).toBe(tip.id)
|
|
82
|
-
// The wrapper span is never itself focusable, so AT users only ever land
|
|
83
|
-
// on the real interactive child below - it must carry the description
|
|
84
|
-
// too, or it's never announced on focus (see SvTooltip.svelte effect).
|
|
85
|
-
const focusEl = target.querySelector<HTMLElement>('.t')!
|
|
86
|
-
expect(focusEl.getAttribute('aria-describedby')).toBe(tip.id)
|
|
87
|
-
destroy()
|
|
88
|
-
} finally { vi.useRealTimers() }
|
|
89
|
-
})
|
|
90
|
-
})
|
|
1
|
+
/**
|
|
2
|
+
* Component tests for the overlay trio: SvModal (dialog semantics, Escape /
|
|
3
|
+
* backdrop close), SvPopover (click open + outside close) and SvTooltip
|
|
4
|
+
* (focus-triggered, aria-describedby wiring).
|
|
5
|
+
*/
|
|
6
|
+
import { describe, expect, it, vi, afterEach } from 'vitest'
|
|
7
|
+
import { mount, unmount, flushSync, createRawSnippet } from 'svelte'
|
|
8
|
+
import SvModal from './SvModal.svelte'
|
|
9
|
+
import SvPopover from './SvPopover.svelte'
|
|
10
|
+
import SvTooltip from './SvTooltip.svelte'
|
|
11
|
+
|
|
12
|
+
const body = createRawSnippet(() => ({ render: () => `<p class="body">hello</p>` }))
|
|
13
|
+
const anchorBtn = createRawSnippet(() => ({ render: () => `<button class="anchor">open</button>` }))
|
|
14
|
+
|
|
15
|
+
function mountC(Comp: any, props: Record<string, unknown>) {
|
|
16
|
+
const target = document.createElement('div')
|
|
17
|
+
document.body.appendChild(target)
|
|
18
|
+
const app = mount(Comp, { target, props: props as any })
|
|
19
|
+
flushSync()
|
|
20
|
+
return { target, destroy: () => { unmount(app); target.remove() } }
|
|
21
|
+
}
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
document.querySelectorAll('.sv-modal__backdrop, .sv-pop__panel, .sv-tip').forEach((n) => n.remove())
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
describe('SvModal', () => {
|
|
27
|
+
it('renders a labelled modal dialog when open', () => {
|
|
28
|
+
const { destroy } = mountC(SvModal, { open: true, title: 'Edit', children: body })
|
|
29
|
+
try {
|
|
30
|
+
const dialog = document.querySelector<HTMLElement>('.sv-modal')!
|
|
31
|
+
expect(dialog.getAttribute('role')).toBe('dialog')
|
|
32
|
+
expect(dialog.getAttribute('aria-modal')).toBe('true')
|
|
33
|
+
const titleId = dialog.getAttribute('aria-labelledby')!
|
|
34
|
+
expect(document.getElementById(titleId)!.textContent).toBe('Edit')
|
|
35
|
+
expect(document.querySelector('.sv-modal .body')!.textContent).toBe('hello')
|
|
36
|
+
} finally { destroy() }
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('Escape and the close button both fire onClose', () => {
|
|
40
|
+
let closed = 0
|
|
41
|
+
const { destroy } = mountC(SvModal, { open: true, title: 'X', onClose: () => (closed++), children: body })
|
|
42
|
+
try {
|
|
43
|
+
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }))
|
|
44
|
+
flushSync()
|
|
45
|
+
expect(closed).toBe(1)
|
|
46
|
+
} finally { destroy() }
|
|
47
|
+
})
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
describe('SvPopover', () => {
|
|
51
|
+
it('opens the panel on anchor click and closes on outside pointerdown', () => {
|
|
52
|
+
const { target, destroy } = mountC(SvPopover, { anchor: anchorBtn, children: body })
|
|
53
|
+
try {
|
|
54
|
+
expect(document.querySelector('.sv-pop__panel')).toBeNull()
|
|
55
|
+
target.querySelector<HTMLElement>('.anchor')!.click()
|
|
56
|
+
flushSync()
|
|
57
|
+
const panel = document.querySelector<HTMLElement>('.sv-pop__panel')!
|
|
58
|
+
expect(panel).not.toBeNull()
|
|
59
|
+
expect(panel.getAttribute('role')).toBe('dialog')
|
|
60
|
+
// Outside click closes.
|
|
61
|
+
document.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true }))
|
|
62
|
+
flushSync()
|
|
63
|
+
expect(document.querySelector('.sv-pop__panel')).toBeNull()
|
|
64
|
+
} finally { destroy() }
|
|
65
|
+
})
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
describe('SvTooltip', () => {
|
|
69
|
+
it('shows after the delay on focus and wires aria-describedby', () => {
|
|
70
|
+
vi.useFakeTimers()
|
|
71
|
+
try {
|
|
72
|
+
const child = createRawSnippet(() => ({ render: () => `<button class="t">i</button>` }))
|
|
73
|
+
const { target, destroy } = mountC(SvTooltip, { text: 'Tip text', delay: 200, children: child })
|
|
74
|
+
const anchor = target.querySelector<HTMLElement>('.sv-tip__anchor')!
|
|
75
|
+
anchor.dispatchEvent(new FocusEvent('focusin', { bubbles: true }))
|
|
76
|
+
vi.advanceTimersByTime(210)
|
|
77
|
+
flushSync()
|
|
78
|
+
const tip = document.querySelector<HTMLElement>('.sv-tip')!
|
|
79
|
+
expect(tip).not.toBeNull()
|
|
80
|
+
expect(tip.getAttribute('role')).toBe('tooltip')
|
|
81
|
+
expect(anchor.getAttribute('aria-describedby')).toBe(tip.id)
|
|
82
|
+
// The wrapper span is never itself focusable, so AT users only ever land
|
|
83
|
+
// on the real interactive child below - it must carry the description
|
|
84
|
+
// too, or it's never announced on focus (see SvTooltip.svelte effect).
|
|
85
|
+
const focusEl = target.querySelector<HTMLElement>('.t')!
|
|
86
|
+
expect(focusEl.getAttribute('aria-describedby')).toBe(tip.id)
|
|
87
|
+
destroy()
|
|
88
|
+
} finally { vi.useRealTimers() }
|
|
89
|
+
})
|
|
90
|
+
})
|
package/src/render-component.ts
CHANGED
|
@@ -1,28 +1,28 @@
|
|
|
1
|
-
import type { Component, ComponentProps, Snippet } from 'svelte'
|
|
2
|
-
|
|
3
|
-
export class RenderComponentConfig<TComponent extends Component> {
|
|
4
|
-
constructor(
|
|
5
|
-
public component: TComponent,
|
|
6
|
-
public props?: ComponentProps<TComponent> | Record<string, never>,
|
|
7
|
-
) {}
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export class RenderSnippetConfig<TProps> {
|
|
11
|
-
constructor(
|
|
12
|
-
public snippet: Snippet<[TProps]>,
|
|
13
|
-
public params?: TProps,
|
|
14
|
-
) {}
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export const renderComponent = <
|
|
18
|
-
TComponent extends Component<any>,
|
|
19
|
-
TProps extends ComponentProps<TComponent>,
|
|
20
|
-
>(
|
|
21
|
-
component: TComponent,
|
|
22
|
-
props?: TProps,
|
|
23
|
-
) => new RenderComponentConfig(component, props)
|
|
24
|
-
|
|
25
|
-
export const renderSnippet = <TProps>(
|
|
26
|
-
snippet: Snippet<[TProps]>,
|
|
27
|
-
params?: TProps,
|
|
28
|
-
) => new RenderSnippetConfig(snippet, params)
|
|
1
|
+
import type { Component, ComponentProps, Snippet } from 'svelte'
|
|
2
|
+
|
|
3
|
+
export class RenderComponentConfig<TComponent extends Component> {
|
|
4
|
+
constructor(
|
|
5
|
+
public component: TComponent,
|
|
6
|
+
public props?: ComponentProps<TComponent> | Record<string, never>,
|
|
7
|
+
) {}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class RenderSnippetConfig<TProps> {
|
|
11
|
+
constructor(
|
|
12
|
+
public snippet: Snippet<[TProps]>,
|
|
13
|
+
public params?: TProps,
|
|
14
|
+
) {}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const renderComponent = <
|
|
18
|
+
TComponent extends Component<any>,
|
|
19
|
+
TProps extends ComponentProps<TComponent>,
|
|
20
|
+
>(
|
|
21
|
+
component: TComponent,
|
|
22
|
+
props?: TProps,
|
|
23
|
+
) => new RenderComponentConfig(component, props)
|
|
24
|
+
|
|
25
|
+
export const renderSnippet = <TProps>(
|
|
26
|
+
snippet: Snippet<[TProps]>,
|
|
27
|
+
params?: TProps,
|
|
28
|
+
) => new RenderSnippetConfig(snippet, params)
|
package/src/row-resize.test.ts
CHANGED
|
@@ -392,3 +392,103 @@ describe('rowResize - update + destroy', () => {
|
|
|
392
392
|
action.destroy()
|
|
393
393
|
})
|
|
394
394
|
})
|
|
395
|
+
|
|
396
|
+
// ---------------------------------------------------------------------------
|
|
397
|
+
// Keyboard resize (#79)
|
|
398
|
+
// ---------------------------------------------------------------------------
|
|
399
|
+
|
|
400
|
+
describe('rowResize - keyboard', () => {
|
|
401
|
+
function key(k: string, init: Partial<KeyboardEvent> = {}): KeyboardEvent {
|
|
402
|
+
return new KeyboardEvent('keydown', { key: k, bubbles: true, cancelable: true, ...init })
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
it('makes the strip focusable', () => {
|
|
406
|
+
const { host, rows } = buildGrid([{}])
|
|
407
|
+
track(host)
|
|
408
|
+
const action = rowResize(host, { onResize: vi.fn() })
|
|
409
|
+
expect(stripOf(rows[0]!)!.tabIndex).toBe(0)
|
|
410
|
+
action.destroy()
|
|
411
|
+
})
|
|
412
|
+
|
|
413
|
+
it('ArrowDown grows the row by 10px and reports the new height', () => {
|
|
414
|
+
const { host, rows } = buildGrid([{ rowIndex: 0 }])
|
|
415
|
+
track(host)
|
|
416
|
+
mockRowHeight(rows[0]!, 32)
|
|
417
|
+
const onResize = vi.fn()
|
|
418
|
+
const action = rowResize(host, { onResize })
|
|
419
|
+
stripOf(rows[0]!)!.dispatchEvent(key('ArrowDown'))
|
|
420
|
+
expect(rows[0]!.style.height).toBe('42px')
|
|
421
|
+
expect(onResize).toHaveBeenCalledWith(0, 42)
|
|
422
|
+
action.destroy()
|
|
423
|
+
})
|
|
424
|
+
|
|
425
|
+
it('ArrowUp shrinks the row and Shift gives a 1px step', () => {
|
|
426
|
+
const { host, rows } = buildGrid([{ rowIndex: 0 }])
|
|
427
|
+
track(host)
|
|
428
|
+
mockRowHeight(rows[0]!, 60)
|
|
429
|
+
const onResize = vi.fn()
|
|
430
|
+
const action = rowResize(host, { onResize })
|
|
431
|
+
const strip = stripOf(rows[0]!)!
|
|
432
|
+
strip.dispatchEvent(key('ArrowUp'))
|
|
433
|
+
expect(onResize).toHaveBeenLastCalledWith(0, 50)
|
|
434
|
+
strip.dispatchEvent(key('ArrowUp', { shiftKey: true }))
|
|
435
|
+
expect(onResize).toHaveBeenLastCalledWith(0, 59)
|
|
436
|
+
action.destroy()
|
|
437
|
+
})
|
|
438
|
+
|
|
439
|
+
it('clamps to the configured min and max', () => {
|
|
440
|
+
const { host, rows } = buildGrid([{ rowIndex: 0 }])
|
|
441
|
+
track(host)
|
|
442
|
+
mockRowHeight(rows[0]!, 42)
|
|
443
|
+
const onResize = vi.fn()
|
|
444
|
+
const action = rowResize(host, { onResize, min: 40, max: 44 })
|
|
445
|
+
const strip = stripOf(rows[0]!)!
|
|
446
|
+
strip.dispatchEvent(key('ArrowUp'))
|
|
447
|
+
expect(onResize).toHaveBeenLastCalledWith(0, 40)
|
|
448
|
+
strip.dispatchEvent(key('ArrowDown'))
|
|
449
|
+
expect(onResize).toHaveBeenLastCalledWith(0, 44)
|
|
450
|
+
action.destroy()
|
|
451
|
+
})
|
|
452
|
+
|
|
453
|
+
it('ignores keys other than the arrows and consumes the ones it handles', () => {
|
|
454
|
+
const { host, rows } = buildGrid([{ rowIndex: 0 }])
|
|
455
|
+
track(host)
|
|
456
|
+
mockRowHeight(rows[0]!, 32)
|
|
457
|
+
const onResize = vi.fn()
|
|
458
|
+
const action = rowResize(host, { onResize })
|
|
459
|
+
const strip = stripOf(rows[0]!)!
|
|
460
|
+
const other = key('Enter')
|
|
461
|
+
strip.dispatchEvent(other)
|
|
462
|
+
expect(onResize).not.toHaveBeenCalled()
|
|
463
|
+
expect(other.defaultPrevented).toBe(false)
|
|
464
|
+
const handled = key('ArrowDown')
|
|
465
|
+
strip.dispatchEvent(handled)
|
|
466
|
+
expect(handled.defaultPrevented).toBe(true)
|
|
467
|
+
action.destroy()
|
|
468
|
+
})
|
|
469
|
+
|
|
470
|
+
it('does nothing while disabled, and stops after destroy', () => {
|
|
471
|
+
const { host, rows } = buildGrid([{ rowIndex: 0 }])
|
|
472
|
+
track(host)
|
|
473
|
+
mockRowHeight(rows[0]!, 32)
|
|
474
|
+
const onResize = vi.fn()
|
|
475
|
+
const action = rowResize(host, { onResize, disabled: true })
|
|
476
|
+
// Disabled removes the strips entirely, so drive a hand-made one.
|
|
477
|
+
const strip = document.createElement('div')
|
|
478
|
+
strip.className = STRIP_CLASS
|
|
479
|
+
gutterOf(rows[0]!).appendChild(strip)
|
|
480
|
+
strip.dispatchEvent(key('ArrowDown'))
|
|
481
|
+
expect(onResize).not.toHaveBeenCalled()
|
|
482
|
+
|
|
483
|
+
action.update({ onResize })
|
|
484
|
+
stripOf(rows[0]!)!.dispatchEvent(key('ArrowDown'))
|
|
485
|
+
expect(onResize).toHaveBeenCalledTimes(1)
|
|
486
|
+
|
|
487
|
+
action.destroy()
|
|
488
|
+
const orphan = document.createElement('div')
|
|
489
|
+
orphan.className = STRIP_CLASS
|
|
490
|
+
gutterOf(rows[0]!).appendChild(orphan)
|
|
491
|
+
orphan.dispatchEvent(key('ArrowDown'))
|
|
492
|
+
expect(onResize).toHaveBeenCalledTimes(1)
|
|
493
|
+
})
|
|
494
|
+
})
|
package/src/row-resize.ts
CHANGED
|
@@ -81,6 +81,37 @@ export function rowResize(node: HTMLElement, opts: RowResizeOptions) {
|
|
|
81
81
|
document.body.style.cursor = ''
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
/**
|
|
85
|
+
* Keyboard resize (#79). The strip is `role="separator"` acting as a
|
|
86
|
+
* splitter, so it has to be reachable by Tab and respond to arrow keys.
|
|
87
|
+
* Up/Down shrink/grow the row, Shift gives a fine 1px step - the same
|
|
88
|
+
* contract the column resize handle uses (Left/Right, Shift = 1px).
|
|
89
|
+
*/
|
|
90
|
+
function onKeyDown(e: KeyboardEvent) {
|
|
91
|
+
if (current.disabled) return
|
|
92
|
+
const t = e.target as HTMLElement | null
|
|
93
|
+
if (!t?.classList.contains(STRIP_CLASS)) return
|
|
94
|
+
const step = e.shiftKey ? 1 : 10
|
|
95
|
+
let delta = 0
|
|
96
|
+
if (e.key === 'ArrowUp') delta = -step
|
|
97
|
+
else if (e.key === 'ArrowDown') delta = step
|
|
98
|
+
else return
|
|
99
|
+
const tr = t.closest<HTMLTableRowElement>('tr.sv-grid-row')
|
|
100
|
+
if (!tr) return
|
|
101
|
+
const rowIndex = rowIndexOf(tr)
|
|
102
|
+
if (!Number.isFinite(rowIndex)) return
|
|
103
|
+
e.preventDefault()
|
|
104
|
+
e.stopPropagation()
|
|
105
|
+
const min = current.min ?? 20
|
|
106
|
+
const max = current.max ?? 320
|
|
107
|
+
const next = Math.round(
|
|
108
|
+
Math.max(min, Math.min(max, tr.getBoundingClientRect().height + delta)),
|
|
109
|
+
)
|
|
110
|
+
tr.style.height = `${next}px`
|
|
111
|
+
current.onResizeMove?.(rowIndex, next)
|
|
112
|
+
current.onResize(rowIndex, next)
|
|
113
|
+
}
|
|
114
|
+
|
|
84
115
|
function onPointerDown(e: PointerEvent) {
|
|
85
116
|
if (current.disabled) return
|
|
86
117
|
const t = e.target as HTMLElement | null
|
|
@@ -134,6 +165,9 @@ export function rowResize(node: HTMLElement, opts: RowResizeOptions) {
|
|
|
134
165
|
strip.setAttribute('role', 'separator')
|
|
135
166
|
strip.setAttribute('aria-orientation', 'horizontal')
|
|
136
167
|
strip.setAttribute('aria-label', 'Resize row')
|
|
168
|
+
// A separator used as an interactive splitter has to be focusable so
|
|
169
|
+
// keyboard-only users can reach it and drive `onKeyDown` (#79).
|
|
170
|
+
strip.tabIndex = 0
|
|
137
171
|
// Inline the geometry + pointer-events so the strip works even
|
|
138
172
|
// before SvGrid.css is parsed; the hover tint still comes from
|
|
139
173
|
// the stylesheet. We keep the strip fully inside the gutter
|
|
@@ -152,6 +186,7 @@ export function rowResize(node: HTMLElement, opts: RowResizeOptions) {
|
|
|
152
186
|
}
|
|
153
187
|
|
|
154
188
|
node.addEventListener('pointerdown', onPointerDown, { capture: true })
|
|
189
|
+
node.addEventListener('keydown', onKeyDown, { capture: true })
|
|
155
190
|
const observer = new MutationObserver(() => decorate())
|
|
156
191
|
observer.observe(node, { childList: true, subtree: true })
|
|
157
192
|
decorate()
|
|
@@ -165,6 +200,7 @@ export function rowResize(node: HTMLElement, opts: RowResizeOptions) {
|
|
|
165
200
|
},
|
|
166
201
|
destroy() {
|
|
167
202
|
node.removeEventListener('pointerdown', onPointerDown, { capture: true })
|
|
203
|
+
node.removeEventListener('keydown', onKeyDown, { capture: true })
|
|
168
204
|
window.removeEventListener('pointermove', onPointerMove)
|
|
169
205
|
window.removeEventListener('pointerup', onPointerUp)
|
|
170
206
|
observer.disconnect()
|