@svgrid/grid 2.2.30 → 2.2.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/SvAvatar.svelte +1 -1
- package/dist/SvCheckBox.svelte +2 -1
- package/dist/SvCircularProgress.svelte +1 -1
- package/dist/SvGrid.controller.svelte.d.ts +20 -0
- package/dist/SvGrid.controller.svelte.js +358 -18
- package/dist/SvGrid.css +103 -3
- package/dist/SvGrid.svelte +162 -10
- package/dist/SvGrid.types.d.ts +81 -0
- package/dist/SvGridDropdown.svelte +38 -6
- package/dist/SvGridDropdown.svelte.d.ts +4 -0
- package/dist/SvGridSelect.svelte +12 -3
- package/dist/SvGridSelect.svelte.d.ts +6 -0
- package/dist/SvGroupCell.svelte +9 -2
- package/dist/SvMultiSelect.svelte +12 -3
- package/dist/SvMultiSelect.svelte.d.ts +6 -0
- package/dist/SvPasswordInput.svelte +1 -1
- package/dist/SvProgress.svelte +1 -1
- package/dist/SvRating.svelte +1 -1
- package/dist/SvResult.svelte +1 -1
- package/dist/SvRichText.svelte +31 -16
- package/dist/SvRichText.svelte.d.ts +9 -0
- package/dist/SvSwitchButton.svelte +2 -1
- package/dist/SvToaster.svelte +1 -1
- package/dist/SvTree.svelte +9 -2
- package/dist/SvTree.svelte.d.ts +5 -0
- package/dist/SvTreeSelect.svelte +6 -2
- package/dist/build-api.js +16 -0
- package/dist/cdn/{GridMenus-Ds3OpzZT.js → GridMenus-C6ixSGWW.js} +33 -33
- package/dist/cdn/{GridMenus-Da_p7SKS.js → GridMenus-Cyk3qpJ7.js} +33 -33
- package/dist/cdn/{SvDateTimePicker-B8GopjhC.js → SvDateTimePicker-CdqaP61H.js} +2 -1
- package/dist/cdn/{SvDateTimePicker-DB0aIEk8.js → SvDateTimePicker-ClqtVIj3.js} +2 -1
- package/dist/cdn/{SvGridDropdown-DjALc_4f.js → SvGridDropdown-D0VdjeR8.js} +129 -123
- package/dist/cdn/{SvGridDropdown-hpnHvOGt.js → SvGridDropdown-Dh-2ej8j.js} +92 -87
- package/dist/cdn/{src-BLJfdyL0.js → src-BgHjWF0Q.js} +4193 -3837
- package/dist/cdn/{src-DYDCiC0D.js → src-CgD-wiuS.js} +5956 -5602
- package/dist/cdn/svgrid.js +8 -8
- package/dist/cdn/svgrid.svelte-external.js +8 -8
- package/dist/cell-render.d.ts +1 -0
- package/dist/cell-render.js +115 -1
- package/dist/cell-values.js +5 -0
- package/dist/core.d.ts +80 -9
- package/dist/core.js +89 -0
- package/dist/editor-contract.d.ts +12 -5
- package/dist/editor-registry.d.ts +18 -7
- package/dist/editor-registry.js +10 -1
- package/dist/group-display.d.ts +39 -0
- package/dist/group-display.js +52 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/keyboard-handlers.js +16 -0
- package/dist/recurrence.js +3 -2
- package/dist/svgrid-wrapper.types.d.ts +8 -2
- package/dist/themes/index.d.ts +10 -0
- package/dist/themes/index.js +26 -0
- package/package.json +1 -1
- package/src/SvAccordion.test.ts +5 -5
- package/src/SvAvatar.svelte +1 -1
- package/src/SvButtonGroup.test.ts +6 -6
- package/src/SvCheckBox.svelte +2 -1
- package/src/SvCircularProgress.svelte +1 -1
- package/src/SvFileUpload.test.ts +1 -1
- package/src/SvGrid.controller.svelte.ts +367 -17
- package/src/SvGrid.css +103 -3
- package/src/SvGrid.svelte +162 -10
- package/src/SvGrid.types.ts +81 -0
- package/src/SvGridDropdown.svelte +38 -6
- package/src/SvGridSelect.svelte +12 -3
- package/src/SvGroupCell.svelte +9 -2
- package/src/SvMultiSelect.svelte +12 -3
- package/src/SvPasswordInput.svelte +1 -1
- package/src/SvProgress.svelte +1 -1
- package/src/SvRating.svelte +1 -1
- package/src/SvResult.svelte +1 -1
- package/src/SvRichText.svelte +31 -16
- package/src/SvSwitchButton.svelte +2 -1
- package/src/SvToaster.svelte +1 -1
- package/src/SvTree.svelte +9 -2
- package/src/SvTreeSelect.svelte +6 -2
- package/src/build-api.ts +15 -0
- package/src/builtin-editors.test.ts +3 -1
- package/src/cell-render.test.ts +27 -3
- package/src/cell-render.ts +122 -1
- package/src/cell-values.ts +4 -0
- package/src/chart-export.test.ts +2 -2
- package/src/clipboard.test.ts +0 -2
- package/src/core.ts +157 -8
- package/src/editing.test.ts +1 -1
- package/src/editor-contract.ts +12 -5
- package/src/editor-registry.grid.test.ts +48 -0
- package/src/editor-registry.test.ts +3 -1
- package/src/editor-registry.ts +23 -7
- package/src/facet-buckets.test.ts +33 -33
- package/src/group-display.test.ts +91 -1
- package/src/group-display.ts +83 -0
- package/src/index.ts +9 -0
- package/src/keyboard-handlers.ts +17 -0
- package/src/list-power.test.ts +2 -2
- package/src/menus.test.ts +4 -4
- package/src/named-views.coverage.test.ts +3 -3
- package/src/recurrence.ts +3 -2
- package/src/row-resize.test.ts +40 -40
- package/src/scheduler-model.ts +1 -1
- package/src/server-data-source.coverage.test.ts +1 -1
- package/src/server-group-model.test.ts +6 -6
- package/src/spreadsheet.test.ts +0 -1
- package/src/svgrid-wrapper.types.ts +11 -9
- package/src/svgrid.async-editor-options.test.ts +271 -0
- package/src/svgrid.auto-row-height.test.ts +204 -0
- package/src/svgrid.charting.test.ts +4 -4
- package/src/svgrid.comments-autocomplete.test.ts +8 -2
- package/src/svgrid.context-menu.test.ts +25 -9
- package/src/svgrid.filter-depth.test.ts +163 -0
- package/src/svgrid.filter-menu-listbox.svelte.test.ts +16 -2
- package/src/svgrid.filter-menu-scroll.test.ts +10 -2
- package/src/svgrid.grand-total.test.ts +188 -0
- package/src/svgrid.group-display-mode.test.ts +171 -0
- package/src/svgrid.group-footers.test.ts +121 -0
- package/src/svgrid.group-pagination.test.ts +153 -0
- package/src/svgrid.tree-data.test.ts +186 -0
- package/src/svgriddropdown.async-panel.svelte.test.ts +195 -0
- package/src/themes/index.ts +42 -0
- package/src/tree-edit.test.ts +4 -4
- package/src/tree-row-model.test.ts +169 -0
- package/src/ui-localization.test.ts +113 -0
- package/themes/ag-alpine.css +22 -0
- package/themes/antd.css +22 -0
- package/themes/atlassian.css +22 -0
- package/themes/bootstrap.css +22 -0
- package/themes/carbon.css +22 -0
- package/themes/catppuccin.css +22 -0
- package/themes/dracula.css +22 -0
- package/themes/ember.css +22 -0
- package/themes/excel.css +22 -0
- package/themes/fluent.css +22 -0
- package/themes/github.css +22 -0
- package/themes/linear.css +22 -0
- package/themes/material.css +22 -0
- package/themes/nord.css +22 -0
- package/themes/notion.css +22 -0
- package/themes/salesforce.css +22 -0
- package/themes/sap.css +22 -0
- package/themes/shadcn.css +22 -0
- package/themes/tailwind.css +22 -0
- package/themes/vercel.css +22 -0
package/src/SvGrid.css
CHANGED
|
@@ -902,8 +902,8 @@
|
|
|
902
902
|
pattern isn't being applied (it matches nothing until it compiles). */
|
|
903
903
|
.sv-grid-filter-value.sv-grid-filter-value-invalid,
|
|
904
904
|
.sv-grid-tp-filter-input.sv-grid-filter-value-invalid {
|
|
905
|
-
border-color: var(--sg-danger, #
|
|
906
|
-
box-shadow: 0 0 0 1px color-mix(in srgb, var(--sg-danger, #
|
|
905
|
+
border-color: var(--sg-danger, #dc2626) !important;
|
|
906
|
+
box-shadow: 0 0 0 1px color-mix(in srgb, var(--sg-danger, #dc2626) 45%, transparent);
|
|
907
907
|
}
|
|
908
908
|
|
|
909
909
|
/* In / Not-in chip (token) input: chips + a flex-growing text box share one
|
|
@@ -2130,6 +2130,104 @@ select.sv-grid-fr-editor {
|
|
|
2130
2130
|
background: var(--sg-selection-bg, #eaf2ff);
|
|
2131
2131
|
}
|
|
2132
2132
|
|
|
2133
|
+
/* Group subtotal row (`groupFooters`). Renders through the normal cell path so
|
|
2134
|
+
its totals sit under their own columns; a top rule + weight is what reads as
|
|
2135
|
+
"this closes the group above". Lighter than the banner on purpose - the
|
|
2136
|
+
banner opens a group, the footer just totals it. */
|
|
2137
|
+
.sv-grid-group-footer-row > .sv-grid-cell {
|
|
2138
|
+
background: var(--sg-header-bg, #eef2f8);
|
|
2139
|
+
border-top: 1px solid var(--sg-border, #d7dce5);
|
|
2140
|
+
font-weight: 600;
|
|
2141
|
+
}
|
|
2142
|
+
|
|
2143
|
+
/* Auto row height (`autoRowHeight`). The default cell truncates to one line
|
|
2144
|
+
(nowrap + ellipsis + overflow hidden), which would make a measured row height
|
|
2145
|
+
pointless - every row would measure the same. Let the text wrap so the row
|
|
2146
|
+
has something to grow to, and pad vertically since there is no fixed height
|
|
2147
|
+
centring the single line any more. */
|
|
2148
|
+
.sv-grid-row-auto-height > .sv-grid-cell {
|
|
2149
|
+
white-space: normal;
|
|
2150
|
+
overflow-wrap: anywhere;
|
|
2151
|
+
overflow: visible;
|
|
2152
|
+
text-overflow: clip;
|
|
2153
|
+
padding-top: 6px;
|
|
2154
|
+
padding-bottom: 6px;
|
|
2155
|
+
/* Cells stretch to the row's height rather than centring on one line. */
|
|
2156
|
+
vertical-align: top;
|
|
2157
|
+
}
|
|
2158
|
+
|
|
2159
|
+
/* Grand total (`grandTotalRow`). One step heavier than a group subtotal: it
|
|
2160
|
+
closes the whole dataset, not one group, so it takes a double top rule. */
|
|
2161
|
+
.sv-grid-grand-total-row > .sv-grid-cell {
|
|
2162
|
+
border-top: 3px double var(--sg-border, #d7dce5);
|
|
2163
|
+
font-weight: 700;
|
|
2164
|
+
}
|
|
2165
|
+
|
|
2166
|
+
/* Client tree data (`treeData`). The indent is a sized spacer rather than
|
|
2167
|
+
padding on the cell so conditional-format backgrounds and data bars still
|
|
2168
|
+
span the full cell width. */
|
|
2169
|
+
.sv-grid-tree-indent {
|
|
2170
|
+
display: inline-block;
|
|
2171
|
+
flex: none;
|
|
2172
|
+
}
|
|
2173
|
+
.sv-grid-tree-toggle {
|
|
2174
|
+
display: inline-flex;
|
|
2175
|
+
align-items: center;
|
|
2176
|
+
justify-content: center;
|
|
2177
|
+
width: 14px;
|
|
2178
|
+
height: 14px;
|
|
2179
|
+
flex: none;
|
|
2180
|
+
margin-right: 3px;
|
|
2181
|
+
padding: 0;
|
|
2182
|
+
border: 0;
|
|
2183
|
+
border-radius: 3px;
|
|
2184
|
+
background: transparent;
|
|
2185
|
+
color: var(--sg-muted, #64748b);
|
|
2186
|
+
font-size: 10px;
|
|
2187
|
+
line-height: 1;
|
|
2188
|
+
cursor: pointer;
|
|
2189
|
+
}
|
|
2190
|
+
.sv-grid-tree-toggle:hover {
|
|
2191
|
+
background: var(--sg-header-bg, #eef2f8);
|
|
2192
|
+
color: var(--sg-fg, #0f172a);
|
|
2193
|
+
}
|
|
2194
|
+
/* One chevron for every expander in the library - the grid's tree rows, its
|
|
2195
|
+
group banners and auto-group columns, SvGroupCell and SvTree all draw the
|
|
2196
|
+
same `chevron-right` and rotate it open, rather than swapping between two
|
|
2197
|
+
different glyphs. */
|
|
2198
|
+
.sv-grid-tree-toggle .sv-grid-icon,
|
|
2199
|
+
.sv-grid-group-toggle .sv-grid-icon {
|
|
2200
|
+
width: 12px;
|
|
2201
|
+
height: 12px;
|
|
2202
|
+
transition: transform 0.15s ease;
|
|
2203
|
+
}
|
|
2204
|
+
.sv-grid-tree-toggle[aria-expanded="true"] .sv-grid-icon,
|
|
2205
|
+
.sv-grid-group-toggle[aria-expanded="true"] .sv-grid-icon {
|
|
2206
|
+
transform: rotate(90deg);
|
|
2207
|
+
}
|
|
2208
|
+
.sv-grid-tree-toggle:focus-visible {
|
|
2209
|
+
outline: 2px solid var(--sg-accent, #2563eb);
|
|
2210
|
+
outline-offset: 1px;
|
|
2211
|
+
}
|
|
2212
|
+
/* Auto-group column cell (`groupDisplayMode: singleColumn | multipleColumns`).
|
|
2213
|
+
The group row is an ordinary row here, so only the label needs weight - the
|
|
2214
|
+
full-width banner styling does not apply. */
|
|
2215
|
+
.sv-grid-autogroup-label {
|
|
2216
|
+
font-weight: 600;
|
|
2217
|
+
color: var(--sg-fg, #0f172a);
|
|
2218
|
+
overflow: hidden;
|
|
2219
|
+
text-overflow: ellipsis;
|
|
2220
|
+
white-space: nowrap;
|
|
2221
|
+
}
|
|
2222
|
+
|
|
2223
|
+
/* Keeps a childless row's value on the same x as its expandable siblings. */
|
|
2224
|
+
.sv-grid-tree-spacer {
|
|
2225
|
+
display: inline-block;
|
|
2226
|
+
width: 14px;
|
|
2227
|
+
flex: none;
|
|
2228
|
+
margin-right: 3px;
|
|
2229
|
+
}
|
|
2230
|
+
|
|
2133
2231
|
.sv-grid-group-cell {
|
|
2134
2232
|
padding: 0 12px;
|
|
2135
2233
|
}
|
|
@@ -2209,7 +2307,9 @@ select.sv-grid-fr-editor {
|
|
|
2209
2307
|
line-height: 1.4;
|
|
2210
2308
|
resize: vertical;
|
|
2211
2309
|
outline: none;
|
|
2212
|
-
|
|
2310
|
+
/* --sg-focus-ring is a COLOR everywhere else (`outline: 2px solid ...`).
|
|
2311
|
+
Consume it as one here too, rather than as a whole box-shadow value. */
|
|
2312
|
+
box-shadow: 0 0 0 2px var(--sg-focus-ring, rgba(99, 102, 241, 0.40));
|
|
2213
2313
|
}
|
|
2214
2314
|
|
|
2215
2315
|
.sv-grid-autocomplete {
|
package/src/SvGrid.svelte
CHANGED
|
@@ -256,13 +256,32 @@
|
|
|
256
256
|
// Server-side group / tree a11y: aria-level on every tree row, aria-expanded
|
|
257
257
|
// on the expandable ones. Undefined (attribute omitted) when serverGroup is off.
|
|
258
258
|
const serverGroup = $derived(opt.serverGroup);
|
|
259
|
+
// Client tree data reuses this same treegrid contract - the row model already
|
|
260
|
+
// records depth and expandability, so the accessors just read the row instead
|
|
261
|
+
// of calling back into consumer callbacks.
|
|
262
|
+
const treeData = $derived(opt.treeData);
|
|
259
263
|
function sgAriaLevel(row: Row<TData>): number | undefined {
|
|
264
|
+
if (treeData) return (row.depth ?? 0) + 1;
|
|
260
265
|
return serverGroup ? serverGroup.level(row.original as TData) + 1 : undefined;
|
|
261
266
|
}
|
|
262
267
|
function sgAriaExpanded(row: Row<TData>): boolean | undefined {
|
|
268
|
+
if (treeData) {
|
|
269
|
+
if (!row.getCanExpand?.()) return undefined;
|
|
270
|
+
return row.getIsExpanded?.() ?? false;
|
|
271
|
+
}
|
|
263
272
|
if (!serverGroup || !serverGroup.isGroup(row.original as TData)) return undefined;
|
|
264
273
|
return serverGroup.expanded?.(row.original as TData) ?? false;
|
|
265
274
|
}
|
|
275
|
+
/** Column that carries the tree expander: explicit, else the first visible one. */
|
|
276
|
+
const treeColumnId = $derived(
|
|
277
|
+
treeData ? (treeData.column ?? allColumns[0]?.id ?? "") : "",
|
|
278
|
+
);
|
|
279
|
+
// Row-grouping display modes: group state lives in synthetic columns instead
|
|
280
|
+
// of a full-width banner row.
|
|
281
|
+
const groupColumnMode = $derived(ctrl.groupColumnMode);
|
|
282
|
+
const autoGroupCell = $derived(ctrl.autoGroupCell);
|
|
283
|
+
const isAutoGroupColumn = (id: string): boolean =>
|
|
284
|
+
id === "__autoGroup" || id.startsWith("__group_");
|
|
266
285
|
const sortDirectionByColumn = $derived(ctrl.sortDirectionByColumn);
|
|
267
286
|
const groupingColumns = $derived(ctrl.groupingColumns);
|
|
268
287
|
const paginationState = $derived(ctrl.paginationState);
|
|
@@ -286,10 +305,25 @@
|
|
|
286
305
|
const rh = opt.rowHeight;
|
|
287
306
|
return typeof rh === "function" ? rh(i) : (rh ?? 30);
|
|
288
307
|
};
|
|
308
|
+
// Auto height applies to both bodies. The non-virtualized one has no
|
|
309
|
+
// `rowItem`, so it sizes from `rowSizePx` - but must still emit `min-height`
|
|
310
|
+
// and the wrap class, or `autoRowHeight` would silently do nothing whenever
|
|
311
|
+
// `virtualization={false}`.
|
|
312
|
+
const rowStyleFor = (i: number): string =>
|
|
313
|
+
autoRowHeightOn
|
|
314
|
+
? `min-height: ${measuredRowHeightPx(i) ?? rowSizePx(i)}px;`
|
|
315
|
+
: `height: ${rowSizePx(i)}px;`;
|
|
289
316
|
const columnVirtualizationEnabled = $derived(
|
|
290
317
|
ctrl.columnVirtualizationEnabled,
|
|
291
318
|
);
|
|
292
319
|
const virtualRows = $derived(ctrl.virtualRows);
|
|
320
|
+
const autoRowHeightOn = $derived(ctrl.autoRowHeightOn);
|
|
321
|
+
const measureRowHeight = $derived(ctrl.measureRowHeight);
|
|
322
|
+
const measuredRowHeightPx = $derived(ctrl.measuredRowHeightPx);
|
|
323
|
+
// Under auto height the row must be free to grow, so it gets `min-height`
|
|
324
|
+
// (the pre-measure estimate) instead of a fixed `height` that would clip it.
|
|
325
|
+
const rowHeightStyle = (size: number): string =>
|
|
326
|
+
autoRowHeightOn ? `min-height: ${size}px;` : `height: ${size}px;`;
|
|
293
327
|
// DOM-space spacer heights + capped total: identical to the logical
|
|
294
328
|
// virtualizer values for normal grids, scaled down past
|
|
295
329
|
// MAX_DOM_SCROLL_HEIGHT so huge grids stay scrollable to the last row on
|
|
@@ -405,6 +439,16 @@
|
|
|
405
439
|
const hasMeasured = $derived(ctrl.hasMeasured);
|
|
406
440
|
const onBodyScroll = $derived(ctrl.onBodyScroll);
|
|
407
441
|
const computeRowClass = $derived(ctrl.computeRowClass);
|
|
442
|
+
// Group subtotal rows are clones of their banner, so they render through the
|
|
443
|
+
// normal cell path and their aggregates land under the right columns; only
|
|
444
|
+
// the class distinguishes them. Typed here rather than cast inline - an `as`
|
|
445
|
+
// inside template markup type-checks but breaks the vitest/build parse.
|
|
446
|
+
const isGroupFooterRow = (row: Row<TData>): boolean =>
|
|
447
|
+
(row as { __groupFooter?: boolean }).__groupFooter === true;
|
|
448
|
+
// The grand total is a group footer that closes the whole set rather than one
|
|
449
|
+
// group, so it carries both classes - shared chrome, heavier top rule.
|
|
450
|
+
const isGrandTotalRow = (row: Row<TData>): boolean =>
|
|
451
|
+
(row as { __grandTotal?: boolean }).__grandTotal === true;
|
|
408
452
|
const computeCellClass = $derived(ctrl.computeCellClass);
|
|
409
453
|
const computeCellTooltip = $derived(ctrl.computeCellTooltip);
|
|
410
454
|
const computeCellValidity = $derived(ctrl.computeCellValidity);
|
|
@@ -412,6 +456,7 @@
|
|
|
412
456
|
const getCellDisplayValue = $derived(ctrl.getCellDisplayValue);
|
|
413
457
|
const getColumnAlign = $derived(ctrl.getColumnAlign);
|
|
414
458
|
const getColumnEditorOptions = $derived(ctrl.getColumnEditorOptions);
|
|
459
|
+
const areEditorOptionsLoading = $derived(ctrl.areEditorOptionsLoading);
|
|
415
460
|
const formatListCellValue = $derived(ctrl.formatListCellValue);
|
|
416
461
|
const formatCellValue = $derived(ctrl.formatCellValue);
|
|
417
462
|
const getPinnedCellValue = $derived(ctrl.getPinnedCellValue);
|
|
@@ -460,6 +505,21 @@
|
|
|
460
505
|
ctrl.editingCell = null;
|
|
461
506
|
ctrl.gridRootEl?.focus({ preventScroll: true });
|
|
462
507
|
},
|
|
508
|
+
// Tab out of an editor: commit, then step to the adjacent cell. Without
|
|
509
|
+
// this a registered editor's Tab walks focus out of the grid (#48) - the
|
|
510
|
+
// built-in editors already route through the same helper.
|
|
511
|
+
onCommitAndMove: (v: unknown, direction: 1 | -1) => {
|
|
512
|
+
if (v !== undefined) updateEditingCellValue(v);
|
|
513
|
+
ctrl.commitAndMoveByTab(direction === -1);
|
|
514
|
+
},
|
|
515
|
+
// Close a popover editor without committing. Same effect as cancel today;
|
|
516
|
+
// named separately so an editor can distinguish "dismiss my panel" from
|
|
517
|
+
// "abandon the edit" if that ever diverges.
|
|
518
|
+
onRequestClose: () => {
|
|
519
|
+
ctrl.editingCell = null;
|
|
520
|
+
ctrl.gridRootEl?.focus({ preventScroll: true });
|
|
521
|
+
},
|
|
522
|
+
inCell: true,
|
|
463
523
|
};
|
|
464
524
|
}
|
|
465
525
|
const onHeaderSortClick = $derived(ctrl.onHeaderSortClick);
|
|
@@ -869,6 +929,11 @@
|
|
|
869
929
|
<path d="M6 6l12 12" />
|
|
870
930
|
{:else if name === "chevron-down"}
|
|
871
931
|
<path d="M6 9l6 6 6-6" />
|
|
932
|
+
{:else if name === "chevron-right"}
|
|
933
|
+
<!-- Same path SvTree's twisty uses, so every expander in the library
|
|
934
|
+
draws the same chevron. Rotated 90deg by CSS when expanded rather
|
|
935
|
+
than swapped for a second glyph. -->
|
|
936
|
+
<path d="m9 18 6-6-6-6" />
|
|
872
937
|
{:else if name === "op-contains"}
|
|
873
938
|
<circle cx="11" cy="11" r="6" />
|
|
874
939
|
<path d="M20 20l-4.5-4.5" />
|
|
@@ -1027,7 +1092,12 @@
|
|
|
1027
1092
|
</svg>
|
|
1028
1093
|
{/if}
|
|
1029
1094
|
{:else}
|
|
1030
|
-
|
|
1095
|
+
<!-- Indent for rows nested under a group BANNER. Tree data and the
|
|
1096
|
+
column display modes draw their own indent next to the expander, so
|
|
1097
|
+
this must not stack on top of theirs - `row.depth` is set in all
|
|
1098
|
+
three cases and this used to fire for every one of them, leaving a
|
|
1099
|
+
20px gap between a tree row's chevron and its text. -->
|
|
1100
|
+
{#if row.depth > 0 && column.id === allColumns[0]?.id && !treeData && !groupColumnMode}
|
|
1031
1101
|
<span
|
|
1032
1102
|
class="sv-grid-group-child-indent"
|
|
1033
1103
|
style={`width: ${row.depth * 20}px;`}
|
|
@@ -1073,10 +1143,75 @@
|
|
|
1073
1143
|
color / weight and the icon-set glyph ride on the content wrapper.
|
|
1074
1144
|
Falls straight through to `cellBody` when no format applies, so the
|
|
1075
1145
|
common (unformatted) path pays nothing. -->
|
|
1146
|
+
<!-- Tree expander + indentation, prefixed to the tree column's cell. Rows
|
|
1147
|
+
without children still get the indent (and a spacer where the chevron
|
|
1148
|
+
would be) so values stay aligned down a level. -->
|
|
1149
|
+
{#snippet treeAffordance(row: Row<TData>)}
|
|
1150
|
+
{@const canExpand = row.getCanExpand?.() ?? false}
|
|
1151
|
+
<span
|
|
1152
|
+
class="sv-grid-tree-indent"
|
|
1153
|
+
style={`width:${(row.depth ?? 0) * (treeData?.indentPx ?? 12)}px;`}
|
|
1154
|
+
aria-hidden="true"
|
|
1155
|
+
></span>
|
|
1156
|
+
{#if canExpand}
|
|
1157
|
+
<button
|
|
1158
|
+
type="button"
|
|
1159
|
+
class="sv-grid-tree-toggle"
|
|
1160
|
+
aria-expanded={row.getIsExpanded?.() ? "true" : "false"}
|
|
1161
|
+
aria-label={row.getIsExpanded?.() ? "Collapse row" : "Expand row"}
|
|
1162
|
+
onclick={(event) => {
|
|
1163
|
+
event.stopPropagation();
|
|
1164
|
+
row.toggleExpanded?.();
|
|
1165
|
+
}}>{@render icon("chevron-right")}</button
|
|
1166
|
+
>
|
|
1167
|
+
{:else}
|
|
1168
|
+
<span class="sv-grid-tree-spacer" aria-hidden="true"></span>
|
|
1169
|
+
{/if}
|
|
1170
|
+
{/snippet}
|
|
1171
|
+
|
|
1172
|
+
<!-- The group label + expander for a `singleColumn` / `multipleColumns`
|
|
1173
|
+
auto-group cell. Indented by level in single-column mode; in multi-column
|
|
1174
|
+
mode each level already has its own column, so no indent. -->
|
|
1175
|
+
{#snippet autoGroupAffordance(row: Row<TData>, cell: { label: string; count: number; depth: number })}
|
|
1176
|
+
<span
|
|
1177
|
+
class="sv-grid-tree-indent"
|
|
1178
|
+
style={`width:${cell.depth * 16}px;`}
|
|
1179
|
+
aria-hidden="true"
|
|
1180
|
+
></span>
|
|
1181
|
+
<button
|
|
1182
|
+
type="button"
|
|
1183
|
+
class="sv-grid-tree-toggle"
|
|
1184
|
+
aria-expanded={row.getIsExpanded?.() ? "true" : "false"}
|
|
1185
|
+
aria-label={row.getIsExpanded?.() ? "Collapse group" : "Expand group"}
|
|
1186
|
+
onclick={(event) => {
|
|
1187
|
+
event.stopPropagation();
|
|
1188
|
+
row.toggleExpanded?.();
|
|
1189
|
+
}}>{@render icon("chevron-right")}</button
|
|
1190
|
+
>
|
|
1191
|
+
<span class="sv-grid-autogroup-label">{cell.label}</span>
|
|
1192
|
+
<span class="sv-grid-group-count">({cell.count})</span>
|
|
1193
|
+
{/snippet}
|
|
1194
|
+
|
|
1076
1195
|
{#snippet cellBodyWithFormat(
|
|
1077
1196
|
row: Row<TData>,
|
|
1078
1197
|
column: Column<TData>,
|
|
1079
1198
|
cellValue: unknown,
|
|
1199
|
+
)}
|
|
1200
|
+
{#if groupColumnMode && isAutoGroupColumn(column.id)}
|
|
1201
|
+
{@const agc = autoGroupCell(row, column.id)}
|
|
1202
|
+
{#if agc}{@render autoGroupAffordance(row, agc)}{/if}
|
|
1203
|
+
{:else}
|
|
1204
|
+
{#if treeData && column.id === treeColumnId}
|
|
1205
|
+
{@render treeAffordance(row)}
|
|
1206
|
+
{/if}
|
|
1207
|
+
{@render cellBodyFormatted(row, column, cellValue)}
|
|
1208
|
+
{/if}
|
|
1209
|
+
{/snippet}
|
|
1210
|
+
|
|
1211
|
+
{#snippet cellBodyFormatted(
|
|
1212
|
+
row: Row<TData>,
|
|
1213
|
+
column: Column<TData>,
|
|
1214
|
+
cellValue: unknown,
|
|
1080
1215
|
)}
|
|
1081
1216
|
{@const cf = cellConditionalFormat(row, column, cellValue)}
|
|
1082
1217
|
{#if cf && (cf.background || cf.dataBar || cf.icon || cf.color || cf.fontWeight != null)}
|
|
@@ -1175,10 +1310,12 @@
|
|
|
1175
1310
|
></button>
|
|
1176
1311
|
{:else if ctrl.editingCell?.editorType === "list"}
|
|
1177
1312
|
{@const opts = getColumnEditorOptions(column, row)}
|
|
1313
|
+
{@const optsLoading = areEditorOptionsLoading(column, row)}
|
|
1178
1314
|
{@const multi = column.columnDef.editorMultiple === true}
|
|
1179
1315
|
{#if DropdownEditor}
|
|
1180
1316
|
<DropdownEditor
|
|
1181
1317
|
options={opts}
|
|
1318
|
+
loading={optsLoading}
|
|
1182
1319
|
value={ctrl.editingCell?.value}
|
|
1183
1320
|
multiple={multi}
|
|
1184
1321
|
placeholder="Select…"
|
|
@@ -1196,9 +1333,10 @@
|
|
|
1196
1333
|
{/if}
|
|
1197
1334
|
{:else if ctrl.editingCell?.editorType === "chips"}
|
|
1198
1335
|
{@const opts = getColumnEditorOptions(column, row)}
|
|
1336
|
+
{@const optsLoading = areEditorOptionsLoading(column, row)}
|
|
1199
1337
|
{@const multi = column.columnDef.editorMultiple === true}
|
|
1200
1338
|
{@const selectedArr = toValueArray(ctrl.editingCell?.value)}
|
|
1201
|
-
{@render chipsEditor(opts, multi, selectedArr)}
|
|
1339
|
+
{@render chipsEditor(opts, multi, selectedArr, optsLoading)}
|
|
1202
1340
|
{:else if ctrl.editingCell?.editorType === "rating"}
|
|
1203
1341
|
{@const ratingVal = Math.max(
|
|
1204
1342
|
0,
|
|
@@ -1241,9 +1379,11 @@
|
|
|
1241
1379
|
<!-- Custom dropdown: opens a themed popover identical in feel to
|
|
1242
1380
|
the existing 'list' editor (single-select, no typeahead). -->
|
|
1243
1381
|
{@const selectOpts = getColumnEditorOptions(column, row)}
|
|
1382
|
+
{@const optsLoading = areEditorOptionsLoading(column, row)}
|
|
1244
1383
|
{#if DropdownEditor}
|
|
1245
1384
|
<DropdownEditor
|
|
1246
1385
|
options={selectOpts}
|
|
1386
|
+
loading={optsLoading}
|
|
1247
1387
|
value={ctrl.editingCell?.value}
|
|
1248
1388
|
multiple={false}
|
|
1249
1389
|
placeholder="Select…"
|
|
@@ -1263,9 +1403,11 @@
|
|
|
1263
1403
|
<!-- Searchable combobox: same popover as 'select' with a
|
|
1264
1404
|
typeahead filter input baked in at the top. -->
|
|
1265
1405
|
{@const richOpts = getColumnEditorOptions(column, row)}
|
|
1406
|
+
{@const optsLoading = areEditorOptionsLoading(column, row)}
|
|
1266
1407
|
{#if DropdownEditor}
|
|
1267
1408
|
<DropdownEditor
|
|
1268
1409
|
options={richOpts}
|
|
1410
|
+
loading={optsLoading}
|
|
1269
1411
|
value={ctrl.editingCell?.value}
|
|
1270
1412
|
multiple={false}
|
|
1271
1413
|
searchable={true}
|
|
@@ -1527,6 +1669,7 @@
|
|
|
1527
1669
|
opts: CellEditorOption[],
|
|
1528
1670
|
multi: boolean,
|
|
1529
1671
|
selectedArr: Array<string | number>,
|
|
1672
|
+
optsLoading: boolean,
|
|
1530
1673
|
)}
|
|
1531
1674
|
{#if opts.length > 0}
|
|
1532
1675
|
<!-- Options-driven chips editor: defer to the custom dropdown,
|
|
@@ -1536,6 +1679,7 @@
|
|
|
1536
1679
|
{#if DropdownEditor}
|
|
1537
1680
|
<DropdownEditor
|
|
1538
1681
|
options={opts}
|
|
1682
|
+
loading={optsLoading}
|
|
1539
1683
|
value={ctrl.editingCell?.value}
|
|
1540
1684
|
multiple={multi}
|
|
1541
1685
|
placeholder="Pick…"
|
|
@@ -1657,7 +1801,7 @@
|
|
|
1657
1801
|
onclick={(event) => {
|
|
1658
1802
|
event.stopPropagation();
|
|
1659
1803
|
row.toggleExpanded?.();
|
|
1660
|
-
}}>{
|
|
1804
|
+
}}>{@render icon("chevron-right")}</button
|
|
1661
1805
|
>
|
|
1662
1806
|
<span class="sv-grid-group-label">{headerLabel}: {groupValue}</span>
|
|
1663
1807
|
<span class="sv-grid-group-count"
|
|
@@ -1853,7 +1997,7 @@
|
|
|
1853
1997
|
activeDescendantId,
|
|
1854
1998
|
rowCount: allRows.length,
|
|
1855
1999
|
colCount: allColumns.length,
|
|
1856
|
-
treegrid: !!opt.serverGroup,
|
|
2000
|
+
treegrid: !!opt.serverGroup || !!opt.treeData,
|
|
1857
2001
|
})}
|
|
1858
2002
|
onkeydown={onGridKeyDown}
|
|
1859
2003
|
onpaste={onGridPaste}
|
|
@@ -2438,14 +2582,14 @@
|
|
|
2438
2582
|
{#if row}
|
|
2439
2583
|
{#if opt.isDetailRow?.(row.original as TData, rowIndex)}
|
|
2440
2584
|
{@render detailRowMarkup(row, rowIndex)}
|
|
2441
|
-
{:else if isGroupRow(row)}
|
|
2585
|
+
{:else if isGroupRow(row) && !groupColumnMode}
|
|
2442
2586
|
<tr
|
|
2443
2587
|
class="sv-grid-row sv-grid-group-row"
|
|
2444
2588
|
class:sv-grid-row-selected={isRowSelected(row.id)}
|
|
2445
2589
|
aria-level={row.depth + 1}
|
|
2446
2590
|
aria-expanded={row.getIsExpanded?.() ? "true" : "false"}
|
|
2447
2591
|
{...getGridRowA11yProps(rowIndex + 1)}
|
|
2448
|
-
style={
|
|
2592
|
+
style={rowHeightStyle(rowItem.size)}
|
|
2449
2593
|
>
|
|
2450
2594
|
<td
|
|
2451
2595
|
class="sv-grid-cell sv-grid-group-cell"
|
|
@@ -2467,11 +2611,15 @@
|
|
|
2467
2611
|
class:sv-grid-row-alt={opt.zebraRows &&
|
|
2468
2612
|
rowIndex % 2 === 1}
|
|
2469
2613
|
class:sv-grid-row-draggable={rowDragManagedEffective}
|
|
2614
|
+
class:sv-grid-group-footer-row={isGroupFooterRow(row)}
|
|
2615
|
+
class:sv-grid-grand-total-row={isGrandTotalRow(row)}
|
|
2616
|
+
class:sv-grid-row-auto-height={autoRowHeightOn}
|
|
2470
2617
|
{...getGridRowA11yProps(rowIndex + 1)}
|
|
2471
2618
|
aria-level={sgAriaLevel(row)}
|
|
2472
2619
|
aria-expanded={sgAriaExpanded(row)}
|
|
2473
2620
|
{...rowDragAttrs(rowIndex)}
|
|
2474
|
-
style={
|
|
2621
|
+
style={rowHeightStyle(rowItem.size)}
|
|
2622
|
+
use:measureRowHeight={rowIndex}
|
|
2475
2623
|
>
|
|
2476
2624
|
{#if showRowNumbersEffective}
|
|
2477
2625
|
<td
|
|
@@ -2690,14 +2838,14 @@
|
|
|
2690
2838
|
{#each allRows as row, rowIndex (row.id)}
|
|
2691
2839
|
{#if opt.isDetailRow?.(row.original as TData, rowIndex)}
|
|
2692
2840
|
{@render detailRowMarkup(row, rowIndex)}
|
|
2693
|
-
{:else if isGroupRow(row)}
|
|
2841
|
+
{:else if isGroupRow(row) && !groupColumnMode}
|
|
2694
2842
|
<tr
|
|
2695
2843
|
class="sv-grid-row sv-grid-group-row"
|
|
2696
2844
|
class:sv-grid-row-selected={isRowSelected(row.id)}
|
|
2697
2845
|
aria-level={row.depth + 1}
|
|
2698
2846
|
aria-expanded={row.getIsExpanded?.() ? "true" : "false"}
|
|
2699
2847
|
{...getGridRowA11yProps(rowIndex + 1)}
|
|
2700
|
-
style={
|
|
2848
|
+
style={rowStyleFor(rowIndex)}
|
|
2701
2849
|
>
|
|
2702
2850
|
<td
|
|
2703
2851
|
class="sv-grid-cell sv-grid-group-cell"
|
|
@@ -2719,11 +2867,15 @@
|
|
|
2719
2867
|
class:sv-grid-row-alt={opt.zebraRows &&
|
|
2720
2868
|
rowIndex % 2 === 1}
|
|
2721
2869
|
class:sv-grid-row-draggable={rowDragManagedEffective}
|
|
2870
|
+
class:sv-grid-group-footer-row={isGroupFooterRow(row)}
|
|
2871
|
+
class:sv-grid-grand-total-row={isGrandTotalRow(row)}
|
|
2872
|
+
class:sv-grid-row-auto-height={autoRowHeightOn}
|
|
2722
2873
|
{...getGridRowA11yProps(rowIndex + 1)}
|
|
2723
2874
|
aria-level={sgAriaLevel(row)}
|
|
2724
2875
|
aria-expanded={sgAriaExpanded(row)}
|
|
2725
2876
|
{...rowDragAttrs(rowIndex)}
|
|
2726
|
-
style={
|
|
2877
|
+
style={rowStyleFor(rowIndex)}
|
|
2878
|
+
use:measureRowHeight={rowIndex}
|
|
2727
2879
|
>
|
|
2728
2880
|
{#if showRowNumbersEffective}
|
|
2729
2881
|
<td
|
package/src/SvGrid.types.ts
CHANGED
|
@@ -9,6 +9,7 @@ import type {
|
|
|
9
9
|
TableFeatures,
|
|
10
10
|
} from "./index";
|
|
11
11
|
import type { ConditionalFormat } from "./conditional-formatting";
|
|
12
|
+
import type { GroupDisplayType } from "./group-display";
|
|
12
13
|
import type { GridMessages } from "./grid-messages";
|
|
13
14
|
import type { GridPivotConfig } from "./pivot-view.svelte";
|
|
14
15
|
import type { MenuItem } from "./SvMenuList.svelte";
|
|
@@ -1029,7 +1030,75 @@ export type Props<TFeatures extends TableFeatures = TableFeatures, TData extends
|
|
|
1029
1030
|
sortable?: boolean;
|
|
1030
1031
|
filterable?: boolean;
|
|
1031
1032
|
editable?: boolean;
|
|
1033
|
+
/**
|
|
1034
|
+
* Client-side tree data: nest rows into a hierarchy by parent id.
|
|
1035
|
+
*
|
|
1036
|
+
* ```svelte
|
|
1037
|
+
* <SvGrid {data} {columns} treeData={{ parentField: 'managerId', column: 'name' }} />
|
|
1038
|
+
* ```
|
|
1039
|
+
*
|
|
1040
|
+
* Unlike grouping, tree rows are real data rows - they keep their cells,
|
|
1041
|
+
* editing and selection, and only gain an expander plus indentation in
|
|
1042
|
+
* `column`. Rows whose parent is missing become roots rather than vanishing.
|
|
1043
|
+
*
|
|
1044
|
+
* For NESTED source data (`children: [...]`), flatten it first with
|
|
1045
|
+
* `flattenTreeData(data, { childrenField: 'children' })` and point
|
|
1046
|
+
* `parentField` at the `__parentId` it stamps on.
|
|
1047
|
+
*
|
|
1048
|
+
* Setting this replaces row grouping - a row cannot be both.
|
|
1049
|
+
*/
|
|
1050
|
+
treeData?: {
|
|
1051
|
+
/** Field holding each row's parent id. */
|
|
1052
|
+
parentField: string;
|
|
1053
|
+
/** Field holding the row's own id. Defaults to `'id'`. */
|
|
1054
|
+
idField?: string;
|
|
1055
|
+
/**
|
|
1056
|
+
* Column id that carries the expander + indentation. Defaults to the first
|
|
1057
|
+
* visible column.
|
|
1058
|
+
*/
|
|
1059
|
+
column?: string;
|
|
1060
|
+
/** Indent per depth level, in px. Default `12`. */
|
|
1061
|
+
indentPx?: number;
|
|
1062
|
+
};
|
|
1032
1063
|
groupable?: boolean;
|
|
1064
|
+
/**
|
|
1065
|
+
* Render a subtotal row after each group's children, carrying that group's
|
|
1066
|
+
* aggregate values under the columns they belong to (the columns with an
|
|
1067
|
+
* `aggregate` set). Off by default.
|
|
1068
|
+
*
|
|
1069
|
+
* Footers are inserted after paging, so they never count against `pageSize` -
|
|
1070
|
+
* a page shows its `pageSize` data rows plus whatever footers close on it.
|
|
1071
|
+
*/
|
|
1072
|
+
groupFooters?: boolean;
|
|
1073
|
+
/**
|
|
1074
|
+
* Append a grand-total row at the very end, aggregating the whole filtered
|
|
1075
|
+
* set (not just the current page) with each column's `aggregate` function.
|
|
1076
|
+
*
|
|
1077
|
+
* Independent of `groupFooters`: use it on a flat grid for a bottom totals
|
|
1078
|
+
* line, or alongside group subtotals for both. Like group footers it is
|
|
1079
|
+
* appended after paging, so it never counts against `pageSize` - but it is
|
|
1080
|
+
* only appended on the LAST page, so a total never appears mid-dataset.
|
|
1081
|
+
* Columns without an `aggregate` are left blank.
|
|
1082
|
+
*/
|
|
1083
|
+
grandTotalRow?: boolean;
|
|
1084
|
+
/**
|
|
1085
|
+
* How grouped rows are displayed.
|
|
1086
|
+
*
|
|
1087
|
+
* - `groupRows` (default): a full-width banner row per group. Unchanged
|
|
1088
|
+
* behaviour - existing grids are untouched.
|
|
1089
|
+
* - `singleColumn`: one synthetic "Group" column holding every level,
|
|
1090
|
+
* indented by depth.
|
|
1091
|
+
* - `multipleColumns`: one synthetic column per grouped field.
|
|
1092
|
+
*
|
|
1093
|
+
* Both column modes hide the grouped source columns, since their values move
|
|
1094
|
+
* into the auto column(s), and render group rows as ordinary rows so their
|
|
1095
|
+
* aggregate cells line up under the real columns.
|
|
1096
|
+
*/
|
|
1097
|
+
groupDisplayMode?: GroupDisplayType;
|
|
1098
|
+
/** Header for the combined `singleColumn` auto-group column. Default `"Group"`. */
|
|
1099
|
+
autoGroupColumnHeader?: string;
|
|
1100
|
+
/** Width (px) of each auto-group column. Default `220`. */
|
|
1101
|
+
autoGroupColumnWidth?: number;
|
|
1033
1102
|
pageable?: boolean;
|
|
1034
1103
|
loading?: boolean;
|
|
1035
1104
|
/**
|
|
@@ -1107,6 +1176,18 @@ export type Props<TFeatures extends TableFeatures = TableFeatures, TData extends
|
|
|
1107
1176
|
* per-row variable heights (e.g. an interactive row-resize feature).
|
|
1108
1177
|
* Defaults to 30. */
|
|
1109
1178
|
rowHeight?: number | ((rowIndex: number) => number);
|
|
1179
|
+
/**
|
|
1180
|
+
* Size each row to its own content instead of a fixed height: cell text wraps
|
|
1181
|
+
* and the row grows to fit the tallest cell. Rows are measured after they
|
|
1182
|
+
* render, so this works with virtualization - `rowHeight` (or 30) is the
|
|
1183
|
+
* estimate used before a row has been measured, which keeps the scrollbar
|
|
1184
|
+
* stable while you scroll into new rows.
|
|
1185
|
+
*
|
|
1186
|
+
* Costs a measurement pass per row, so prefer a fixed `rowHeight` when your
|
|
1187
|
+
* content is uniform. Ignored when `rowHeight` is a function (you are already
|
|
1188
|
+
* supplying per-row heights).
|
|
1189
|
+
*/
|
|
1190
|
+
autoRowHeight?: boolean;
|
|
1110
1191
|
/**
|
|
1111
1192
|
* Height (px) of a single column-header level row. With multi-level
|
|
1112
1193
|
* (grouped) headers the total header height is `levels * headerHeight`,
|
|
@@ -41,6 +41,10 @@
|
|
|
41
41
|
onCommit?: () => void
|
|
42
42
|
/** Called when the user dismisses (Escape, blur with no change). */
|
|
43
43
|
onCancel?: () => void
|
|
44
|
+
/** The option list is still being fetched (async `editorOptions`). Shows a
|
|
45
|
+
* loading line instead of "No options", which would read as "nothing to
|
|
46
|
+
* pick" when the truth is "not here yet". */
|
|
47
|
+
loading?: boolean
|
|
44
48
|
}
|
|
45
49
|
|
|
46
50
|
let {
|
|
@@ -54,6 +58,7 @@
|
|
|
54
58
|
onChange,
|
|
55
59
|
onCommit,
|
|
56
60
|
onCancel,
|
|
61
|
+
loading = false,
|
|
57
62
|
}: Props = $props()
|
|
58
63
|
|
|
59
64
|
/** Stable per-instance id used to build option ids for
|
|
@@ -158,9 +163,13 @@
|
|
|
158
163
|
function updatePanelPosition() {
|
|
159
164
|
if (!triggerEl) return
|
|
160
165
|
// Use the smaller of (option count, 10) for the upward-flip math so
|
|
161
|
-
// a 3-option list doesn't think it needs 320px of headroom.
|
|
162
|
-
|
|
163
|
-
|
|
166
|
+
// a 3-option list doesn't think it needs 320px of headroom. Measured off
|
|
167
|
+
// the VISIBLE options - the search-filtered list is what actually renders,
|
|
168
|
+
// and an async list is empty at open time.
|
|
169
|
+
const visibleCount = Math.min(visibleOptions.length, 10)
|
|
170
|
+
const searchHeight = searchable ? 38 : 0
|
|
171
|
+
const estimatedHeight =
|
|
172
|
+
visibleCount * optionRowHeight + (multiple ? 40 : 0) + searchHeight + 8
|
|
164
173
|
panelRect = anchoredRect(triggerEl.getBoundingClientRect(), {
|
|
165
174
|
estimatedHeight,
|
|
166
175
|
// Preserve the original behavior: anchor to the trigger, no clamping.
|
|
@@ -181,6 +190,20 @@
|
|
|
181
190
|
}
|
|
182
191
|
})
|
|
183
192
|
|
|
193
|
+
// Re-anchor when the option list itself changes while the panel is open.
|
|
194
|
+
// The geometry is derived from the option COUNT, so an async list - which
|
|
195
|
+
// lands after the panel opened, and a search filter, which shrinks it - would
|
|
196
|
+
// otherwise leave the panel sized for the list it had a moment ago. With an
|
|
197
|
+
// async source that means a panel sized for ZERO options: 8px tall, with the
|
|
198
|
+
// arriving options spilling outside its background and the grid showing
|
|
199
|
+
// through behind them.
|
|
200
|
+
$effect(() => {
|
|
201
|
+
if (!open) return
|
|
202
|
+
void visibleOptions.length
|
|
203
|
+
void optionRowHeight
|
|
204
|
+
updatePanelPosition()
|
|
205
|
+
})
|
|
206
|
+
|
|
184
207
|
function closePanel() {
|
|
185
208
|
open = false
|
|
186
209
|
highlighted = -1
|
|
@@ -387,9 +410,15 @@
|
|
|
387
410
|
{@const needsScroll = options.length > 10}
|
|
388
411
|
<!-- Cap to the comfortable 10-row height AND the room the viewport allows,
|
|
389
412
|
so a panel near the bottom edge scrolls instead of overflowing. -->
|
|
413
|
+
<!-- Capped by the REAL room the viewport allows (`availHeight`), never by
|
|
414
|
+
`maxHeight` - that one is clamped to our estimated height, and the
|
|
415
|
+
estimate assumes a 32px row. Themes with a larger font render taller
|
|
416
|
+
rows, so the estimate came up a few px short and the last option was
|
|
417
|
+
cut off by the panel edge. The estimate still decides which way to
|
|
418
|
+
flip; it must not decide how tall the panel is allowed to be. -->
|
|
390
419
|
{@const panelMax = Math.min(
|
|
391
420
|
needsScroll ? 10 * optionRowHeight + (multiple ? 40 : 0) + 8 : Number.POSITIVE_INFINITY,
|
|
392
|
-
panelRect.
|
|
421
|
+
panelRect.availHeight,
|
|
393
422
|
)}
|
|
394
423
|
<div
|
|
395
424
|
class="sv-grid-dropdown-panel"
|
|
@@ -400,7 +429,8 @@
|
|
|
400
429
|
use:portalToBody
|
|
401
430
|
use:popIn={{ up: panelRect.openUpward }}
|
|
402
431
|
style:position="fixed"
|
|
403
|
-
style:top={`${panelRect.top}px`}
|
|
432
|
+
style:top={panelRect.openUpward ? null : `${panelRect.top}px`}
|
|
433
|
+
style:bottom={panelRect.openUpward ? `${panelRect.bottom}px` : null}
|
|
404
434
|
style:left={`${panelRect.left}px`}
|
|
405
435
|
style:width={`${panelRect.width}px`}
|
|
406
436
|
style:max-height={panelMax !== null ? `${panelMax}px` : null}
|
|
@@ -422,7 +452,9 @@
|
|
|
422
452
|
}}
|
|
423
453
|
/>
|
|
424
454
|
{/if}
|
|
425
|
-
{#if visibleOptions.length === 0}
|
|
455
|
+
{#if loading && visibleOptions.length === 0}
|
|
456
|
+
<div class="sv-grid-dropdown-empty" role="status" aria-live="polite">Loading…</div>
|
|
457
|
+
{:else if visibleOptions.length === 0}
|
|
426
458
|
<div class="sv-grid-dropdown-empty">{searchable && searchQuery ? `No matches for "${searchQuery}"` : 'No options'}</div>
|
|
427
459
|
{:else}
|
|
428
460
|
{#each visibleOptions as opt, i (String(opt.value))}
|