@svgrid/grid 2.1.6 → 2.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/SvCarousel.svelte +35 -6
- package/dist/SvCarousel.svelte.d.ts +3 -0
- package/dist/SvColorInput.svelte +0 -1
- package/dist/SvContextMenu.svelte +26 -6
- package/dist/SvDrawer.svelte +5 -3
- package/dist/SvGrid.controller.svelte.d.ts +85 -2
- package/dist/SvGrid.controller.svelte.js +494 -1
- package/dist/SvGrid.css +142 -0
- package/dist/SvGrid.svelte +49 -27
- package/dist/SvGrid.types.d.ts +78 -0
- package/dist/SvGridBoard.svelte +71 -13
- package/dist/SvGridChart.svelte +41 -7
- package/dist/SvGridChart.svelte.d.ts +4 -0
- package/dist/SvGridChartPanel.svelte +485 -0
- package/dist/SvGridChartPanel.svelte.d.ts +28 -0
- package/dist/SvGridDropdown.svelte +18 -2
- package/dist/SvGroupCell.svelte +5 -5
- package/dist/SvMenu.svelte +25 -2
- package/dist/SvMenu.svelte.d.ts +3 -0
- package/dist/SvMenuList.svelte +31 -9
- package/dist/SvMenuList.svelte.d.ts +9 -1
- package/dist/SvNumberInput.svelte +1 -1
- package/dist/SvOtpInput.svelte +3 -2
- package/dist/SvPopover.svelte +13 -1
- package/dist/SvRowGroupPanel.svelte +23 -0
- package/dist/SvScrollArea.svelte +13 -2
- package/dist/SvScrollArea.svelte.d.ts +2 -0
- package/dist/SvTooltip.svelte +27 -2
- package/dist/SvTooltip.svelte.d.ts +3 -2
- package/dist/SvTour.svelte +30 -5
- package/dist/SvTour.svelte.d.ts +3 -0
- package/dist/SvTree.svelte +7 -3
- package/dist/SvTreeSelect.svelte +16 -9
- package/dist/build-api.js +51 -0
- package/dist/cdn/svgrid.js +17394 -16052
- package/dist/cdn/svgrid.svelte-external.js +16784 -15644
- package/dist/chart-export.d.ts +21 -0
- package/dist/chart-export.js +52 -11
- package/dist/chart.d.ts +30 -0
- package/dist/chart.js +53 -13
- package/dist/createAutocomplete.svelte.d.ts +6 -0
- package/dist/createAutocomplete.svelte.js +10 -0
- package/dist/createCombobox.svelte.d.ts +1 -0
- package/dist/createCombobox.svelte.js +1 -0
- package/dist/createCountryInput.svelte.d.ts +5 -0
- package/dist/createCountryInput.svelte.js +11 -0
- package/dist/createDropdownList.svelte.d.ts +4 -0
- package/dist/createDropdownList.svelte.js +7 -0
- package/dist/createPopoverSelect.svelte.d.ts +5 -3
- package/dist/createPopoverSelect.svelte.js +5 -3
- package/dist/createTree.svelte.d.ts +1 -0
- package/dist/createTree.svelte.js +15 -2
- package/dist/index.d.ts +4 -2
- package/dist/index.js +3 -2
- package/dist/svgrid-wrapper.types.d.ts +58 -0
- package/package.json +1 -1
- package/src/SvCarousel.svelte +35 -6
- package/src/SvColorInput.svelte +0 -1
- package/src/SvContextMenu.svelte +26 -6
- package/src/SvDrawer.svelte +5 -3
- package/src/SvGrid.controller.svelte.ts +481 -0
- package/src/SvGrid.css +142 -0
- package/src/SvGrid.svelte +49 -27
- package/src/SvGrid.types.ts +83 -0
- package/src/SvGridBoard.svelte +71 -13
- package/src/SvGridChart.svelte +41 -7
- package/src/SvGridChartPanel.svelte +485 -0
- package/src/SvGridDropdown.svelte +18 -2
- package/src/SvGroupCell.svelte +5 -5
- package/src/SvMenu.svelte +25 -2
- package/src/SvMenuList.svelte +31 -9
- package/src/SvNumberInput.svelte +1 -1
- package/src/SvOtpInput.svelte +3 -2
- package/src/SvPopover.svelte +13 -1
- package/src/SvRowGroupPanel.svelte +23 -0
- package/src/SvScrollArea.svelte +13 -2
- package/src/SvTooltip.svelte +27 -2
- package/src/SvTour.svelte +30 -5
- package/src/SvTree.svelte +7 -3
- package/src/SvTreeSelect.svelte +16 -9
- package/src/build-api.ts +49 -0
- package/src/chart-export.ts +57 -0
- package/src/chart.ts +70 -13
- package/src/createAutocomplete.svelte.ts +11 -0
- package/src/createCombobox.svelte.ts +1 -0
- package/src/createCountryInput.svelte.ts +12 -0
- package/src/createDropdownList.svelte.ts +8 -0
- package/src/createPopoverSelect.svelte.ts +5 -3
- package/src/createTree.svelte.ts +15 -2
- package/src/index.ts +6 -0
- package/src/overlays.test.ts +5 -0
- package/src/svgrid-wrapper.types.ts +62 -0
- package/src/svgrid.charting.test.ts +527 -0
- package/src/test-setup.ts +22 -0
package/src/SvMenuList.svelte
CHANGED
|
@@ -21,19 +21,29 @@
|
|
|
21
21
|
* Rendered inside a portalled panel by SvMenu; it self-imports for submenus.
|
|
22
22
|
*/
|
|
23
23
|
import Self from './SvMenuList.svelte'
|
|
24
|
+
import { type EditorDir } from './editor-contract'
|
|
24
25
|
|
|
25
26
|
let {
|
|
26
27
|
items,
|
|
27
28
|
onclose,
|
|
28
29
|
onselect,
|
|
29
30
|
submenu = false,
|
|
31
|
+
oncollapse,
|
|
32
|
+
dir,
|
|
30
33
|
}: {
|
|
31
34
|
items: ReadonlyArray<MenuItem>
|
|
32
|
-
/** Close the WHOLE menu (a leaf was chosen
|
|
35
|
+
/** Close the WHOLE menu (a leaf was chosen, Escape, or outside-click). */
|
|
33
36
|
onclose: () => void
|
|
34
37
|
/** Report the chosen leaf item. */
|
|
35
38
|
onselect: (item: MenuItem) => void
|
|
36
39
|
submenu?: boolean
|
|
40
|
+
/** Collapse just THIS submenu level (ArrowLeft, or ArrowRight under `rtl`)
|
|
41
|
+
* and refocus the item that opened it. Only meaningful when `submenu` is
|
|
42
|
+
* true; the root list has nowhere to collapse into. */
|
|
43
|
+
oncollapse?: () => void
|
|
44
|
+
/** Text direction; under `rtl` the ArrowRight/ArrowLeft submenu open/close
|
|
45
|
+
* keys swap (see createTree.svelte.ts for the reference pattern). */
|
|
46
|
+
dir?: EditorDir
|
|
37
47
|
} = $props()
|
|
38
48
|
|
|
39
49
|
// Indexes of real (non-separator) items, for roving focus.
|
|
@@ -61,20 +71,25 @@
|
|
|
61
71
|
}
|
|
62
72
|
function onKeydown(e: KeyboardEvent, i: number) {
|
|
63
73
|
const item = items[i]
|
|
74
|
+
const rtl = dir === 'rtl'
|
|
75
|
+
// Open this item's submenu (opposite key under rtl).
|
|
76
|
+
const openThis = () => { if (item?.children?.length) { e.preventDefault(); openSub = i } }
|
|
77
|
+
// Collapse just THIS list back into its parent item (opposite key under
|
|
78
|
+
// rtl) - `oncollapse` is local to this nesting level, unlike `onclose`.
|
|
79
|
+
const collapseThis = () => { if (submenu) { e.preventDefault(); oncollapse?.() } }
|
|
64
80
|
switch (e.key) {
|
|
65
81
|
case 'ArrowDown': e.preventDefault(); move(1); break
|
|
66
82
|
case 'ArrowUp': e.preventDefault(); move(-1); break
|
|
67
83
|
case 'Home': e.preventDefault(); focusItem(focusable[0] ?? 0); break
|
|
68
84
|
case 'End': e.preventDefault(); focusItem(focusable.at(-1) ?? 0); break
|
|
69
|
-
case 'ArrowRight':
|
|
70
|
-
|
|
71
|
-
break
|
|
72
|
-
case 'ArrowLeft':
|
|
73
|
-
if (submenu) { e.preventDefault(); onclose() } // parent listens via its own onclose wiring
|
|
74
|
-
break
|
|
85
|
+
case 'ArrowRight': (rtl ? collapseThis : openThis)(); break
|
|
86
|
+
case 'ArrowLeft': (rtl ? openThis : collapseThis)(); break
|
|
75
87
|
case 'Enter':
|
|
76
88
|
case ' ': e.preventDefault(); if (item) choose(item); break
|
|
77
89
|
case 'Escape': e.preventDefault(); onclose(); break
|
|
90
|
+
// Tab leaves the menu entirely (WAI-ARIA menu button pattern): close the
|
|
91
|
+
// whole tree rather than trapping focus inside a portalled panel.
|
|
92
|
+
case 'Tab': onclose(); break
|
|
78
93
|
}
|
|
79
94
|
}
|
|
80
95
|
</script>
|
|
@@ -108,7 +123,14 @@
|
|
|
108
123
|
</button>
|
|
109
124
|
{#if item.children?.length && openSub === i}
|
|
110
125
|
<div class="sv-menu__flyout">
|
|
111
|
-
<Self
|
|
126
|
+
<Self
|
|
127
|
+
items={item.children}
|
|
128
|
+
{onclose}
|
|
129
|
+
{onselect}
|
|
130
|
+
oncollapse={() => { openSub = -1; focusItem(i) }}
|
|
131
|
+
{dir}
|
|
132
|
+
submenu
|
|
133
|
+
/>
|
|
112
134
|
</div>
|
|
113
135
|
{/if}
|
|
114
136
|
</div>
|
|
@@ -134,7 +156,7 @@
|
|
|
134
156
|
.sv-menu__label { flex: 1; white-space: nowrap; }
|
|
135
157
|
.sv-menu__shortcut { color: var(--sg-muted, #94a3b8); font-size: 11.5px; margin-inline-start: 12px; }
|
|
136
158
|
.sv-menu__chev { color: var(--sg-muted, #94a3b8); flex: none; }
|
|
137
|
-
.sv-menu[dir='rtl'] .sv-menu__chev, .sv-menu__list.is-submenu .sv-menu__chev { }
|
|
159
|
+
.sv-menu[dir='rtl'] .sv-menu__chev, .sv-menu__list.is-submenu .sv-menu__chev { transform: scaleX(-1); }
|
|
138
160
|
.sv-menu__flyout {
|
|
139
161
|
position: absolute; top: -5px; inset-inline-start: 100%; z-index: 1;
|
|
140
162
|
background: var(--sg-bg, #fff); border: 1px solid var(--sg-border, #e2e8f0);
|
package/src/SvNumberInput.svelte
CHANGED
|
@@ -118,7 +118,7 @@
|
|
|
118
118
|
.sv-num.is-disabled { opacity: 0.6; }
|
|
119
119
|
.sv-num__input {
|
|
120
120
|
flex: 1; min-width: 0; border: 0; background: none; outline: none; color: inherit; font: inherit;
|
|
121
|
-
text-align:
|
|
121
|
+
text-align: end; padding: 0 8px;
|
|
122
122
|
}
|
|
123
123
|
.sv-num--sm { height: 28px; font-size: 12px; }
|
|
124
124
|
.sv-num--md { height: 34px; font-size: 13px; }
|
package/src/SvOtpInput.svelte
CHANGED
|
@@ -77,6 +77,7 @@
|
|
|
77
77
|
if (ch) focusCell(i + 1)
|
|
78
78
|
}
|
|
79
79
|
function onCellKeydown(i: number, e: KeyboardEvent) {
|
|
80
|
+
const rtl = dir === 'rtl'
|
|
80
81
|
if (e.key === 'Backspace') {
|
|
81
82
|
e.preventDefault()
|
|
82
83
|
const arr = currentCells()
|
|
@@ -90,10 +91,10 @@
|
|
|
90
91
|
}
|
|
91
92
|
} else if (e.key === 'ArrowLeft') {
|
|
92
93
|
e.preventDefault()
|
|
93
|
-
focusCell(i - 1)
|
|
94
|
+
focusCell(rtl ? i + 1 : i - 1)
|
|
94
95
|
} else if (e.key === 'ArrowRight') {
|
|
95
96
|
e.preventDefault()
|
|
96
|
-
focusCell(i + 1)
|
|
97
|
+
focusCell(rtl ? i - 1 : i + 1)
|
|
97
98
|
}
|
|
98
99
|
}
|
|
99
100
|
function onPaste(i: number, e: ClipboardEvent) {
|
package/src/SvPopover.svelte
CHANGED
|
@@ -84,6 +84,14 @@
|
|
|
84
84
|
let hoverTimer: ReturnType<typeof setTimeout> | undefined
|
|
85
85
|
function onEnter() { if (trigger === 'hover') { clearTimeout(hoverTimer); setOpen(true) } }
|
|
86
86
|
function onLeave() { if (trigger === 'hover') { hoverTimer = setTimeout(() => setOpen(false), 120) } }
|
|
87
|
+
|
|
88
|
+
/** Fallback accessible name for the panel when `ariaLabel` isn't given: the
|
|
89
|
+
* anchor's own aria-label, else its trimmed text content. */
|
|
90
|
+
function anchorFallbackLabel(): string | undefined {
|
|
91
|
+
const label = anchorEl?.getAttribute('aria-label')
|
|
92
|
+
if (label) return label
|
|
93
|
+
return anchorEl?.textContent?.trim() || undefined
|
|
94
|
+
}
|
|
87
95
|
</script>
|
|
88
96
|
|
|
89
97
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
@@ -93,6 +101,8 @@
|
|
|
93
101
|
onclick={() => trigger === 'click' && toggle()}
|
|
94
102
|
onpointerenter={onEnter}
|
|
95
103
|
onpointerleave={onLeave}
|
|
104
|
+
onfocusin={onEnter}
|
|
105
|
+
onfocusout={onLeave}
|
|
96
106
|
>
|
|
97
107
|
{@render anchor?.()}
|
|
98
108
|
</span>
|
|
@@ -108,9 +118,11 @@
|
|
|
108
118
|
style:left={`${rect.left}px`}
|
|
109
119
|
style:min-width={minWidth ? `${rect.width}px` : undefined}
|
|
110
120
|
role="dialog"
|
|
111
|
-
aria-label={ariaLabel}
|
|
121
|
+
aria-label={ariaLabel ?? anchorFallbackLabel()}
|
|
112
122
|
onpointerenter={onEnter}
|
|
113
123
|
onpointerleave={onLeave}
|
|
124
|
+
onfocusin={onEnter}
|
|
125
|
+
onfocusout={onLeave}
|
|
114
126
|
>
|
|
115
127
|
{@render children?.()}
|
|
116
128
|
</div>
|
|
@@ -48,6 +48,24 @@
|
|
|
48
48
|
const id = e.dataTransfer?.getData('text/sv-column')
|
|
49
49
|
if (id) add(id)
|
|
50
50
|
}
|
|
51
|
+
/** Keyboard equivalent of dragging a chip: moves the chip at `i` one slot earlier/later,
|
|
52
|
+
* reusing `reorder()` (via `dragIndex`) so there is a single source of truth for the move. */
|
|
53
|
+
function moveByKeyboard(i: number, delta: number) {
|
|
54
|
+
const to = i + delta
|
|
55
|
+
if (to < 0 || to >= groupBy.length) return
|
|
56
|
+
dragIndex = i
|
|
57
|
+
reorder(to)
|
|
58
|
+
}
|
|
59
|
+
function onChipKeydown(e: KeyboardEvent, i: number) {
|
|
60
|
+
if (!e.altKey) return
|
|
61
|
+
if (e.key === 'ArrowLeft') {
|
|
62
|
+
e.preventDefault()
|
|
63
|
+
moveByKeyboard(i, -1)
|
|
64
|
+
} else if (e.key === 'ArrowRight') {
|
|
65
|
+
e.preventDefault()
|
|
66
|
+
moveByKeyboard(i, 1)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
51
69
|
</script>
|
|
52
70
|
|
|
53
71
|
<div
|
|
@@ -63,14 +81,19 @@
|
|
|
63
81
|
{:else}
|
|
64
82
|
{#each groupBy as id, i (id)}
|
|
65
83
|
{#if i > 0}<span class="sv-rgp-sep" aria-hidden="true">›</span>{/if}
|
|
84
|
+
<!-- svelte-ignore a11y_no_noninteractive_tabindex, a11y_no_noninteractive_element_interactions -->
|
|
66
85
|
<span
|
|
67
86
|
class="sv-rgp-chip"
|
|
68
87
|
class:sv-rgp-chip-drag={dragIndex === i}
|
|
88
|
+
role="group"
|
|
89
|
+
tabindex="0"
|
|
90
|
+
aria-label={`Grouped by ${labelOf(id)}, position ${i + 1} of ${groupBy.length}. Press Alt+Arrow keys to reorder.`}
|
|
69
91
|
draggable="true"
|
|
70
92
|
ondragstart={() => (dragIndex = i)}
|
|
71
93
|
ondragend={() => (dragIndex = null)}
|
|
72
94
|
ondragover={(e) => e.preventDefault()}
|
|
73
95
|
ondrop={() => reorder(i)}
|
|
96
|
+
onkeydown={(e) => onChipKeydown(e, i)}
|
|
74
97
|
>
|
|
75
98
|
<span class="sv-rgp-grip" aria-hidden="true">⠿</span>
|
|
76
99
|
{labelOf(id)}
|
package/src/SvScrollArea.svelte
CHANGED
|
@@ -17,13 +17,24 @@
|
|
|
17
17
|
height?: string
|
|
18
18
|
/** Scroll horizontally instead of vertically. */
|
|
19
19
|
horizontal?: boolean
|
|
20
|
+
/** Accessible name for the scrollable region, announced by screen readers. */
|
|
21
|
+
ariaLabel?: string
|
|
20
22
|
children?: Snippet
|
|
21
23
|
}
|
|
22
24
|
|
|
23
|
-
let { maxHeight, height, horizontal = false, children }: Props = $props()
|
|
25
|
+
let { maxHeight, height, horizontal = false, ariaLabel = 'Scrollable content', children }: Props = $props()
|
|
24
26
|
</script>
|
|
25
27
|
|
|
26
|
-
|
|
28
|
+
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
29
|
+
<div
|
|
30
|
+
class="sv-scroll"
|
|
31
|
+
class:is-x={horizontal}
|
|
32
|
+
style:max-height={maxHeight}
|
|
33
|
+
style:height={height}
|
|
34
|
+
tabindex="0"
|
|
35
|
+
role="region"
|
|
36
|
+
aria-label={ariaLabel}
|
|
37
|
+
>
|
|
27
38
|
{@render children?.()}
|
|
28
39
|
</div>
|
|
29
40
|
|
package/src/SvTooltip.svelte
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* SvTooltip - a small hover/focus tooltip anchored to its child, portalled to
|
|
4
4
|
* <body> so it is never clipped. Shows after a short delay, hides on leave /
|
|
5
|
-
* blur / Escape, and is wired via `aria-describedby` (role="tooltip")
|
|
6
|
-
*
|
|
5
|
+
* blur / Escape, and is wired via `aria-describedby` (role="tooltip") onto the
|
|
6
|
+
* focusable child itself, so AT users get it announced on focus, not just
|
|
7
|
+
* sighted users on hover.
|
|
7
8
|
*
|
|
8
9
|
* ```svelte
|
|
9
10
|
* <SvTooltip text="Delete row"><button aria-label="Delete">🗑</button></SvTooltip>
|
|
@@ -12,6 +13,7 @@
|
|
|
12
13
|
import type { Snippet } from 'svelte'
|
|
13
14
|
import { portalToBody, popIn } from './popover'
|
|
14
15
|
import { nextEditorId } from './editor-contract'
|
|
16
|
+
import { getFocusable } from './a11y/focus-trap'
|
|
15
17
|
|
|
16
18
|
type Props = {
|
|
17
19
|
text: string
|
|
@@ -65,6 +67,29 @@
|
|
|
65
67
|
document.removeEventListener('keydown', onKey)
|
|
66
68
|
}
|
|
67
69
|
})
|
|
70
|
+
|
|
71
|
+
// The wrapper span above is never itself Tab-focusable, so putting
|
|
72
|
+
// aria-describedby only on it does nothing for keyboard/AT users - they
|
|
73
|
+
// focus the real interactive child (e.g. the <button> in the doc example),
|
|
74
|
+
// and that's the element whose accessible description actually gets
|
|
75
|
+
// announced. Mirror the id onto that child too, merging with (and later
|
|
76
|
+
// restoring) any aria-describedby it already carries.
|
|
77
|
+
$effect(() => {
|
|
78
|
+
if (!open || !anchorEl) return
|
|
79
|
+
const child = getFocusable(anchorEl)[0]
|
|
80
|
+
if (!child) return
|
|
81
|
+
const before = child.getAttribute('aria-describedby')
|
|
82
|
+
const ids = new Set((before ?? '').split(/\s+/).filter(Boolean))
|
|
83
|
+
ids.add(tid)
|
|
84
|
+
child.setAttribute('aria-describedby', [...ids].join(' '))
|
|
85
|
+
return () => {
|
|
86
|
+
const remaining = (child.getAttribute('aria-describedby') ?? '')
|
|
87
|
+
.split(/\s+/)
|
|
88
|
+
.filter((id) => id && id !== tid)
|
|
89
|
+
if (remaining.length) child.setAttribute('aria-describedby', remaining.join(' '))
|
|
90
|
+
else child.removeAttribute('aria-describedby')
|
|
91
|
+
}
|
|
92
|
+
})
|
|
68
93
|
</script>
|
|
69
94
|
|
|
70
95
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
package/src/SvTour.svelte
CHANGED
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import type { TourStep } from './ui-app.types'
|
|
20
20
|
import { portalToBody } from './popover'
|
|
21
|
+
import { createFocusTrap } from './a11y/focus-trap'
|
|
22
|
+
import type { EditorDir } from './editor-contract'
|
|
21
23
|
|
|
22
24
|
type Props = {
|
|
23
25
|
steps: ReadonlyArray<TourStep>
|
|
@@ -25,12 +27,15 @@
|
|
|
25
27
|
onFinish?: () => void
|
|
26
28
|
onSkip?: () => void
|
|
27
29
|
onStep?: (index: number) => void
|
|
30
|
+
/** Text direction; under `'rtl'` the Left/Right advance/back keys swap. */
|
|
31
|
+
dir?: EditorDir
|
|
28
32
|
}
|
|
29
33
|
|
|
30
|
-
let { steps, open = $bindable(false), onFinish, onSkip, onStep }: Props = $props()
|
|
34
|
+
let { steps, open = $bindable(false), onFinish, onSkip, onStep, dir }: Props = $props()
|
|
31
35
|
|
|
32
36
|
let index = $state(0)
|
|
33
37
|
let rect = $state<DOMRect | null>(null)
|
|
38
|
+
let popEl = $state<HTMLDivElement | null>(null)
|
|
34
39
|
const step = $derived(steps[index])
|
|
35
40
|
|
|
36
41
|
function targetEl(): HTMLElement | null {
|
|
@@ -43,10 +48,16 @@
|
|
|
43
48
|
el?.scrollIntoView({ block: 'center', behavior: 'smooth' })
|
|
44
49
|
rect = el ? el.getBoundingClientRect() : null
|
|
45
50
|
}
|
|
51
|
+
// Move focus into the popover on open + on every step change (its Next button
|
|
52
|
+
// is the sensible default target); the Tab trap (below) keeps it there.
|
|
53
|
+
function focusPopover() {
|
|
54
|
+
const btn = popEl?.querySelector<HTMLElement>('.sv-tour__next')
|
|
55
|
+
;(btn ?? popEl)?.focus()
|
|
56
|
+
}
|
|
46
57
|
function goto(i: number) {
|
|
47
58
|
index = Math.max(0, Math.min(i, steps.length - 1))
|
|
48
59
|
onStep?.(index)
|
|
49
|
-
queueMicrotask(measure)
|
|
60
|
+
queueMicrotask(() => { measure(); focusPopover() })
|
|
50
61
|
}
|
|
51
62
|
function next() { if (index >= steps.length - 1) finish(); else goto(index + 1) }
|
|
52
63
|
function back() { goto(index - 1) }
|
|
@@ -56,14 +67,15 @@
|
|
|
56
67
|
$effect(() => {
|
|
57
68
|
if (!open) return
|
|
58
69
|
index = 0
|
|
59
|
-
queueMicrotask(measure)
|
|
70
|
+
queueMicrotask(() => { measure(); focusPopover() })
|
|
60
71
|
const reflow = () => measure()
|
|
61
72
|
window.addEventListener('resize', reflow)
|
|
62
73
|
window.addEventListener('scroll', reflow, true)
|
|
74
|
+
const rtl = dir === 'rtl'
|
|
63
75
|
const onKey = (e: KeyboardEvent) => {
|
|
64
76
|
if (e.key === 'Escape') { e.preventDefault(); skip() }
|
|
65
|
-
else if (e.key === 'ArrowRight') { e.preventDefault(); next() }
|
|
66
|
-
else if (e.key === 'ArrowLeft') { e.preventDefault(); back() }
|
|
77
|
+
else if (e.key === 'ArrowRight') { e.preventDefault(); (rtl ? back : next)() }
|
|
78
|
+
else if (e.key === 'ArrowLeft') { e.preventDefault(); (rtl ? next : back)() }
|
|
67
79
|
}
|
|
68
80
|
window.addEventListener('keydown', onKey)
|
|
69
81
|
return () => {
|
|
@@ -73,6 +85,18 @@
|
|
|
73
85
|
}
|
|
74
86
|
})
|
|
75
87
|
|
|
88
|
+
// Focus trap for the duration the tour is open, from the shared primitive
|
|
89
|
+
// (same one SvModal/SvDrawer use): traps Tab within the popover and restores
|
|
90
|
+
// focus to whatever triggered the tour once it closes.
|
|
91
|
+
$effect(() => {
|
|
92
|
+
if (!open || !popEl) return
|
|
93
|
+
const trap = createFocusTrap(popEl, {
|
|
94
|
+
initialFocus: () => popEl?.querySelector<HTMLElement>('.sv-tour__next') ?? null,
|
|
95
|
+
})
|
|
96
|
+
trap.activate()
|
|
97
|
+
return () => trap.release()
|
|
98
|
+
})
|
|
99
|
+
|
|
76
100
|
const POP_W = 300
|
|
77
101
|
const pop = $derived.by(() => {
|
|
78
102
|
if (typeof window === 'undefined' || !rect) {
|
|
@@ -100,6 +124,7 @@
|
|
|
100
124
|
{/if}
|
|
101
125
|
|
|
102
126
|
<div
|
|
127
|
+
bind:this={popEl}
|
|
103
128
|
class="sv-tour__pop sv-tour__pop--{pop.place}"
|
|
104
129
|
style:top={pop.top}
|
|
105
130
|
style:left={pop.left}
|
package/src/SvTree.svelte
CHANGED
|
@@ -249,6 +249,7 @@
|
|
|
249
249
|
{#snippet treeRow(item)}
|
|
250
250
|
{@const cs = checkable ? tree.checkStateOf(item.node) : 'unchecked'}
|
|
251
251
|
{@const isLoading = loadingIds.has(item.node.id)}
|
|
252
|
+
{@const rowProps = tree.itemProps(item)}
|
|
252
253
|
<div
|
|
253
254
|
class="sv-tree__row"
|
|
254
255
|
class:is-selected={item.node.id === selected}
|
|
@@ -267,8 +268,11 @@
|
|
|
267
268
|
ondrop={reorderable ? (e) => onRowDrop(e, item.node) : undefined}
|
|
268
269
|
ondragend={reorderable ? endDrag : undefined}
|
|
269
270
|
ondblclick={editable ? () => startEdit(item.node) : undefined}
|
|
270
|
-
|
|
271
|
-
{
|
|
271
|
+
{...rowProps}
|
|
272
|
+
onkeydown={(e) => {
|
|
273
|
+
rowProps.onkeydown(e)
|
|
274
|
+
if (editable && e.key === 'F2') { e.preventDefault(); startEdit(item.node) }
|
|
275
|
+
}}
|
|
272
276
|
>
|
|
273
277
|
<button
|
|
274
278
|
type="button"
|
|
@@ -277,7 +281,7 @@
|
|
|
277
281
|
class:is-leaf={!item.hasChildren}
|
|
278
282
|
class:is-loading={isLoading}
|
|
279
283
|
tabindex="-1"
|
|
280
|
-
aria-hidden=
|
|
284
|
+
aria-hidden="true"
|
|
281
285
|
onclick={(e) => { e.stopPropagation(); tree.toggleExpand(item.node) }}
|
|
282
286
|
>
|
|
283
287
|
{#if isLoading}
|
package/src/SvTreeSelect.svelte
CHANGED
|
@@ -104,16 +104,23 @@
|
|
|
104
104
|
estimatedHeight: () => 300,
|
|
105
105
|
onExtraKey: (e, act) => {
|
|
106
106
|
const row = rows[act]
|
|
107
|
-
if (
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
107
|
+
if (!row) return false
|
|
108
|
+
// "expand" opens the node or steps into the first child; "collapse" closes
|
|
109
|
+
// it or jumps to the parent - same fallback as createTree's core.
|
|
110
|
+
const expand = () => {
|
|
111
|
+
if (row.hasChildren && !row.expanded) setExpanded(row.node.value, true)
|
|
112
|
+
else if (row.hasChildren && row.expanded) ts.active = Math.min(act + 1, rows.length - 1)
|
|
111
113
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
114
|
+
const collapse = () => {
|
|
115
|
+
if (row.hasChildren && row.expanded) setExpanded(row.node.value, false)
|
|
116
|
+
else {
|
|
117
|
+
let pi = act - 1
|
|
118
|
+
while (pi >= 0 && rows[pi]!.depth >= row.depth) pi--
|
|
119
|
+
if (pi >= 0) ts.active = pi
|
|
120
|
+
}
|
|
116
121
|
}
|
|
122
|
+
if (e.key === 'ArrowRight') { e.preventDefault(); expand(); return true }
|
|
123
|
+
if (e.key === 'ArrowLeft') { e.preventDefault(); collapse(); return true }
|
|
117
124
|
return false
|
|
118
125
|
},
|
|
119
126
|
idPrefix: 'sv-ts',
|
|
@@ -129,7 +136,7 @@
|
|
|
129
136
|
class:is-invalid={invalid}
|
|
130
137
|
{disabled}
|
|
131
138
|
onclick={() => ts.toggle()}
|
|
132
|
-
{...ts.triggerProps()}
|
|
139
|
+
{...ts.triggerProps('tree')}
|
|
133
140
|
{...editorAria({ id: uid, invalid, required, error, hint, ariaLabel })}
|
|
134
141
|
>
|
|
135
142
|
<span class="sv-ts__value" class:is-placeholder={!triggerText}>{triggerText || placeholder}</span>
|
package/src/build-api.ts
CHANGED
|
@@ -239,6 +239,25 @@ export function createGridApi<
|
|
|
239
239
|
[r.minRow, r.minCol, r.maxRow, r.maxCol] as [number, number, number, number],
|
|
240
240
|
);
|
|
241
241
|
},
|
|
242
|
+
openChart() {
|
|
243
|
+
ctx.chartPanelOpen = true;
|
|
244
|
+
},
|
|
245
|
+
closeChart() {
|
|
246
|
+
ctx.chartPanelOpen = false;
|
|
247
|
+
},
|
|
248
|
+
getChartSpec() {
|
|
249
|
+
return ctx.chartSpec;
|
|
250
|
+
},
|
|
251
|
+
chartRange(ranges) {
|
|
252
|
+
if (ranges && ranges.length) this.selectCells(ranges);
|
|
253
|
+
ctx.chartPanelOpen = true;
|
|
254
|
+
},
|
|
255
|
+
configureChart(config) {
|
|
256
|
+
if (ctx.chartingEnabled) ctx.applyChartConfig(config);
|
|
257
|
+
},
|
|
258
|
+
setChartAiHandler(handler) {
|
|
259
|
+
ctx.chartAiHandler = handler ?? null;
|
|
260
|
+
},
|
|
242
261
|
addRow(row, position = "bottom") {
|
|
243
262
|
this.addRows([row], position);
|
|
244
263
|
},
|
|
@@ -752,6 +771,21 @@ export function createGridApi<
|
|
|
752
771
|
Array.from(set),
|
|
753
772
|
]),
|
|
754
773
|
),
|
|
774
|
+
...(ctx.chartingEnabled
|
|
775
|
+
? {
|
|
776
|
+
chart: {
|
|
777
|
+
open: ctx.chartPanelOpen,
|
|
778
|
+
type: ctx.chartType,
|
|
779
|
+
dimension: ctx.chartDimensionId,
|
|
780
|
+
series: ctx.chartSeriesId,
|
|
781
|
+
measure: ctx.chartMeasureId,
|
|
782
|
+
reduce: ctx.chartReduce,
|
|
783
|
+
stacked: ctx.chartStacked,
|
|
784
|
+
},
|
|
785
|
+
charts: ctx.getChartsState(),
|
|
786
|
+
chartActive: ctx.activeChartIndex,
|
|
787
|
+
}
|
|
788
|
+
: {}),
|
|
755
789
|
};
|
|
756
790
|
},
|
|
757
791
|
setState(state) {
|
|
@@ -785,6 +819,21 @@ export function createGridApi<
|
|
|
785
819
|
next[k] = new Set(arr);
|
|
786
820
|
ctx.valueFilters = next;
|
|
787
821
|
}
|
|
822
|
+
if ((state.chart || state.charts) && ctx.chartingEnabled) {
|
|
823
|
+
if (Array.isArray(state.charts) && state.charts.length) {
|
|
824
|
+
ctx.applyChartsState(state.charts, state.chartActive);
|
|
825
|
+
}
|
|
826
|
+
if (state.chart) {
|
|
827
|
+
const c = state.chart;
|
|
828
|
+
ctx.chartPanelOpen = c.open;
|
|
829
|
+
ctx.chartType = c.type;
|
|
830
|
+
ctx.chartDimensionId = c.dimension;
|
|
831
|
+
ctx.chartSeriesId = c.series;
|
|
832
|
+
ctx.chartMeasureId = c.measure;
|
|
833
|
+
ctx.chartReduce = c.reduce;
|
|
834
|
+
ctx.chartStacked = c.stacked;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
788
837
|
},
|
|
789
838
|
refresh() {
|
|
790
839
|
ctx.grid.store.setState((prev: any) => ({ ...prev }));
|
package/src/chart-export.ts
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
* Pass the chart's `<svg class="sv-grid-chart-svg">` element (or any ancestor
|
|
10
10
|
* that contains it).
|
|
11
11
|
*/
|
|
12
|
+
import type { ChartSpec } from './chart'
|
|
13
|
+
|
|
12
14
|
function resolveSvg(el: SVGSVGElement | HTMLElement): SVGSVGElement | null {
|
|
13
15
|
if (el instanceof SVGSVGElement) return el
|
|
14
16
|
return el.querySelector('svg.sv-grid-chart-svg') ?? el.querySelector('svg')
|
|
@@ -47,6 +49,29 @@ export function chartToSvgString(
|
|
|
47
49
|
clone.setAttribute('width', String(w))
|
|
48
50
|
clone.setAttribute('height', String(h))
|
|
49
51
|
clone.setAttribute('xmlns', 'http://www.w3.org/2000/svg')
|
|
52
|
+
|
|
53
|
+
// Inline computed paint/text styles so the export is what's on screen -
|
|
54
|
+
// dynamic SVG presentation attributes (a line's `stroke={color}`) and scoped
|
|
55
|
+
// component CSS don't survive a raw clone + XMLSerializer. Walk the live tree
|
|
56
|
+
// and its clone (1:1 here, before we prune hit layers) and copy the resolved
|
|
57
|
+
// style, so line paths etc. export with their real stroke instead of nothing.
|
|
58
|
+
const PAINT_PROPS = [
|
|
59
|
+
'fill', 'fill-opacity', 'stroke', 'stroke-width', 'stroke-opacity',
|
|
60
|
+
'stroke-dasharray', 'stroke-linecap', 'stroke-linejoin', 'opacity',
|
|
61
|
+
'font-size', 'font-family', 'font-weight', 'text-anchor',
|
|
62
|
+
]
|
|
63
|
+
const liveNodes = svg.querySelectorAll('*')
|
|
64
|
+
const cloneNodes = clone.querySelectorAll('*')
|
|
65
|
+
for (let i = 0; i < liveNodes.length; i += 1) {
|
|
66
|
+
const el = cloneNodes[i] as (SVGElement & { style: CSSStyleDeclaration }) | undefined
|
|
67
|
+
if (!el || !el.style) continue
|
|
68
|
+
const cs = getComputedStyle(liveNodes[i]!)
|
|
69
|
+
for (const prop of PAINT_PROPS) {
|
|
70
|
+
const v = cs.getPropertyValue(prop)
|
|
71
|
+
if (v && v !== 'normal') el.style.setProperty(prop, v)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
50
75
|
// Drop interaction-only hit layers.
|
|
51
76
|
clone.querySelectorAll('.sv-grid-chart-hit').forEach((n) => n.remove())
|
|
52
77
|
|
|
@@ -142,3 +167,35 @@ export async function downloadChartPng(
|
|
|
142
167
|
triggerDownload(url, filename)
|
|
143
168
|
setTimeout(() => URL.revokeObjectURL(url), 1000)
|
|
144
169
|
}
|
|
170
|
+
|
|
171
|
+
const csvCell = (v: unknown): string => {
|
|
172
|
+
const s = v == null ? '' : String(v)
|
|
173
|
+
return /[",\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Serialize a chart's data to CSV: one row per category, one column per series
|
|
178
|
+
* (`Category, <series 1>, ...`). Empty for exotic specs (sankey/treemap/gauge/
|
|
179
|
+
* calendar) that don't carry a rectangular categories x series grid.
|
|
180
|
+
*/
|
|
181
|
+
export function chartSpecToCsv(spec: ChartSpec): string {
|
|
182
|
+
const cats = spec.categories ?? []
|
|
183
|
+
const series = spec.series ?? []
|
|
184
|
+
if (!cats.length || !series.length) return ''
|
|
185
|
+
const header = ['Category', ...series.map((s) => s.label)].map(csvCell).join(',')
|
|
186
|
+
const rows = cats.map((cat, i) =>
|
|
187
|
+
[cat, ...series.map((s) => s.values[i] ?? '')].map(csvCell).join(','),
|
|
188
|
+
)
|
|
189
|
+
return [header, ...rows].join('\n')
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Download the chart's data as a `.csv` file. Returns false if there's no
|
|
193
|
+
* rectangular category/series data to export. */
|
|
194
|
+
export function downloadChartCsv(spec: ChartSpec, filename = 'chart.csv'): boolean {
|
|
195
|
+
const csv = chartSpecToCsv(spec)
|
|
196
|
+
if (!csv) return false
|
|
197
|
+
const url = URL.createObjectURL(new Blob([csv], { type: 'text/csv;charset=utf-8' }))
|
|
198
|
+
triggerDownload(url, filename)
|
|
199
|
+
setTimeout(() => URL.revokeObjectURL(url), 1000)
|
|
200
|
+
return true
|
|
201
|
+
}
|