apex-grid 3.5.0 → 4.0.0-beta.1

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 (215) hide show
  1. package/CHANGELOG.md +573 -0
  2. package/LICENSE.md +125 -0
  3. package/README.md +143 -23
  4. package/components/cell.d.ts +43 -5
  5. package/components/cell.js +225 -64
  6. package/components/cell.js.map +1 -1
  7. package/components/filter-row.d.ts +2 -0
  8. package/components/filter-row.js +82 -28
  9. package/components/filter-row.js.map +1 -1
  10. package/components/grid.d.ts +281 -49
  11. package/components/grid.js +600 -159
  12. package/components/grid.js.map +1 -1
  13. package/components/group-header-row.js +28 -25
  14. package/components/group-header-row.js.map +1 -1
  15. package/components/header-row.d.ts +6 -10
  16. package/components/header-row.js +12 -28
  17. package/components/header-row.js.map +1 -1
  18. package/components/header.js +71 -22
  19. package/components/header.js.map +1 -1
  20. package/components/paginator.d.ts +20 -5
  21. package/components/paginator.js +54 -17
  22. package/components/paginator.js.map +1 -1
  23. package/components/row.d.ts +19 -3
  24. package/components/row.js +211 -56
  25. package/components/row.js.map +1 -1
  26. package/components/toolbar.d.ts +5 -1
  27. package/components/toolbar.js +6 -2
  28. package/components/toolbar.js.map +1 -1
  29. package/components/virtualize-rows.d.ts +38 -0
  30. package/components/virtualize-rows.js +286 -0
  31. package/components/virtualize-rows.js.map +1 -0
  32. package/components/virtualizer.d.ts +111 -1
  33. package/components/virtualizer.js +192 -5
  34. package/components/virtualizer.js.map +1 -1
  35. package/controllers/dom.d.ts +14 -7
  36. package/controllers/dom.js +45 -27
  37. package/controllers/dom.js.map +1 -1
  38. package/controllers/editing.d.ts +104 -37
  39. package/controllers/editing.js +230 -78
  40. package/controllers/editing.js.map +1 -1
  41. package/controllers/expansion.d.ts +14 -5
  42. package/controllers/expansion.js +24 -1
  43. package/controllers/expansion.js.map +1 -1
  44. package/controllers/filter.d.ts +10 -2
  45. package/controllers/filter.js +62 -17
  46. package/controllers/filter.js.map +1 -1
  47. package/controllers/history.d.ts +45 -8
  48. package/controllers/history.js +58 -20
  49. package/controllers/history.js.map +1 -1
  50. package/controllers/navigation.d.ts +130 -13
  51. package/controllers/navigation.js +474 -60
  52. package/controllers/navigation.js.map +1 -1
  53. package/controllers/pagination.d.ts +13 -6
  54. package/controllers/pagination.js +29 -22
  55. package/controllers/pagination.js.map +1 -1
  56. package/controllers/reorder.js +38 -22
  57. package/controllers/reorder.js.map +1 -1
  58. package/controllers/resize.d.ts +17 -2
  59. package/controllers/resize.js +16 -6
  60. package/controllers/resize.js.map +1 -1
  61. package/controllers/row-pin.d.ts +26 -2
  62. package/controllers/row-pin.js +24 -11
  63. package/controllers/row-pin.js.map +1 -1
  64. package/controllers/row-reorder.d.ts +40 -12
  65. package/controllers/row-reorder.js +137 -43
  66. package/controllers/row-reorder.js.map +1 -1
  67. package/controllers/selection.d.ts +33 -4
  68. package/controllers/selection.js +49 -11
  69. package/controllers/selection.js.map +1 -1
  70. package/controllers/sort.d.ts +22 -2
  71. package/controllers/sort.js +30 -25
  72. package/controllers/sort.js.map +1 -1
  73. package/controllers/state.d.ts +74 -7
  74. package/controllers/state.js +117 -36
  75. package/controllers/state.js.map +1 -1
  76. package/controllers/tree.d.ts +15 -10
  77. package/controllers/tree.js +0 -0
  78. package/controllers/tree.js.map +1 -1
  79. package/custom-elements.json +5091 -1023
  80. package/docs/assets/hierarchy.js +1 -1
  81. package/docs/assets/highlight.css +15 -8
  82. package/docs/assets/navigation.js +1 -1
  83. package/docs/assets/search.js +1 -1
  84. package/docs/classes/ApexGrid.html +233 -87
  85. package/docs/classes/ApexGridPaginator.html +39 -8
  86. package/docs/classes/ApexGridToolbar.html +11 -7
  87. package/docs/functions/localize.html +2 -2
  88. package/docs/hierarchy.html +1 -1
  89. package/docs/index.html +159 -26
  90. package/docs/interfaces/ApexCellValidationFailedEvent.html +3 -2
  91. package/docs/interfaces/ApexCellValueChangedEvent.html +4 -2
  92. package/docs/interfaces/ApexCellValueChangingEvent.html +3 -2
  93. package/docs/interfaces/ApexGridEventMap.html +5 -2
  94. package/docs/interfaces/ApexPageChangedEvent.html +3 -1
  95. package/docs/interfaces/ApexRowEditEndedEvent.html +2 -1
  96. package/docs/interfaces/ApexRowEditStartedEvent.html +2 -1
  97. package/docs/interfaces/ApexRowMovingEvent.html +9 -3
  98. package/docs/interfaces/ApexRowPinningEvent.html +1 -1
  99. package/docs/interfaces/ApexTreeRowExpandedEvent.html +4 -1
  100. package/docs/interfaces/ApexTreeRowExpandingEvent.html +4 -1
  101. package/docs/interfaces/BaseApexCellContext.html +18 -5
  102. package/docs/interfaces/BaseApexEditorContext.html +20 -5
  103. package/docs/interfaces/BaseFilterExpression.html +3 -3
  104. package/docs/interfaces/CSVExportOptions.html +12 -4
  105. package/docs/interfaces/DataPipelineConfiguration.html +4 -9
  106. package/docs/interfaces/ExportOptions.html +5 -3
  107. package/docs/interfaces/GridExpansionConfiguration.html +6 -2
  108. package/docs/interfaces/GridRowReorderingConfiguration.html +1 -1
  109. package/docs/interfaces/GridSelectionConfiguration.html +19 -4
  110. package/docs/interfaces/PaginationConfiguration.html +11 -4
  111. package/docs/interfaces/PaginationState.html +4 -2
  112. package/docs/interfaces/SetStateResult.html +3 -2
  113. package/docs/interfaces/ValidatorContext.html +7 -2
  114. package/docs/media/_token-values.scss +118 -0
  115. package/docs/media/package.json +9 -5
  116. package/docs/modules.html +1 -1
  117. package/docs/types/DataPipelineParams.html +2 -2
  118. package/docs/types/ExportSource.html +6 -4
  119. package/docs/types/OperandKeys.html +9 -0
  120. package/docs/types/PaginationMode.html +5 -4
  121. package/docs/variables/EN_LOCALE.html +2 -2
  122. package/i18n/en.d.ts +7 -0
  123. package/i18n/en.js +7 -0
  124. package/i18n/en.js.map +1 -1
  125. package/i18n/es.js +7 -0
  126. package/i18n/es.js.map +1 -1
  127. package/internal/column-types.d.ts +38 -2
  128. package/internal/column-types.js +83 -55
  129. package/internal/column-types.js.map +1 -1
  130. package/internal/constants.d.ts +1 -2
  131. package/internal/constants.js +0 -4
  132. package/internal/constants.js.map +1 -1
  133. package/internal/document-styles.d.ts +30 -0
  134. package/internal/document-styles.js +48 -0
  135. package/internal/document-styles.js.map +1 -0
  136. package/internal/export.d.ts +45 -7
  137. package/internal/export.js +39 -34
  138. package/internal/export.js.map +1 -1
  139. package/internal/feature-module.d.ts +67 -3
  140. package/internal/feature-module.js +6 -0
  141. package/internal/feature-module.js.map +1 -1
  142. package/internal/flip.d.ts +28 -16
  143. package/internal/flip.js +42 -10
  144. package/internal/flip.js.map +1 -1
  145. package/internal/index.d.ts +7 -5
  146. package/internal/index.js +6 -4
  147. package/internal/index.js.map +1 -1
  148. package/internal/normalize-case.d.ts +8 -1
  149. package/internal/normalize-case.js +3 -2
  150. package/internal/normalize-case.js.map +1 -1
  151. package/internal/state-snapshot.d.ts +60 -8
  152. package/internal/state-snapshot.js +87 -19
  153. package/internal/state-snapshot.js.map +1 -1
  154. package/internal/types.d.ts +92 -21
  155. package/internal/types.js.map +1 -1
  156. package/internal/utils.d.ts +34 -6
  157. package/internal/utils.js +30 -21
  158. package/internal/utils.js.map +1 -1
  159. package/internal/validators.js +1 -0
  160. package/internal/validators.js.map +1 -1
  161. package/operations/filter/operands/number.js +9 -6
  162. package/operations/filter/operands/number.js.map +1 -1
  163. package/operations/filter/operands/string.js +2 -3
  164. package/operations/filter/operands/string.js.map +1 -1
  165. package/operations/filter/types.d.ts +14 -2
  166. package/operations/filter/types.js.map +1 -1
  167. package/operations/filter.d.ts +6 -0
  168. package/operations/filter.js +23 -7
  169. package/operations/filter.js.map +1 -1
  170. package/operations/quick-filter.js +2 -5
  171. package/operations/quick-filter.js.map +1 -1
  172. package/operations/sort.d.ts +21 -2
  173. package/operations/sort.js +41 -14
  174. package/operations/sort.js.map +1 -1
  175. package/package.json +9 -4
  176. package/setup.d.ts +11 -0
  177. package/setup.js +9 -19
  178. package/setup.js.map +1 -1
  179. package/styles/_token-values.css.d.ts +1 -0
  180. package/styles/_token-values.css.js +3 -0
  181. package/styles/_token-values.css.js.map +1 -0
  182. package/styles/_tokens.css.js +1 -1
  183. package/styles/_tokens.css.js.map +1 -1
  184. package/styles/body-cell/body-cell.css.js +1 -1
  185. package/styles/body-cell/body-cell.css.js.map +1 -1
  186. package/styles/body-cell/cell-renderers.css.js +1 -1
  187. package/styles/body-cell/cell-renderers.css.js.map +1 -1
  188. package/styles/body-row/body-row.css.js +1 -1
  189. package/styles/body-row/body-row.css.js.map +1 -1
  190. package/styles/document-tokens.css.d.ts +1 -0
  191. package/styles/document-tokens.css.js +3 -0
  192. package/styles/document-tokens.css.js.map +1 -0
  193. package/styles/filter-row/filter-row.css.js +1 -1
  194. package/styles/filter-row/filter-row.css.js.map +1 -1
  195. package/styles/grid/_grid.base.css.js +1 -1
  196. package/styles/grid/_grid.base.css.js.map +1 -1
  197. package/styles/grid/grid.css.js +1 -1
  198. package/styles/grid/grid.css.js.map +1 -1
  199. package/styles/group-header-row/group-header-row.css.js +1 -1
  200. package/styles/group-header-row/group-header-row.css.js.map +1 -1
  201. package/styles/header-cell/header-cell.css.js +1 -1
  202. package/styles/header-cell/header-cell.css.js.map +1 -1
  203. package/styles/header-row/header-row.base.css.js +1 -1
  204. package/styles/header-row/header-row.base.css.js.map +1 -1
  205. package/styles/paginator/paginator.css.js +1 -1
  206. package/styles/paginator/paginator.css.js.map +1 -1
  207. package/styles/toolbar/toolbar.css.js +1 -1
  208. package/styles/toolbar/toolbar.css.js.map +1 -1
  209. package/tokens.css +78 -0
  210. package/LICENSE +0 -80
  211. package/docs/media/LICENSE +0 -80
  212. package/docs/media/_tokens.scss +0 -214
  213. package/operations/pagination.d.ts +0 -19
  214. package/operations/pagination.js +0 -12
  215. package/operations/pagination.js.map +0 -1
@@ -4,22 +4,26 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
4
4
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
+ var _a;
7
8
  import { ContextProvider } from '@lit/context';
8
9
  import { html, nothing } from 'lit';
9
10
  import { eventOptions, property, query, queryAll, state } from 'lit/decorators.js';
10
11
  import { styleMap } from 'lit/directives/style-map.js';
11
12
  import { DataOperationsController } from '../controllers/data-operation.js';
12
13
  import { GridDOMController } from '../controllers/dom.js';
14
+ import { viewIndexer } from '../controllers/history.js';
15
+ import { DEFAULT_PAGINATION } from '../controllers/pagination.js';
13
16
  import { gridStateContext, StateController } from '../controllers/state.js';
14
17
  import { localize as resolveLocaleText, } from '../i18n/index.js';
15
18
  import { DEFAULT_COLUMN_CONFIG, PIPELINE, SENTINEL_NODE } from '../internal/constants.js';
19
+ import { adoptGridDocumentStyles } from '../internal/document-styles.js';
16
20
  import { buildCSV, downloadBlob, } from '../internal/export.js';
17
21
  import { EventEmitterBase } from '../internal/mixins/event-emitter.js';
18
22
  import { registerComponent } from '../internal/register.js';
19
23
  import { columnSchema, operandsByType } from '../internal/state-schema.js';
20
- import { applyColumnLayout, deserializeFilter, resolveRowRefs, serializeColumnLayout, serializeFilter, serializeRowRefs, serializeSort, } from '../internal/state-snapshot.js';
24
+ import { applyColumnLayout, deserializeFilter, resolveFilterOperand, resolveRowRefs, SET_STATE_SLICES, serializeColumnLayout, serializeFilter, serializeRowRefs, serializeSort, } from '../internal/state-snapshot.js';
21
25
  import { GRID_TAG } from '../internal/tags.js';
22
- import { asArray, autoGenerateColumns, getDisplayColumns, getFilterOperandsFor, } from '../internal/utils.js';
26
+ import { asArray, autoGenerateColumns, getDisplayColumns } from '../internal/utils.js';
23
27
  import { watch } from '../internal/watch.js';
24
28
  import { styles as gridStyles } from '../styles/grid/grid.css.js';
25
29
  import ApexGridCell from './cell.js';
@@ -29,20 +33,37 @@ import ApexGridHeaderRow from './header-row.js';
29
33
  import ApexGridPaginator from './paginator.js';
30
34
  import ApexGridRow from './row.js';
31
35
  import ApexGridToolbar from './toolbar.js';
32
- import ApexVirtualizer from './virtualizer.js';
33
- const SET_STATE_SLICES = [
36
+ import ApexVirtualizer, { quietResizeObserverLoopNotice } from './virtualizer.js';
37
+ const SORT_DIRECTIONS = ['ascending', 'descending', 'none'];
38
+ const VIEW_SHAPING_SLICES = new Set([
34
39
  'columns',
35
- 'sort',
36
40
  'filter',
37
41
  'quickFilter',
38
42
  'modules',
39
- 'rowOrder',
40
43
  'tree',
41
- 'expansion',
42
- 'rowPinning',
43
- 'selection',
44
- 'pagination',
45
- ];
44
+ ]);
45
+ const VIEW_SHAPING_PROPERTIES = new Map([
46
+ ['data', null],
47
+ ['quickFilter', null],
48
+ ['pagination', ['enabled', 'mode', 'pageSize', 'totalItems']],
49
+ ['tree', ['enabled', 'getDataPath', 'defaultExpanded']],
50
+ ['dataPipelineConfiguration', ['sort', 'filter', 'quickFilter']],
51
+ ]);
52
+ function viewShapeChanged(fields, before, after) {
53
+ if (Object.is(before, after))
54
+ return false;
55
+ if (!fields)
56
+ return true;
57
+ const a = (before ?? {});
58
+ const b = (after ?? {});
59
+ return fields.some((field) => !Object.is(a[field], b[field]));
60
+ }
61
+ function keyChanged(key, previous) {
62
+ return key.some((value, i) => value !== previous[i]);
63
+ }
64
+ function warnDropped(message) {
65
+ console.warn(`[apex-grid] ${message}`);
66
+ }
46
67
  export class ApexGrid extends EventEmitterBase {
47
68
  constructor() {
48
69
  super(...arguments);
@@ -55,36 +76,75 @@ export class ApexGrid extends EventEmitterBase {
55
76
  });
56
77
  this.dataState = [];
57
78
  this.columns = [];
58
- this.data = [];
79
+ this.#data = [];
59
80
  this.autoGenerate = false;
60
81
  this.sortConfiguration = {
61
82
  multiple: true,
62
83
  triState: true,
63
84
  };
64
- this.quickFilter = '';
85
+ this.#quickFilter = '';
86
+ this.#quickFilterWrites = 0;
65
87
  this.showQuickFilter = false;
66
88
  this.showExport = false;
67
89
  this.columnReordering = false;
68
90
  this.columnSeparator = true;
91
+ this.#rowRecycling = _a.defaultRowRecycling;
69
92
  this.coordinateHints = false;
70
93
  this.columnMenu = true;
71
- this.#autoAriaLabel = null;
72
- this.#stateChangedListeners = 0;
94
+ this.showHeader = true;
95
+ this.#bodyKey = [];
96
+ this.#bodyRows = [];
97
+ this.#pageKey = [];
98
+ this.#pageItems = [];
99
+ this.#viewChangePending = false;
100
+ this.#shapingPending = false;
101
+ this.#pipelineRun = 0;
102
+ this.#pipelineSettled = Promise.resolve();
103
+ this.#settlePipeline = null;
104
+ this.#pinKey = [];
105
+ this.#stateChangedListeners = new Map();
73
106
  this.#lastStateJson = null;
107
+ this.#labelObserver = new MutationObserver(() => this.requestUpdate());
74
108
  }
75
109
  static get tagName() {
76
110
  return GRID_TAG;
77
111
  }
78
112
  static { this.styles = gridStyles; }
113
+ static { this.defaultRowRecycling = globalThis.__apexRowKeying !== 'item'; }
79
114
  static register() {
80
- registerComponent(ApexGrid, ApexVirtualizer, ApexGridRow, ApexGridGroupHeaderRow, ApexGridHeaderRow, ApexFilterRow, ApexGridPaginator, ApexGridToolbar);
115
+ registerComponent(_a, ApexVirtualizer, ApexGridRow, ApexGridGroupHeaderRow, ApexGridHeaderRow, ApexFilterRow, ApexGridPaginator, ApexGridToolbar);
81
116
  }
82
117
  createStateController() {
83
118
  return new StateController(this);
84
119
  }
120
+ #data;
121
+ get data() {
122
+ return this.#data;
123
+ }
124
+ set data(value) {
125
+ if (value == null && !this.#data.length)
126
+ return;
127
+ this.#data = value ?? [];
128
+ }
129
+ get quickFilter() {
130
+ return this.#quickFilter;
131
+ }
132
+ set quickFilter(value) {
133
+ this.#quickFilter = value;
134
+ this.#quickFilterWrites++;
135
+ }
136
+ #quickFilter;
137
+ #quickFilterWrites;
85
138
  localize(key, params, fallback) {
86
139
  return resolveLocaleText(this.localeText, key, params, fallback);
87
140
  }
141
+ get rowRecycling() {
142
+ return this.#rowRecycling;
143
+ }
144
+ set rowRecycling(value) {
145
+ this.#rowRecycling = value ?? _a.defaultRowRecycling;
146
+ }
147
+ #rowRecycling;
88
148
  get selectedRows() {
89
149
  return this.stateController.selection.selectedRows();
90
150
  }
@@ -160,26 +220,50 @@ export class ApexGrid extends EventEmitterBase {
160
220
  isTreeRowExpanded(row) {
161
221
  return this.stateController.tree.isExpanded(row);
162
222
  }
223
+ #pendingSort;
224
+ #pendingFilter;
163
225
  set sortExpressions(expressions) {
164
- if (expressions.length) {
165
- this.sort(expressions);
166
- }
226
+ this.#pendingSort = [...(expressions ?? [])];
227
+ this.requestUpdate(PIPELINE);
167
228
  }
168
229
  get sortExpressions() {
230
+ if (this.#pendingSort)
231
+ return [...this.#pendingSort];
169
232
  return Array.from(this.stateController.sorting.state.values());
170
233
  }
171
234
  set filterExpressions(expressions) {
172
- if (expressions.length) {
173
- this.filter(expressions);
174
- }
235
+ this.#pendingFilter = [...(expressions ?? [])];
236
+ this.#markViewChange();
175
237
  }
176
238
  get filterExpressions() {
239
+ if (this.#pendingFilter) {
240
+ return this.#pendingFilter.map((each) => {
241
+ const resolved = this.#resolveFilter(each);
242
+ return typeof resolved === 'string' ? each : resolved;
243
+ });
244
+ }
177
245
  const expressions = [];
178
246
  for (const each of this.stateController.filtering.state.values) {
179
247
  expressions.push(...each.all);
180
248
  }
181
249
  return expressions;
182
250
  }
251
+ #resolveFilter(each) {
252
+ const column = each ? this.getColumn(each.key) : undefined;
253
+ if (!column)
254
+ return `filter: unknown column "${String(each?.key)}"; dropped`;
255
+ const defaults = this.stateController.filtering.getDefaultExpression(column);
256
+ const named = each.condition ?? defaults.condition;
257
+ const condition = typeof named === 'string' ? resolveFilterOperand(column, named) : named;
258
+ if (!condition)
259
+ return `filter: unknown operand "${named}" for "${String(each.key)}"; dropped`;
260
+ return {
261
+ ...defaults,
262
+ ...each,
263
+ condition,
264
+ criteria: each.criteria ?? 'and',
265
+ };
266
+ }
183
267
  get rows() {
184
268
  return Array.from(this._rows);
185
269
  }
@@ -190,31 +274,75 @@ export class ApexGrid extends EventEmitterBase {
190
274
  return getDisplayColumns(this.columns);
191
275
  }
192
276
  get totalItems() {
193
- if (this.pagination?.mode === 'remote') {
194
- return Math.max(0, this.pagination?.totalItems ?? 0);
277
+ return this.stateController.pagination.totalItems;
278
+ }
279
+ get bodyRows() {
280
+ const { rowPin, dataVersion } = this.stateController;
281
+ const key = [this.dataState, dataVersion, rowPin.version, rowPin.enabled];
282
+ if (keyChanged(key, this.#bodyKey)) {
283
+ this.#bodyKey = key;
284
+ this.#bodyRows = rowPin.hasPinnedRows
285
+ ? this.dataState.filter((row) => !rowPin.isPinned(row))
286
+ : this.dataState;
195
287
  }
196
- return this.dataState.length;
288
+ return this.#bodyRows;
197
289
  }
290
+ #bodyKey;
291
+ #bodyRows;
198
292
  get pageItems() {
199
- const rowPin = this.stateController.rowPin;
200
- const base = rowPin?.enabled && rowPin.hasPinnedRows
201
- ? this.dataState.filter((row) => !rowPin.isPinned(row))
202
- : this.dataState;
203
- if (!this.stateController.pagination.enabled)
204
- return base;
205
- if (this.pagination?.mode === 'remote')
206
- return base;
207
- const { page, pageSize } = this.stateController.pagination;
208
- if (!pageSize)
209
- return base;
210
- const start = page * pageSize;
211
- return base.slice(start, start + pageSize);
293
+ const { pagination, dataVersion } = this.stateController;
294
+ const key = [
295
+ this.bodyRows,
296
+ dataVersion,
297
+ pagination.enabled,
298
+ this.pagination?.mode,
299
+ pagination.page,
300
+ pagination.pageSize,
301
+ ];
302
+ if (keyChanged(key, this.#pageKey)) {
303
+ this.#pageKey = key;
304
+ const base = this.bodyRows;
305
+ const { page, pageSize } = pagination;
306
+ this.#pageItems =
307
+ !pagination.enabled || this.pagination?.mode === 'remote' || !pageSize
308
+ ? base
309
+ : base.slice(page * pageSize, (page + 1) * pageSize);
310
+ }
311
+ return this.#pageItems;
212
312
  }
313
+ #pageKey;
314
+ #pageItems;
213
315
  get page() {
214
316
  return this.stateController.pagination.page;
215
317
  }
216
318
  set page(value) {
217
- this.stateController.pagination.gotoPage(value);
319
+ if (this.#viewStale) {
320
+ this.stateController.pagination.page = Number.isFinite(value)
321
+ ? Math.max(0, Math.trunc(value))
322
+ : 0;
323
+ this.requestUpdate(PIPELINE);
324
+ }
325
+ else {
326
+ this.stateController.pagination.gotoPage(value);
327
+ }
328
+ }
329
+ #viewChangePending;
330
+ #shapingPending;
331
+ get #viewStale() {
332
+ return !this.hasUpdated || this.#viewChangePending || this.#shapingPending;
333
+ }
334
+ #markViewChange() {
335
+ this.#viewChangePending = true;
336
+ this.requestUpdate(PIPELINE);
337
+ }
338
+ requestUpdate(name, oldValue, options, useNewValue, newValue) {
339
+ const fields = name === undefined ? undefined : VIEW_SHAPING_PROPERTIES.get(name);
340
+ if (this.hasUpdated &&
341
+ fields !== undefined &&
342
+ viewShapeChanged(fields, oldValue, useNewValue ? newValue : this[name])) {
343
+ this.#viewChangePending = true;
344
+ }
345
+ super.requestUpdate(name, oldValue, options, useNewValue, newValue);
218
346
  }
219
347
  get pageSize() {
220
348
  return this.stateController.pagination.pageSize;
@@ -228,21 +356,31 @@ export class ApexGrid extends EventEmitterBase {
228
356
  watchColumns(_, newConfig = []) {
229
357
  this.columns = newConfig.map((config) => ({ ...DEFAULT_COLUMN_CONFIG, ...config }));
230
358
  }
359
+ historyRecords() {
360
+ return this.data;
361
+ }
231
362
  dataChanged() {
232
363
  let next = [...this.data];
233
364
  autoGenerateColumns(this);
365
+ this.stateController?.history?.reconcile(() => this.historyRecords());
234
366
  this.stateController?.tree?.resetForDataChange();
367
+ const carry = (rows) => this.#carryRows(rows);
368
+ this.stateController?.rowReorder.resetForDataChange(carry);
369
+ this.stateController?.rowPin.resetForDataChange(carry);
235
370
  if (this.stateController?.tree?.enabled) {
236
371
  next = this.stateController.tree.process(next);
237
372
  }
238
373
  this.dataState = this.stateController ? this.stateController.applyModuleTransforms(next) : next;
239
- if (this.hasUpdated) {
240
- this.pipeline();
241
- }
374
+ if (this.hasUpdated)
375
+ this.requestUpdate(PIPELINE);
242
376
  }
243
- async pipeline() {
244
- this.dataState = await this.dataController.apply([...this.data], this.stateController);
245
- this.stateController.pagination.reclamp();
377
+ #carryRows(rows) {
378
+ const rowId = this.rowId;
379
+ const records = [...this.historyRecords()];
380
+ if (rowId)
381
+ return resolveRowRefs(serializeRowRefs(rows, [], rowId), records, rowId);
382
+ const present = new Set(records);
383
+ return rows.filter((row) => present.has(row));
246
384
  }
247
385
  paginationChanged() {
248
386
  const ctrl = this.stateController?.pagination;
@@ -255,63 +393,169 @@ export class ApexGrid extends EventEmitterBase {
255
393
  if (typeof next.page === 'number') {
256
394
  ctrl.page = next.page;
257
395
  }
258
- if (this.hasUpdated) {
259
- this.pipeline();
260
- }
396
+ if (this.hasUpdated)
397
+ this.requestUpdate(PIPELINE);
261
398
  }
262
399
  quickFilterChanged() {
263
- if (this.hasUpdated) {
264
- this.pipeline();
400
+ if (this.hasUpdated || this.quickFilter)
401
+ this.requestUpdate(PIPELINE);
402
+ }
403
+ treeChanged(previous) {
404
+ if (!viewShapeChanged(VIEW_SHAPING_PROPERTIES.get('tree'), previous, this.tree))
405
+ return;
406
+ this.stateController.tree.resetForDataChange();
407
+ this.requestUpdate(PIPELINE);
408
+ }
409
+ #pipelineRun;
410
+ #pipelineSettled;
411
+ #settlePipeline;
412
+ pipeline() {
413
+ if (!this.#settlePipeline) {
414
+ this.#pipelineSettled = new Promise((settle) => {
415
+ this.#settlePipeline = settle;
416
+ });
265
417
  }
418
+ return this.#runPipeline(++this.#pipelineRun);
266
419
  }
267
- updated() {
268
- this.setAttribute('role', this.stateController.tree.enabled ? 'treegrid' : 'grid');
269
- const pinned = this.stateController.rowPin.pinnedRows;
270
- this.setAttribute('aria-rowcount', String(this.headerRowCount + pinned.top.length + this.pageItems.length + pinned.bottom.length));
271
- const visibleColumns = this.columns.filter((column) => !column.hidden).length;
272
- const extras = (this.stateController.selection.showCheckboxColumn ? 1 : 0) +
273
- (this.stateController.expansion.showToggleColumn ? 1 : 0);
274
- this.setAttribute('aria-colcount', String(visibleColumns + extras));
275
- const selection = this.stateController.selection;
276
- if (selection.enabled && selection.mode === 'multiple') {
277
- this.setAttribute('aria-multiselectable', 'true');
420
+ async #runPipeline(run) {
421
+ try {
422
+ this.#flushPendingExpressions();
423
+ if (this.#viewChangePending || !this.hasUpdated)
424
+ this.#shapingPending = true;
425
+ this.#viewChangePending = false;
426
+ const next = await this.dataController.apply([...this.data], this.stateController);
427
+ if (run !== this.#pipelineRun)
428
+ return;
429
+ this.dataState = next;
430
+ this.stateController.dataVersion++;
431
+ this.stateController.pagination.reclamp();
278
432
  }
279
- else {
280
- this.removeAttribute('aria-multiselectable');
433
+ finally {
434
+ if (run === this.#pipelineRun) {
435
+ this.#settlePipeline?.();
436
+ this.#settlePipeline = null;
437
+ if (this.#shapingPending) {
438
+ this.#shapingPending = false;
439
+ this.#baselineStateIfUnset();
440
+ }
441
+ }
281
442
  }
282
- if (this.scrollContainer) {
283
- this.scrollContainer.tabIndex = this.stateController.active === SENTINEL_NODE ? 0 : -1;
443
+ }
444
+ async pipelineSettled() {
445
+ await this.updateComplete;
446
+ let settled;
447
+ do {
448
+ settled = this.#pipelineSettled;
449
+ await settled;
450
+ await this.updateComplete;
451
+ } while (settled !== this.#pipelineSettled);
452
+ }
453
+ #flushPendingExpressions() {
454
+ const sort = this.#pendingSort;
455
+ const filter = this.#pendingFilter;
456
+ this.#pendingSort = undefined;
457
+ this.#pendingFilter = undefined;
458
+ if (sort)
459
+ this.#replaceSort(sort, warnDropped);
460
+ if (filter)
461
+ this.#replaceFilter(filter);
462
+ }
463
+ #replaceSort(entries, warn) {
464
+ this.stateController.sorting.reset();
465
+ const valid = [];
466
+ for (const entry of entries) {
467
+ const { direction, ...rest } = (entry ?? {});
468
+ if (typeof entry !== 'object' || entry === null || !this.getColumn(rest.key)) {
469
+ warn(`sort: unknown column "${String(rest.key)}"; dropped`);
470
+ }
471
+ else if (direction === undefined) {
472
+ valid.push(rest);
473
+ }
474
+ else if (SORT_DIRECTIONS.includes(direction)) {
475
+ valid.push(entry);
476
+ }
477
+ else {
478
+ warn(`sort: invalid direction "${String(direction)}"; dropped`);
479
+ }
284
480
  }
285
- const currentLabel = this.getAttribute('aria-label');
286
- const authorLabeled = this.hasAttribute('aria-labelledby') ||
287
- (currentLabel !== null && currentLabel !== this.#autoAriaLabel);
288
- if (!authorLabeled) {
289
- const label = this.localize('grid.label');
290
- if (currentLabel !== label)
291
- this.setAttribute('aria-label', label);
292
- this.#autoAriaLabel = label;
481
+ if (valid.length)
482
+ this.sort(valid);
483
+ this.requestUpdate(PIPELINE);
484
+ }
485
+ #replaceFilter(entries) {
486
+ this.#markViewChange();
487
+ this.stateController.filtering.reset();
488
+ if (entries.length)
489
+ this.filter([...entries]);
490
+ }
491
+ willUpdate(changed) {
492
+ super.willUpdate(changed);
493
+ const { rowPin, pagination } = this.stateController;
494
+ const key = [rowPin.version, rowPin.enabled];
495
+ if (keyChanged(key, this.#pinKey)) {
496
+ this.#pinKey = key;
497
+ if (!changed.has(PIPELINE) && !this.#viewStale)
498
+ pagination.reclamp();
293
499
  }
500
+ }
501
+ #pinKey;
502
+ updated() {
503
+ this.#viewChangePending = false;
504
+ this.#syncBodyTabStop();
505
+ this.stateController.navigation.reconcileFocus();
294
506
  this.#emitStateChangedIfNeeded();
295
507
  }
296
- #autoAriaLabel;
297
508
  #stateChangedListeners;
298
509
  #lastStateJson;
299
510
  addEventListener(type, listener, options) {
300
- if (type === 'stateChanged') {
301
- this.#stateChangedListeners += 1;
302
- this.#lastStateJson = this.hasUpdated ? JSON.stringify(this.getState()) : null;
303
- }
511
+ if (type === 'stateChanged' && listener)
512
+ this.#trackStateChangedListener(listener, options);
304
513
  super.addEventListener(type, listener, options);
305
514
  }
515
+ #trackStateChangedListener(listener, options) {
516
+ const capture = typeof options === 'boolean' ? options : Boolean(options?.capture);
517
+ const captures = this.#stateChangedListeners.get(listener) ?? new Map();
518
+ if (!captures.has(capture) && !options?.signal?.aborted) {
519
+ if (!this.#stateChangedListeners.size) {
520
+ this.#lastStateJson = this.hasUpdated ? JSON.stringify(this.getState()) : null;
521
+ }
522
+ const token = {};
523
+ captures.set(capture, token);
524
+ this.#stateChangedListeners.set(listener, captures);
525
+ const forget = () => this.#forgetStateChangedListener(listener, capture, token);
526
+ if (typeof options === 'object') {
527
+ if (options.once) {
528
+ HTMLElement.prototype.addEventListener.call(this, 'stateChanged', forget, {
529
+ capture,
530
+ once: true,
531
+ });
532
+ }
533
+ options.signal?.addEventListener('abort', forget, { once: true });
534
+ }
535
+ }
536
+ }
537
+ #forgetStateChangedListener(listener, capture, token) {
538
+ const captures = this.#stateChangedListeners.get(listener);
539
+ if (!captures || (token && captures.get(capture) !== token))
540
+ return;
541
+ captures.delete(capture);
542
+ if (!captures.size)
543
+ this.#stateChangedListeners.delete(listener);
544
+ }
306
545
  removeEventListener(type, listener, options) {
307
- if (type === 'stateChanged') {
308
- this.#stateChangedListeners = Math.max(0, this.#stateChangedListeners - 1);
546
+ if (type === 'stateChanged' && listener) {
547
+ const capture = typeof options === 'boolean' ? options : Boolean(options?.capture);
548
+ this.#forgetStateChangedListener(listener, capture);
309
549
  }
310
550
  super.removeEventListener(type, listener, options);
311
551
  }
312
552
  #emitStateChangedIfNeeded() {
313
- if (this.#stateChangedListeners === 0)
553
+ if (!this.#stateChangedListeners.size)
554
+ return;
555
+ if (this.#lastStateJson === null) {
556
+ this.#baselineStateIfUnset();
314
557
  return;
558
+ }
315
559
  const snapshot = this.getState();
316
560
  const json = JSON.stringify(snapshot);
317
561
  if (json === this.#lastStateJson)
@@ -319,6 +563,20 @@ export class ApexGrid extends EventEmitterBase {
319
563
  this.#lastStateJson = json;
320
564
  this.emitEvent('stateChanged', { detail: { state: snapshot } });
321
565
  }
566
+ #baselineStateIfUnset() {
567
+ if (this.#lastStateJson === null && this.#stateChangedListeners.size && !this.#shapingPending) {
568
+ this.#lastStateJson = JSON.stringify(this.getState());
569
+ }
570
+ }
571
+ connectedCallback() {
572
+ super.connectedCallback();
573
+ adoptGridDocumentStyles(this);
574
+ quietResizeObserverLoopNotice();
575
+ this.#labelObserver.observe(this, {
576
+ attributeFilter: ['aria-label', 'aria-labelledby', 'aria-describedby'],
577
+ });
578
+ }
579
+ #labelObserver;
322
580
  firstUpdated() {
323
581
  if (typeof getComputedStyle === 'function' && getComputedStyle(this).display !== 'grid') {
324
582
  console.warn('[apex-grid] Host `display` has been overridden. The grid requires ' +
@@ -328,26 +586,44 @@ export class ApexGrid extends EventEmitterBase {
328
586
  }
329
587
  }
330
588
  filter(config) {
331
- this.stateController.filtering.filter(asArray(config).map((each) => typeof each.condition === 'string'
332
- ?
333
- Object.assign(each, {
334
- condition: getFilterOperandsFor(this.getColumn(each.key))[each.condition],
335
- })
336
- : each));
589
+ if (this.#pendingFilter) {
590
+ const added = asArray(config);
591
+ this.#pendingFilter.push(...added);
592
+ if (added.length)
593
+ this.#markViewChange();
594
+ return;
595
+ }
596
+ const expressions = [];
597
+ for (const each of asArray(config)) {
598
+ const resolved = this.#resolveFilter(each);
599
+ if (typeof resolved === 'string')
600
+ warnDropped(resolved);
601
+ else
602
+ expressions.push(resolved);
603
+ }
604
+ if (!expressions.length)
605
+ return;
606
+ this.#markViewChange();
607
+ this.stateController.filtering.filter(expressions);
337
608
  }
338
609
  sort(expressions) {
610
+ if (this.#pendingSort) {
611
+ this.#pendingSort.push(...asArray(expressions));
612
+ this.requestUpdate(PIPELINE);
613
+ return;
614
+ }
339
615
  this.stateController.sorting.sort(expressions);
340
616
  }
341
617
  getState(options) {
342
618
  const rowId = options?.rowId ?? this.rowId;
343
- const pinned = this.stateController.rowPin.pinnedRows;
619
+ const pinned = this.stateController.rowPin.pins;
344
620
  const manualOrder = this.stateController.rowReorder.getManualOrder();
345
621
  return {
346
622
  version: 1,
347
623
  columns: serializeColumnLayout(this.columns),
348
624
  sort: serializeSort(this.sortExpressions),
349
625
  filter: serializeFilter(this.filterExpressions),
350
- quickFilter: this.quickFilter,
626
+ quickFilter: this.quickFilter ?? '',
351
627
  pagination: { page: this.page, pageSize: this.pageSize },
352
628
  selection: serializeRowRefs(this.selectedRows, this.data, rowId),
353
629
  expansion: serializeRowRefs(this.expandedRows, this.data, rowId),
@@ -367,7 +643,7 @@ export class ApexGrid extends EventEmitterBase {
367
643
  columns: this.columns.map((column) => columnSchema(column)),
368
644
  capabilities: {
369
645
  sort: {
370
- directions: ['ascending', 'descending'],
646
+ directions: SORT_DIRECTIONS.filter((direction) => direction !== 'none'),
371
647
  multi: Boolean(this.sortConfiguration?.multiple),
372
648
  },
373
649
  filter: { operandsByType: operandsByType(this.columns) },
@@ -384,6 +660,7 @@ export class ApexGrid extends EventEmitterBase {
384
660
  const strict = options?.strict ?? false;
385
661
  const applied = [];
386
662
  const warnings = [];
663
+ const viewSettled = !this.#viewStale;
387
664
  const warn = (message) => {
388
665
  if (strict)
389
666
  throw new Error(`[apex-grid] setState: ${message}`);
@@ -410,31 +687,23 @@ export class ApexGrid extends EventEmitterBase {
410
687
  }
411
688
  const columns = asArray(state.columns, 'columns');
412
689
  if (columns) {
690
+ const unkeyed = columns.filter((entry) => typeof entry?.key !== 'string').length;
691
+ if (unkeyed)
692
+ warn(`columns: ${unkeyed} entr${unkeyed === 1 ? 'y' : 'ies'} without a key; dropped`);
413
693
  this.columns = applyColumnLayout(this.columns, columns);
414
694
  applied.push('columns');
415
695
  }
416
696
  const sort = asArray(state.sort, 'sort');
417
697
  if (sort) {
418
- this.stateController.sorting.reset();
419
- const valid = [];
420
- for (const entry of sort) {
421
- if (this.getColumn(entry.key)) {
422
- valid.push(entry);
423
- }
424
- else {
425
- warn(`sort: unknown column "${entry.key}"; dropped`);
426
- }
427
- }
428
- if (valid.length)
429
- this.sort(valid);
698
+ this.#pendingSort = undefined;
699
+ this.#replaceSort(sort, warn);
430
700
  applied.push('sort');
431
701
  }
432
702
  const filter = asArray(state.filter, 'filter');
433
703
  if (filter) {
434
- this.stateController.filtering.reset();
704
+ this.#pendingFilter = undefined;
435
705
  const expressions = deserializeFilter(filter, (key) => this.getColumn(key), (snapshot, reason) => warn(`filter: "${snapshot.key}" dropped (${reason})`));
436
- if (expressions.length)
437
- this.filter(expressions);
706
+ this.#replaceFilter(expressions);
438
707
  applied.push('filter');
439
708
  }
440
709
  if (state.quickFilter !== undefined) {
@@ -449,6 +718,7 @@ export class ApexGrid extends EventEmitterBase {
449
718
  if (state.modules !== undefined) {
450
719
  if (state.modules && typeof state.modules === 'object') {
451
720
  this.stateController.restoreModuleState(state.modules);
721
+ this.#markViewChange();
452
722
  applied.push('modules');
453
723
  }
454
724
  else {
@@ -476,13 +746,22 @@ export class ApexGrid extends EventEmitterBase {
476
746
  if (state.treeExpanded !== undefined || state.treeExpandedKeys !== undefined) {
477
747
  const refs = asArray(state.treeExpanded, 'treeExpanded') ?? [];
478
748
  const keys = asArray(state.treeExpandedKeys, 'treeExpandedKeys') ?? [];
479
- this.stateController.tree.restoreExpansion(resolveCounted(refs, 'treeExpanded'), keys);
749
+ this.stateController.tree.restoreExpansion(resolveCounted(refs, 'treeExpanded'), keys.filter((key) => typeof key === 'string'));
750
+ this.#markViewChange();
480
751
  applied.push('tree');
481
752
  }
482
753
  const expansion = asArray(state.expansion, 'expansion');
483
754
  if (expansion) {
484
- this.expandedRows = resolveCounted(expansion, 'expansion');
485
- applied.push('expansion');
755
+ const ctrl = this.stateController.expansion;
756
+ const predicate = this.expansion?.isExpandable;
757
+ const rows = resolveCounted(expansion, 'expansion');
758
+ const expected = predicate ? rows.filter((row) => predicate(row)) : rows;
759
+ if (this.#replaceRows('expansion', ctrl.enabled, expected, warn, () => {
760
+ void ctrl.replaceExpansion(expected);
761
+ return ctrl.expandedRows();
762
+ })) {
763
+ applied.push('expansion');
764
+ }
486
765
  }
487
766
  if (state.rowPinning !== undefined) {
488
767
  const pinning = state.rowPinning;
@@ -496,8 +775,19 @@ export class ApexGrid extends EventEmitterBase {
496
775
  }
497
776
  const selection = asArray(state.selection, 'selection');
498
777
  if (selection) {
499
- this.selectedRows = resolveCounted(selection, 'selection');
500
- applied.push('selection');
778
+ const ctrl = this.stateController.selection;
779
+ const resolved = resolveCounted(selection, 'selection');
780
+ const rows = ctrl.selectable(resolved);
781
+ if (rows.length < resolved.length) {
782
+ warn(`selection: ${resolved.length - rows.length} row(s) are not selectable; skipped`);
783
+ }
784
+ const expected = ctrl.mode === 'single' ? rows.slice(0, 1) : rows;
785
+ if (this.#replaceRows('selection', ctrl.enabled, expected, warn, () => {
786
+ void ctrl.replaceSelection(expected);
787
+ return ctrl.selectedRows();
788
+ })) {
789
+ applied.push('selection');
790
+ }
501
791
  }
502
792
  if (state.pagination !== undefined) {
503
793
  const pagination = state.pagination;
@@ -514,10 +804,13 @@ export class ApexGrid extends EventEmitterBase {
514
804
  if (page !== undefined) {
515
805
  if (typeof page === 'number' && Number.isFinite(page)) {
516
806
  const requested = Math.trunc(page);
517
- this.page = requested;
518
- if (this.page !== requested) {
519
- warn(`pagination: page ${requested} out of range; clamped to ${this.page}`);
807
+ const reshaped = applied.some((slice) => VIEW_SHAPING_SLICES.has(slice));
808
+ const ctrl = this.stateController.pagination;
809
+ const next = viewSettled && !reshaped ? ctrl.clamp(requested) : Math.max(0, requested);
810
+ if (next !== requested) {
811
+ warn(`pagination: page ${requested} out of range; clamped to ${next}`);
520
812
  }
813
+ ctrl.page = next;
521
814
  }
522
815
  else {
523
816
  warn(`pagination: invalid page ${String(page)}; ignored`);
@@ -533,13 +826,37 @@ export class ApexGrid extends EventEmitterBase {
533
826
  const skipped = SET_STATE_SLICES.filter((slice) => !applied.includes(slice));
534
827
  return { applied, skipped, warnings };
535
828
  }
829
+ #replaceRows(label, enabled, expected, warn, replace) {
830
+ if (!enabled) {
831
+ if (expected.length)
832
+ warn(`${label}: the feature is disabled on this grid; skipped`);
833
+ return !expected.length;
834
+ }
835
+ const actual = new Set(replace());
836
+ const took = actual.size === new Set(expected).size && expected.every((row) => actual.has(row));
837
+ if (!took)
838
+ warn(`${label}: the change was cancelled by a listener; skipped`);
839
+ return took;
840
+ }
536
841
  clearSort(key) {
842
+ if (this.#pendingSort && key !== undefined) {
843
+ this.#pendingSort = this.#pendingSort.filter((each) => each.key !== key);
844
+ }
845
+ else {
846
+ this.#pendingSort = undefined;
847
+ }
537
848
  this.stateController.sorting.reset(key);
538
849
  this.requestUpdate(PIPELINE);
539
850
  }
540
851
  clearFilter(key) {
852
+ if (this.#pendingFilter && key !== undefined) {
853
+ this.#pendingFilter = this.#pendingFilter.filter((each) => each.key !== key);
854
+ }
855
+ else {
856
+ this.#pendingFilter = undefined;
857
+ }
858
+ this.#markViewChange();
541
859
  this.stateController.filtering.reset(key);
542
- this.requestUpdate(PIPELINE);
543
860
  }
544
861
  gotoPage(page) {
545
862
  return this.stateController.pagination.gotoPage(page);
@@ -563,15 +880,21 @@ export class ApexGrid extends EventEmitterBase {
563
880
  const next = value ?? '';
564
881
  if (next === this.quickFilter)
565
882
  return false;
883
+ const previous = this.quickFilter;
884
+ const writes = this.#quickFilterWrites;
566
885
  const proceed = this.emitEvent('quickFilterChanging', {
567
- detail: { value: this.quickFilter, nextValue: next },
886
+ detail: { value: previous, nextValue: next },
568
887
  cancelable: true,
569
888
  });
570
889
  if (!proceed)
571
890
  return false;
572
- this.quickFilter = next;
573
- this.stateController.pagination.page = 0;
574
- await this.updateComplete;
891
+ if (this.#quickFilterWrites === writes)
892
+ this.quickFilter = next;
893
+ if (this.quickFilter === previous)
894
+ return false;
895
+ const pageReset = this.stateController.pagination.resetPage();
896
+ await this.pipelineSettled();
897
+ await pageReset;
575
898
  this.emitEvent('quickFilterChanged', { detail: { value: this.quickFilter } });
576
899
  return true;
577
900
  }
@@ -643,16 +966,22 @@ export class ApexGrid extends EventEmitterBase {
643
966
  return tally;
644
967
  const editing = this.stateController.editing;
645
968
  const items = this.pageItems;
969
+ const indexOf = viewIndexer(items, edits.length);
970
+ const version = editing.validationVersion;
646
971
  this.stateController.history.beginBatch();
647
972
  try {
648
- for (const { rowIndex, column, value } of edits) {
649
- const record = items.at(rowIndex);
973
+ for (const { row, rowIndex, column, value } of edits) {
974
+ const record = row ?? items[rowIndex];
650
975
  const config = this.getColumn(column);
651
- if (!record || rowIndex < 0 || !config || !editing.isEditable(config)) {
976
+ if (!record ||
977
+ !config ||
978
+ !editing.isEditable(config) ||
979
+ !this.stateController.isDataRow(record)) {
652
980
  tally.skipped += 1;
653
981
  continue;
654
982
  }
655
- tally[editing.applyCellEdit(rowIndex, column, record, value)] += 1;
983
+ const index = row ? indexOf(row) : rowIndex;
984
+ tally[editing.applyCellEdit(index, column, record, value)] += 1;
656
985
  }
657
986
  }
658
987
  finally {
@@ -660,6 +989,8 @@ export class ApexGrid extends EventEmitterBase {
660
989
  }
661
990
  if (tally.applied > 0)
662
991
  this.requestUpdate(PIPELINE);
992
+ else if (editing.validationVersion !== version)
993
+ this.requestUpdate();
663
994
  return tally;
664
995
  }
665
996
  cancelEdit() {
@@ -731,22 +1062,20 @@ export class ApexGrid extends EventEmitterBase {
731
1062
  const target = this.columns[toIndex];
732
1063
  if ((source.pinned ?? null) !== (target.pinned ?? null))
733
1064
  return false;
734
- const proceed = this.emitEvent('columnMoving', {
735
- detail: { key: fromKey, fromIndex, toKey, position },
736
- cancelable: true,
737
- });
738
- if (!proceed)
739
- return false;
740
1065
  const next = this.columns.slice();
741
1066
  const [moved] = next.splice(fromIndex, 1);
742
1067
  let insertIndex = next.findIndex((column) => column.key === toKey);
743
- if (insertIndex < 0)
744
- return false;
745
1068
  if (position === 'after')
746
1069
  insertIndex += 1;
747
1070
  next.splice(insertIndex, 0, moved);
748
1071
  if (next.every((column, index) => column === this.columns[index]))
749
1072
  return false;
1073
+ const proceed = this.emitEvent('columnMoving', {
1074
+ detail: { key: fromKey, fromIndex, toKey, position },
1075
+ cancelable: true,
1076
+ });
1077
+ if (!proceed)
1078
+ return false;
750
1079
  this.columns = next;
751
1080
  await this.updateComplete;
752
1081
  const resolvedIndex = this.columns.findIndex((column) => column.key === fromKey);
@@ -755,8 +1084,43 @@ export class ApexGrid extends EventEmitterBase {
755
1084
  });
756
1085
  return true;
757
1086
  }
1087
+ ownsEvent(event) {
1088
+ for (const node of event.composedPath()) {
1089
+ if (node === this || node === this.scrollContainer)
1090
+ return true;
1091
+ if (node instanceof _a || node instanceof ApexVirtualizer)
1092
+ return false;
1093
+ }
1094
+ return false;
1095
+ }
1096
+ #syncBodyTabStop() {
1097
+ const body = this.scrollContainer;
1098
+ if (!body)
1099
+ return;
1100
+ const active = this.stateController.active;
1101
+ body.tabIndex = active !== SENTINEL_NODE && body.isRendered(active.row) ? -1 : 0;
1102
+ }
1103
+ bodyRangeChanged(event) {
1104
+ const { first, last } = event;
1105
+ this.#syncBodyTabStop();
1106
+ const focused = this.renderRoot instanceof ShadowRoot ? this.renderRoot.activeElement : null;
1107
+ if (focused instanceof ApexGridRow &&
1108
+ focused.parentElement === this.scrollContainer &&
1109
+ (focused.index < first || focused.index > last)) {
1110
+ this.scrollContainer.focus({ preventScroll: true });
1111
+ }
1112
+ this.stateController.navigation.reconcileFocus();
1113
+ }
1114
+ #ownCell(event) {
1115
+ if (!this.ownsEvent(event))
1116
+ return undefined;
1117
+ return event.composedPath().find((el) => el instanceof ApexGridCell);
1118
+ }
758
1119
  bodyClickHandler(event) {
759
- const target = event.composedPath().find((el) => el instanceof ApexGridCell);
1120
+ if (!this.ownsEvent(event))
1121
+ return;
1122
+ const path = event.composedPath();
1123
+ const target = path.find((el) => el instanceof ApexGridCell);
760
1124
  if (target) {
761
1125
  this.stateController.active = {
762
1126
  column: target.column.key,
@@ -765,26 +1129,43 @@ export class ApexGrid extends EventEmitterBase {
765
1129
  if (!target.editing)
766
1130
  target.focus({ preventScroll: true });
767
1131
  }
1132
+ else {
1133
+ const row = path.find((el) => el instanceof ApexGridRow);
1134
+ const cell = row?.presentedCell;
1135
+ if (row && cell && path.includes(cell)) {
1136
+ const { column } = this.stateController.active;
1137
+ this.stateController.active = { column, row: row.index };
1138
+ }
1139
+ }
768
1140
  }
769
1141
  bodyKeydownHandler(event) {
1142
+ if (!this.ownsEvent(event))
1143
+ return;
770
1144
  const origin = event.composedPath()[0];
771
1145
  const fromBody = this.scrollContainer.isSameNode(event.target);
772
- const fromCell = origin instanceof ApexGridCell && !origin.editing;
1146
+ const fromCell = (origin instanceof ApexGridCell && !origin.editing) ||
1147
+ (event.target instanceof ApexGridRow && event.target.presentedCell === origin);
773
1148
  if (fromBody || fromCell) {
774
1149
  this.stateController.navigation.navigate(event);
775
1150
  }
776
1151
  }
777
1152
  bodyFocusHandler(event) {
778
- if (this.scrollContainer.isSameNode(event.target)) {
779
- this.stateController.navigation.focusActiveCell();
1153
+ const navigation = this.stateController.navigation;
1154
+ navigation.focusEntered(event, this.ownsEvent(event));
1155
+ if (this.scrollContainer.isSameNode(event.target) &&
1156
+ navigation.forwardsBodyFocus) {
1157
+ navigation.focusActiveCell();
780
1158
  }
781
1159
  }
1160
+ bodyFocusOutHandler(event) {
1161
+ this.stateController.navigation.focusLeft(event, this.ownsEvent(event));
1162
+ }
782
1163
  bodyPointerHandler(event) {
783
1164
  if (this.stateController.modules.size === 0)
784
1165
  return;
785
1166
  if (event.type === 'pointermove' && event.buttons === 0)
786
1167
  return;
787
- const target = event.composedPath().find((el) => el instanceof ApexGridCell);
1168
+ const target = this.#ownCell(event);
788
1169
  if (!target?.row)
789
1170
  return;
790
1171
  const kind = event.type === 'pointerdown' ? 'down' : event.type === 'pointerup' ? 'up' : 'over';
@@ -804,6 +1185,7 @@ export class ApexGrid extends EventEmitterBase {
804
1185
  return nothing;
805
1186
  return html `
806
1187
  <apex-grid-group-header-row
1188
+ ?data-visually-hidden=${!this.showHeader}
807
1189
  style=${styleMap(this.DOM.columnSizes)}
808
1190
  .columns=${this.DOM.displayColumns}
809
1191
  .pinOffsets=${this.DOM.pinOffsets}
@@ -814,6 +1196,7 @@ export class ApexGrid extends EventEmitterBase {
814
1196
  renderHeaderRow() {
815
1197
  return html `
816
1198
  <apex-grid-header-row
1199
+ ?data-visually-hidden=${!this.showHeader}
817
1200
  style=${styleMap(this.DOM.columnSizes)}
818
1201
  .columns=${this.DOM.displayColumns}
819
1202
  .pinOffsets=${this.DOM.pinOffsets}
@@ -824,45 +1207,42 @@ export class ApexGrid extends EventEmitterBase {
824
1207
  renderBody() {
825
1208
  return html `
826
1209
  <apex-virtualizer
1210
+ role="rowgroup"
827
1211
  .items=${this.pageItems}
828
1212
  .renderItem=${this.DOM.rowRenderer}
1213
+ .rowMode=${this.rowRecycling ? 'slot' : 'item'}
829
1214
  @click=${this.bodyClickHandler}
830
1215
  @keydown=${this.bodyKeydownHandler}
831
1216
  @focusin=${this.bodyFocusHandler}
1217
+ @focusout=${this.bodyFocusOutHandler}
1218
+ @rangeChanged=${this.bodyRangeChanged}
832
1219
  @pointerdown=${this.bodyPointerHandler}
833
1220
  @pointermove=${this.bodyPointerHandler}
834
1221
  @pointerup=${this.bodyPointerHandler}
835
1222
  ></apex-virtualizer>
836
1223
  `;
837
1224
  }
838
- get headerRowCount() {
839
- return this.columnGroupDepth + 1 + (this.columns.some((column) => column.filter) ? 1 : 0);
840
- }
841
1225
  renderPinnedTop() {
842
1226
  const rowPin = this.stateController.rowPin;
843
- if (!rowPin.enabled)
844
- return nothing;
845
1227
  const rows = rowPin.pinnedRows.top;
846
1228
  if (rows.length === 0)
847
1229
  return nothing;
848
1230
  return html `<div part="pinned-rows pinned-top">
849
- ${rows.map((data, i) => this.DOM.pinnedRowRenderer(data, i, this.headerRowCount))}
1231
+ ${rows.map((data, i) => this.DOM.pinnedRowRenderer(data, i, this.DOM.headerRowCount))}
850
1232
  </div>`;
851
1233
  }
852
1234
  renderPinnedBottom() {
853
1235
  const rowPin = this.stateController.rowPin;
854
- if (!rowPin.enabled)
855
- return nothing;
856
1236
  const rows = rowPin.pinnedRows.bottom;
857
1237
  if (rows.length === 0)
858
1238
  return nothing;
859
- const offset = this.headerRowCount + rowPin.pinnedRows.top.length + this.pageItems.length;
1239
+ const offset = this.DOM.headerRowCount + rowPin.pinnedRows.top.length + this.pageItems.length;
860
1240
  return html `<div part="pinned-rows pinned-bottom">
861
1241
  ${rows.map((data, i) => this.DOM.pinnedRowRenderer(data, i, offset))}
862
1242
  </div>`;
863
1243
  }
864
1244
  renderFilterRow() {
865
- return this.columns.some((column) => column.filter)
1245
+ return this.showHeader && this.columns.some((column) => column.filter)
866
1246
  ? html `<apex-filter-row></apex-filter-row>`
867
1247
  : nothing;
868
1248
  }
@@ -871,22 +1251,27 @@ export class ApexGrid extends EventEmitterBase {
871
1251
  return nothing;
872
1252
  }
873
1253
  return html `<apex-grid-toolbar
1254
+ part="toolbar-host"
1255
+ exportparts="toolbar,toolbar-search,search-field,search-icon,search-input,toolbar-actions,toolbar-action,export,export-trigger,export-icon,export-caret,export-menu,export-menu-item"
874
1256
  .value=${this.quickFilter}
875
1257
  .showQuickFilter=${this.showQuickFilter}
876
1258
  .showExport=${this.showExport}
877
1259
  @apex-quick-filter=${(event) => {
878
1260
  event.stopPropagation();
879
1261
  this.setQuickFilter(event.detail);
1262
+ if (this.quickFilter !== event.detail)
1263
+ event.preventDefault();
880
1264
  }}
881
1265
  ></apex-grid-toolbar>`;
882
1266
  }
883
1267
  renderPaginator() {
884
1268
  if (!this.stateController.pagination.enabled)
885
1269
  return nothing;
886
- const options = this.pagination?.pageSizeOptions && this.pagination.pageSizeOptions.length > 0
887
- ? this.pagination.pageSizeOptions
888
- : [10, 25, 50, 100];
889
- return html `<apex-grid-paginator .pageSizeOptions=${options}></apex-grid-paginator>`;
1270
+ return html `<apex-grid-paginator
1271
+ part="paginator-host"
1272
+ exportparts="paginator,paginator-size,paginator-info,paginator-controls,paginator-page,paginator-button,paginator-first,paginator-prev,paginator-next,paginator-last"
1273
+ .pageSizeOptions=${this.pagination?.pageSizeOptions ?? DEFAULT_PAGINATION.pageSizeOptions}
1274
+ ></apex-grid-paginator>`;
890
1275
  }
891
1276
  renderLiveRegion() {
892
1277
  return html `<div
@@ -899,6 +1284,22 @@ export class ApexGrid extends EventEmitterBase {
899
1284
  ${this.stateController.announcement}
900
1285
  </div>`;
901
1286
  }
1287
+ renderColumnDragGhost() {
1288
+ const state = this.stateController.reordering.state;
1289
+ if (!state)
1290
+ return nothing;
1291
+ return html `<div
1292
+ part="drag-ghost"
1293
+ style=${styleMap({
1294
+ left: `${state.ghostX}px`,
1295
+ top: `${state.ghostY}px`,
1296
+ width: `${state.ghostWidth}px`,
1297
+ height: `${state.ghostHeight}px`,
1298
+ })}
1299
+ >
1300
+ ${state.label}
1301
+ </div>`;
1302
+ }
902
1303
  renderRowDragGhost() {
903
1304
  const ghost = this.stateController.rowReorder.ghost;
904
1305
  if (!ghost)
@@ -928,18 +1329,45 @@ export class ApexGrid extends EventEmitterBase {
928
1329
  >`)}
929
1330
  </div>`;
930
1331
  }
1332
+ #refs(attr) {
1333
+ const root = this.getRootNode();
1334
+ const found = (this.getAttribute(attr) ?? '')
1335
+ .split(/\s+/)
1336
+ .flatMap((id) => (id && root.getElementById?.(id)) || []);
1337
+ return found.length ? found : null;
1338
+ }
1339
+ renderTable() {
1340
+ const state = this.stateController;
1341
+ const { top, bottom } = state.rowPin.pinnedRows;
1342
+ const selection = state.selection;
1343
+ const labelledBy = this.#refs('aria-labelledby');
1344
+ const describedBy = this.#refs('aria-describedby');
1345
+ const copy = (refs) => !('ariaLabelledByElements' in Element.prototype) &&
1346
+ refs?.map((el) => el.textContent?.trim()).join(' ');
1347
+ return html `<div
1348
+ part="table"
1349
+ role=${state.hierarchical ? 'treegrid' : 'grid'}
1350
+ aria-rowcount=${this.DOM.headerRowCount + top.length + this.pageItems.length + bottom.length}
1351
+ aria-colcount=${state.colCount}
1352
+ aria-multiselectable=${selection.enabled && selection.mode === 'multiple' ? 'true' : nothing}
1353
+ aria-label=${copy(labelledBy) || this.getAttribute('aria-label') || this.localize('grid.label')}
1354
+ aria-description=${copy(describedBy) || nothing}
1355
+ .ariaLabelledByElements=${labelledBy}
1356
+ .ariaDescribedByElements=${describedBy}
1357
+ >
1358
+ ${this.renderGroupHeaderRow()} ${this.renderHeaderRow()} ${this.renderPinnedTop()}
1359
+ ${this.renderBody()} ${this.renderPinnedBottom()}
1360
+ </div>`;
1361
+ }
931
1362
  render() {
932
1363
  return html `
933
1364
  ${this.stateController.resizing.renderIndicator()}
934
1365
  ${this.renderToolbar()}
935
- ${this.renderGroupHeaderRow()}
936
- ${this.renderHeaderRow()}
1366
+ ${this.renderTable()}
937
1367
  ${this.renderFilterRow()}
938
- ${this.renderPinnedTop()}
939
- ${this.renderBody()}
940
- ${this.renderPinnedBottom()}
941
1368
  ${this.renderPaginator()}
942
1369
  ${this.renderLiveRegion()}
1370
+ ${this.renderColumnDragGhost()}
943
1371
  ${this.renderRowDragGhost()}
944
1372
  `;
945
1373
  }
@@ -947,6 +1375,7 @@ export class ApexGrid extends EventEmitterBase {
947
1375
  this.stateController.setAnnouncement(message);
948
1376
  }
949
1377
  }
1378
+ _a = ApexGrid;
950
1379
  __decorate([
951
1380
  query(ApexVirtualizer.tagName)
952
1381
  ], ApexGrid.prototype, "scrollContainer", void 0);
@@ -973,7 +1402,7 @@ __decorate([
973
1402
  ], ApexGrid.prototype, "columns", void 0);
974
1403
  __decorate([
975
1404
  property({ attribute: false })
976
- ], ApexGrid.prototype, "data", void 0);
1405
+ ], ApexGrid.prototype, "data", null);
977
1406
  __decorate([
978
1407
  property({ attribute: false })
979
1408
  ], ApexGrid.prototype, "rowId", void 0);
@@ -991,7 +1420,7 @@ __decorate([
991
1420
  ], ApexGrid.prototype, "pagination", void 0);
992
1421
  __decorate([
993
1422
  property({ type: String, attribute: 'quick-filter' })
994
- ], ApexGrid.prototype, "quickFilter", void 0);
1423
+ ], ApexGrid.prototype, "quickFilter", null);
995
1424
  __decorate([
996
1425
  property({ type: Boolean, attribute: 'show-quick-filter' })
997
1426
  ], ApexGrid.prototype, "showQuickFilter", void 0);
@@ -1007,6 +1436,9 @@ __decorate([
1007
1436
  __decorate([
1008
1437
  property({ type: Boolean, attribute: 'column-separator', reflect: true })
1009
1438
  ], ApexGrid.prototype, "columnSeparator", void 0);
1439
+ __decorate([
1440
+ property({ attribute: 'row-recycling', converter: (value) => value !== 'false' })
1441
+ ], ApexGrid.prototype, "rowRecycling", null);
1010
1442
  __decorate([
1011
1443
  property({ attribute: false })
1012
1444
  ], ApexGrid.prototype, "editing", void 0);
@@ -1028,6 +1460,9 @@ __decorate([
1028
1460
  __decorate([
1029
1461
  property({ attribute: false })
1030
1462
  ], ApexGrid.prototype, "columnMenu", void 0);
1463
+ __decorate([
1464
+ property({ attribute: 'show-header', converter: (value) => value !== 'false' })
1465
+ ], ApexGrid.prototype, "showHeader", void 0);
1031
1466
  __decorate([
1032
1467
  property({ attribute: false })
1033
1468
  ], ApexGrid.prototype, "columnGroups", void 0);
@@ -1052,16 +1487,22 @@ __decorate([
1052
1487
  __decorate([
1053
1488
  watch('data')
1054
1489
  ], ApexGrid.prototype, "dataChanged", null);
1055
- __decorate([
1056
- watch(PIPELINE)
1057
- ], ApexGrid.prototype, "pipeline", null);
1058
1490
  __decorate([
1059
1491
  watch('pagination')
1060
1492
  ], ApexGrid.prototype, "paginationChanged", null);
1061
1493
  __decorate([
1062
1494
  watch('quickFilter')
1063
1495
  ], ApexGrid.prototype, "quickFilterChanged", null);
1496
+ __decorate([
1497
+ watch('tree', { waitUntilFirstUpdate: true })
1498
+ ], ApexGrid.prototype, "treeChanged", null);
1499
+ __decorate([
1500
+ watch(PIPELINE)
1501
+ ], ApexGrid.prototype, "pipeline", null);
1064
1502
  __decorate([
1065
1503
  eventOptions({ capture: true })
1066
1504
  ], ApexGrid.prototype, "bodyClickHandler", null);
1505
+ __decorate([
1506
+ eventOptions({ capture: true })
1507
+ ], ApexGrid.prototype, "bodyFocusOutHandler", null);
1067
1508
  //# sourceMappingURL=grid.js.map