@svgrid/grid 2.2.28 → 2.2.29

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.
Files changed (60) hide show
  1. package/dist/GridMenus.svelte +148 -100
  2. package/dist/SvGrid.controller.svelte.d.ts +14 -1
  3. package/dist/SvGrid.controller.svelte.js +112 -28
  4. package/dist/SvGrid.css +15 -34
  5. package/dist/SvGrid.svelte +49 -21
  6. package/dist/SvGrid.types.d.ts +20 -5
  7. package/dist/SvListBox.svelte +42 -3
  8. package/dist/SvListBox.svelte.d.ts +10 -1
  9. package/dist/cdn/GridMenus-Da_p7SKS.js +486 -0
  10. package/dist/cdn/GridMenus-Ds3OpzZT.js +490 -0
  11. package/dist/cdn/{src-BxNEslEo.js → src-BLJfdyL0.js} +4014 -3869
  12. package/dist/cdn/{src-C4yKQZ5t.js → src-DYDCiC0D.js} +7036 -6891
  13. package/dist/cdn/svgrid.js +8 -8
  14. package/dist/cdn/svgrid.svelte-external.js +8 -8
  15. package/dist/column-groups.js +8 -10
  16. package/dist/column-id.d.ts +9 -0
  17. package/dist/column-id.js +25 -0
  18. package/dist/core.js +2 -1
  19. package/dist/createListbox.svelte.d.ts +15 -2
  20. package/dist/createListbox.svelte.js +27 -6
  21. package/dist/editing.d.ts +1 -0
  22. package/dist/editing.js +45 -10
  23. package/dist/filtering/excel-filters.d.ts +30 -10
  24. package/dist/filtering/excel-filters.js +96 -15
  25. package/dist/index.d.ts +2 -2
  26. package/dist/index.js +2 -2
  27. package/dist/menus.d.ts +2 -0
  28. package/dist/menus.js +25 -0
  29. package/dist/row-resize.js +40 -0
  30. package/dist/selection.js +1 -1
  31. package/package.json +1 -1
  32. package/src/GridMenus.svelte +148 -100
  33. package/src/SvGrid.controller.svelte.ts +123 -27
  34. package/src/SvGrid.css +15 -34
  35. package/src/SvGrid.svelte +49 -21
  36. package/src/SvGrid.types.ts +20 -5
  37. package/src/SvListBox.svelte +42 -3
  38. package/src/column-groups.test.ts +48 -0
  39. package/src/column-groups.ts +8 -10
  40. package/src/column-id.ts +30 -0
  41. package/src/core.ts +2 -1
  42. package/src/createListbox.svelte.ts +39 -7
  43. package/src/editing.test.ts +90 -0
  44. package/src/editing.ts +46 -12
  45. package/src/filtering/excel-filters.test.ts +44 -1
  46. package/src/filtering/excel-filters.ts +91 -14
  47. package/src/index.ts +2 -0
  48. package/src/menus.test.ts +40 -4
  49. package/src/menus.ts +27 -0
  50. package/src/row-resize.test.ts +100 -0
  51. package/src/row-resize.ts +36 -0
  52. package/src/selection.ts +1 -1
  53. package/src/svgrid.api-extensions.test.ts +8 -3
  54. package/src/svgrid.cell-dom-ids.test.ts +96 -0
  55. package/src/svgrid.conditional-stat-scope.test.ts +111 -0
  56. package/src/svgrid.filter-menu-listbox.svelte.test.ts +308 -0
  57. package/src/svgrid.filter-menu-scroll.test.ts +3 -1
  58. package/src/svgrid.group-header-ids.test.ts +121 -0
  59. package/dist/cdn/GridMenus-BbzEvTYO.js +0 -421
  60. package/dist/cdn/GridMenus-E220X2kM.js +0 -417
package/dist/editing.js CHANGED
@@ -3,6 +3,7 @@
3
3
  // ($state/$derived/$effect) stays in the controller.
4
4
  import { parseEditorValue, } from "./index";
5
5
  import "./sv-grid-scrollbar";
6
+ import { getNextActiveCell } from "./keyboard";
6
7
  import { getCellKey, toValueArray, } from "./SvGrid.helpers";
7
8
  import { getColumnBaseValue, isGroupRow, } from "./cell-values";
8
9
  export function createEditing(ctx) {
@@ -96,6 +97,7 @@ export function createEditing(ctx) {
96
97
  columnId: column.id,
97
98
  editorType,
98
99
  value: char,
100
+ rowRef: row.original,
99
101
  };
100
102
  ctx.setActiveCell(rowIndex, colIndex);
101
103
  ctx.setSelection(rowIndex, colIndex);
@@ -104,15 +106,22 @@ export function createEditing(ctx) {
104
106
  function saveEditingCell() {
105
107
  if (!ctx.editingCell)
106
108
  return;
107
- const row = ctx.allRows.find((entry) => entry.id === ctx.editingCell?.rowId);
109
+ const editing = ctx.editingCell;
110
+ // Resolve the row this edit belongs to. `allRows` is only the current page,
111
+ // so fall back to the full filtered model, then to the data object captured
112
+ // when the edit started. Without that last hop an edit is silently dropped
113
+ // when a filter typed mid-edit hides the row from both models (#49).
114
+ const row = ctx.allRows.find((entry) => entry.id === editing.rowId) ??
115
+ ctx.allRowsBeforePagination.find((entry) => entry.id === editing.rowId);
116
+ const rowData = (row?.original ?? editing.rowRef);
108
117
  const column = ctx.allColumns.find((entry) => entry.id === ctx.editingCell?.columnId);
109
118
  const parsedValue = parseEditorValue(ctx.editingCell.editorType, ctx.editingCell.value, {
110
119
  multiple: column?.columnDef.editorMultiple === true,
111
120
  });
112
121
  let oldValue = undefined;
113
122
  let finalValue = parsedValue;
114
- if (row?.original && column?.columnDef.field) {
115
- oldValue = row.original[column.columnDef.field];
123
+ if (rowData && column?.columnDef.field) {
124
+ oldValue = rowData[column.columnDef.field];
116
125
  // Per-column valueParser refines the type-coerced value before storing.
117
126
  const parser = column.columnDef.valueParser;
118
127
  if (parser) {
@@ -120,12 +129,11 @@ export function createEditing(ctx) {
120
129
  newValue: parsedValue,
121
130
  oldValue,
122
131
  rawInput: ctx.editingCell.value,
123
- data: row.original,
132
+ data: rowData,
124
133
  columnId: ctx.editingCell.columnId,
125
134
  });
126
135
  }
127
- row.original[column.columnDef.field] =
128
- finalValue;
136
+ rowData[column.columnDef.field] = finalValue;
129
137
  }
130
138
  const key = getCellKey(ctx.editingCell.rowId, ctx.editingCell.columnId);
131
139
  ctx.editedCellValues = {
@@ -136,7 +144,7 @@ export function createEditing(ctx) {
136
144
  // Record into the history at the current pointer. Any forward
137
145
  // history (steps the user could have redone) is truncated - this
138
146
  // is the standard "edit invalidates redo" rule.
139
- if (oldValue !== finalValue && row?.original && column?.columnDef.field) {
147
+ if (oldValue !== finalValue && rowData && column?.columnDef.field) {
140
148
  const step = {
141
149
  rowId: ctx.editingCell.rowId,
142
150
  columnId: ctx.editingCell.columnId,
@@ -162,14 +170,14 @@ export function createEditing(ctx) {
162
170
  // Notify the consumer AFTER the row has been updated so any callback-
163
171
  // driven recompute (cascade totals, server save, undo stack) sees the
164
172
  // post-write state. `rowIndex` matches the position in `props.data`.
165
- if (ctx.props.onCellValueChange && row?.original && column) {
166
- const rowIndex = ctx.internalData.indexOf(row.original);
173
+ if (ctx.props.onCellValueChange && rowData && column) {
174
+ const rowIndex = ctx.internalData.indexOf(rowData);
167
175
  ctx.props.onCellValueChange({
168
176
  rowIndex,
169
177
  columnId: column.id,
170
178
  oldValue,
171
179
  newValue: finalValue,
172
- row: row.original,
180
+ row: rowData,
173
181
  });
174
182
  }
175
183
  ctx.editingCell = null;
@@ -200,6 +208,27 @@ export function createEditing(ctx) {
200
208
  function updateEditingCellValue(value) {
201
209
  ctx.editingCell = ctx.editingCell ? { ...ctx.editingCell, value } : ctx.editingCell;
202
210
  }
211
+ /**
212
+ * Commit the open editor and move the active cell one column along, wrapping
213
+ * at row boundaries the way Excel does. Editors `stopPropagation()` every key
214
+ * so the grid's own Tab handler never sees this one - without the explicit
215
+ * move, Tab committed the edit and then let the browser walk focus out of the
216
+ * grid entirely (#48). Shared by every editor type, including the textarea.
217
+ */
218
+ function commitAndMoveByTab(shiftKey) {
219
+ const active = ctx.activeCell;
220
+ saveEditingCell();
221
+ ctx.gridRootEl?.focus({ preventScroll: true });
222
+ if (!active)
223
+ return;
224
+ const next = getNextActiveCell(active, shiftKey ? "tabPrev" : "tabNext", {
225
+ maxRow: ctx.allRows.length - 1,
226
+ maxCol: ctx.allColumns.length - 1,
227
+ });
228
+ ctx.setActiveCell(next.rowIndex, next.colIndex);
229
+ ctx.setSelection(next.rowIndex, next.colIndex);
230
+ ctx.scrollActiveCellIntoView(next.rowIndex, next.colIndex);
231
+ }
203
232
  function onEditorKeyDown(event) {
204
233
  event.stopPropagation();
205
234
  if (event.key === "Enter") {
@@ -207,6 +236,10 @@ export function createEditing(ctx) {
207
236
  saveEditingCell();
208
237
  ctx.gridRootEl?.focus({ preventScroll: true });
209
238
  }
239
+ else if (event.key === "Tab") {
240
+ event.preventDefault();
241
+ commitAndMoveByTab(event.shiftKey);
242
+ }
210
243
  else if (event.key === "Escape") {
211
244
  event.preventDefault();
212
245
  ctx.editingCell = null;
@@ -269,6 +302,7 @@ export function createEditing(ctx) {
269
302
  columnId: column.id,
270
303
  editorType,
271
304
  value: initialValue,
305
+ rowRef: row.original,
272
306
  };
273
307
  ctx.setActiveCell(rowIndex, colIndex);
274
308
  ctx.setSelection(rowIndex, colIndex);
@@ -528,6 +562,7 @@ export function createEditing(ctx) {
528
562
  applyHistoryStep,
529
563
  updateEditingCellValue,
530
564
  onEditorKeyDown,
565
+ commitAndMoveByTab,
531
566
  focusOnMount,
532
567
  onCellDoubleClick,
533
568
  pasteFromClipboard,
@@ -25,19 +25,39 @@ export type ExcelFilterOptions = {
25
25
  export declare function normalizeForFilter(s: string, locale?: string | ReadonlyArray<string>): string;
26
26
  /**
27
27
  * Delimiter used to serialise the `in` / `notIn` value list into the single
28
- * `value` string that the filter model carries. A newline is used because it
29
- * (unlike a comma) practically never appears inside a typed token, so tokens
30
- * survive the round-trip unescaped. The chip input in the filter row reads and
31
- * writes with the same separator via {@link splitInTokens} / {@link joinInTokens}.
28
+ * `value` string that the filter model carries.
29
+ *
30
+ * This has to be single-line safe: the tool panel and the filter menu show the
31
+ * serialised list in a plain `<input type="text">`, and the HTML input value
32
+ * sanitiser STRIPS newlines - so a newline-joined list came back out of the DOM
33
+ * as one run-together token. `splitInTokens` accepts newlines too, so values
34
+ * serialised by older builds still parse.
32
35
  */
33
- export declare const IN_TOKEN_SEP = "\n";
36
+ export declare const IN_TOKEN_SEP = ", ";
34
37
  /**
35
- * Split a serialised `in` / `notIn` value into its individual tokens. Accepts
36
- * both the newline separator the chip input writes and a plain comma-separated
37
- * string (what a raw text field, e.g. the tool-panel filter, produces), so the
38
- * same value round-trips through either UI.
38
+ * Split a serialised `in` / `notIn` value into its individual tokens.
39
+ *
40
+ * The grammar is CSV-shaped: comma OR newline separates, and a token may be
41
+ * double-quoted to carry a separator literally (`""` escapes a quote inside
42
+ * one). Quoting is what makes a comma separator safe - facet values commonly
43
+ * contain commas of their own, because numeric bucket labels are built with
44
+ * `toLocaleString` ("1,234 - 5,678") and date labels with a short month
45
+ * ("Aug 17, 2026"). A quote only opens a token when it is the token's first
46
+ * non-space character, so an unquoted `5" pipe` stays literal.
39
47
  */
40
48
  export declare function splitInTokens(value: unknown): string[];
41
- /** Serialise a list of tokens back into the single `in` / `notIn` value. */
49
+ /**
50
+ * Serialise a list of tokens back into the single `in` / `notIn` value,
51
+ * quoting any token that carries a separator or a quote of its own.
52
+ */
42
53
  export declare function joinInTokens(tokens: ReadonlyArray<string>): string;
54
+ /**
55
+ * The token still being typed at the end of an `in` / `notIn` value - the text
56
+ * after the last separator. `'AAPL, MS'` -> `'MS'`; a value ending in a
57
+ * separator (or empty) has no trailing token.
58
+ *
59
+ * Inputs that hold the whole token list use this to drive the value-suggestion
60
+ * dropdown: the trailing fragment is the search query, not a committed token.
61
+ */
62
+ export declare function trailingInToken(value: unknown): string;
43
63
  export declare function applyExcelFilter(cellValue: unknown, filter: ExcelFilter, options?: ExcelFilterOptions): boolean;
@@ -15,27 +15,108 @@ export function normalizeForFilter(s, locale) {
15
15
  }
16
16
  /**
17
17
  * Delimiter used to serialise the `in` / `notIn` value list into the single
18
- * `value` string that the filter model carries. A newline is used because it
19
- * (unlike a comma) practically never appears inside a typed token, so tokens
20
- * survive the round-trip unescaped. The chip input in the filter row reads and
21
- * writes with the same separator via {@link splitInTokens} / {@link joinInTokens}.
18
+ * `value` string that the filter model carries.
19
+ *
20
+ * This has to be single-line safe: the tool panel and the filter menu show the
21
+ * serialised list in a plain `<input type="text">`, and the HTML input value
22
+ * sanitiser STRIPS newlines - so a newline-joined list came back out of the DOM
23
+ * as one run-together token. `splitInTokens` accepts newlines too, so values
24
+ * serialised by older builds still parse.
22
25
  */
23
- export const IN_TOKEN_SEP = '\n';
26
+ export const IN_TOKEN_SEP = ', ';
24
27
  /**
25
- * Split a serialised `in` / `notIn` value into its individual tokens. Accepts
26
- * both the newline separator the chip input writes and a plain comma-separated
27
- * string (what a raw text field, e.g. the tool-panel filter, produces), so the
28
- * same value round-trips through either UI.
28
+ * Split a serialised `in` / `notIn` value into its individual tokens.
29
+ *
30
+ * The grammar is CSV-shaped: comma OR newline separates, and a token may be
31
+ * double-quoted to carry a separator literally (`""` escapes a quote inside
32
+ * one). Quoting is what makes a comma separator safe - facet values commonly
33
+ * contain commas of their own, because numeric bucket labels are built with
34
+ * `toLocaleString` ("1,234 - 5,678") and date labels with a short month
35
+ * ("Aug 17, 2026"). A quote only opens a token when it is the token's first
36
+ * non-space character, so an unquoted `5" pipe` stays literal.
29
37
  */
30
38
  export function splitInTokens(value) {
31
- return String(value ?? '')
32
- .split(/[\n,]/)
33
- .map((t) => t.trim())
34
- .filter((t) => t.length > 0);
39
+ const { tokens, trailing } = scanInTokens(value);
40
+ if (trailing)
41
+ tokens.push(trailing);
42
+ return tokens;
35
43
  }
36
- /** Serialise a list of tokens back into the single `in` / `notIn` value. */
44
+ /**
45
+ * Serialise a list of tokens back into the single `in` / `notIn` value,
46
+ * quoting any token that carries a separator or a quote of its own.
47
+ */
37
48
  export function joinInTokens(tokens) {
38
- return tokens.map((t) => t.trim()).filter((t) => t.length > 0).join(IN_TOKEN_SEP);
49
+ return tokens
50
+ .map((t) => t.trim())
51
+ .filter((t) => t.length > 0)
52
+ .map((t) => (/["\n,]/.test(t) ? `"${t.replace(/"/g, '""')}"` : t))
53
+ .join(IN_TOKEN_SEP);
54
+ }
55
+ /**
56
+ * The token still being typed at the end of an `in` / `notIn` value - the text
57
+ * after the last separator. `'AAPL, MS'` -> `'MS'`; a value ending in a
58
+ * separator (or empty) has no trailing token.
59
+ *
60
+ * Inputs that hold the whole token list use this to drive the value-suggestion
61
+ * dropdown: the trailing fragment is the search query, not a committed token.
62
+ */
63
+ export function trailingInToken(value) {
64
+ return scanInTokens(value).trailing;
65
+ }
66
+ /**
67
+ * One pass over a serialised `in` / `notIn` value, splitting it into the
68
+ * tokens that are terminated by a separator and the unterminated fragment at
69
+ * the end. Every token helper above is a view onto this, so they can never
70
+ * disagree about where a quoted token starts or ends.
71
+ */
72
+ function scanInTokens(value) {
73
+ const raw = String(value ?? '');
74
+ const tokens = [];
75
+ let buf = '';
76
+ let quoted = false;
77
+ let wasQuoted = false;
78
+ let closed = false;
79
+ // A quoted token keeps its inner spacing; an unquoted one is trimmed.
80
+ const take = () => {
81
+ const t = wasQuoted ? buf : buf.trim();
82
+ buf = '';
83
+ wasQuoted = false;
84
+ closed = false;
85
+ return t;
86
+ };
87
+ for (let i = 0; i < raw.length; i += 1) {
88
+ const ch = raw[i];
89
+ if (quoted) {
90
+ if (ch !== '"') {
91
+ buf += ch;
92
+ continue;
93
+ }
94
+ // A doubled quote is an escaped literal quote, not the closing one.
95
+ if (raw[i + 1] === '"') {
96
+ buf += '"';
97
+ i += 1;
98
+ continue;
99
+ }
100
+ quoted = false;
101
+ closed = true;
102
+ continue;
103
+ }
104
+ if (ch === ',' || ch === '\n') {
105
+ const t = take();
106
+ if (t.length)
107
+ tokens.push(t);
108
+ continue;
109
+ }
110
+ // Only opens a quoted token at the token's start - `5" pipe` is literal.
111
+ if (ch === '"' && !closed && !buf.trim()) {
112
+ quoted = true;
113
+ wasQuoted = true;
114
+ buf = '';
115
+ continue;
116
+ }
117
+ buf += ch;
118
+ }
119
+ return { tokens, trailing: take() };
39
120
  }
40
121
  export function applyExcelFilter(cellValue, filter, options) {
41
122
  const text = String(cellValue ?? '');
package/dist/index.d.ts CHANGED
@@ -56,7 +56,7 @@ export { default as SvAspectRatio } from './SvAspectRatio.svelte';
56
56
  export { registerCellEditor, getCellEditor, hasCellEditor, unregisterCellEditor, registeredCellEditorTypes, defaultEditorProps, resolveEditorProps, type CellEditorContext, type CellEditorRegistration, } from './editor-registry';
57
57
  export { registerBuiltinEditors } from './builtin-editors';
58
58
  export { default as SvField } from './SvField.svelte';
59
- export { createListbox, toSelectedArray, type Listbox, type ListboxConfig, type ListboxValue, type ListboxRootProps, type OptionProps, } from './createListbox.svelte';
59
+ export { createListbox, toSelectedArray, toSelectedSet, type Listbox, type ListboxConfig, type ListboxValue, type ListboxRootProps, type OptionProps, } from './createListbox.svelte';
60
60
  export { createCombobox, type Combobox, type ComboboxConfig, type ComboboxValue } from './createCombobox.svelte';
61
61
  export { createDropdownList, type DropdownList, type DropdownListConfig, type DropdownValue } from './createDropdownList.svelte';
62
62
  export { createPopoverSelect, type PopoverSelect, type PopoverSelectConfig } from './createPopoverSelect.svelte';
@@ -214,7 +214,7 @@ export { createColumnVirtualizer } from './virtualization/column-virtualizer';
214
214
  export type { VirtualItem, VirtualizerOptions, VirtualizerState } from './virtualization/types';
215
215
  export type { SvGridApi, SvGridFilterOperator, SvGridWrapperProps } from './svgrid-wrapper.types';
216
216
  export { parseEditorValue, normalizeEditorOptions, type CellEditorType, type CellEditorOption, } from './editors/cell-editors';
217
- export { applyExcelFilter, normalizeForFilter, splitInTokens, joinInTokens, type ExcelFilter, type ExcelFilterOperator, type ExcelFilterOptions, } from './filtering/excel-filters';
217
+ export { applyExcelFilter, normalizeForFilter, splitInTokens, joinInTokens, trailingInToken, type ExcelFilter, type ExcelFilterOperator, type ExcelFilterOptions, } from './filtering/excel-filters';
218
218
  export { getGridCellDomId, getGridCellA11yProps, getGridHeaderA11yProps, getGridRootA11yProps, getGridRowA11yProps, type GridCellA11yInput, type GridColumnA11yInput, type GridSortDirection, } from './a11y';
219
219
  /**
220
220
  * Compatibility alias for teams migrating from `createTable`.
package/dist/index.js CHANGED
@@ -76,7 +76,7 @@ export { registerBuiltinEditors } from './builtin-editors';
76
76
  // UI kit - shared field chrome (label + hint + error + direction wrapper)
77
77
  export { default as SvField } from './SvField.svelte';
78
78
  // UI kit - headless cores (state + prop-getters you render yourself, like createSvGrid)
79
- export { createListbox, toSelectedArray, } from './createListbox.svelte';
79
+ export { createListbox, toSelectedArray, toSelectedSet, } from './createListbox.svelte';
80
80
  // Selection family cores
81
81
  export { createCombobox } from './createCombobox.svelte';
82
82
  export { createDropdownList } from './createDropdownList.svelte';
@@ -249,7 +249,7 @@ export { createVirtualizer } from './virtualization/virtualizer';
249
249
  export { createSvelteVirtualizer } from './virtualization/svelte-virtualizer.svelte';
250
250
  export { createColumnVirtualizer } from './virtualization/column-virtualizer';
251
251
  export { parseEditorValue, normalizeEditorOptions, } from './editors/cell-editors';
252
- export { applyExcelFilter, normalizeForFilter, splitInTokens, joinInTokens, } from './filtering/excel-filters';
252
+ export { applyExcelFilter, normalizeForFilter, splitInTokens, joinInTokens, trailingInToken, } from './filtering/excel-filters';
253
253
  export { getGridCellDomId, getGridCellA11yProps, getGridHeaderA11yProps, getGridRootA11yProps, getGridRowA11yProps, } from './a11y';
254
254
  /**
255
255
  * Compatibility alias for teams migrating from `createTable`.
package/dist/menus.d.ts CHANGED
@@ -24,6 +24,8 @@ export declare function createMenus<TFeatures extends TableFeatures = TableFeatu
24
24
  clearGroupingFromMenu: (columnId: string) => void;
25
25
  isFacetChecked: (columnId: string, value: string) => any;
26
26
  toggleFacetValue: (columnId: string, value: string) => void;
27
+ setFacetSelection: (columnId: string, next: ReadonlySet<string>) => void;
28
+ setFilterTokens: (columnId: string, tokens: Iterable<string>) => void;
27
29
  isAllFacetsChecked: (columnId: string) => boolean;
28
30
  toggleAllFacets: (columnId: string) => void;
29
31
  clearColumnFilter: (columnId: string) => void;
package/dist/menus.js CHANGED
@@ -73,6 +73,15 @@ export function createMenus(ctx) {
73
73
  else
74
74
  addFilterToken(columnId, token);
75
75
  }
76
+ /**
77
+ * Replace a column's `in` / `notIn` list wholesale. The suggestions dropdown
78
+ * hands back the full token set after a click, which keeps tokens the user
79
+ * typed by hand (or that the search box is currently hiding) intact.
80
+ */
81
+ function setFilterTokens(columnId, tokens) {
82
+ const list = [...tokens].map((t) => t.trim()).filter(Boolean);
83
+ updateFilterRow(columnId, joinInTokens(list));
84
+ }
76
85
  function toggleCheckboxWithKeyboard(event, toggle) {
77
86
  if (event.key !== "Enter" && event.key !== " ")
78
87
  return;
@@ -226,6 +235,20 @@ export function createMenus(ctx) {
226
235
  ctx.valueFilters = { ...ctx.valueFilters, [columnId]: next };
227
236
  }
228
237
  }
238
+ /**
239
+ * Replace a column's checked-facet set. "Everything checked" is stored as an
240
+ * ABSENT entry (so an unfiltered high-cardinality column never holds a set of
241
+ * every value), which is why the full-size case deletes instead of writing.
242
+ */
243
+ function setFacetSelection(columnId, next) {
244
+ if (next.size >= ctx.columnMenuFacetValues.length) {
245
+ const copy = { ...ctx.valueFilters };
246
+ delete copy[columnId];
247
+ ctx.valueFilters = copy;
248
+ return;
249
+ }
250
+ ctx.valueFilters = { ...ctx.valueFilters, [columnId]: new Set(next) };
251
+ }
229
252
  function isAllFacetsChecked(columnId) {
230
253
  const selected = ctx.valueFilters[columnId];
231
254
  return !selected || selected.size >= ctx.columnMenuFacetValues.length;
@@ -464,6 +487,8 @@ export function createMenus(ctx) {
464
487
  clearGroupingFromMenu,
465
488
  isFacetChecked,
466
489
  toggleFacetValue,
490
+ setFacetSelection,
491
+ setFilterTokens,
467
492
  isAllFacetsChecked,
468
493
  toggleAllFacets,
469
494
  clearColumnFilter,
@@ -60,6 +60,41 @@ export function rowResize(node, opts) {
60
60
  window.removeEventListener('pointerup', onPointerUp);
61
61
  document.body.style.cursor = '';
62
62
  }
63
+ /**
64
+ * Keyboard resize (#79). The strip is `role="separator"` acting as a
65
+ * splitter, so it has to be reachable by Tab and respond to arrow keys.
66
+ * Up/Down shrink/grow the row, Shift gives a fine 1px step - the same
67
+ * contract the column resize handle uses (Left/Right, Shift = 1px).
68
+ */
69
+ function onKeyDown(e) {
70
+ if (current.disabled)
71
+ return;
72
+ const t = e.target;
73
+ if (!t?.classList.contains(STRIP_CLASS))
74
+ return;
75
+ const step = e.shiftKey ? 1 : 10;
76
+ let delta = 0;
77
+ if (e.key === 'ArrowUp')
78
+ delta = -step;
79
+ else if (e.key === 'ArrowDown')
80
+ delta = step;
81
+ else
82
+ return;
83
+ const tr = t.closest('tr.sv-grid-row');
84
+ if (!tr)
85
+ return;
86
+ const rowIndex = rowIndexOf(tr);
87
+ if (!Number.isFinite(rowIndex))
88
+ return;
89
+ e.preventDefault();
90
+ e.stopPropagation();
91
+ const min = current.min ?? 20;
92
+ const max = current.max ?? 320;
93
+ const next = Math.round(Math.max(min, Math.min(max, tr.getBoundingClientRect().height + delta)));
94
+ tr.style.height = `${next}px`;
95
+ current.onResizeMove?.(rowIndex, next);
96
+ current.onResize(rowIndex, next);
97
+ }
63
98
  function onPointerDown(e) {
64
99
  if (current.disabled)
65
100
  return;
@@ -122,6 +157,9 @@ export function rowResize(node, opts) {
122
157
  strip.setAttribute('role', 'separator');
123
158
  strip.setAttribute('aria-orientation', 'horizontal');
124
159
  strip.setAttribute('aria-label', 'Resize row');
160
+ // A separator used as an interactive splitter has to be focusable so
161
+ // keyboard-only users can reach it and drive `onKeyDown` (#79).
162
+ strip.tabIndex = 0;
125
163
  // Inline the geometry + pointer-events so the strip works even
126
164
  // before SvGrid.css is parsed; the hover tint still comes from
127
165
  // the stylesheet. We keep the strip fully inside the gutter
@@ -139,6 +177,7 @@ export function rowResize(node, opts) {
139
177
  node.querySelectorAll(`.${STRIP_CLASS}`).forEach((el) => el.remove());
140
178
  }
141
179
  node.addEventListener('pointerdown', onPointerDown, { capture: true });
180
+ node.addEventListener('keydown', onKeyDown, { capture: true });
142
181
  const observer = new MutationObserver(() => decorate());
143
182
  observer.observe(node, { childList: true, subtree: true });
144
183
  decorate();
@@ -152,6 +191,7 @@ export function rowResize(node, opts) {
152
191
  },
153
192
  destroy() {
154
193
  node.removeEventListener('pointerdown', onPointerDown, { capture: true });
194
+ node.removeEventListener('keydown', onKeyDown, { capture: true });
155
195
  window.removeEventListener('pointermove', onPointerMove);
156
196
  window.removeEventListener('pointerup', onPointerUp);
157
197
  observer.disconnect();
package/dist/selection.js CHANGED
@@ -30,7 +30,7 @@ export function createSelection(ctx) {
30
30
  ctx.grid.setActiveCell({
31
31
  rowIndex,
32
32
  colIndex,
33
- cellId: getGridCellDomId("svgrid", rowIndex, colIndex),
33
+ cellId: getGridCellDomId(ctx.gridDomId, rowIndex, colIndex),
34
34
  });
35
35
  // Notify consumers (toolbars, ribbons) so they stay synced without
36
36
  // having to listen on the DOM. Fired on EVERY active-cell move -
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@svgrid/grid",
3
- "version": "2.2.28",
3
+ "version": "2.2.29",
4
4
  "description": "Svelte 5-native data grid and data table. Headless-first engine with virtual scrolling for 100k+ rows, Excel-style filtering, inline editing, grouping, tree data, pivot, and server-side data. Drop-in <SvGrid> component. TypeScript, SSR-ready.",
5
5
  "author": "jQWidgets <boikom@jqwidgets.com>",
6
6
  "license": "MIT",