@svgrid/grid 2.1.21 → 2.1.23
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 +9 -2
- package/dist/DockNodeView.svelte +306 -0
- package/dist/DockNodeView.svelte.d.ts +7 -0
- package/dist/DockPopoutHost.svelte +17 -0
- package/dist/DockPopoutHost.svelte.d.ts +14 -0
- package/dist/GridMenus.svelte +83 -3
- package/dist/SvAutoComplete.svelte +5 -3
- package/dist/SvCarousel.svelte +20 -42
- package/dist/SvColorInput.svelte +4 -3
- package/dist/SvColorInput.svelte.d.ts +1 -1
- package/dist/SvComboBox.svelte +7 -3
- package/dist/SvCommand.svelte +28 -85
- package/dist/SvCountryInput.svelte +1 -1
- package/dist/SvDateRangeInput.svelte +1 -1
- package/dist/SvDateTimePicker.svelte +9 -3
- package/dist/SvDateTimePicker.svelte.d.ts +3 -0
- package/dist/SvDockLayout.svelte +244 -0
- package/dist/SvDockLayout.svelte.d.ts +77 -0
- package/dist/SvDockManager.svelte +922 -0
- package/dist/SvDockManager.svelte.d.ts +106 -0
- package/dist/SvDrawer.svelte +69 -47
- package/dist/SvDrawer.svelte.d.ts +4 -0
- package/dist/SvDropDownList.svelte +85 -13
- package/dist/SvDropDownList.svelte.d.ts +3 -0
- package/dist/SvField.svelte +21 -4
- package/dist/SvField.svelte.d.ts +1 -0
- package/dist/SvForm.svelte +201 -91
- package/dist/SvForm.svelte.d.ts +15 -22
- package/dist/SvGrid.controller.svelte.d.ts +32 -1
- package/dist/SvGrid.controller.svelte.js +156 -27
- package/dist/SvGrid.css +193 -5
- package/dist/SvGrid.svelte +357 -12
- package/dist/SvGrid.types.d.ts +452 -1
- package/dist/SvGridDropdown.svelte +8 -1
- package/dist/SvGridSelect.svelte +1 -0
- package/dist/SvListBox.svelte +36 -14
- package/dist/SvMaskedInput.svelte +4 -3
- package/dist/SvMaskedInput.svelte.d.ts +1 -1
- package/dist/SvMenu.svelte +1 -1
- package/dist/SvMenuList.svelte +23 -71
- package/dist/SvMenuList.svelte.d.ts +2 -13
- package/dist/SvModal.svelte +12 -29
- package/dist/SvMultiSelect.svelte +1 -0
- package/dist/SvNumberInput.svelte +4 -3
- package/dist/SvNumberInput.svelte.d.ts +1 -1
- package/dist/SvPagination.svelte +18 -18
- package/dist/SvPasswordInput.svelte +3 -2
- package/dist/SvPasswordInput.svelte.d.ts +1 -1
- package/dist/SvPhoneInput.svelte +3 -2
- package/dist/SvPhoneInput.svelte.d.ts +1 -1
- package/dist/SvPopover.svelte +1 -1
- package/dist/SvStepper.svelte +6 -9
- package/dist/SvTagsInput.svelte +3 -2
- package/dist/SvTagsInput.svelte.d.ts +1 -1
- package/dist/SvTooltip.svelte +12 -23
- package/dist/SvTour.svelte +32 -4
- package/dist/SvTreeSelect.svelte +1 -0
- package/dist/build-api.js +76 -54
- package/dist/cdn/svgrid.js +10907 -6444
- package/dist/cdn/svgrid.svelte-external.js +9622 -5475
- package/dist/columns.js +6 -0
- package/dist/conditional-formatting.d.ts +67 -3
- package/dist/conditional-formatting.js +124 -17
- package/dist/createCarousel.svelte.d.ts +72 -0
- package/dist/createCarousel.svelte.js +80 -0
- package/dist/createCommand.svelte.d.ts +62 -0
- package/dist/createCommand.svelte.js +109 -0
- package/dist/createForm.svelte.d.ts +62 -0
- package/dist/createForm.svelte.js +374 -0
- package/dist/createMenu.svelte.d.ts +88 -0
- package/dist/createMenu.svelte.js +153 -0
- package/dist/createOverlay.svelte.d.ts +38 -0
- package/dist/createOverlay.svelte.js +68 -0
- package/dist/createPagination.d.ts +74 -0
- package/dist/createPagination.js +71 -0
- package/dist/createPopoverSelect.svelte.js +6 -12
- package/dist/createStepper.d.ts +44 -0
- package/dist/createStepper.js +26 -0
- package/dist/createTooltip.svelte.d.ts +32 -0
- package/dist/createTooltip.svelte.js +61 -0
- package/dist/datetime/timezone.d.ts +36 -0
- package/dist/datetime/timezone.js +110 -0
- package/dist/dock-context.d.ts +52 -0
- package/dist/dock-context.js +1 -0
- package/dist/dock-manager-model.d.ts +136 -0
- package/dist/dock-manager-model.js +514 -0
- package/dist/dock-model.d.ts +105 -0
- package/dist/dock-model.js +288 -0
- package/dist/dock-popout.d.ts +30 -0
- package/dist/dock-popout.js +81 -0
- package/dist/editor-contract.d.ts +2 -0
- package/dist/filter-operators.d.ts +6 -0
- package/dist/filter-operators.js +35 -2
- package/dist/filtering/excel-filters.d.ts +18 -1
- package/dist/filtering/excel-filters.js +56 -0
- package/dist/form-field.d.ts +73 -0
- package/dist/form-field.js +15 -0
- package/dist/index.d.ts +25 -5
- package/dist/index.js +40 -4
- package/dist/list-nav.d.ts +18 -0
- package/dist/list-nav.js +30 -0
- package/dist/list-option.d.ts +2 -0
- package/dist/menu-item.d.ts +20 -0
- package/dist/menu-item.js +1 -0
- package/dist/menus.d.ts +5 -0
- package/dist/menus.js +68 -16
- package/dist/panel-resize.d.ts +25 -0
- package/dist/panel-resize.js +40 -0
- package/dist/popover.d.ts +29 -3
- package/dist/popover.js +22 -6
- package/dist/recurrence.d.ts +33 -7
- package/dist/recurrence.js +151 -13
- package/dist/row-drag.d.ts +8 -0
- package/dist/row-drag.js +11 -0
- package/dist/scheduler-ical.d.ts +30 -0
- package/dist/scheduler-ical.js +180 -0
- package/dist/scheduler-model.d.ts +269 -0
- package/dist/scheduler-model.js +602 -0
- package/dist/scheduler-view.svelte.d.ts +22 -0
- package/dist/scheduler-view.svelte.js +13 -0
- package/dist/spreadsheet.js +20 -12
- package/dist/ui-app.types.d.ts +3 -0
- package/package.json +5 -1
- package/src/DockNodeView.svelte +306 -0
- package/src/DockPopoutHost.svelte +17 -0
- package/src/GridMenus.svelte +83 -3
- package/src/SvAutoComplete.svelte +5 -3
- package/src/SvCarousel.svelte +20 -42
- package/src/SvCarousel.test.ts +68 -0
- package/src/SvColorInput.svelte +4 -3
- package/src/SvComboBox.svelte +7 -3
- package/src/SvCommand.svelte +28 -85
- package/src/SvCommand.test.ts +78 -0
- package/src/SvCountryInput.svelte +1 -1
- package/src/SvDateRangeInput.svelte +1 -1
- package/src/SvDateTimePicker.svelte +9 -3
- package/src/SvDockLayout.svelte +244 -0
- package/src/SvDockManager.svelte +922 -0
- package/src/SvDrawer.svelte +69 -47
- package/src/SvDropDownList.svelte +85 -13
- package/src/SvField.svelte +21 -4
- package/src/SvForm.svelte +201 -91
- package/src/SvForm.test.ts +299 -0
- package/src/SvGrid.controller.svelte.ts +152 -28
- package/src/SvGrid.css +193 -5
- package/src/SvGrid.svelte +357 -12
- package/src/SvGrid.types.ts +463 -1
- package/src/SvGridDropdown.svelte +8 -1
- package/src/SvGridSelect.svelte +1 -0
- package/src/SvListBox.svelte +36 -14
- package/src/SvMaskedInput.svelte +4 -3
- package/src/SvMenu.svelte +1 -1
- package/src/SvMenu.test.ts +24 -0
- package/src/SvMenuList.svelte +23 -71
- package/src/SvModal.svelte +12 -29
- package/src/SvModal.test.ts +82 -0
- package/src/SvMultiSelect.svelte +1 -0
- package/src/SvNumberInput.svelte +4 -3
- package/src/SvPagination.svelte +18 -18
- package/src/SvPasswordInput.svelte +3 -2
- package/src/SvPhoneInput.svelte +3 -2
- package/src/SvPopover.svelte +1 -1
- package/src/SvStepper.svelte +6 -9
- package/src/SvTagsInput.svelte +3 -2
- package/src/SvTooltip.svelte +12 -23
- package/src/SvTooltip.test.ts +73 -0
- package/src/SvTour.svelte +32 -4
- package/src/SvTreeSelect.svelte +1 -0
- package/src/build-api.ts +74 -51
- package/src/columns.ts +5 -0
- package/src/conditional-formatting.test.ts +104 -0
- package/src/conditional-formatting.ts +176 -19
- package/src/createCarousel.svelte.ts +104 -0
- package/src/createCommand.svelte.ts +125 -0
- package/src/createForm.svelte.ts +333 -0
- package/src/createMenu.svelte.ts +202 -0
- package/src/createOverlay.svelte.ts +101 -0
- package/src/createPagination.test.ts +65 -0
- package/src/createPagination.ts +84 -0
- package/src/createPopoverSelect.svelte.ts +5 -9
- package/src/createStepper.test.ts +62 -0
- package/src/createStepper.ts +61 -0
- package/src/createTooltip.svelte.ts +86 -0
- package/src/datetime/timezone.test.ts +69 -0
- package/src/datetime/timezone.ts +134 -0
- package/src/dock-context.ts +47 -0
- package/src/dock-manager-model.test.ts +275 -0
- package/src/dock-manager-model.ts +593 -0
- package/src/dock-model.test.ts +163 -0
- package/src/dock-model.ts +368 -0
- package/src/dock-popout.ts +94 -0
- package/src/editor-contract.ts +2 -0
- package/src/filter-operators.test.ts +59 -0
- package/src/filter-operators.ts +35 -2
- package/src/filtering/excel-filters.test.ts +86 -1
- package/src/filtering/excel-filters.ts +65 -0
- package/src/form-field.ts +87 -0
- package/src/index.ts +158 -4
- package/src/list-nav.test.ts +49 -0
- package/src/list-nav.ts +29 -0
- package/src/list-option.ts +2 -0
- package/src/menu-item.ts +21 -0
- package/src/menus.test.ts +48 -0
- package/src/menus.ts +73 -16
- package/src/panel-resize.ts +52 -0
- package/src/popover.bounds.test.ts +93 -0
- package/src/popover.ts +51 -7
- package/src/recurrence.test.ts +88 -1
- package/src/recurrence.ts +166 -15
- package/src/row-drag.ts +10 -0
- package/src/scheduler-ical.test.ts +68 -0
- package/src/scheduler-ical.ts +180 -0
- package/src/scheduler-model.test.ts +546 -0
- package/src/scheduler-model.ts +853 -0
- package/src/scheduler-view.svelte.ts +33 -0
- package/src/spreadsheet.ts +20 -12
- package/src/svgrid.effect-driven-filter.svelte.test.ts +116 -0
- package/src/ui-app.types.ts +3 -0
- package/src/virtual.test.ts +6 -4
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Component tests for SvTooltip, exercising the shared createTooltip core: it
|
|
3
|
+
* opens after the show delay on hover, hides on Escape, respects `disabled`, and
|
|
4
|
+
* wires aria-describedby onto the focusable child while open.
|
|
5
|
+
*/
|
|
6
|
+
import { describe, expect, it, beforeEach, afterEach, vi } from 'vitest'
|
|
7
|
+
import { mount, unmount, flushSync, createRawSnippet } from 'svelte'
|
|
8
|
+
import SvTooltip from './SvTooltip.svelte'
|
|
9
|
+
|
|
10
|
+
const child = createRawSnippet(() => ({ render: () => `<button class="btn">Del</button>` }))
|
|
11
|
+
|
|
12
|
+
function mountTip(props: Record<string, unknown>) {
|
|
13
|
+
const target = document.createElement('div')
|
|
14
|
+
document.body.appendChild(target)
|
|
15
|
+
const app = mount(SvTooltip, { target, props: { children: child, ...props } as any })
|
|
16
|
+
flushSync()
|
|
17
|
+
return { target, destroy: () => { unmount(app); target.remove() } }
|
|
18
|
+
}
|
|
19
|
+
const tip = () => document.body.querySelector<HTMLElement>('.sv-tip')
|
|
20
|
+
|
|
21
|
+
describe('SvTooltip', () => {
|
|
22
|
+
beforeEach(() => vi.useFakeTimers())
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
vi.useRealTimers()
|
|
25
|
+
document.querySelectorAll('.sv-tip').forEach((n) => n.remove())
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('shows after the delay on pointerenter and hides on Escape', () => {
|
|
29
|
+
const { target, destroy } = mountTip({ text: 'Delete row', delay: 300 })
|
|
30
|
+
try {
|
|
31
|
+
const anchor = target.querySelector<HTMLElement>('.sv-tip__anchor')!
|
|
32
|
+
expect(tip()).toBeNull()
|
|
33
|
+
anchor.dispatchEvent(new PointerEvent('pointerenter', { bubbles: true }))
|
|
34
|
+
// Nothing yet - still within the delay window.
|
|
35
|
+
flushSync()
|
|
36
|
+
expect(tip()).toBeNull()
|
|
37
|
+
vi.advanceTimersByTime(300)
|
|
38
|
+
flushSync()
|
|
39
|
+
const el = tip()!
|
|
40
|
+
expect(el).not.toBeNull()
|
|
41
|
+
expect(el.getAttribute('role')).toBe('tooltip')
|
|
42
|
+
expect(el.textContent).toContain('Delete row')
|
|
43
|
+
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }))
|
|
44
|
+
flushSync()
|
|
45
|
+
expect(tip()).toBeNull()
|
|
46
|
+
} finally { destroy() }
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('does not show while disabled', () => {
|
|
50
|
+
const { target, destroy } = mountTip({ text: 'x', disabled: true })
|
|
51
|
+
try {
|
|
52
|
+
target.querySelector<HTMLElement>('.sv-tip__anchor')!
|
|
53
|
+
.dispatchEvent(new PointerEvent('pointerenter', { bubbles: true }))
|
|
54
|
+
vi.advanceTimersByTime(500)
|
|
55
|
+
flushSync()
|
|
56
|
+
expect(tip()).toBeNull()
|
|
57
|
+
} finally { destroy() }
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('wires aria-describedby onto the focusable child while open', () => {
|
|
61
|
+
const { target, destroy } = mountTip({ text: 'Delete row', delay: 100 })
|
|
62
|
+
try {
|
|
63
|
+
target.querySelector<HTMLElement>('.sv-tip__anchor')!
|
|
64
|
+
.dispatchEvent(new PointerEvent('pointerenter', { bubbles: true }))
|
|
65
|
+
vi.advanceTimersByTime(100)
|
|
66
|
+
flushSync()
|
|
67
|
+
const btn = target.querySelector<HTMLElement>('.btn')!
|
|
68
|
+
const describedby = btn.getAttribute('aria-describedby')
|
|
69
|
+
expect(describedby).toBeTruthy()
|
|
70
|
+
expect(document.getElementById(describedby!)?.getAttribute('role')).toBe('tooltip')
|
|
71
|
+
} finally { destroy() }
|
|
72
|
+
})
|
|
73
|
+
})
|
package/src/SvTour.svelte
CHANGED
|
@@ -98,14 +98,34 @@
|
|
|
98
98
|
})
|
|
99
99
|
|
|
100
100
|
const POP_W = 300
|
|
101
|
+
// Measured popover height, so placement can keep the WHOLE popover on-screen
|
|
102
|
+
// (long content + tall targets used to push it off the top).
|
|
103
|
+
let popH = $state(200)
|
|
101
104
|
const pop = $derived.by(() => {
|
|
102
105
|
if (typeof window === 'undefined' || !rect) {
|
|
103
106
|
return { top: '50%', left: '50%', transform: 'translate(-50%, -50%)', place: 'center' as const }
|
|
104
107
|
}
|
|
105
|
-
const
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
108
|
+
const vw = window.innerWidth
|
|
109
|
+
const vh = window.innerHeight
|
|
110
|
+
const gap = 12
|
|
111
|
+
const h = popH || 200
|
|
112
|
+
const clampTop = (t: number) => Math.max(gap, Math.min(t, vh - h - gap))
|
|
113
|
+
const clampLeft = (l: number) => Math.max(gap, Math.min(l, vw - POP_W - gap))
|
|
114
|
+
// Prefer directly below the target, then directly above - but only when the
|
|
115
|
+
// target is short enough to leave room for the whole popover.
|
|
116
|
+
if (vh - rect.bottom >= h + gap) {
|
|
117
|
+
return { top: `${rect.bottom + 10}px`, left: `${clampLeft(rect.left)}px`, transform: 'none', place: 'bottom' as const }
|
|
118
|
+
}
|
|
119
|
+
if (rect.top >= h + gap) {
|
|
120
|
+
return { top: `${clampTop(rect.top - 10 - h)}px`, left: `${clampLeft(rect.left)}px`, transform: 'none', place: 'top' as const }
|
|
121
|
+
}
|
|
122
|
+
// Tall target (a full-height rail / canvas / inspector): sit BESIDE it,
|
|
123
|
+
// vertically centred on the target, on whichever side has room.
|
|
124
|
+
const top = clampTop(rect.top + rect.height / 2 - h / 2)
|
|
125
|
+
if (vw - rect.right >= POP_W + gap) return { top: `${top}px`, left: `${rect.right + 10}px`, transform: 'none', place: 'center' as const }
|
|
126
|
+
if (rect.left >= POP_W + gap) return { top: `${top}px`, left: `${rect.left - POP_W - 10}px`, transform: 'none', place: 'center' as const }
|
|
127
|
+
// No side room either - centre horizontally (still fully visible).
|
|
128
|
+
return { top: `${top}px`, left: `${clampLeft((vw - POP_W) / 2)}px`, transform: 'none', place: 'center' as const }
|
|
109
129
|
})
|
|
110
130
|
</script>
|
|
111
131
|
|
|
@@ -125,6 +145,7 @@
|
|
|
125
145
|
|
|
126
146
|
<div
|
|
127
147
|
bind:this={popEl}
|
|
148
|
+
bind:clientHeight={popH}
|
|
128
149
|
class="sv-tour__pop sv-tour__pop--{pop.place}"
|
|
129
150
|
style:top={pop.top}
|
|
130
151
|
style:left={pop.left}
|
|
@@ -133,6 +154,11 @@
|
|
|
133
154
|
aria-modal="true"
|
|
134
155
|
aria-label={step.title ?? 'Tour step'}
|
|
135
156
|
>
|
|
157
|
+
{#if step.image}
|
|
158
|
+
<div class="sv-tour__media">
|
|
159
|
+
{#if /^(https?:|data:|\/)/.test(step.image)}<img class="sv-tour__img" src={step.image} alt="" />{:else}{@html step.image}{/if}
|
|
160
|
+
</div>
|
|
161
|
+
{/if}
|
|
136
162
|
{#if step.title}<div class="sv-tour__title">{step.title}</div>{/if}
|
|
137
163
|
<div class="sv-tour__body">{step.content}</div>
|
|
138
164
|
<div class="sv-tour__foot">
|
|
@@ -163,6 +189,8 @@
|
|
|
163
189
|
}
|
|
164
190
|
@keyframes sv-tour-in { from { opacity: 0; transform: translateY(4px) } }
|
|
165
191
|
:global(.sv-tour__pop--top) { animation: none; }
|
|
192
|
+
:global(.sv-tour__media) { margin: -2px -4px 12px; border-radius: 9px; overflow: hidden; line-height: 0; }
|
|
193
|
+
:global(.sv-tour__media svg), :global(.sv-tour__img) { display: block; width: 100%; height: auto; }
|
|
166
194
|
:global(.sv-tour__title) { font-size: 14.5px; font-weight: 650; margin-bottom: 4px; }
|
|
167
195
|
:global(.sv-tour__body) { font-size: 13px; line-height: 1.55; color: var(--sg-fg, #334155); }
|
|
168
196
|
:global(.sv-tour__foot) { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-top: 14px; }
|
package/src/SvTreeSelect.svelte
CHANGED
package/src/build-api.ts
CHANGED
|
@@ -40,6 +40,7 @@ import {
|
|
|
40
40
|
type TableFeatures,
|
|
41
41
|
} from "./index";
|
|
42
42
|
import "./sv-grid-scrollbar";
|
|
43
|
+
import { untrack } from "svelte";
|
|
43
44
|
import type { Snippet } from "svelte";
|
|
44
45
|
import { getKeyboardIntent, getNextActiveCell } from "./keyboard";
|
|
45
46
|
import {
|
|
@@ -372,63 +373,77 @@ export function createGridApi<
|
|
|
372
373
|
ctx.grid.setGrouping([...columnIds]);
|
|
373
374
|
},
|
|
374
375
|
setFilter(columnId, filter) {
|
|
375
|
-
//
|
|
376
|
-
//
|
|
377
|
-
//
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
376
|
+
// `untrack`: these imperative mutators read the same filter state they
|
|
377
|
+
// write (read-modify-write on `filterMenuValues`). When a consumer
|
|
378
|
+
// calls them from inside a reactive `$effect`, that internal read would
|
|
379
|
+
// otherwise be captured as a dependency of the caller's effect, and the
|
|
380
|
+
// write would re-dirty it - an infinite update loop. Untracking the
|
|
381
|
+
// body keeps the API safe to drive from an effect.
|
|
382
|
+
untrack(() => {
|
|
383
|
+
// Honour the column's `filterable` opt-out. Same approach as
|
|
384
|
+
// `setSort` - `getCanFilter()` already combines the feature flag
|
|
385
|
+
// and the per-column field.
|
|
386
|
+
const col = ctx.allColumns.find((c: any) => c.id === columnId);
|
|
387
|
+
if (col && !col.getCanFilter?.()) return;
|
|
388
|
+
if (!filter) {
|
|
389
|
+
this.clearFilter(columnId);
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
ctx.filterMenuValues = {
|
|
393
|
+
...ctx.filterMenuValues,
|
|
394
|
+
[columnId]: {
|
|
395
|
+
operator: filter.operator,
|
|
396
|
+
value: filter.value ?? "",
|
|
397
|
+
...(filter.operator === "between"
|
|
398
|
+
? { valueTo: filter.valueTo ?? "" }
|
|
399
|
+
: {}),
|
|
400
|
+
// Optional second condition + join (multi-condition filtering).
|
|
401
|
+
...(filter.operator2
|
|
402
|
+
? {
|
|
403
|
+
operator2: filter.operator2,
|
|
404
|
+
value2: filter.value2 ?? "",
|
|
405
|
+
join: filter.join ?? "AND",
|
|
406
|
+
...(filter.operator2 === "between"
|
|
407
|
+
? { valueTo2: filter.valueTo2 ?? "" }
|
|
408
|
+
: {}),
|
|
409
|
+
}
|
|
410
|
+
: {}),
|
|
411
|
+
},
|
|
412
|
+
};
|
|
413
|
+
});
|
|
405
414
|
},
|
|
406
415
|
setFacetFilter(columnId, values) {
|
|
407
|
-
//
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
416
|
+
// See `setFilter` for why the read-modify-write is untracked.
|
|
417
|
+
untrack(() => {
|
|
418
|
+
// Empty array OR null both mean "clear this column's facet" -
|
|
419
|
+
// mirrors how the column menu treats unchecking the last value.
|
|
420
|
+
if (!values || values.length === 0) {
|
|
421
|
+
if (ctx.valueFilters[columnId]) {
|
|
422
|
+
const next = { ...ctx.valueFilters };
|
|
423
|
+
delete next[columnId];
|
|
424
|
+
ctx.valueFilters = next;
|
|
425
|
+
}
|
|
426
|
+
return;
|
|
414
427
|
}
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
};
|
|
428
|
+
ctx.valueFilters = {
|
|
429
|
+
...ctx.valueFilters,
|
|
430
|
+
[columnId]: new Set(values),
|
|
431
|
+
};
|
|
432
|
+
});
|
|
421
433
|
},
|
|
422
434
|
clearFilter(columnId) {
|
|
423
435
|
ctx.clearColumnFilter(columnId);
|
|
424
436
|
},
|
|
425
437
|
clearAllFilters() {
|
|
426
|
-
//
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
438
|
+
// See `setFilter` for why the read-modify-write is untracked.
|
|
439
|
+
untrack(() => {
|
|
440
|
+
// Wipe every filter surface in one go: column-menu, filter-row, set
|
|
441
|
+
// filters, and the global search box.
|
|
442
|
+
ctx.filterMenuValues = {};
|
|
443
|
+
ctx.filterRowValues = {};
|
|
444
|
+
ctx.valueFilters = {};
|
|
445
|
+
if (ctx.globalFilter !== "") ctx.globalFilter = "";
|
|
446
|
+
});
|
|
432
447
|
},
|
|
433
448
|
getFilters() {
|
|
434
449
|
// Return a defensive copy so callers can't mutate internal state.
|
|
@@ -793,7 +808,12 @@ export function createGridApi<
|
|
|
793
808
|
const sorting = state.sorting.map((s) => ({ id: s.id, desc: s.desc }));
|
|
794
809
|
ctx.grid.store.setState((prev: any) => ({ ...prev, sorting }));
|
|
795
810
|
}
|
|
796
|
-
if (state.grouping)
|
|
811
|
+
if (state.grouping) {
|
|
812
|
+
// Drop grouping by columns that no longer exist (#53) - otherwise a
|
|
813
|
+
// saved view collapses every row into a group named 'undefined'.
|
|
814
|
+
const existing = new Set((ctx.grid.getAllColumns() as any[]).map((c) => c.id));
|
|
815
|
+
ctx.grid.setGrouping(state.grouping.filter((id: string) => existing.has(id)));
|
|
816
|
+
}
|
|
797
817
|
if (state.pagination) {
|
|
798
818
|
const p = state.pagination;
|
|
799
819
|
ctx.grid.setPagination((prev: any) => ({ ...prev, ...p }));
|
|
@@ -806,7 +826,10 @@ export function createGridApi<
|
|
|
806
826
|
};
|
|
807
827
|
if (state.columnOrder) ctx.setColumnOrderInternal(state.columnOrder);
|
|
808
828
|
if (state.hiddenColumns) {
|
|
809
|
-
|
|
829
|
+
// Seed from columns declared `visible: false` so a partial/empty saved
|
|
830
|
+
// state never un-hides an app-declared-hidden column (#54); then layer
|
|
831
|
+
// the saved user-driven hides on top.
|
|
832
|
+
const next: Record<string, boolean> = { ...ctx.declaredHiddenColumns };
|
|
810
833
|
for (const id of state.hiddenColumns) next[id] = true;
|
|
811
834
|
ctx.hiddenColumns = next;
|
|
812
835
|
}
|
package/src/columns.ts
CHANGED
|
@@ -283,6 +283,11 @@ export function createColumns<
|
|
|
283
283
|
ctx.resizingColumnId = columnId;
|
|
284
284
|
ctx.resizeStartX = event.clientX;
|
|
285
285
|
ctx.resizeStartWidth = getColumnWidth(columnId);
|
|
286
|
+
// Capture the pointer so the drag keeps tracking when it leaves the 4px
|
|
287
|
+
// handle - without this, touch (and fast mouse) drags drop mid-resize (#59).
|
|
288
|
+
try {
|
|
289
|
+
(event.currentTarget as HTMLElement | null)?.setPointerCapture?.(event.pointerId);
|
|
290
|
+
} catch { /* capture is best-effort */ }
|
|
286
291
|
document.addEventListener("pointermove", onColumnResizeMove);
|
|
287
292
|
document.addEventListener("pointerup", endColumnResize);
|
|
288
293
|
document.addEventListener("pointercancel", endColumnResize);
|
|
@@ -104,4 +104,108 @@ describe('resolveCellFormat', () => {
|
|
|
104
104
|
formatsNeedingStats([{ type: 'dataBar', color: '#000' }]),
|
|
105
105
|
).toBe(true)
|
|
106
106
|
})
|
|
107
|
+
|
|
108
|
+
it('compareColumn formats do not need column stats', () => {
|
|
109
|
+
expect(
|
|
110
|
+
formatsNeedingStats([
|
|
111
|
+
{ type: 'colorScale', min: '#000', max: '#fff', compareColumn: 'target' },
|
|
112
|
+
]),
|
|
113
|
+
).toBe(false)
|
|
114
|
+
})
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
describe('resolveCellFormat - gradient extensions', () => {
|
|
118
|
+
const stat = { min: 0, max: 100 }
|
|
119
|
+
|
|
120
|
+
it('alpha mode interpolates opacity over a single base color', () => {
|
|
121
|
+
const fmts: ConditionalFormat[] = [
|
|
122
|
+
{ type: 'colorScale', mode: 'alpha', base: '#2563eb', alphaBounds: [0, 1] },
|
|
123
|
+
]
|
|
124
|
+
expect(resolveCellFormat(0, {}, 'c', fmts, stat).background).toBe('rgba(37, 99, 235, 0.000)')
|
|
125
|
+
expect(resolveCellFormat(100, {}, 'c', fmts, stat).background).toBe('rgba(37, 99, 235, 1.000)')
|
|
126
|
+
expect(resolveCellFormat(50, {}, 'c', fmts, stat).background).toBe('rgba(37, 99, 235, 0.500)')
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('alpha mode leaves the text color untouched (translucent tint)', () => {
|
|
130
|
+
const fmts: ConditionalFormat[] = [{ type: 'colorScale', mode: 'alpha' }]
|
|
131
|
+
expect(resolveCellFormat(80, {}, 'c', fmts, stat).color).toBeUndefined()
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('reverse flips the ramp position', () => {
|
|
135
|
+
const fmts: ConditionalFormat[] = [
|
|
136
|
+
{ type: 'colorScale', min: '#ff0000', max: '#00ff00', reverse: true },
|
|
137
|
+
]
|
|
138
|
+
expect(resolveCellFormat(0, {}, 'c', fmts, stat).background).toBe('#00ff00')
|
|
139
|
+
expect(resolveCellFormat(100, {}, 'c', fmts, stat).background).toBe('#ff0000')
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('zeroCentred maps 0 to the midpoint symmetrically', () => {
|
|
143
|
+
const fmts: ConditionalFormat[] = [
|
|
144
|
+
{ type: 'colorScale', min: '#ff0000', mid: '#ffffff', max: '#00ff00', zeroCentred: true },
|
|
145
|
+
]
|
|
146
|
+
const s = { min: -80, max: 100 } // bound = max(|-80|,|100|) = 100
|
|
147
|
+
expect(resolveCellFormat(0, {}, 'c', fmts, s).background).toBe('#ffffff')
|
|
148
|
+
expect(resolveCellFormat(100, {}, 'c', fmts, s).background).toBe('#00ff00')
|
|
149
|
+
expect(resolveCellFormat(-100, {}, 'c', fmts, s).background).toBe('#ff0000')
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
it('percent bounds map 0..100 onto the column span', () => {
|
|
153
|
+
const fmts: ConditionalFormat[] = [
|
|
154
|
+
{ type: 'colorScale', min: '#000000', max: '#ffffff', bounds: 'percent', minValue: 50, maxValue: 100 },
|
|
155
|
+
]
|
|
156
|
+
// span 0..100, so minValue 50 -> data 50, below it clamps to min color
|
|
157
|
+
expect(resolveCellFormat(50, {}, 'c', fmts, stat).background).toBe('#000000')
|
|
158
|
+
expect(resolveCellFormat(100, {}, 'c', fmts, stat).background).toBe('#ffffff')
|
|
159
|
+
expect(resolveCellFormat(25, {}, 'c', fmts, stat).background).toBe('#000000')
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('N-stop scale interpolates within a band', () => {
|
|
163
|
+
const fmts: ConditionalFormat[] = [
|
|
164
|
+
{
|
|
165
|
+
type: 'colorScale',
|
|
166
|
+
stops: [
|
|
167
|
+
{ offset: 0, color: '#ff0000' },
|
|
168
|
+
{ offset: 0.5, color: '#ffffff' },
|
|
169
|
+
{ offset: 1, color: '#00ff00' },
|
|
170
|
+
],
|
|
171
|
+
},
|
|
172
|
+
]
|
|
173
|
+
expect(resolveCellFormat(50, {}, 'c', fmts, stat).background).toBe('#ffffff')
|
|
174
|
+
expect(resolveCellFormat(0, {}, 'c', fmts, stat).background).toBe('#ff0000')
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
it('compareColumn tints by proportion of another field, no column stats', () => {
|
|
178
|
+
const fmts: ConditionalFormat[] = [
|
|
179
|
+
{ type: 'colorScale', min: '#000000', max: '#ffffff', compareColumn: 'target' },
|
|
180
|
+
]
|
|
181
|
+
expect(resolveCellFormat(50, { target: 100 }, 'c', fmts, null).background).toBe('#808080')
|
|
182
|
+
expect(resolveCellFormat(100, { target: 100 }, 'c', fmts, null).background).toBe('#ffffff')
|
|
183
|
+
// zero / missing target -> skip
|
|
184
|
+
expect(resolveCellFormat(50, { target: 0 }, 'c', fmts, null).background).toBeUndefined()
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
it('dataBar compareColumn sizes the bar against another field', () => {
|
|
188
|
+
const fmts: ConditionalFormat[] = [
|
|
189
|
+
{ type: 'dataBar', color: '#2563eb', compareColumn: 'target' },
|
|
190
|
+
]
|
|
191
|
+
expect(resolveCellFormat(30, { target: 120 }, 'c', fmts, null).dataBar?.percent).toBeCloseTo(25)
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
it('tooltip surfaces value or percent position', () => {
|
|
195
|
+
const valFmt: ConditionalFormat[] = [
|
|
196
|
+
{ type: 'colorScale', min: '#000', max: '#fff', tooltip: 'value' },
|
|
197
|
+
]
|
|
198
|
+
expect(resolveCellFormat(42, {}, 'c', valFmt, stat).title).toBe('42')
|
|
199
|
+
const pctFmt: ConditionalFormat[] = [
|
|
200
|
+
{ type: 'colorScale', min: '#000', max: '#fff', tooltip: 'percent' },
|
|
201
|
+
]
|
|
202
|
+
expect(resolveCellFormat(25, {}, 'c', pctFmt, stat).title).toBe('25%')
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
it('data bar gradient flag flows through to the resolved bar', () => {
|
|
206
|
+
const fmts: ConditionalFormat[] = [
|
|
207
|
+
{ type: 'dataBar', color: '#2563eb', gradient: true },
|
|
208
|
+
]
|
|
209
|
+
expect(resolveCellFormat(50, {}, 'c', fmts, stat).dataBar?.gradient).toBe(true)
|
|
210
|
+
})
|
|
107
211
|
})
|