@svgrid/grid 2.2.16 → 2.2.17
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 +1 -1
- package/dist/GridFooter.svelte +12 -11
- package/dist/SvAnchor.svelte +65 -0
- package/dist/SvAnchor.svelte.d.ts +24 -0
- package/dist/SvAspectRatio.svelte +38 -0
- package/dist/SvAspectRatio.svelte.d.ts +16 -0
- package/dist/SvAutoComplete.svelte +52 -7
- package/dist/SvAutoComplete.svelte.d.ts +4 -1
- package/dist/SvBlockquote.svelte +43 -0
- package/dist/SvBlockquote.svelte.d.ts +16 -0
- package/dist/SvCalendar.svelte +33 -2
- package/dist/SvCalendar.svelte.d.ts +8 -1
- package/dist/SvCheckBox.svelte +6 -2
- package/dist/SvCode.svelte +52 -0
- package/dist/SvCode.svelte.d.ts +19 -0
- package/dist/SvCollapsible.svelte +75 -0
- package/dist/SvCollapsible.svelte.d.ts +27 -0
- package/dist/SvColorInput.svelte +11 -17
- package/dist/SvColorInput.svelte.d.ts +2 -0
- package/dist/SvComboBox.svelte +85 -8
- package/dist/SvComboBox.svelte.d.ts +14 -0
- package/dist/SvCountryInput.svelte +2 -2
- package/dist/SvCountryInput.svelte.d.ts +1 -1
- package/dist/SvDropDownList.svelte +154 -51
- package/dist/SvDropDownList.svelte.d.ts +17 -3
- package/dist/SvDurationInput.svelte +59 -41
- package/dist/SvDurationInput.svelte.d.ts +18 -0
- package/dist/SvField.svelte +203 -12
- package/dist/SvField.svelte.d.ts +45 -8
- package/dist/SvForm.svelte +195 -21
- package/dist/SvForm.svelte.d.ts +23 -0
- package/dist/SvGrid.controller.svelte.d.ts +12 -1
- package/dist/SvGrid.controller.svelte.js +87 -7
- package/dist/SvGrid.svelte +383 -181
- package/dist/SvGrid.types.d.ts +123 -2
- package/dist/SvGridChartView.svelte +70 -0
- package/dist/SvGridChartView.svelte.d.ts +11 -0
- package/dist/SvGridDropdown.svelte +9 -2
- package/dist/SvGridSelect.svelte +5 -2
- package/dist/SvGridSelect.svelte.d.ts +1 -1
- package/dist/SvGroup.svelte +41 -0
- package/dist/SvGroup.svelte.d.ts +27 -0
- package/dist/SvHoverCard.svelte +70 -0
- package/dist/SvHoverCard.svelte.d.ts +41 -0
- package/dist/SvKbd.svelte +51 -0
- package/dist/SvKbd.svelte.d.ts +18 -0
- package/dist/SvList.svelte +55 -0
- package/dist/SvList.svelte.d.ts +23 -0
- package/dist/SvListBox.svelte +166 -77
- package/dist/SvListBox.svelte.d.ts +17 -4
- package/dist/SvLoadingOverlay.svelte +53 -0
- package/dist/SvLoadingOverlay.svelte.d.ts +28 -0
- package/dist/SvMark.svelte +36 -0
- package/dist/SvMark.svelte.d.ts +14 -0
- package/dist/SvMaskedInput.svelte +50 -35
- package/dist/SvMaskedInput.svelte.d.ts +17 -2
- package/dist/SvMenuList.svelte +32 -3
- package/dist/SvMenubar.svelte +200 -0
- package/dist/SvMenubar.svelte.d.ts +18 -0
- package/dist/SvMultiSelect.svelte +7 -4
- package/dist/SvMultiSelect.svelte.d.ts +1 -1
- package/dist/SvNumberInput.svelte +79 -42
- package/dist/SvNumberInput.svelte.d.ts +25 -0
- package/dist/SvOtpInput.svelte +2 -2
- package/dist/SvPasswordInput.svelte +51 -31
- package/dist/SvPasswordInput.svelte.d.ts +17 -0
- package/dist/SvPhoneInput.svelte +39 -28
- package/dist/SvPhoneInput.svelte.d.ts +4 -0
- package/dist/SvPopconfirm.svelte +79 -0
- package/dist/SvPopconfirm.svelte.d.ts +42 -0
- package/dist/SvPopover.svelte +87 -29
- package/dist/SvPopover.svelte.d.ts +18 -4
- package/dist/SvRadioGroup.svelte +5 -1
- package/dist/SvResult.svelte +73 -0
- package/dist/SvResult.svelte.d.ts +28 -0
- package/dist/SvSegmented.svelte +114 -0
- package/dist/SvSegmented.svelte.d.ts +29 -0
- package/dist/SvSimpleGrid.svelte +41 -0
- package/dist/SvSimpleGrid.svelte.d.ts +25 -0
- package/dist/SvSlider.svelte +23 -10
- package/dist/SvSpinner.svelte +43 -0
- package/dist/SvSpinner.svelte.d.ts +20 -0
- package/dist/SvStack.svelte +37 -0
- package/dist/SvStack.svelte.d.ts +24 -0
- package/dist/SvSwitchButton.svelte +6 -1
- package/dist/SvTagsInput.svelte +9 -5
- package/dist/SvText.svelte +76 -0
- package/dist/SvText.svelte.d.ts +31 -0
- package/dist/SvTextInput.svelte +75 -43
- package/dist/SvTextInput.svelte.d.ts +28 -1
- package/dist/SvTimePicker.svelte +32 -2
- package/dist/SvTimePicker.svelte.d.ts +8 -9
- package/dist/SvTitle.svelte +57 -0
- package/dist/SvTitle.svelte.d.ts +24 -0
- package/dist/SvToaster.svelte +41 -6
- package/dist/SvToggleButton.svelte +4 -1
- package/dist/SvTooltip.svelte +68 -34
- package/dist/SvTooltip.svelte.d.ts +14 -2
- package/dist/SvTree.svelte +81 -21
- package/dist/SvTree.svelte.d.ts +3 -2
- package/dist/SvTreeSelect.svelte +5 -2
- package/dist/SvTreeSelect.svelte.d.ts +1 -1
- package/dist/SvVisuallyHidden.svelte +49 -0
- package/dist/SvVisuallyHidden.svelte.d.ts +19 -0
- package/dist/ai.d.ts +289 -0
- package/dist/ai.js +1013 -0
- package/dist/board-view.svelte.d.ts +22 -0
- package/dist/board-view.svelte.js +13 -0
- package/dist/build-api.js +36 -6
- package/dist/cdn/GridMenus-0AK9O7qG.js +420 -0
- package/dist/cdn/GridMenus-BjUoCXoD.js +416 -0
- package/dist/cdn/SvDateTimePicker-CYC7gRER.js +2176 -0
- package/dist/cdn/SvDateTimePicker-DtQKd4Ns.js +2198 -0
- package/dist/cdn/SvGridChart-BcLCYvw9.js +1195 -0
- package/dist/cdn/SvGridChart-BhnFdLd0.js +1203 -0
- package/dist/cdn/SvGridChartPanel-Cmx9DHAS.js +561 -0
- package/dist/cdn/SvGridChartPanel-DvjwxLvK.js +577 -0
- package/dist/cdn/SvGridChartView-B40DTJSX.js +54 -0
- package/dist/cdn/SvGridChartView-PwqI4s02.js +53 -0
- package/dist/cdn/SvGridDropdown-DjALc_4f.js +255 -0
- package/dist/cdn/SvGridDropdown-hpnHvOGt.js +254 -0
- package/dist/cdn/chart-T1usDN0o.js +1212 -0
- package/dist/cdn/disclose-version-DoD9AFD_.js +2958 -0
- package/dist/cdn/export-format-JT5pWVT8.js +561 -0
- package/dist/cdn/popover-P-9P2YYn.js +69 -0
- package/dist/cdn/rolldown-runtime-Dy4uBu1J.js +11 -0
- package/dist/cdn/src-BLhdLmoN.js +25707 -0
- package/dist/cdn/src-BwnUVj3T.js +25623 -0
- package/dist/cdn/svgrid.js +9 -32897
- package/dist/cdn/svgrid.svelte-external.js +9 -30052
- package/dist/cell-formatting.d.ts +2 -2
- package/dist/cell-render.js +4 -4
- package/dist/chart-view.svelte.d.ts +25 -0
- package/dist/chart-view.svelte.js +14 -0
- package/dist/createCombobox.svelte.d.ts +5 -0
- package/dist/createCombobox.svelte.js +21 -5
- package/dist/createDropdownList.svelte.d.ts +3 -0
- package/dist/createDropdownList.svelte.js +8 -2
- package/dist/createForm.svelte.d.ts +18 -0
- package/dist/createForm.svelte.js +51 -6
- package/dist/createMenu.svelte.js +27 -0
- package/dist/createPopoverSelect.svelte.d.ts +3 -0
- package/dist/createPopoverSelect.svelte.js +9 -1
- package/dist/createSlider.svelte.d.ts +8 -21
- package/dist/createSlider.svelte.js +34 -0
- package/dist/createTooltip.svelte.d.ts +12 -0
- package/dist/createTooltip.svelte.js +72 -9
- package/dist/editor-contract.d.ts +15 -0
- package/dist/export-provider.d.ts +27 -0
- package/dist/export-provider.js +9 -0
- package/dist/filter-operators.d.ts +6 -0
- package/dist/filter-operators.js +33 -0
- package/dist/form-field.d.ts +45 -2
- package/dist/grid-messages.d.ts +61 -0
- package/dist/grid-messages.js +72 -0
- package/dist/group-display.d.ts +62 -0
- package/dist/group-display.js +78 -0
- package/dist/index.d.ts +36 -4
- package/dist/index.js +43 -2
- package/dist/js-scroller.svelte.d.ts +35 -0
- package/dist/js-scroller.svelte.js +154 -0
- package/dist/list-option.d.ts +46 -0
- package/dist/list-option.js +53 -0
- package/dist/menus.js +25 -11
- package/dist/pivot-view.svelte.d.ts +72 -0
- package/dist/pivot-view.svelte.js +13 -0
- package/dist/positioning.d.ts +111 -0
- package/dist/positioning.js +191 -0
- package/dist/scheduler-model.d.ts +3 -0
- package/dist/scheduler-model.js +22 -5
- package/dist/svgrid-wrapper.types.d.ts +18 -0
- package/dist/toast-store.svelte.d.ts +54 -0
- package/dist/toast-store.svelte.js +65 -15
- package/package.json +9 -1
- package/src/GridFooter.svelte +12 -11
- package/src/SvAnchor.svelte +65 -0
- package/src/SvAspectRatio.svelte +38 -0
- package/src/SvAutoComplete.svelte +52 -7
- package/src/SvBlockquote.svelte +43 -0
- package/src/SvCalendar.svelte +33 -2
- package/src/SvCalendar.test.ts +15 -0
- package/src/SvCheckBox.svelte +6 -2
- package/src/SvCode.svelte +52 -0
- package/src/SvCollapsible.svelte +75 -0
- package/src/SvColorInput.svelte +11 -17
- package/src/SvComboBox.svelte +85 -8
- package/src/SvCountryInput.svelte +2 -2
- package/src/SvDropDownList.svelte +154 -51
- package/src/SvDurationInput.svelte +59 -41
- package/src/SvField.svelte +203 -12
- package/src/SvForm.svelte +195 -21
- package/src/SvForm.test.ts +112 -0
- package/src/SvGrid.controller.svelte.ts +88 -7
- package/src/SvGrid.svelte +383 -181
- package/src/SvGrid.types.ts +125 -1
- package/src/SvGridChartView.svelte +70 -0
- package/src/SvGridDropdown.svelte +9 -2
- package/src/SvGridSelect.svelte +5 -2
- package/src/SvGroup.svelte +41 -0
- package/src/SvHoverCard.svelte +70 -0
- package/src/SvKbd.svelte +51 -0
- package/src/SvList.svelte +55 -0
- package/src/SvListBox.svelte +166 -77
- package/src/SvLoadingOverlay.svelte +53 -0
- package/src/SvMark.svelte +36 -0
- package/src/SvMaskedInput.svelte +50 -35
- package/src/SvMenuList.svelte +32 -3
- package/src/SvMenubar.svelte +200 -0
- package/src/SvMultiSelect.svelte +7 -4
- package/src/SvNumberInput.svelte +79 -42
- package/src/SvOtpInput.svelte +2 -2
- package/src/SvPasswordInput.svelte +51 -31
- package/src/SvPhoneInput.svelte +39 -28
- package/src/SvPopconfirm.svelte +79 -0
- package/src/SvPopover.svelte +87 -29
- package/src/SvRadioGroup.svelte +5 -1
- package/src/SvResult.svelte +73 -0
- package/src/SvSegmented.svelte +114 -0
- package/src/SvSimpleGrid.svelte +41 -0
- package/src/SvSlider.svelte +23 -10
- package/src/SvSpinner.svelte +43 -0
- package/src/SvStack.svelte +37 -0
- package/src/SvSwitchButton.svelte +6 -1
- package/src/SvTagsInput.svelte +9 -5
- package/src/SvText.svelte +76 -0
- package/src/SvTextInput.svelte +75 -43
- package/src/SvTimePicker.svelte +32 -2
- package/src/SvTitle.svelte +57 -0
- package/src/SvToaster.svelte +41 -6
- package/src/SvToaster.test.ts +32 -0
- package/src/SvToggleButton.svelte +4 -1
- package/src/SvTooltip.svelte +68 -34
- package/src/SvTree.svelte +81 -21
- package/src/SvTreeSelect.svelte +5 -2
- package/src/SvVisuallyHidden.svelte +49 -0
- package/src/ai.test.ts +502 -0
- package/src/ai.ts +1391 -0
- package/src/board-view.svelte.ts +33 -0
- package/src/build-api.coverage.test.ts +101 -0
- package/src/build-api.ts +43 -18
- package/src/builtin-editors.grid.test.ts +1 -1
- package/src/cell-formatting.ts +5 -5
- package/src/cell-render.ts +4 -4
- package/src/chart-view.svelte.ts +36 -0
- package/src/createCombobox.svelte.ts +17 -5
- package/src/createDropdownList.svelte.ts +8 -2
- package/src/createForm.svelte.ts +59 -6
- package/src/createMenu.svelte.ts +23 -0
- package/src/createPopoverSelect.svelte.ts +8 -1
- package/src/createSlider.svelte.ts +20 -0
- package/src/createTooltip.svelte.ts +67 -9
- package/src/editor-contract.ts +16 -0
- package/src/export-provider.ts +40 -0
- package/src/filter-operators.ts +37 -0
- package/src/form-field.ts +42 -2
- package/src/grid-messages.test.ts +24 -0
- package/src/grid-messages.ts +137 -0
- package/src/group-display.test.ts +77 -0
- package/src/group-display.ts +117 -0
- package/src/i18n.grid.test.ts +73 -0
- package/src/index.ts +91 -2
- package/src/js-scroller.svelte.ts +173 -0
- package/src/list-option.test.ts +56 -0
- package/src/list-option.ts +81 -0
- package/src/menus.test.ts +32 -0
- package/src/menus.ts +25 -12
- package/src/pivot-view.svelte.ts +90 -0
- package/src/pivot-view.test.ts +38 -0
- package/src/pivot.grid.test.ts +102 -0
- package/src/positioning.test.ts +141 -0
- package/src/positioning.ts +266 -0
- package/src/scheduler-model.test.ts +16 -0
- package/src/scheduler-model.ts +24 -5
- package/src/svgrid-wrapper.types.ts +23 -0
- package/src/svgrid.charting.test.ts +8 -1
- package/src/svgrid.comments-autocomplete.test.ts +7 -2
- package/src/svgrid.context-menu.test.ts +8 -3
- package/src/toast-store.svelte.ts +98 -0
- package/src/toast-store.test.ts +62 -1
- package/src/ui-breadth.test.ts +138 -0
- package/src/ui-buttons.test.ts +39 -0
- package/src/ui-inputs.test.ts +2 -2
- package/src/ui-range.test.ts +9 -0
- package/src/ui-selection.test.ts +36 -0
- package/src/ui-tier1.test.ts +1 -1
- package/src/virtual.test.ts +20 -6
- package/dist/SvGridBoard.svelte +0 -2339
- package/dist/SvGridBoard.svelte.d.ts +0 -31
- package/src/SvGridBoard.svelte +0 -2339
- package/src/board.test.ts +0 -927
package/src/createMenu.svelte.ts
CHANGED
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
import type { MenuItem } from './menu-item'
|
|
32
32
|
import type { EditorDir } from './editor-contract'
|
|
33
33
|
import { enabledIndices, wrapMove } from './list-nav'
|
|
34
|
+
import { createTypeaheadBuffer, isTypeaheadKey } from './list-option'
|
|
34
35
|
|
|
35
36
|
/** Reactive inputs are getters so the core tracks live prop changes (the same
|
|
36
37
|
* controlled pattern as `createListbox`). */
|
|
@@ -76,6 +77,22 @@ export function createMenu(config: MenuConfig) {
|
|
|
76
77
|
let active = $state(-1)
|
|
77
78
|
let openSub = $state(-1)
|
|
78
79
|
|
|
80
|
+
// Type-ahead: printable keys jump to the next item whose label starts with the
|
|
81
|
+
// accumulated buffer (WAI-ARIA menu behavior), wrapping past the active item.
|
|
82
|
+
const typeahead = createTypeaheadBuffer()
|
|
83
|
+
function typeaheadMatch(buffer: string): number {
|
|
84
|
+
const b = buffer.trim().toLowerCase()
|
|
85
|
+
if (!b) return -1
|
|
86
|
+
const its = items()
|
|
87
|
+
const n = its.length
|
|
88
|
+
for (let k = 1; k <= n; k++) {
|
|
89
|
+
const i = (active + k + n) % n
|
|
90
|
+
const it = its[i]
|
|
91
|
+
if (it && !it.disabled && !it.separator && (it.label ?? '').toLowerCase().startsWith(b)) return i
|
|
92
|
+
}
|
|
93
|
+
return -1
|
|
94
|
+
}
|
|
95
|
+
|
|
79
96
|
// Real (non-separator, non-disabled) item indices, for roving focus.
|
|
80
97
|
const focusables = $derived(
|
|
81
98
|
enabledIndices(items().length, (i) => {
|
|
@@ -153,6 +170,12 @@ export function createMenu(config: MenuConfig) {
|
|
|
153
170
|
// Tab leaves the menu entirely (WAI-ARIA menu-button pattern): close the
|
|
154
171
|
// whole tree rather than trapping focus inside a portalled panel.
|
|
155
172
|
case 'Tab': config.onClose(); break
|
|
173
|
+
default:
|
|
174
|
+
// Type-ahead: a bare printable key jumps to the matching item.
|
|
175
|
+
if (isTypeaheadKey(e)) {
|
|
176
|
+
const idx = typeaheadMatch(typeahead.push(e.key))
|
|
177
|
+
if (idx >= 0) { e.preventDefault(); focus(idx) }
|
|
178
|
+
}
|
|
156
179
|
}
|
|
157
180
|
}
|
|
158
181
|
|
|
@@ -23,6 +23,8 @@ export type PopoverSelectConfig = {
|
|
|
23
23
|
itemCount: () => number
|
|
24
24
|
/** Whether item `i` is disabled (skipped by roving, not selectable). */
|
|
25
25
|
disabled?: (index: number) => boolean
|
|
26
|
+
/** Read-only: value shown, trigger focusable, but the panel will not open. */
|
|
27
|
+
readonly?: () => boolean
|
|
26
28
|
/** Commit the item at `index` (Enter or a click the component forwards). */
|
|
27
29
|
onSelect: (index: number) => void
|
|
28
30
|
/** Notified whenever the open state changes. */
|
|
@@ -59,6 +61,7 @@ export function createPopoverSelect(config: PopoverSelectConfig) {
|
|
|
59
61
|
|
|
60
62
|
const count = () => config.itemCount()
|
|
61
63
|
const isDisabled = (i: number) => config.disabled?.(i) ?? false
|
|
64
|
+
const readonly = () => config.readonly?.() ?? false
|
|
62
65
|
|
|
63
66
|
function enabledIndices(): number[] {
|
|
64
67
|
return enabledIndicesOf(count(), isDisabled)
|
|
@@ -74,6 +77,7 @@ export function createPopoverSelect(config: PopoverSelectConfig) {
|
|
|
74
77
|
config.onOpenChange?.(v)
|
|
75
78
|
}
|
|
76
79
|
function openPanel() {
|
|
80
|
+
if (readonly()) return
|
|
77
81
|
active = clampActive(config.initialActive?.() ?? 0)
|
|
78
82
|
setOpen(true)
|
|
79
83
|
}
|
|
@@ -82,6 +86,7 @@ export function createPopoverSelect(config: PopoverSelectConfig) {
|
|
|
82
86
|
config.getTrigger()?.focus()
|
|
83
87
|
}
|
|
84
88
|
function toggle() {
|
|
89
|
+
if (readonly()) return
|
|
85
90
|
open ? setOpen(false) : openPanel()
|
|
86
91
|
}
|
|
87
92
|
|
|
@@ -92,7 +97,7 @@ export function createPopoverSelect(config: PopoverSelectConfig) {
|
|
|
92
97
|
function first() { active = enabledIndices()[0] ?? 0 }
|
|
93
98
|
function last() { active = enabledIndices().at(-1) ?? 0 }
|
|
94
99
|
function selectActive() {
|
|
95
|
-
if (count() === 0 || isDisabled(active)) return
|
|
100
|
+
if (readonly() || count() === 0 || isDisabled(active)) return
|
|
96
101
|
config.onSelect(active)
|
|
97
102
|
if (config.closeOnSelect?.() ?? true) closePanel()
|
|
98
103
|
}
|
|
@@ -129,6 +134,7 @@ export function createPopoverSelect(config: PopoverSelectConfig) {
|
|
|
129
134
|
})
|
|
130
135
|
|
|
131
136
|
function onTriggerKeydown(e: KeyboardEvent) {
|
|
137
|
+
if (readonly()) return
|
|
132
138
|
if (e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ') {
|
|
133
139
|
e.preventDefault()
|
|
134
140
|
openPanel()
|
|
@@ -185,6 +191,7 @@ export function createPopoverSelect(config: PopoverSelectConfig) {
|
|
|
185
191
|
'aria-haspopup': haspopup,
|
|
186
192
|
'aria-expanded': open,
|
|
187
193
|
'aria-controls': open ? panelId : undefined,
|
|
194
|
+
'aria-readonly': readonly() || undefined,
|
|
188
195
|
onkeydown: onTriggerKeydown,
|
|
189
196
|
}),
|
|
190
197
|
/** Attributes for the element that OWNS focus + activedescendant (the panel,
|
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
* </div>
|
|
20
20
|
* ```
|
|
21
21
|
*/
|
|
22
|
+
import { editorAria } from './editor-contract'
|
|
23
|
+
|
|
22
24
|
export type SliderValue = number | [number, number]
|
|
23
25
|
export type SliderOrientation = 'horizontal' | 'vertical'
|
|
24
26
|
export type SliderThumb = 'single' | 'lo' | 'hi'
|
|
@@ -37,6 +39,12 @@ export type SliderConfig = {
|
|
|
37
39
|
ariaLabel?: () => string | undefined
|
|
38
40
|
/** Right-to-left: mirrors horizontal pointer + Left/Right key direction. */
|
|
39
41
|
rtl?: () => boolean
|
|
42
|
+
// Editor contract (validation ARIA) - folded into thumbProps.
|
|
43
|
+
id?: () => string | undefined
|
|
44
|
+
invalid?: () => boolean | undefined
|
|
45
|
+
required?: () => boolean | undefined
|
|
46
|
+
error?: () => string | undefined
|
|
47
|
+
hint?: () => string | undefined
|
|
40
48
|
}
|
|
41
49
|
|
|
42
50
|
export function createSlider(config: SliderConfig) {
|
|
@@ -153,6 +161,15 @@ export function createSlider(config: SliderConfig) {
|
|
|
153
161
|
const vmin = which === 'hi' ? lo : min()
|
|
154
162
|
const vmax = which === 'lo' ? hi : max()
|
|
155
163
|
const label = which === 'lo' ? 'Minimum' : which === 'hi' ? 'Maximum' : (config.ariaLabel?.() ?? 'Value')
|
|
164
|
+
// Validation ARIA (invalid / required / describedby) from the editor
|
|
165
|
+
// contract; keep the thumb's own value-oriented aria-label.
|
|
166
|
+
const va = editorAria({
|
|
167
|
+
id: config.id?.(),
|
|
168
|
+
invalid: config.invalid?.(),
|
|
169
|
+
required: config.required?.(),
|
|
170
|
+
error: config.error?.(),
|
|
171
|
+
hint: config.hint?.(),
|
|
172
|
+
})
|
|
156
173
|
return {
|
|
157
174
|
role: 'slider' as const,
|
|
158
175
|
tabindex: disabled() ? -1 : 0,
|
|
@@ -162,6 +179,9 @@ export function createSlider(config: SliderConfig) {
|
|
|
162
179
|
'aria-label': label,
|
|
163
180
|
'aria-orientation': (isVert() ? 'vertical' : 'horizontal') as SliderOrientation,
|
|
164
181
|
'aria-disabled': disabled() || undefined,
|
|
182
|
+
'aria-invalid': va['aria-invalid'],
|
|
183
|
+
'aria-required': va['aria-required'],
|
|
184
|
+
'aria-describedby': va['aria-describedby'],
|
|
165
185
|
onkeydown: (e: KeyboardEvent) => onThumbKey(e, which),
|
|
166
186
|
}
|
|
167
187
|
},
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* createTooltip - the HEADLESS core behind <SvTooltip>: the hover/focus open
|
|
3
|
-
* state machine with a show delay,
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
3
|
+
* state machine with a show delay, an optional close delay, a shared delay-group
|
|
4
|
+
* (so moving between grouped tooltips skips the re-delay), Escape-to-hide, and the
|
|
5
|
+
* `aria-describedby` / `role="tooltip"` wiring - as prop-getters you spread onto
|
|
6
|
+
* your own anchor and tooltip elements. No styles, no positioning (that is a DOM
|
|
7
|
+
* concern the renderer owns), like `createListbox`.
|
|
7
8
|
*
|
|
8
9
|
* ```svelte
|
|
9
10
|
* <script lang="ts">
|
|
@@ -25,6 +26,14 @@ export type TooltipConfig = {
|
|
|
25
26
|
disabled?: () => boolean
|
|
26
27
|
/** Show delay in ms after hover/focus. Default 300. */
|
|
27
28
|
delay?: () => number
|
|
29
|
+
/** Hide delay in ms after leave/blur. Default 0. Give it room for interactive tips. */
|
|
30
|
+
closeDelay?: () => number
|
|
31
|
+
/**
|
|
32
|
+
* Delay-group id. Tooltips sharing a group open INSTANTLY while any group
|
|
33
|
+
* member is open or was open within a short grace window - so scanning a
|
|
34
|
+
* toolbar of icon buttons does not re-wait the show delay each time.
|
|
35
|
+
*/
|
|
36
|
+
group?: () => string | undefined
|
|
28
37
|
}
|
|
29
38
|
|
|
30
39
|
export type TooltipAnchorProps = {
|
|
@@ -40,20 +49,65 @@ export type TooltipProps = {
|
|
|
40
49
|
role: 'tooltip'
|
|
41
50
|
}
|
|
42
51
|
|
|
52
|
+
/** Grace window (ms) after a group tooltip closes during which the next opens instantly. */
|
|
53
|
+
const GROUP_SKIP_WINDOW = 300
|
|
54
|
+
type GroupEntry = { openCount: number; lastCloseAt: number }
|
|
55
|
+
const groupRegistry = new Map<string, GroupEntry>()
|
|
56
|
+
function groupEntry(id: string): GroupEntry {
|
|
57
|
+
let e = groupRegistry.get(id)
|
|
58
|
+
if (!e) { e = { openCount: 0, lastCloseAt: 0 }; groupRegistry.set(id, e) }
|
|
59
|
+
return e
|
|
60
|
+
}
|
|
61
|
+
const now = () => (typeof Date !== 'undefined' ? Date.now() : 0)
|
|
62
|
+
|
|
43
63
|
export function createTooltip(config: TooltipConfig = {}) {
|
|
44
64
|
const id = nextEditorId('sv-tip')
|
|
45
65
|
let open = $state(false)
|
|
46
66
|
let showTimer: ReturnType<typeof setTimeout> | undefined
|
|
67
|
+
let hideTimer: ReturnType<typeof setTimeout> | undefined
|
|
68
|
+
let counted = false // whether this instance is currently counted in its group
|
|
69
|
+
|
|
70
|
+
function groupId() { return config.group?.() }
|
|
71
|
+
|
|
72
|
+
/** Effective show delay: 0 when the group is warm (a member open / just closed). */
|
|
73
|
+
function effectiveDelay(): number {
|
|
74
|
+
const base = config.delay?.() ?? 300
|
|
75
|
+
const g = groupId()
|
|
76
|
+
if (!g) return base
|
|
77
|
+
const e = groupEntry(g)
|
|
78
|
+
if (e.openCount > 0 || now() - e.lastCloseAt < GROUP_SKIP_WINDOW) return 0
|
|
79
|
+
return base
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function markOpen() {
|
|
83
|
+
open = true
|
|
84
|
+
const g = groupId()
|
|
85
|
+
if (g && !counted) { counted = true; groupEntry(g).openCount++ }
|
|
86
|
+
}
|
|
87
|
+
function reallyHide() {
|
|
88
|
+
open = false
|
|
89
|
+
clearTimeout(showTimer)
|
|
90
|
+
clearTimeout(hideTimer)
|
|
91
|
+
const g = groupId()
|
|
92
|
+
if (g && counted) { counted = false; const e = groupEntry(g); e.openCount--; e.lastCloseAt = now() }
|
|
93
|
+
}
|
|
47
94
|
|
|
48
95
|
function show() {
|
|
49
96
|
if ((config.disabled?.() ?? false) || !(config.text?.() ?? '')) return
|
|
97
|
+
clearTimeout(hideTimer)
|
|
98
|
+
if (open) return
|
|
50
99
|
clearTimeout(showTimer)
|
|
51
|
-
showTimer = setTimeout(
|
|
100
|
+
showTimer = setTimeout(markOpen, effectiveDelay())
|
|
52
101
|
}
|
|
53
|
-
|
|
102
|
+
/** Delayed hide (honors `closeDelay`) - used by pointer/focus leave. */
|
|
103
|
+
function scheduleHide() {
|
|
54
104
|
clearTimeout(showTimer)
|
|
55
|
-
|
|
105
|
+
const cd = config.closeDelay?.() ?? 0
|
|
106
|
+
if (cd > 0 && open) { clearTimeout(hideTimer); hideTimer = setTimeout(reallyHide, cd) }
|
|
107
|
+
else reallyHide()
|
|
56
108
|
}
|
|
109
|
+
/** Immediate hide - used by Escape and imperative callers. */
|
|
110
|
+
function hide() { reallyHide() }
|
|
57
111
|
|
|
58
112
|
// Escape hides the tooltip while it is open (WAI-ARIA tooltip pattern).
|
|
59
113
|
$effect(() => {
|
|
@@ -69,14 +123,18 @@ export function createTooltip(config: TooltipConfig = {}) {
|
|
|
69
123
|
tipId: id,
|
|
70
124
|
show,
|
|
71
125
|
hide,
|
|
126
|
+
/** Keep an interactive tip open while the pointer is over it. */
|
|
127
|
+
keepOpen: show,
|
|
128
|
+
/** Delayed hide honoring `closeDelay` (pointer/focus leave). */
|
|
129
|
+
scheduleHide,
|
|
72
130
|
/** Spread onto the anchor/trigger wrapper. Also mirror `tipId` onto the real
|
|
73
131
|
* focusable child's `aria-describedby` so AT announces it on focus. */
|
|
74
132
|
anchorProps: (): TooltipAnchorProps => ({
|
|
75
133
|
'aria-describedby': open ? id : undefined,
|
|
76
134
|
onpointerenter: show,
|
|
77
|
-
onpointerleave:
|
|
135
|
+
onpointerleave: scheduleHide,
|
|
78
136
|
onfocusin: show,
|
|
79
|
-
onfocusout:
|
|
137
|
+
onfocusout: scheduleHide,
|
|
80
138
|
}),
|
|
81
139
|
/** Spread onto the tooltip element. */
|
|
82
140
|
tooltipProps: (): TooltipProps => ({ id, role: 'tooltip' }),
|
package/src/editor-contract.ts
CHANGED
|
@@ -12,6 +12,22 @@
|
|
|
12
12
|
|
|
13
13
|
export type EditorSize = 'sm' | 'md' | 'lg'
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* A small action button rendered inside a framed field (via `SvField`'s
|
|
17
|
+
* `actions`) - the generalized form of the clear / reveal buttons, for a lookup,
|
|
18
|
+
* a generate, a copy, etc. `icon` is a Svelte snippet; when absent the `label`
|
|
19
|
+
* text is shown. (Type-only Svelte import, so this module stays runtime-free.)
|
|
20
|
+
*/
|
|
21
|
+
export type EditorAction = {
|
|
22
|
+
/** Accessible name + tooltip for the button. */
|
|
23
|
+
label: string
|
|
24
|
+
/** Invoked when the button is pressed. */
|
|
25
|
+
onClick: () => void
|
|
26
|
+
/** Optional icon snippet; falls back to the `label` text. */
|
|
27
|
+
icon?: import('svelte').Snippet
|
|
28
|
+
disabled?: boolean
|
|
29
|
+
}
|
|
30
|
+
|
|
15
31
|
/** Text direction. `'auto'` defers to the surrounding document/CSS. */
|
|
16
32
|
export type EditorDir = 'ltr' | 'rtl' | 'auto'
|
|
17
33
|
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* export-provider seam. The free grid ships built-in text export (CSV / TSV /
|
|
3
|
+
* JSON via the api) but the rich Excel / PDF / HTML export engine lives in
|
|
4
|
+
* `@svgrid/enterprise`. `aiExport` (a free, built-in AI feature) plans an export
|
|
5
|
+
* and, when it needs to actually WRITE an enterprise format, delegates to the
|
|
6
|
+
* engine registered here. Enterprise registers `exportGrid` at install time; if
|
|
7
|
+
* it is absent, `aiExport` returns the plan without running the download.
|
|
8
|
+
*
|
|
9
|
+
* ```ts
|
|
10
|
+
* // in @svgrid/enterprise, at install:
|
|
11
|
+
* import { registerExportProvider } from '@svgrid/grid'
|
|
12
|
+
* import { exportGrid } from './export'
|
|
13
|
+
* registerExportProvider(exportGrid)
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
import type { SvGridApi } from './index'
|
|
17
|
+
|
|
18
|
+
export type GridExportRunOptions = {
|
|
19
|
+
format: string
|
|
20
|
+
filename?: string
|
|
21
|
+
rows?: ReadonlyArray<Record<string, unknown>>
|
|
22
|
+
groupBy?: string[]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type GridExportProvider = (
|
|
26
|
+
api: SvGridApi<any, any>,
|
|
27
|
+
opts: GridExportRunOptions,
|
|
28
|
+
) => Promise<unknown>
|
|
29
|
+
|
|
30
|
+
let provider: GridExportProvider | null = null
|
|
31
|
+
|
|
32
|
+
/** Register the export engine that runs AI-planned exports. Enterprise calls this. */
|
|
33
|
+
export function registerExportProvider(fn: GridExportProvider | null): void {
|
|
34
|
+
provider = fn
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The registered export engine, or null when @svgrid/enterprise is not installed. */
|
|
38
|
+
export function getExportProvider(): GridExportProvider | null {
|
|
39
|
+
return provider
|
|
40
|
+
}
|
package/src/filter-operators.ts
CHANGED
|
@@ -118,3 +118,40 @@ export function operatorLabelFor<TData extends RowData>(
|
|
|
118
118
|
}
|
|
119
119
|
return option.label;
|
|
120
120
|
}
|
|
121
|
+
|
|
122
|
+
/** GridMessages key for each operator's label (see grid-messages.ts). */
|
|
123
|
+
const OPERATOR_MESSAGE_KEY: Record<FilterOperator, string> = {
|
|
124
|
+
contains: "opContains",
|
|
125
|
+
notContains: "opNotContains",
|
|
126
|
+
equals: "opEquals",
|
|
127
|
+
notEquals: "opNotEquals",
|
|
128
|
+
startsWith: "opStartsWith",
|
|
129
|
+
endsWith: "opEndsWith",
|
|
130
|
+
regex: "opRegex",
|
|
131
|
+
in: "opIn",
|
|
132
|
+
notIn: "opNotIn",
|
|
133
|
+
greaterThan: "opGreaterThan",
|
|
134
|
+
lessThan: "opLessThan",
|
|
135
|
+
between: "opBetween",
|
|
136
|
+
isBlank: "opIsBlank",
|
|
137
|
+
isNotBlank: "opIsNotBlank",
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Localized operator label: honors date `Before`/`After` relabels, then the
|
|
142
|
+
* `localeText` override for the operator, falling back to the English
|
|
143
|
+
* `option.label`. `messages` is a loose string map to avoid a type cycle.
|
|
144
|
+
*/
|
|
145
|
+
export function localizeOperatorLabel<TData extends RowData>(
|
|
146
|
+
option: FilterOption,
|
|
147
|
+
column: Column<TData> | undefined,
|
|
148
|
+
messages?: Record<string, string> | null,
|
|
149
|
+
): string {
|
|
150
|
+
const editorType = column?.columnDef.editorType;
|
|
151
|
+
if (editorType === "date" || editorType === "datetime") {
|
|
152
|
+
if (option.value === "lessThan") return messages?.opBefore || "Before";
|
|
153
|
+
if (option.value === "greaterThan") return messages?.opAfter || "After";
|
|
154
|
+
}
|
|
155
|
+
const key = OPERATOR_MESSAGE_KEY[option.value];
|
|
156
|
+
return (key && messages?.[key]) || option.label;
|
|
157
|
+
}
|
package/src/form-field.ts
CHANGED
|
@@ -8,7 +8,9 @@ import type { Validator } from './validators'
|
|
|
8
8
|
*/
|
|
9
9
|
export type FormFieldType =
|
|
10
10
|
| 'text' | 'email' | 'tel' | 'textarea' | 'number' | 'password'
|
|
11
|
-
| 'select' | 'checkbox' | 'switch' | 'date' | 'color' | 'rating'
|
|
11
|
+
| 'select' | 'multiselect' | 'checkbox' | 'switch' | 'date' | 'color' | 'rating'
|
|
12
|
+
| 'radio' | 'slider' | 'tags' | 'phone' | 'country' | 'mask'
|
|
13
|
+
| 'datetime' | 'time' | 'daterange' | 'combobox' | 'file'
|
|
12
14
|
| 'array'
|
|
13
15
|
|
|
14
16
|
export type FormField = {
|
|
@@ -16,8 +18,44 @@ export type FormField = {
|
|
|
16
18
|
label: string
|
|
17
19
|
type?: FormFieldType
|
|
18
20
|
required?: boolean
|
|
21
|
+
/** Show the field but block edits (still submitted, unlike hidden fields). */
|
|
22
|
+
readonly?: boolean
|
|
23
|
+
/** Helper text shown under the control (distinct from a validation error). */
|
|
24
|
+
help?: string
|
|
19
25
|
placeholder?: string
|
|
20
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Options for select/combobox/radio/multiselect. A function makes them CASCADE:
|
|
28
|
+
* the list is derived from the current form values (e.g. cities for the picked
|
|
29
|
+
* country). Pair with `dependsOn` so the child clears when the parent changes.
|
|
30
|
+
*/
|
|
31
|
+
options?:
|
|
32
|
+
| Array<{ value: string | number; label: string; color?: string }>
|
|
33
|
+
| ((values: Record<string, any>) => Array<{ value: string | number; label: string; color?: string }>)
|
|
34
|
+
/** Field name(s) this one depends on; when a parent changes, this field's value
|
|
35
|
+
* is cleared (cascading selects). */
|
|
36
|
+
dependsOn?: string | string[]
|
|
37
|
+
/** Mask pattern for `type: 'mask'` (`#`=digit, `A`=letter, `*`=alnum). */
|
|
38
|
+
mask?: string
|
|
39
|
+
/** Numeric bounds for `type: 'number'` / `'slider'`. */
|
|
40
|
+
min?: number
|
|
41
|
+
max?: number
|
|
42
|
+
step?: number
|
|
43
|
+
precision?: number
|
|
44
|
+
/** Text affixes for `type: 'number'` (e.g. `$` / `%`). */
|
|
45
|
+
prefix?: string
|
|
46
|
+
suffix?: string
|
|
47
|
+
/** Remote option loader for `type: 'combobox'` (debounced typeahead). The
|
|
48
|
+
* current form `values` are passed so it can depend on other fields. */
|
|
49
|
+
loadOptions?: (query: string, values: Record<string, any>) => Promise<Array<{ value: string | number; label: string }>>
|
|
50
|
+
/** File input hints for `type: 'file'`. */
|
|
51
|
+
accept?: string
|
|
52
|
+
multiple?: boolean
|
|
53
|
+
/**
|
|
54
|
+
* Derive this field's value from the other form values (e.g. a total). A
|
|
55
|
+
* computed field is read-only, recomputes reactively, is not user-validated,
|
|
56
|
+
* and its derived value is included in the submitted payload.
|
|
57
|
+
*/
|
|
58
|
+
computed?: (values: Record<string, any>) => any
|
|
21
59
|
/** Declarative validation rules (see `rules` - email/pattern/min/compare...). */
|
|
22
60
|
rules?: ReadonlyArray<Validator>
|
|
23
61
|
/** Return an error message, or null/undefined when valid. */
|
|
@@ -33,6 +71,8 @@ export type FormField = {
|
|
|
33
71
|
asyncDebounce?: number
|
|
34
72
|
/** Span full width in the grid. */
|
|
35
73
|
full?: boolean
|
|
74
|
+
/** Columns to span (generalizes `full`); `2`+ widens the field in a grid. */
|
|
75
|
+
span?: number
|
|
36
76
|
/**
|
|
37
77
|
* Show the field only when this is `true` / returns `true` (default: always).
|
|
38
78
|
* A field derived from other values, e.g. `visible: (v) => v.hasAddress`.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { defaultGridMessages, resolveGridMessages } from './grid-messages'
|
|
3
|
+
|
|
4
|
+
describe('resolveGridMessages', () => {
|
|
5
|
+
it('returns the full English defaults when no overrides are given', () => {
|
|
6
|
+
expect(resolveGridMessages()).toEqual(defaultGridMessages)
|
|
7
|
+
expect(resolveGridMessages(null)).toEqual(defaultGridMessages)
|
|
8
|
+
expect(resolveGridMessages({})).toEqual(defaultGridMessages)
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
it('replaces only the provided keys, keeping English for the rest', () => {
|
|
12
|
+
const m = resolveGridMessages({ noRows: 'Aucune ligne', pageSize: 'Taille :' })
|
|
13
|
+
expect(m.noRows).toBe('Aucune ligne')
|
|
14
|
+
expect(m.pageSize).toBe('Taille :')
|
|
15
|
+
// untouched keys stay English
|
|
16
|
+
expect(m.columns).toBe('Columns')
|
|
17
|
+
expect(m.opContains).toBe('Contains')
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('falls back to the default for empty-string overrides', () => {
|
|
21
|
+
const m = resolveGridMessages({ noRows: '' })
|
|
22
|
+
expect(m.noRows).toBe(defaultGridMessages.noRows)
|
|
23
|
+
})
|
|
24
|
+
})
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Grid chrome localization. Every user-visible UI string the grid renders itself
|
|
3
|
+
* (empty state, tool panel, pager, status bar, group labels, filter operators,
|
|
4
|
+
* context-menu items, upsell notes) lives here as a flat message map with English
|
|
5
|
+
* defaults. Pass `localeText` on `<SvGrid>` to override any subset; unset keys
|
|
6
|
+
* fall back to English, so omitting `localeText` is a no-op.
|
|
7
|
+
*
|
|
8
|
+
* This mirrors the editor kit's `resolveMessages` pattern (editor-contract.ts) so
|
|
9
|
+
* chrome and editors share one localization idiom.
|
|
10
|
+
*/
|
|
11
|
+
import { resolveMessages } from './editor-contract'
|
|
12
|
+
|
|
13
|
+
/** Every localizable chrome string, grouped by area in the comments. */
|
|
14
|
+
export type GridMessages = {
|
|
15
|
+
// Empty / loading state
|
|
16
|
+
noRows: string
|
|
17
|
+
loading: string
|
|
18
|
+
// Tool panel
|
|
19
|
+
columns: string
|
|
20
|
+
filters: string
|
|
21
|
+
clearFilter: string
|
|
22
|
+
// Pagination
|
|
23
|
+
pageSize: string
|
|
24
|
+
page: string
|
|
25
|
+
of: string
|
|
26
|
+
firstPage: string
|
|
27
|
+
prevPage: string
|
|
28
|
+
nextPage: string
|
|
29
|
+
lastPage: string
|
|
30
|
+
// Status bar aggregate labels
|
|
31
|
+
statCount: string
|
|
32
|
+
statSum: string
|
|
33
|
+
statAvg: string
|
|
34
|
+
statMin: string
|
|
35
|
+
statMax: string
|
|
36
|
+
// Grouping chrome
|
|
37
|
+
group: string
|
|
38
|
+
grandTotal: string
|
|
39
|
+
total: string
|
|
40
|
+
rowsSuffix: string
|
|
41
|
+
rowSuffix: string
|
|
42
|
+
// Filter operator labels (mirror filterOperatorOptions ids)
|
|
43
|
+
opContains: string
|
|
44
|
+
opNotContains: string
|
|
45
|
+
opEquals: string
|
|
46
|
+
opNotEquals: string
|
|
47
|
+
opStartsWith: string
|
|
48
|
+
opEndsWith: string
|
|
49
|
+
opRegex: string
|
|
50
|
+
opIn: string
|
|
51
|
+
opNotIn: string
|
|
52
|
+
opGreaterThan: string
|
|
53
|
+
opLessThan: string
|
|
54
|
+
opBetween: string
|
|
55
|
+
opIsBlank: string
|
|
56
|
+
opIsNotBlank: string
|
|
57
|
+
// Date-column relabels for less/greater than
|
|
58
|
+
opBefore: string
|
|
59
|
+
opAfter: string
|
|
60
|
+
// Context menu
|
|
61
|
+
menuCopy: string
|
|
62
|
+
menuCut: string
|
|
63
|
+
menuPaste: string
|
|
64
|
+
menuClear: string
|
|
65
|
+
menuInsertRowAbove: string
|
|
66
|
+
menuInsertRowBelow: string
|
|
67
|
+
menuRemoveRow: string
|
|
68
|
+
menuRemoveColumn: string
|
|
69
|
+
menuEditComment: string
|
|
70
|
+
chartRange: string
|
|
71
|
+
// Upsell notes (enterprise views not installed)
|
|
72
|
+
pivotUpsellTitle: string
|
|
73
|
+
pivotUpsellBody: string
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** English defaults - the literal strings the grid shipped before localization. */
|
|
77
|
+
export const defaultGridMessages: GridMessages = {
|
|
78
|
+
noRows: 'No rows to display.',
|
|
79
|
+
loading: 'Loading grid data...',
|
|
80
|
+
columns: 'Columns',
|
|
81
|
+
filters: 'Filters',
|
|
82
|
+
clearFilter: 'Clear filter',
|
|
83
|
+
pageSize: 'Page Size:',
|
|
84
|
+
page: 'Page',
|
|
85
|
+
of: 'of',
|
|
86
|
+
firstPage: 'First page',
|
|
87
|
+
prevPage: 'Previous page',
|
|
88
|
+
nextPage: 'Next page',
|
|
89
|
+
lastPage: 'Last page',
|
|
90
|
+
statCount: 'Count',
|
|
91
|
+
statSum: 'Sum',
|
|
92
|
+
statAvg: 'Avg',
|
|
93
|
+
statMin: 'Min',
|
|
94
|
+
statMax: 'Max',
|
|
95
|
+
group: 'Group',
|
|
96
|
+
grandTotal: 'Grand total',
|
|
97
|
+
total: 'Total',
|
|
98
|
+
rowsSuffix: 'rows',
|
|
99
|
+
rowSuffix: 'row',
|
|
100
|
+
menuCopy: 'Copy',
|
|
101
|
+
menuCut: 'Cut',
|
|
102
|
+
menuPaste: 'Paste',
|
|
103
|
+
menuClear: 'Clear',
|
|
104
|
+
menuInsertRowAbove: 'Insert row above',
|
|
105
|
+
menuInsertRowBelow: 'Insert row below',
|
|
106
|
+
menuRemoveRow: 'Remove row',
|
|
107
|
+
menuRemoveColumn: 'Remove column',
|
|
108
|
+
menuEditComment: 'Edit comment',
|
|
109
|
+
opContains: 'Contains',
|
|
110
|
+
opNotContains: 'Not contains',
|
|
111
|
+
opEquals: 'Equals',
|
|
112
|
+
opNotEquals: 'Not equals',
|
|
113
|
+
opStartsWith: 'Starts with',
|
|
114
|
+
opEndsWith: 'Ends with',
|
|
115
|
+
opRegex: 'Regex',
|
|
116
|
+
opIn: 'In',
|
|
117
|
+
opNotIn: 'Not in',
|
|
118
|
+
opGreaterThan: 'Greater than',
|
|
119
|
+
opLessThan: 'Less than',
|
|
120
|
+
opBetween: 'Between',
|
|
121
|
+
opIsBlank: 'Blank',
|
|
122
|
+
opIsNotBlank: 'Not blank',
|
|
123
|
+
opBefore: 'Before',
|
|
124
|
+
opAfter: 'After',
|
|
125
|
+
chartRange: 'Chart selected range',
|
|
126
|
+
pivotUpsellTitle: 'Pivot mode',
|
|
127
|
+
pivotUpsellBody: 'Pivot mode is an Enterprise feature. Install @svgrid/enterprise and call enablePivot() to use it.',
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Merge a consumer's `localeText` over the English defaults. Undefined/empty
|
|
132
|
+
* values fall back to the default (via `resolveMessages`), so a partial map only
|
|
133
|
+
* replaces the keys it sets.
|
|
134
|
+
*/
|
|
135
|
+
export function resolveGridMessages(overrides?: Partial<GridMessages> | null): GridMessages {
|
|
136
|
+
return resolveMessages(defaultGridMessages, overrides)
|
|
137
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { buildAutoGroupColumns, insertGroupFooters } from './group-display'
|
|
3
|
+
|
|
4
|
+
type R = { id: string; depth: number; group?: boolean; total?: number; footer?: boolean }
|
|
5
|
+
|
|
6
|
+
const rows: R[] = [
|
|
7
|
+
{ id: 'gA', depth: 0, group: true, total: 30 },
|
|
8
|
+
{ id: 'a1', depth: 1 },
|
|
9
|
+
{ id: 'a2', depth: 1 },
|
|
10
|
+
{ id: 'gB', depth: 0, group: true, total: 20 },
|
|
11
|
+
{ id: 'b1', depth: 1 },
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
const opts = {
|
|
15
|
+
getDepth: (r: R) => r.depth,
|
|
16
|
+
isGroup: (r: R) => !!r.group,
|
|
17
|
+
makeFooter: (g: R): R => ({ id: `${g.id}__footer`, depth: g.depth, footer: true, total: g.total }),
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe('insertGroupFooters', () => {
|
|
21
|
+
it('is a no-op when no footer flags are set', () => {
|
|
22
|
+
expect(insertGroupFooters(rows, opts)).toEqual(rows)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('inserts one footer after each group, preserving order', () => {
|
|
26
|
+
const out = insertGroupFooters(rows, { ...opts, includeGroupFooter: true })
|
|
27
|
+
expect(out.map((r) => r.id)).toEqual([
|
|
28
|
+
'gA', 'a1', 'a2', 'gA__footer',
|
|
29
|
+
'gB', 'b1', 'gB__footer',
|
|
30
|
+
])
|
|
31
|
+
// footers carry the group's aggregate
|
|
32
|
+
expect(out.find((r) => r.id === 'gA__footer')!.total).toBe(30)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('closes nested groups deepest-first', () => {
|
|
36
|
+
const nested: R[] = [
|
|
37
|
+
{ id: 'g0', depth: 0, group: true, total: 100 },
|
|
38
|
+
{ id: 'g0a', depth: 1, group: true, total: 60 },
|
|
39
|
+
{ id: 'leaf1', depth: 2 },
|
|
40
|
+
{ id: 'g0b', depth: 1, group: true, total: 40 },
|
|
41
|
+
{ id: 'leaf2', depth: 2 },
|
|
42
|
+
]
|
|
43
|
+
const out = insertGroupFooters(nested, { ...opts, includeGroupFooter: true })
|
|
44
|
+
expect(out.map((r) => r.id)).toEqual([
|
|
45
|
+
'g0', 'g0a', 'leaf1', 'g0a__footer',
|
|
46
|
+
'g0b', 'leaf2', 'g0b__footer', 'g0__footer',
|
|
47
|
+
])
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('appends a grand-total footer at the end', () => {
|
|
51
|
+
const out = insertGroupFooters(rows, {
|
|
52
|
+
...opts,
|
|
53
|
+
includeGrandTotalFooter: true,
|
|
54
|
+
makeGrandTotal: () => ({ id: '__grand', depth: 0, footer: true, total: 50 }),
|
|
55
|
+
})
|
|
56
|
+
expect(out[out.length - 1]!.id).toBe('__grand')
|
|
57
|
+
})
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
describe('buildAutoGroupColumns', () => {
|
|
61
|
+
it('returns nothing for groupRows / no grouping', () => {
|
|
62
|
+
expect(buildAutoGroupColumns(['region'], 'groupRows')).toEqual({ autoColumns: [], hiddenSourceIds: new Set() })
|
|
63
|
+
expect(buildAutoGroupColumns([], 'singleColumn').autoColumns).toHaveLength(0)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('singleColumn: one combined auto column, all grouped columns hidden', () => {
|
|
67
|
+
const r = buildAutoGroupColumns(['region', 'category'], 'singleColumn')
|
|
68
|
+
expect(r.autoColumns).toEqual([{ id: '__autoGroup', field: null, level: 0 }])
|
|
69
|
+
expect([...r.hiddenSourceIds].sort()).toEqual(['category', 'region'])
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('multipleColumns: one auto column per grouped field', () => {
|
|
73
|
+
const r = buildAutoGroupColumns(['region', 'category'], 'multipleColumns')
|
|
74
|
+
expect(r.autoColumns.map((c) => c.id)).toEqual(['__group_region', '__group_category'])
|
|
75
|
+
expect(r.autoColumns.map((c) => c.field)).toEqual(['region', 'category'])
|
|
76
|
+
})
|
|
77
|
+
})
|