@svgrid/grid 2.1.22 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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/FlexRender.svelte +96 -96
- package/dist/GridFooter.svelte +178 -178
- package/dist/GridMenus.svelte +83 -3
- package/dist/SvAutoComplete.svelte +5 -3
- package/dist/SvCarousel.svelte +141 -163
- package/dist/SvColorInput.svelte +187 -186
- package/dist/SvColorInput.svelte.d.ts +1 -1
- package/dist/SvComboBox.svelte +7 -3
- package/dist/SvCommand.svelte +28 -85
- package/dist/SvContextMenu.svelte +116 -116
- 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 +250 -228
- 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 +3131 -2786
- package/dist/SvGrid.types.d.ts +452 -1
- package/dist/SvGridBoard.svelte +2339 -2339
- package/dist/SvGridChart.svelte +1724 -1724
- package/dist/SvGridChartPanel.svelte +485 -485
- package/dist/SvGridDropdown.svelte +689 -682
- package/dist/SvGridSelect.svelte +1 -0
- package/dist/SvGroupCell.svelte +109 -109
- package/dist/SvListBox.svelte +36 -14
- package/dist/SvMaskedInput.svelte +4 -3
- package/dist/SvMaskedInput.svelte.d.ts +1 -1
- package/dist/SvMenu.svelte +124 -124
- package/dist/SvMenuList.svelte +117 -165
- package/dist/SvMenuList.svelte.d.ts +2 -13
- package/dist/SvModal.svelte +12 -29
- package/dist/SvMultiSelect.svelte +1 -0
- package/dist/SvNumberInput.svelte +131 -130
- package/dist/SvNumberInput.svelte.d.ts +1 -1
- package/dist/SvOtpInput.svelte +158 -158
- 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 +139 -139
- package/dist/SvRowGroupPanel.svelte +170 -170
- package/dist/SvScrollArea.svelte +61 -61
- package/dist/SvStepper.svelte +6 -9
- package/dist/SvTagsInput.svelte +3 -2
- package/dist/SvTagsInput.svelte.d.ts +1 -1
- package/dist/SvTooltip.svelte +127 -138
- package/dist/SvTour.svelte +204 -176
- package/dist/SvTree.svelte +377 -377
- package/dist/SvTreeSelect.svelte +230 -229
- package/dist/build-api.js +76 -54
- package/dist/cdn/svgrid.js +10913 -6450
- package/dist/cdn/svgrid.svelte-external.js +9628 -5481
- package/dist/chart-export.js +8 -8
- 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 +14 -10
- package/src/DockNodeView.svelte +306 -0
- package/src/DockPopoutHost.svelte +17 -0
- package/src/FlexRender.svelte +96 -96
- package/src/GridFooter.svelte +178 -178
- package/src/GridMenus.svelte +83 -3
- package/src/SvAutoComplete.svelte +5 -3
- package/src/SvCarousel.svelte +141 -163
- package/src/SvCarousel.test.ts +68 -0
- package/src/SvColorInput.svelte +187 -186
- package/src/SvComboBox.svelte +7 -3
- package/src/SvCommand.svelte +28 -85
- package/src/SvCommand.test.ts +78 -0
- package/src/SvContextMenu.svelte +116 -116
- 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 +250 -228
- 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 +3158 -3034
- package/src/SvGrid.css +193 -5
- package/src/SvGrid.svelte +3131 -2786
- package/src/SvGrid.types.ts +1489 -1027
- package/src/SvGridBoard.svelte +2339 -2339
- package/src/SvGridChart.svelte +1724 -1724
- package/src/SvGridChartPanel.svelte +485 -485
- package/src/SvGridDropdown.svelte +689 -682
- package/src/SvGridSelect.svelte +1 -0
- package/src/SvGroupCell.svelte +109 -109
- package/src/SvListBox.svelte +36 -14
- package/src/SvMaskedInput.svelte +4 -3
- package/src/SvMenu.svelte +124 -124
- package/src/SvMenu.test.ts +97 -73
- package/src/SvMenuList.svelte +117 -165
- package/src/SvModal.svelte +12 -29
- package/src/SvModal.test.ts +82 -0
- package/src/SvMultiSelect.svelte +1 -0
- package/src/SvNumberInput.svelte +131 -130
- package/src/SvOtpInput.svelte +158 -158
- package/src/SvPagination.svelte +18 -18
- package/src/SvPasswordInput.svelte +3 -2
- package/src/SvPhoneInput.svelte +3 -2
- package/src/SvPopover.svelte +139 -139
- package/src/SvRowGroupPanel.svelte +170 -170
- package/src/SvScrollArea.svelte +61 -61
- package/src/SvStepper.svelte +6 -9
- package/src/SvTagsInput.svelte +3 -2
- package/src/SvTooltip.svelte +127 -138
- package/src/SvTooltip.test.ts +73 -0
- package/src/SvTour.svelte +204 -176
- package/src/SvTree.svelte +377 -377
- package/src/SvTreeSelect.svelte +230 -229
- package/src/a11y.contract.test.ts +49 -49
- package/src/a11y.test.ts +59 -59
- package/src/a11y.ts +61 -61
- package/src/build-api.ts +870 -847
- package/src/cell-formatting.ts +169 -169
- package/src/cell-render.ts +469 -469
- package/src/chart-export.ts +201 -201
- package/src/chart.ts +2296 -2296
- package/src/collaboration.test.ts +104 -104
- package/src/collaboration.ts +167 -167
- package/src/columns.ts +5 -0
- package/src/conditional-formatting.test.ts +104 -0
- package/src/conditional-formatting.ts +176 -19
- package/src/core.performance.test.ts +30 -30
- package/src/core.ts +1111 -1111
- package/src/createAutocomplete.svelte.ts +132 -132
- package/src/createCarousel.svelte.ts +104 -0
- package/src/createCombobox.svelte.ts +179 -179
- package/src/createCommand.svelte.ts +125 -0
- package/src/createCountryInput.svelte.ts +157 -157
- package/src/createDropdownList.svelte.ts +162 -162
- package/src/createForm.svelte.ts +333 -0
- package/src/createGrid.svelte.ts +42 -42
- package/src/createGrid.test.ts +10 -10
- package/src/createGridState.svelte.ts +17 -17
- 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 +206 -210
- package/src/createStepper.test.ts +62 -0
- package/src/createStepper.ts +61 -0
- package/src/createTooltip.svelte.ts +86 -0
- package/src/createTree.svelte.ts +319 -319
- 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/editing.test.ts +859 -859
- package/src/editing.ts +675 -675
- package/src/editor-contract.ts +2 -0
- package/src/export-data-api.test.ts +126 -126
- package/src/export-format.test.ts +107 -107
- package/src/export-format.ts +598 -598
- package/src/filter-operators.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/flex-render.ts +3 -3
- package/src/form-field.ts +87 -0
- package/src/index.ts +623 -469
- package/src/keyboard.test.ts +59 -59
- package/src/keyboard.ts +97 -97
- 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 +639 -582
- package/src/merge-objects.ts +48 -48
- package/src/overlays.test.ts +90 -90
- 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/render-component.ts +28 -28
- 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/selection.test.ts +754 -754
- package/src/selection.ts +600 -600
- package/src/server-data-source.test.ts +289 -289
- package/src/server-data-source.ts +413 -413
- package/src/sparkline.test.ts +68 -68
- package/src/sparkline.ts +169 -169
- package/src/spreadsheet.test.ts +489 -489
- package/src/spreadsheet.ts +312 -304
- package/src/static-functions.ts +11 -11
- package/src/subscribe.ts +38 -38
- package/src/svgrid-wrapper.types.ts +501 -501
- package/src/svgrid.behavior.test.ts +706 -706
- package/src/svgrid.charting.test.ts +527 -527
- package/src/svgrid.effect-driven-filter.svelte.test.ts +116 -0
- package/src/svgrid.features.test.ts +157 -157
- package/src/svgrid.new-features.wrapper.test.ts +251 -251
- package/src/svgrid.wrapper.test.ts +40 -40
- package/src/test-setup.ts +62 -62
- package/src/themes/index.ts +159 -159
- package/src/ui-app.types.ts +3 -0
- package/src/virtual.test.ts +6 -4
- package/src/virtualization/column-virtualizer.test.ts +27 -27
- package/src/virtualization/column-virtualizer.ts +30 -30
- package/src/virtualization/svelte-virtualizer.svelte.ts +26 -26
- package/src/virtualization/types.ts +30 -30
- package/src/virtualization/virtualizer.test.ts +47 -47
- package/src/virtualization/virtualizer.ts +296 -296
package/src/SvGridSelect.svelte
CHANGED
package/src/SvGroupCell.svelte
CHANGED
|
@@ -1,109 +1,109 @@
|
|
|
1
|
-
<!--
|
|
2
|
-
Built-in group cell for the server-side group/tree model. Drop it into the
|
|
3
|
-
grouping column and it draws the expander + indentation for group rows (and an
|
|
4
|
-
indented leaf value) - no hand-written cell recipe. Pair with `serverGroupRows`
|
|
5
|
-
to feed `<SvGrid data>`, and wire `onToggle` to the controller's `toggleGroup`.
|
|
6
|
-
|
|
7
|
-
cell: (ctx) => renderComponent(SvGroupCell, {
|
|
8
|
-
row: ctx.row.original, onToggle: ctl.toggleGroup, leafField: 'name',
|
|
9
|
-
})
|
|
10
|
-
-->
|
|
11
|
-
<script lang="ts" generics="TData">
|
|
12
|
-
import type { ServerDisplayRow } from './server-data-source'
|
|
13
|
-
|
|
14
|
-
type GroupGridRow = { __group?: ServerDisplayRow<TData> } & Record<string, unknown>
|
|
15
|
-
type Props = {
|
|
16
|
-
/** The grid row from `serverGroupRows` (carries the `__group` marker). */
|
|
17
|
-
row: GroupGridRow
|
|
18
|
-
/**
|
|
19
|
-
* Expand / collapse a group, or load more children. Receives the grid row;
|
|
20
|
-
* wire to `serverGroupNav(ctl).onToggle` (which drives clicks and keyboard).
|
|
21
|
-
*/
|
|
22
|
-
onToggle: (row: GroupGridRow) => void
|
|
23
|
-
/** Field to show for leaf rows in the group column (blank if omitted). */
|
|
24
|
-
leafField?: string
|
|
25
|
-
/** Pixels of indentation per level. Default 18. */
|
|
26
|
-
indent?: number
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
let { row, onToggle, leafField, indent = 18 }: Props = $props()
|
|
30
|
-
const meta = $derived(row.__group)
|
|
31
|
-
</script>
|
|
32
|
-
|
|
33
|
-
{#if meta && meta.kind === 'group'}
|
|
34
|
-
<button
|
|
35
|
-
type="button"
|
|
36
|
-
class="sv-group-cell"
|
|
37
|
-
style={`padding-inline-start: ${meta.level * indent}px`}
|
|
38
|
-
aria-expanded={meta.expanded}
|
|
39
|
-
aria-label={meta.expanded ? 'Collapse group' : 'Expand group'}
|
|
40
|
-
onclick={() => onToggle(row)}
|
|
41
|
-
>
|
|
42
|
-
<span class="sv-group-chev" class:open={meta.expanded} aria-hidden="true">▸</span>
|
|
43
|
-
<span class="sv-group-key">{meta.key}</span>
|
|
44
|
-
{#if meta.loading}<span class="sv-group-load" aria-hidden="true">…</span>{/if}
|
|
45
|
-
</button>
|
|
46
|
-
{:else if meta && meta.kind === 'more'}
|
|
47
|
-
<button
|
|
48
|
-
type="button"
|
|
49
|
-
class="sv-group-more"
|
|
50
|
-
style={`padding-inline-start: ${meta.level * indent}px`}
|
|
51
|
-
disabled={meta.loading}
|
|
52
|
-
onclick={() => onToggle(row)}
|
|
53
|
-
>
|
|
54
|
-
{meta.loading ? 'Loading...' : `Load ${meta.remaining} more`}
|
|
55
|
-
</button>
|
|
56
|
-
{:else if meta && meta.kind === 'footer'}
|
|
57
|
-
<span class="sv-group-footer" style={`padding-inline-start: ${meta.level * indent}px`}>Total</span>
|
|
58
|
-
{:else if meta && meta.kind === 'skeleton'}
|
|
59
|
-
<span class="sv-group-skeleton" style={`margin-inline-start: ${meta.level * indent}px`} aria-hidden="true"></span>
|
|
60
|
-
{:else if meta}
|
|
61
|
-
<span class="sv-group-leaf" style={`padding-inline-start: ${(meta.level + 1) * indent}px`}>
|
|
62
|
-
{leafField ? (row[leafField] ?? '') : ''}
|
|
63
|
-
</span>
|
|
64
|
-
{/if}
|
|
65
|
-
|
|
66
|
-
<style>
|
|
67
|
-
.sv-group-cell {
|
|
68
|
-
display: inline-flex;
|
|
69
|
-
align-items: center;
|
|
70
|
-
gap: 6px;
|
|
71
|
-
background: none;
|
|
72
|
-
border: 0;
|
|
73
|
-
padding: 0;
|
|
74
|
-
font: inherit;
|
|
75
|
-
color: inherit;
|
|
76
|
-
cursor: pointer;
|
|
77
|
-
}
|
|
78
|
-
.sv-group-chev {
|
|
79
|
-
display: inline-block;
|
|
80
|
-
transition: transform 0.15s;
|
|
81
|
-
color: var(--sg-muted, #64748b);
|
|
82
|
-
}
|
|
83
|
-
.sv-group-chev.open { transform: rotate(90deg); }
|
|
84
|
-
.sv-group-key { font-weight: 600; }
|
|
85
|
-
.sv-group-load { color: var(--sg-muted, #64748b); font-size: 12px; }
|
|
86
|
-
.sv-group-leaf { color: var(--sg-muted, #475569); }
|
|
87
|
-
.sv-group-more {
|
|
88
|
-
background: none;
|
|
89
|
-
border: 0;
|
|
90
|
-
padding: 0;
|
|
91
|
-
font: inherit;
|
|
92
|
-
font-size: 13px;
|
|
93
|
-
color: var(--sg-accent, #2563eb);
|
|
94
|
-
cursor: pointer;
|
|
95
|
-
}
|
|
96
|
-
.sv-group-more:disabled { color: var(--sg-muted, #64748b); cursor: default; }
|
|
97
|
-
.sv-group-footer { font-weight: 600; color: var(--sg-muted, #64748b); font-size: 12px; text-transform: uppercase; letter-spacing: 0.03em; }
|
|
98
|
-
.sv-group-skeleton {
|
|
99
|
-
display: inline-block;
|
|
100
|
-
width: 60%;
|
|
101
|
-
height: 10px;
|
|
102
|
-
border-radius: 5px;
|
|
103
|
-
background: linear-gradient(90deg, var(--sg-border, #e2e8f0) 25%, var(--sg-header-bg, #f1f5f9) 50%, var(--sg-border, #e2e8f0) 75%);
|
|
104
|
-
background-size: 200% 100%;
|
|
105
|
-
animation: sv-group-shimmer 1.2s infinite;
|
|
106
|
-
}
|
|
107
|
-
@keyframes sv-group-shimmer { from { background-position: 200% 0; } to { background-position: -200% 0; } }
|
|
108
|
-
@media (prefers-reduced-motion: reduce) { .sv-group-skeleton { animation: none; } }
|
|
109
|
-
</style>
|
|
1
|
+
<!--
|
|
2
|
+
Built-in group cell for the server-side group/tree model. Drop it into the
|
|
3
|
+
grouping column and it draws the expander + indentation for group rows (and an
|
|
4
|
+
indented leaf value) - no hand-written cell recipe. Pair with `serverGroupRows`
|
|
5
|
+
to feed `<SvGrid data>`, and wire `onToggle` to the controller's `toggleGroup`.
|
|
6
|
+
|
|
7
|
+
cell: (ctx) => renderComponent(SvGroupCell, {
|
|
8
|
+
row: ctx.row.original, onToggle: ctl.toggleGroup, leafField: 'name',
|
|
9
|
+
})
|
|
10
|
+
-->
|
|
11
|
+
<script lang="ts" generics="TData">
|
|
12
|
+
import type { ServerDisplayRow } from './server-data-source'
|
|
13
|
+
|
|
14
|
+
type GroupGridRow = { __group?: ServerDisplayRow<TData> } & Record<string, unknown>
|
|
15
|
+
type Props = {
|
|
16
|
+
/** The grid row from `serverGroupRows` (carries the `__group` marker). */
|
|
17
|
+
row: GroupGridRow
|
|
18
|
+
/**
|
|
19
|
+
* Expand / collapse a group, or load more children. Receives the grid row;
|
|
20
|
+
* wire to `serverGroupNav(ctl).onToggle` (which drives clicks and keyboard).
|
|
21
|
+
*/
|
|
22
|
+
onToggle: (row: GroupGridRow) => void
|
|
23
|
+
/** Field to show for leaf rows in the group column (blank if omitted). */
|
|
24
|
+
leafField?: string
|
|
25
|
+
/** Pixels of indentation per level. Default 18. */
|
|
26
|
+
indent?: number
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
let { row, onToggle, leafField, indent = 18 }: Props = $props()
|
|
30
|
+
const meta = $derived(row.__group)
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
{#if meta && meta.kind === 'group'}
|
|
34
|
+
<button
|
|
35
|
+
type="button"
|
|
36
|
+
class="sv-group-cell"
|
|
37
|
+
style={`padding-inline-start: ${meta.level * indent}px`}
|
|
38
|
+
aria-expanded={meta.expanded}
|
|
39
|
+
aria-label={meta.expanded ? 'Collapse group' : 'Expand group'}
|
|
40
|
+
onclick={() => onToggle(row)}
|
|
41
|
+
>
|
|
42
|
+
<span class="sv-group-chev" class:open={meta.expanded} aria-hidden="true">▸</span>
|
|
43
|
+
<span class="sv-group-key">{meta.key}</span>
|
|
44
|
+
{#if meta.loading}<span class="sv-group-load" aria-hidden="true">…</span>{/if}
|
|
45
|
+
</button>
|
|
46
|
+
{:else if meta && meta.kind === 'more'}
|
|
47
|
+
<button
|
|
48
|
+
type="button"
|
|
49
|
+
class="sv-group-more"
|
|
50
|
+
style={`padding-inline-start: ${meta.level * indent}px`}
|
|
51
|
+
disabled={meta.loading}
|
|
52
|
+
onclick={() => onToggle(row)}
|
|
53
|
+
>
|
|
54
|
+
{meta.loading ? 'Loading...' : `Load ${meta.remaining} more`}
|
|
55
|
+
</button>
|
|
56
|
+
{:else if meta && meta.kind === 'footer'}
|
|
57
|
+
<span class="sv-group-footer" style={`padding-inline-start: ${meta.level * indent}px`}>Total</span>
|
|
58
|
+
{:else if meta && meta.kind === 'skeleton'}
|
|
59
|
+
<span class="sv-group-skeleton" style={`margin-inline-start: ${meta.level * indent}px`} aria-hidden="true"></span>
|
|
60
|
+
{:else if meta}
|
|
61
|
+
<span class="sv-group-leaf" style={`padding-inline-start: ${(meta.level + 1) * indent}px`}>
|
|
62
|
+
{leafField ? (row[leafField] ?? '') : ''}
|
|
63
|
+
</span>
|
|
64
|
+
{/if}
|
|
65
|
+
|
|
66
|
+
<style>
|
|
67
|
+
.sv-group-cell {
|
|
68
|
+
display: inline-flex;
|
|
69
|
+
align-items: center;
|
|
70
|
+
gap: 6px;
|
|
71
|
+
background: none;
|
|
72
|
+
border: 0;
|
|
73
|
+
padding: 0;
|
|
74
|
+
font: inherit;
|
|
75
|
+
color: inherit;
|
|
76
|
+
cursor: pointer;
|
|
77
|
+
}
|
|
78
|
+
.sv-group-chev {
|
|
79
|
+
display: inline-block;
|
|
80
|
+
transition: transform 0.15s;
|
|
81
|
+
color: var(--sg-muted, #64748b);
|
|
82
|
+
}
|
|
83
|
+
.sv-group-chev.open { transform: rotate(90deg); }
|
|
84
|
+
.sv-group-key { font-weight: 600; }
|
|
85
|
+
.sv-group-load { color: var(--sg-muted, #64748b); font-size: 12px; }
|
|
86
|
+
.sv-group-leaf { color: var(--sg-muted, #475569); }
|
|
87
|
+
.sv-group-more {
|
|
88
|
+
background: none;
|
|
89
|
+
border: 0;
|
|
90
|
+
padding: 0;
|
|
91
|
+
font: inherit;
|
|
92
|
+
font-size: 13px;
|
|
93
|
+
color: var(--sg-accent, #2563eb);
|
|
94
|
+
cursor: pointer;
|
|
95
|
+
}
|
|
96
|
+
.sv-group-more:disabled { color: var(--sg-muted, #64748b); cursor: default; }
|
|
97
|
+
.sv-group-footer { font-weight: 600; color: var(--sg-muted, #64748b); font-size: 12px; text-transform: uppercase; letter-spacing: 0.03em; }
|
|
98
|
+
.sv-group-skeleton {
|
|
99
|
+
display: inline-block;
|
|
100
|
+
width: 60%;
|
|
101
|
+
height: 10px;
|
|
102
|
+
border-radius: 5px;
|
|
103
|
+
background: linear-gradient(90deg, var(--sg-border, #e2e8f0) 25%, var(--sg-header-bg, #f1f5f9) 50%, var(--sg-border, #e2e8f0) 75%);
|
|
104
|
+
background-size: 200% 100%;
|
|
105
|
+
animation: sv-group-shimmer 1.2s infinite;
|
|
106
|
+
}
|
|
107
|
+
@keyframes sv-group-shimmer { from { background-position: 200% 0; } to { background-position: -200% 0; } }
|
|
108
|
+
@media (prefers-reduced-motion: reduce) { .sv-group-skeleton { animation: none; } }
|
|
109
|
+
</style>
|
package/src/SvListBox.svelte
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* via `value` (scalar or array) + `onChange`. Scales to huge option sets with
|
|
6
6
|
* `virtual` (fixed-row windowing), and each row can be an `itemTemplate`.
|
|
7
7
|
*/
|
|
8
|
-
import {
|
|
8
|
+
import { type Snippet } from 'svelte'
|
|
9
9
|
import { groupOptions, hasGroups, type ListOption } from './list-option'
|
|
10
10
|
import { virtualRange, scrollToIndex } from './virtual'
|
|
11
11
|
import { createListbox } from './createListbox.svelte'
|
|
@@ -101,17 +101,25 @@
|
|
|
101
101
|
class:is-invalid={invalid}
|
|
102
102
|
class:is-virtual={useVirtual}
|
|
103
103
|
style:--sv-rows={rows}
|
|
104
|
-
|
|
104
|
+
style:--sv-row-h={`${rowHeight}px`}
|
|
105
|
+
onscroll={(e) => (scrollTop = e.currentTarget.scrollTop)}
|
|
105
106
|
bind:clientHeight={viewportH}
|
|
106
107
|
{...lb.rootProps()}
|
|
107
108
|
>
|
|
108
109
|
{#if useVirtual}
|
|
109
|
-
<!--
|
|
110
|
-
|
|
111
|
-
|
|
110
|
+
<!-- Windowing by top/bottom spacers with the visible rows IN FLOW between
|
|
111
|
+
them (padTop + rows + padBottom always sum to the full height). Plain
|
|
112
|
+
flow rows (no absolute positioning, no will-change) keep the scroller
|
|
113
|
+
off its own compositor layer, so a fast jump repaints in step with the
|
|
114
|
+
scroll instead of showing the blank frame a promoted layer would. The
|
|
115
|
+
window updates from `scrollTop` state, which Svelte flushes before the
|
|
116
|
+
next paint - no flushSync (it over-flushes app-wide and, on a fling,
|
|
117
|
+
starves the very handler that has to keep up with the scroll). -->
|
|
118
|
+
<li class="sv-listbox__spacer" aria-hidden="true" style:height={`${vr.padTop}px`}></li>
|
|
112
119
|
{#each windowed as opt (opt.value)}
|
|
113
120
|
{@render optionLi(opt, opt.index)}
|
|
114
121
|
{/each}
|
|
122
|
+
<li class="sv-listbox__spacer" aria-hidden="true" style:height={`${vr.padBottom}px`}></li>
|
|
115
123
|
{:else}
|
|
116
124
|
{#each groupOptions(options) as g (g.group ?? ' ')}
|
|
117
125
|
{#if g.group != null}<li class="sv-listbox__group" role="presentation">{g.group}</li>{/if}
|
|
@@ -132,7 +140,6 @@
|
|
|
132
140
|
class:is-active={lb.isActive(index)}
|
|
133
141
|
class:is-disabled={opt.disabled}
|
|
134
142
|
style:height={`${rowHeight}px`}
|
|
135
|
-
style:transform={useVirtual ? `translateY(${index * rowHeight}px)` : undefined}
|
|
136
143
|
{...lb.optionProps(index)}
|
|
137
144
|
>
|
|
138
145
|
{#if multiple}<span class="sv-listbox__check" aria-hidden="true">{isSel(opt) ? '✓' : ''}</span>{/if}
|
|
@@ -144,7 +151,7 @@
|
|
|
144
151
|
.sv-listbox {
|
|
145
152
|
--_accent: var(--sg-accent, #2563eb);
|
|
146
153
|
margin: 0; padding: 4px; list-style: none;
|
|
147
|
-
max-height: calc(var(--sv-rows, 7) * 32px + 8px); overflow-y: auto;
|
|
154
|
+
max-height: calc(var(--sv-rows, 7) * var(--sv-row-h, 32px) + 8px); overflow-y: auto;
|
|
148
155
|
background: var(--sg-input-bg, #fff); color: var(--sg-fg, #0f172a);
|
|
149
156
|
border: 1px solid var(--sg-input-border, var(--sg-border, #cbd5e1)); border-radius: var(--sg-radius, 8px);
|
|
150
157
|
width: 220px; outline: none;
|
|
@@ -152,15 +159,30 @@
|
|
|
152
159
|
.sv-listbox:focus-visible { border-color: var(--_accent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--_accent) 22%, transparent); }
|
|
153
160
|
.sv-listbox.is-invalid { border-color: var(--sg-danger, #dc2626); }
|
|
154
161
|
.sv-listbox.is-invalid:focus-visible { box-shadow: 0 0 0 2px color-mix(in srgb, var(--sg-danger, #dc2626) 22%, transparent); }
|
|
155
|
-
/*
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
162
|
+
/* Spacers reserve the off-screen height above/below the windowed rows. They
|
|
163
|
+
also paint faint per-row skeleton bars: on a fast scrollbar-thumb drag the
|
|
164
|
+
compositor can reveal this padding before JS re-windows (a drag jumps many
|
|
165
|
+
rows in one frame - overscan can't cover it), so the user would see blank.
|
|
166
|
+
The skeleton fills that gap with placeholder rows that the real rows paint
|
|
167
|
+
over a frame later. Bars are one per `--sv-row-h` band, aligned to the row
|
|
168
|
+
grid (padTop/padBottom are exact multiples of the row height). */
|
|
169
|
+
.sv-listbox__spacer {
|
|
170
|
+
padding: 0; margin: 0; flex: none; pointer-events: none; list-style: none;
|
|
171
|
+
background-image: linear-gradient(
|
|
172
|
+
to bottom,
|
|
173
|
+
transparent 0,
|
|
174
|
+
transparent calc((var(--sv-row-h, 32px) - 10px) / 2),
|
|
175
|
+
var(--sg-skeleton, color-mix(in srgb, currentColor 9%, transparent)) calc((var(--sv-row-h, 32px) - 10px) / 2),
|
|
176
|
+
var(--sg-skeleton, color-mix(in srgb, currentColor 9%, transparent)) calc((var(--sv-row-h, 32px) + 10px) / 2),
|
|
177
|
+
transparent calc((var(--sv-row-h, 32px) + 10px) / 2),
|
|
178
|
+
transparent var(--sv-row-h, 32px)
|
|
179
|
+
);
|
|
180
|
+
background-size: 62% var(--sv-row-h, 32px);
|
|
181
|
+
background-repeat: repeat-y;
|
|
182
|
+
background-position: 12px 0;
|
|
161
183
|
}
|
|
162
184
|
.sv-listbox__opt {
|
|
163
|
-
display: flex; align-items: center; gap: 8px; box-sizing: border-box; padding: 0 10px;
|
|
185
|
+
display: flex; align-items: center; gap: 8px; box-sizing: border-box; flex: none; padding: 0 10px;
|
|
164
186
|
border-radius: 6px; cursor: pointer; font-size: 13px;
|
|
165
187
|
}
|
|
166
188
|
.sv-listbox__opt.is-active { background: var(--sg-row-hover-bg, #f1f5f9); }
|
package/src/SvMaskedInput.svelte
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
let {
|
|
30
|
-
value = '',
|
|
30
|
+
value = $bindable(''),
|
|
31
31
|
onChange,
|
|
32
32
|
mask = '',
|
|
33
33
|
placeholder,
|
|
@@ -53,7 +53,8 @@
|
|
|
53
53
|
|
|
54
54
|
const mi = createMaskedInput({
|
|
55
55
|
value: () => value,
|
|
56
|
-
|
|
56
|
+
// Write the masked `value` (so `bind:value` works) AND fire `onChange`.
|
|
57
|
+
onChange: (m, r, c) => { value = m; onChange?.(m, r, c) },
|
|
57
58
|
mask: () => mask,
|
|
58
59
|
placeholder: () => placeholder,
|
|
59
60
|
disabled: () => disabled,
|
|
@@ -72,7 +73,7 @@
|
|
|
72
73
|
{#if prefixIcon}<span class="sv-masked__adorn">{@render prefixIcon()}</span>{/if}
|
|
73
74
|
<input class="sv-masked__input" {...mi.inputProps()} />
|
|
74
75
|
{#if clearable && mi.masked && !disabled && !readonly}
|
|
75
|
-
<button type="button" class="sv-masked__clear" aria-label="Clear" tabindex="-1" onclick={() => onChange?.('', '', false)}>
|
|
76
|
+
<button type="button" class="sv-masked__clear" aria-label="Clear" tabindex="-1" onclick={() => { value = ''; onChange?.('', '', false) }}>
|
|
76
77
|
<svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M6 6l12 12M18 6L6 18" /></svg>
|
|
77
78
|
</button>
|
|
78
79
|
{/if}
|
package/src/SvMenu.svelte
CHANGED
|
@@ -1,124 +1,124 @@
|
|
|
1
|
-
<script lang="ts" module>
|
|
2
|
-
export type { MenuItem } from './SvMenuList.svelte'
|
|
3
|
-
</script>
|
|
4
|
-
|
|
5
|
-
<script lang="ts">
|
|
6
|
-
/**
|
|
7
|
-
* SvMenu - a dropdown / context menu: a trigger opens a portalled, animated
|
|
8
|
-
* menu surface with submenus, separators, icons, shortcuts and full keyboard
|
|
9
|
-
* (arrows, Enter, Escape, ArrowRight/Left for submenus). Portals to <body> so it
|
|
10
|
-
* escapes any clipping. Parity: Smart menu / context-menu.
|
|
11
|
-
*
|
|
12
|
-
* ```svelte
|
|
13
|
-
* <SvMenu items={items} onSelect={(i) => run(i)}>
|
|
14
|
-
* {#snippet anchor()}<button>Actions</button>{/snippet}
|
|
15
|
-
* </SvMenu>
|
|
16
|
-
* ```
|
|
17
|
-
*/
|
|
18
|
-
import type { Snippet } from 'svelte'
|
|
19
|
-
import { anchoredRect, portalToBody, popIn, type AnchoredRect } from './popover'
|
|
20
|
-
import { createDismissableLayer } from './a11y/dismissable'
|
|
21
|
-
import { getFocusable } from './a11y/focus-trap'
|
|
22
|
-
import { type EditorDir } from './editor-contract'
|
|
23
|
-
import SvMenuList, { type MenuItem } from './SvMenuList.svelte'
|
|
24
|
-
|
|
25
|
-
let {
|
|
26
|
-
items,
|
|
27
|
-
open = $bindable(false),
|
|
28
|
-
onOpenChange,
|
|
29
|
-
onSelect,
|
|
30
|
-
ariaLabel,
|
|
31
|
-
anchor,
|
|
32
|
-
dir,
|
|
33
|
-
}: {
|
|
34
|
-
items: ReadonlyArray<MenuItem>
|
|
35
|
-
open?: boolean
|
|
36
|
-
onOpenChange?: (open: boolean) => void
|
|
37
|
-
onSelect?: (item: MenuItem) => void
|
|
38
|
-
ariaLabel?: string
|
|
39
|
-
anchor?: Snippet
|
|
40
|
-
/** Text direction (rtl mirrors the submenu chevron + flips ArrowLeft/Right). */
|
|
41
|
-
dir?: EditorDir
|
|
42
|
-
} = $props()
|
|
43
|
-
|
|
44
|
-
const resolvedDir = $derived(dir === 'ltr' || dir === 'rtl' ? dir : undefined)
|
|
45
|
-
|
|
46
|
-
let anchorEl = $state<HTMLSpanElement | null>(null)
|
|
47
|
-
let panelEl = $state<HTMLDivElement | null>(null)
|
|
48
|
-
let rect = $state<AnchoredRect>({ top: 0, left: 0, width: 0, openUpward: false })
|
|
49
|
-
|
|
50
|
-
function setOpen(v: boolean) { if (v !== open) { open = v; onOpenChange?.(v) } }
|
|
51
|
-
function toggle() { setOpen(!open) }
|
|
52
|
-
function updatePos() {
|
|
53
|
-
if (!anchorEl) return
|
|
54
|
-
rect = anchoredRect(anchorEl.getBoundingClientRect(), { estimatedHeight: Math.min(items.length, 10) * 34 + 8, minWidth: 180 })
|
|
55
|
-
}
|
|
56
|
-
/** Return focus to the trigger. Every dismissal path (Escape, item selection,
|
|
57
|
-
* outside-click, Tab) funnels through the effect cleanup below, so this
|
|
58
|
-
* fires uniformly instead of dropping focus to <body> on close. */
|
|
59
|
-
function focusTrigger() {
|
|
60
|
-
if (!anchorEl) return
|
|
61
|
-
;(getFocusable(anchorEl)[0] ?? anchorEl)?.focus?.()
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
$effect(() => {
|
|
65
|
-
if (!open) return
|
|
66
|
-
updatePos()
|
|
67
|
-
queueMicrotask(() => panelEl?.querySelector<HTMLElement>('[role="menuitem"]:not([disabled])')?.focus())
|
|
68
|
-
const rp = () => updatePos()
|
|
69
|
-
window.addEventListener('scroll', rp, true)
|
|
70
|
-
window.addEventListener('resize', rp)
|
|
71
|
-
// Outside-click dismissal via the shared layer stack (Escape stays owned by
|
|
72
|
-
// SvMenuList's keyboard handling, so closeOnEscape is off here).
|
|
73
|
-
const layer = createDismissableLayer({
|
|
74
|
-
element: () => [anchorEl, panelEl],
|
|
75
|
-
onDismiss: () => setOpen(false),
|
|
76
|
-
closeOnEscape: false,
|
|
77
|
-
})
|
|
78
|
-
layer.activate()
|
|
79
|
-
return () => {
|
|
80
|
-
window.removeEventListener('scroll', rp, true)
|
|
81
|
-
window.removeEventListener('resize', rp)
|
|
82
|
-
layer.release()
|
|
83
|
-
// Only on an actual close (not a mid-open rerun from e.g. `items` changing).
|
|
84
|
-
if (!open) focusTrigger()
|
|
85
|
-
}
|
|
86
|
-
})
|
|
87
|
-
</script>
|
|
88
|
-
|
|
89
|
-
<!-- svelte-ignore a11y_no_static_element_interactions a11y_click_events_have_key_events -->
|
|
90
|
-
<span
|
|
91
|
-
bind:this={anchorEl}
|
|
92
|
-
class="sv-menu__anchor"
|
|
93
|
-
aria-haspopup="menu"
|
|
94
|
-
aria-expanded={open}
|
|
95
|
-
onclick={toggle}
|
|
96
|
-
>
|
|
97
|
-
{@render anchor?.()}
|
|
98
|
-
</span>
|
|
99
|
-
|
|
100
|
-
{#if open}
|
|
101
|
-
<div
|
|
102
|
-
bind:this={panelEl}
|
|
103
|
-
class="sv-menu"
|
|
104
|
-
use:portalToBody
|
|
105
|
-
use:popIn={{ up: rect.openUpward }}
|
|
106
|
-
style:position="fixed"
|
|
107
|
-
style:top={`${rect.top}px`}
|
|
108
|
-
style:left={`${rect.left}px`}
|
|
109
|
-
dir={resolvedDir}
|
|
110
|
-
aria-label={ariaLabel}
|
|
111
|
-
>
|
|
112
|
-
<SvMenuList {items} onclose={() => setOpen(false)} onselect={(i) => onSelect?.(i)} dir={resolvedDir} />
|
|
113
|
-
</div>
|
|
114
|
-
{/if}
|
|
115
|
-
|
|
116
|
-
<style>
|
|
117
|
-
.sv-menu__anchor { display: inline-flex; }
|
|
118
|
-
:global(.sv-menu) {
|
|
119
|
-
z-index: 2147483646; min-width: 180px;
|
|
120
|
-
background: var(--sg-bg, #fff); color: var(--sg-fg, #0f172a);
|
|
121
|
-
border: 1px solid var(--sg-border, #e2e8f0); border-radius: 10px;
|
|
122
|
-
box-shadow: 0 16px 48px -12px rgba(15, 23, 42, 0.35); font-size: 13px;
|
|
123
|
-
}
|
|
124
|
-
</style>
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
export type { MenuItem } from './SvMenuList.svelte'
|
|
3
|
+
</script>
|
|
4
|
+
|
|
5
|
+
<script lang="ts">
|
|
6
|
+
/**
|
|
7
|
+
* SvMenu - a dropdown / context menu: a trigger opens a portalled, animated
|
|
8
|
+
* menu surface with submenus, separators, icons, shortcuts and full keyboard
|
|
9
|
+
* (arrows, Enter, Escape, ArrowRight/Left for submenus). Portals to <body> so it
|
|
10
|
+
* escapes any clipping. Parity: Smart menu / context-menu.
|
|
11
|
+
*
|
|
12
|
+
* ```svelte
|
|
13
|
+
* <SvMenu items={items} onSelect={(i) => run(i)}>
|
|
14
|
+
* {#snippet anchor()}<button>Actions</button>{/snippet}
|
|
15
|
+
* </SvMenu>
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
import type { Snippet } from 'svelte'
|
|
19
|
+
import { anchoredRect, portalToBody, popIn, type AnchoredRect } from './popover'
|
|
20
|
+
import { createDismissableLayer } from './a11y/dismissable'
|
|
21
|
+
import { getFocusable } from './a11y/focus-trap'
|
|
22
|
+
import { type EditorDir } from './editor-contract'
|
|
23
|
+
import SvMenuList, { type MenuItem } from './SvMenuList.svelte'
|
|
24
|
+
|
|
25
|
+
let {
|
|
26
|
+
items,
|
|
27
|
+
open = $bindable(false),
|
|
28
|
+
onOpenChange,
|
|
29
|
+
onSelect,
|
|
30
|
+
ariaLabel,
|
|
31
|
+
anchor,
|
|
32
|
+
dir,
|
|
33
|
+
}: {
|
|
34
|
+
items: ReadonlyArray<MenuItem>
|
|
35
|
+
open?: boolean
|
|
36
|
+
onOpenChange?: (open: boolean) => void
|
|
37
|
+
onSelect?: (item: MenuItem) => void
|
|
38
|
+
ariaLabel?: string
|
|
39
|
+
anchor?: Snippet
|
|
40
|
+
/** Text direction (rtl mirrors the submenu chevron + flips ArrowLeft/Right). */
|
|
41
|
+
dir?: EditorDir
|
|
42
|
+
} = $props()
|
|
43
|
+
|
|
44
|
+
const resolvedDir = $derived(dir === 'ltr' || dir === 'rtl' ? dir : undefined)
|
|
45
|
+
|
|
46
|
+
let anchorEl = $state<HTMLSpanElement | null>(null)
|
|
47
|
+
let panelEl = $state<HTMLDivElement | null>(null)
|
|
48
|
+
let rect = $state<AnchoredRect>({ top: 0, left: 0, width: 0, openUpward: false, maxHeight: 0, availHeight: 0 })
|
|
49
|
+
|
|
50
|
+
function setOpen(v: boolean) { if (v !== open) { open = v; onOpenChange?.(v) } }
|
|
51
|
+
function toggle() { setOpen(!open) }
|
|
52
|
+
function updatePos() {
|
|
53
|
+
if (!anchorEl) return
|
|
54
|
+
rect = anchoredRect(anchorEl.getBoundingClientRect(), { estimatedHeight: Math.min(items.length, 10) * 34 + 8, minWidth: 180 })
|
|
55
|
+
}
|
|
56
|
+
/** Return focus to the trigger. Every dismissal path (Escape, item selection,
|
|
57
|
+
* outside-click, Tab) funnels through the effect cleanup below, so this
|
|
58
|
+
* fires uniformly instead of dropping focus to <body> on close. */
|
|
59
|
+
function focusTrigger() {
|
|
60
|
+
if (!anchorEl) return
|
|
61
|
+
;(getFocusable(anchorEl)[0] ?? anchorEl)?.focus?.()
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
$effect(() => {
|
|
65
|
+
if (!open) return
|
|
66
|
+
updatePos()
|
|
67
|
+
queueMicrotask(() => panelEl?.querySelector<HTMLElement>('[role="menuitem"]:not([disabled])')?.focus())
|
|
68
|
+
const rp = () => updatePos()
|
|
69
|
+
window.addEventListener('scroll', rp, true)
|
|
70
|
+
window.addEventListener('resize', rp)
|
|
71
|
+
// Outside-click dismissal via the shared layer stack (Escape stays owned by
|
|
72
|
+
// SvMenuList's keyboard handling, so closeOnEscape is off here).
|
|
73
|
+
const layer = createDismissableLayer({
|
|
74
|
+
element: () => [anchorEl, panelEl],
|
|
75
|
+
onDismiss: () => setOpen(false),
|
|
76
|
+
closeOnEscape: false,
|
|
77
|
+
})
|
|
78
|
+
layer.activate()
|
|
79
|
+
return () => {
|
|
80
|
+
window.removeEventListener('scroll', rp, true)
|
|
81
|
+
window.removeEventListener('resize', rp)
|
|
82
|
+
layer.release()
|
|
83
|
+
// Only on an actual close (not a mid-open rerun from e.g. `items` changing).
|
|
84
|
+
if (!open) focusTrigger()
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
</script>
|
|
88
|
+
|
|
89
|
+
<!-- svelte-ignore a11y_no_static_element_interactions a11y_click_events_have_key_events -->
|
|
90
|
+
<span
|
|
91
|
+
bind:this={anchorEl}
|
|
92
|
+
class="sv-menu__anchor"
|
|
93
|
+
aria-haspopup="menu"
|
|
94
|
+
aria-expanded={open}
|
|
95
|
+
onclick={toggle}
|
|
96
|
+
>
|
|
97
|
+
{@render anchor?.()}
|
|
98
|
+
</span>
|
|
99
|
+
|
|
100
|
+
{#if open}
|
|
101
|
+
<div
|
|
102
|
+
bind:this={panelEl}
|
|
103
|
+
class="sv-menu"
|
|
104
|
+
use:portalToBody
|
|
105
|
+
use:popIn={{ up: rect.openUpward }}
|
|
106
|
+
style:position="fixed"
|
|
107
|
+
style:top={`${rect.top}px`}
|
|
108
|
+
style:left={`${rect.left}px`}
|
|
109
|
+
dir={resolvedDir}
|
|
110
|
+
aria-label={ariaLabel}
|
|
111
|
+
>
|
|
112
|
+
<SvMenuList {items} onclose={() => setOpen(false)} onselect={(i) => onSelect?.(i)} dir={resolvedDir} />
|
|
113
|
+
</div>
|
|
114
|
+
{/if}
|
|
115
|
+
|
|
116
|
+
<style>
|
|
117
|
+
.sv-menu__anchor { display: inline-flex; }
|
|
118
|
+
:global(.sv-menu) {
|
|
119
|
+
z-index: 2147483646; min-width: 180px;
|
|
120
|
+
background: var(--sg-bg, #fff); color: var(--sg-fg, #0f172a);
|
|
121
|
+
border: 1px solid var(--sg-border, #e2e8f0); border-radius: 10px;
|
|
122
|
+
box-shadow: 0 16px 48px -12px rgba(15, 23, 42, 0.35); font-size: 13px;
|
|
123
|
+
}
|
|
124
|
+
</style>
|