apex-grid 3.5.0 → 3.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (197) hide show
  1. package/CHANGELOG.md +338 -0
  2. package/LICENSE.md +125 -0
  3. package/README.md +64 -10
  4. package/components/cell.d.ts +30 -4
  5. package/components/cell.js +199 -53
  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 +146 -36
  11. package/components/grid.js +442 -103
  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 +7 -5
  16. package/components/header-row.js +11 -9
  17. package/components/header-row.js.map +1 -1
  18. package/components/header.js +32 -20
  19. package/components/header.js.map +1 -1
  20. package/components/row.d.ts +19 -3
  21. package/components/row.js +69 -23
  22. package/components/row.js.map +1 -1
  23. package/components/toolbar.js +2 -1
  24. package/components/toolbar.js.map +1 -1
  25. package/components/virtualizer.d.ts +40 -0
  26. package/components/virtualizer.js +116 -5
  27. package/components/virtualizer.js.map +1 -1
  28. package/controllers/dom.d.ts +4 -4
  29. package/controllers/dom.js +40 -22
  30. package/controllers/dom.js.map +1 -1
  31. package/controllers/editing.d.ts +104 -37
  32. package/controllers/editing.js +230 -78
  33. package/controllers/editing.js.map +1 -1
  34. package/controllers/expansion.d.ts +14 -5
  35. package/controllers/expansion.js +24 -1
  36. package/controllers/expansion.js.map +1 -1
  37. package/controllers/filter.d.ts +10 -2
  38. package/controllers/filter.js +62 -17
  39. package/controllers/filter.js.map +1 -1
  40. package/controllers/history.d.ts +45 -8
  41. package/controllers/history.js +58 -20
  42. package/controllers/history.js.map +1 -1
  43. package/controllers/navigation.d.ts +67 -13
  44. package/controllers/navigation.js +191 -51
  45. package/controllers/navigation.js.map +1 -1
  46. package/controllers/pagination.d.ts +6 -0
  47. package/controllers/pagination.js +10 -2
  48. package/controllers/pagination.js.map +1 -1
  49. package/controllers/reorder.js +38 -22
  50. package/controllers/reorder.js.map +1 -1
  51. package/controllers/resize.d.ts +17 -2
  52. package/controllers/resize.js +16 -6
  53. package/controllers/resize.js.map +1 -1
  54. package/controllers/row-pin.d.ts +20 -2
  55. package/controllers/row-pin.js +16 -9
  56. package/controllers/row-pin.js.map +1 -1
  57. package/controllers/row-reorder.d.ts +31 -11
  58. package/controllers/row-reorder.js +117 -37
  59. package/controllers/row-reorder.js.map +1 -1
  60. package/controllers/selection.d.ts +11 -4
  61. package/controllers/selection.js +26 -11
  62. package/controllers/selection.js.map +1 -1
  63. package/controllers/sort.d.ts +22 -2
  64. package/controllers/sort.js +29 -23
  65. package/controllers/sort.js.map +1 -1
  66. package/controllers/state.d.ts +56 -7
  67. package/controllers/state.js +76 -6
  68. package/controllers/state.js.map +1 -1
  69. package/controllers/tree.d.ts +15 -10
  70. package/controllers/tree.js +0 -0
  71. package/controllers/tree.js.map +1 -1
  72. package/custom-elements.json +3156 -277
  73. package/docs/assets/hierarchy.js +1 -1
  74. package/docs/assets/highlight.css +15 -8
  75. package/docs/assets/navigation.js +1 -1
  76. package/docs/assets/search.js +1 -1
  77. package/docs/classes/ApexGrid.html +155 -81
  78. package/docs/classes/ApexGridPaginator.html +1 -1
  79. package/docs/classes/ApexGridToolbar.html +3 -3
  80. package/docs/functions/localize.html +2 -2
  81. package/docs/hierarchy.html +1 -1
  82. package/docs/index.html +74 -11
  83. package/docs/interfaces/ApexCellValidationFailedEvent.html +3 -2
  84. package/docs/interfaces/ApexCellValueChangedEvent.html +4 -2
  85. package/docs/interfaces/ApexCellValueChangingEvent.html +3 -2
  86. package/docs/interfaces/ApexGridEventMap.html +5 -2
  87. package/docs/interfaces/ApexRowEditEndedEvent.html +2 -1
  88. package/docs/interfaces/ApexRowEditStartedEvent.html +2 -1
  89. package/docs/interfaces/ApexRowMovingEvent.html +9 -3
  90. package/docs/interfaces/ApexRowPinningEvent.html +1 -1
  91. package/docs/interfaces/ApexTreeRowExpandedEvent.html +4 -1
  92. package/docs/interfaces/ApexTreeRowExpandingEvent.html +4 -1
  93. package/docs/interfaces/BaseApexCellContext.html +1 -1
  94. package/docs/interfaces/BaseApexEditorContext.html +1 -1
  95. package/docs/interfaces/BaseFilterExpression.html +2 -2
  96. package/docs/interfaces/CSVExportOptions.html +12 -4
  97. package/docs/interfaces/DataPipelineConfiguration.html +1 -1
  98. package/docs/interfaces/ExportOptions.html +5 -3
  99. package/docs/interfaces/GridExpansionConfiguration.html +6 -2
  100. package/docs/interfaces/GridRowReorderingConfiguration.html +1 -1
  101. package/docs/interfaces/PaginationState.html +1 -1
  102. package/docs/interfaces/SetStateResult.html +3 -2
  103. package/docs/interfaces/ValidatorContext.html +7 -2
  104. package/docs/media/_token-values.scss +118 -0
  105. package/docs/media/package.json +3 -2
  106. package/docs/modules.html +1 -1
  107. package/docs/types/ExportSource.html +6 -4
  108. package/docs/types/OperandKeys.html +9 -0
  109. package/docs/variables/EN_LOCALE.html +2 -2
  110. package/i18n/en.d.ts +5 -0
  111. package/i18n/en.js +5 -0
  112. package/i18n/en.js.map +1 -1
  113. package/i18n/es.js +5 -0
  114. package/i18n/es.js.map +1 -1
  115. package/internal/column-types.d.ts +27 -1
  116. package/internal/column-types.js +75 -49
  117. package/internal/column-types.js.map +1 -1
  118. package/internal/constants.d.ts +1 -2
  119. package/internal/constants.js +0 -4
  120. package/internal/constants.js.map +1 -1
  121. package/internal/document-styles.d.ts +30 -0
  122. package/internal/document-styles.js +48 -0
  123. package/internal/document-styles.js.map +1 -0
  124. package/internal/export.d.ts +39 -7
  125. package/internal/export.js +35 -34
  126. package/internal/export.js.map +1 -1
  127. package/internal/feature-module.d.ts +60 -3
  128. package/internal/feature-module.js +6 -0
  129. package/internal/feature-module.js.map +1 -1
  130. package/internal/index.d.ts +6 -4
  131. package/internal/index.js +5 -3
  132. package/internal/index.js.map +1 -1
  133. package/internal/normalize-case.d.ts +8 -1
  134. package/internal/normalize-case.js +3 -2
  135. package/internal/normalize-case.js.map +1 -1
  136. package/internal/state-snapshot.d.ts +60 -8
  137. package/internal/state-snapshot.js +87 -19
  138. package/internal/state-snapshot.js.map +1 -1
  139. package/internal/types.d.ts +17 -2
  140. package/internal/types.js.map +1 -1
  141. package/internal/utils.d.ts +34 -6
  142. package/internal/utils.js +30 -21
  143. package/internal/utils.js.map +1 -1
  144. package/internal/validators.js +1 -0
  145. package/internal/validators.js.map +1 -1
  146. package/operations/filter/operands/number.js +9 -6
  147. package/operations/filter/operands/number.js.map +1 -1
  148. package/operations/filter/operands/string.js +2 -3
  149. package/operations/filter/operands/string.js.map +1 -1
  150. package/operations/filter/types.d.ts +13 -1
  151. package/operations/filter/types.js.map +1 -1
  152. package/operations/filter.d.ts +6 -0
  153. package/operations/filter.js +23 -7
  154. package/operations/filter.js.map +1 -1
  155. package/operations/quick-filter.js +2 -5
  156. package/operations/quick-filter.js.map +1 -1
  157. package/operations/sort.d.ts +21 -2
  158. package/operations/sort.js +41 -14
  159. package/operations/sort.js.map +1 -1
  160. package/package.json +8 -3
  161. package/setup.d.ts +11 -0
  162. package/setup.js +9 -19
  163. package/setup.js.map +1 -1
  164. package/styles/_token-values.css.d.ts +1 -0
  165. package/styles/_token-values.css.js +3 -0
  166. package/styles/_token-values.css.js.map +1 -0
  167. package/styles/_tokens.css.js +1 -1
  168. package/styles/_tokens.css.js.map +1 -1
  169. package/styles/body-cell/body-cell.css.js +1 -1
  170. package/styles/body-cell/body-cell.css.js.map +1 -1
  171. package/styles/body-cell/cell-renderers.css.js +1 -1
  172. package/styles/body-cell/cell-renderers.css.js.map +1 -1
  173. package/styles/body-row/body-row.css.js +1 -1
  174. package/styles/body-row/body-row.css.js.map +1 -1
  175. package/styles/document-tokens.css.d.ts +1 -0
  176. package/styles/document-tokens.css.js +3 -0
  177. package/styles/document-tokens.css.js.map +1 -0
  178. package/styles/filter-row/filter-row.css.js +1 -1
  179. package/styles/filter-row/filter-row.css.js.map +1 -1
  180. package/styles/grid/_grid.base.css.js +1 -1
  181. package/styles/grid/_grid.base.css.js.map +1 -1
  182. package/styles/grid/grid.css.js +1 -1
  183. package/styles/grid/grid.css.js.map +1 -1
  184. package/styles/group-header-row/group-header-row.css.js +1 -1
  185. package/styles/group-header-row/group-header-row.css.js.map +1 -1
  186. package/styles/header-cell/header-cell.css.js +1 -1
  187. package/styles/header-cell/header-cell.css.js.map +1 -1
  188. package/styles/header-row/header-row.base.css.js +1 -1
  189. package/styles/header-row/header-row.base.css.js.map +1 -1
  190. package/styles/paginator/paginator.css.js +1 -1
  191. package/styles/paginator/paginator.css.js.map +1 -1
  192. package/styles/toolbar/toolbar.css.js +1 -1
  193. package/styles/toolbar/toolbar.css.js.map +1 -1
  194. package/tokens.css +78 -0
  195. package/LICENSE +0 -80
  196. package/docs/media/LICENSE +0 -80
  197. package/docs/media/_tokens.scss +0 -214
@@ -1 +1 @@
1
- {"version":3,"file":"history.js","sourceRoot":"","sources":["../../src/controllers/history.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ReactiveController } from 'lit';\nimport { PIPELINE } from '../internal/constants.js';\nimport type { GridHost, Keys } from '../internal/types.js';\n\n/** Default number of commands retained on the undo stack. */\nconst DEFAULT_STACK_SIZE = 100;\n\n/**\n * A single recorded cell mutation: the record reference, its column, and the\n * before / after values. `rowIndex` is the view-relative index at record time\n * and is used only for the replayed `cellValueChanged` event (the live record\n * reference is authoritative).\n */\nexport interface CellChange<T extends object> {\n record: T;\n key: Keys<T>;\n rowIndex: number;\n oldValue: unknown;\n newValue: unknown;\n}\n\n/**\n * A reversible unit of work on the history stacks.\n *\n * @remarks\n * `apply` re-performs the change (redo); `revert` undoes it. Cell-data edits use\n * {@link CellEditCommand}; the interface is deliberately open so later command\n * kinds (e.g. view-state) can join the same stacks.\n */\nexport interface EditCommand {\n apply(): void;\n revert(): void;\n}\n\n/**\n * One or more cell mutations that undo / redo as a single step. A single-cell\n * edit holds one change; a row-mode commit or a bulk paste / fill holds many so\n * one Ctrl+Z reverts the whole batch.\n */\nexport class CellEditCommand<T extends object> implements EditCommand {\n constructor(\n private host: GridHost<T>,\n public readonly changes: ReadonlyArray<CellChange<T>>\n ) {}\n\n public apply(): void {\n for (const change of this.changes) {\n this.#write(change, change.newValue);\n }\n }\n\n public revert(): void {\n // Revert in reverse so overlapping writes to the same cell unwind cleanly.\n for (let i = this.changes.length - 1; i >= 0; i -= 1) {\n this.#write(this.changes[i], this.changes[i].oldValue);\n }\n }\n\n #write(change: CellChange<T>, value: unknown): void {\n (change.record as Record<string, unknown>)[change.key as string] = value;\n // Re-emit `cellValueChanged` (never the cancellable `cellValueChanging`) so\n // data-bound consumers stay in sync. Validation and history recording are\n // intentionally skipped on replay. Prefer the record's current view index\n // when it is on the page; fall back to the recorded one.\n const liveIndex = this.host.pageItems.indexOf(change.record);\n this.host.emitEvent('cellValueChanged', {\n detail: {\n key: change.key,\n rowIndex: liveIndex >= 0 ? liveIndex : change.rowIndex,\n data: change.record,\n value,\n },\n });\n }\n}\n\n/**\n * Reactive controller owning the undo / redo stacks for cell-data edits.\n *\n * @remarks\n * Recording is driven from the {@link EditingController.applyCellEdit} choke\n * point: every successful write calls {@link record}. Single edits push one\n * command; {@link beginBatch} / {@link endBatch} coalesce a row-mode commit or a\n * bulk paste / fill into one command. Disabled unless\n * `editing.history.enabled` is set, so the community grid pays nothing.\n */\nexport class HistoryController<T extends object> implements ReactiveController {\n #undo: EditCommand[] = [];\n #redo: EditCommand[] = [];\n #batch: CellChange<T>[] | null = null;\n\n constructor(protected host: GridHost<T>) {\n this.host.addController(this);\n }\n\n public hostConnected(): void {}\n\n /** Whether undo / redo tracking is enabled. */\n public get enabled(): boolean {\n return Boolean(this.host.editing?.history?.enabled);\n }\n\n /** Maximum number of commands retained on the undo stack. */\n public get stackSize(): number {\n const size = this.host.editing?.history?.stackSize;\n return typeof size === 'number' && size > 0 ? size : DEFAULT_STACK_SIZE;\n }\n\n /** Whether there is at least one command to undo. */\n public get canUndo(): boolean {\n return this.#undo.length > 0;\n }\n\n /** Whether there is at least one command to redo. */\n public get canRedo(): boolean {\n return this.#redo.length > 0;\n }\n\n /**\n * Records a single cell mutation. Routed to the open batch when one is active,\n * otherwise pushed immediately as a standalone command. No-op when history is\n * disabled.\n */\n public record(change: CellChange<T>): void {\n if (!this.enabled) return;\n if (this.#batch) {\n this.#batch.push(change);\n return;\n }\n this.#push(new CellEditCommand(this.host, [change]));\n }\n\n /**\n * Opens a batch so subsequent {@link record} calls coalesce into one command.\n * Nestable calls are flattened (the outermost {@link endBatch} commits).\n */\n public beginBatch(): void {\n if (!this.enabled) return;\n this.#batch ??= [];\n }\n\n /**\n * Closes the open batch, pushing it as a single command when it captured any\n * changes.\n */\n public endBatch(): void {\n const batch = this.#batch;\n this.#batch = null;\n if (batch && batch.length > 0) {\n this.#push(new CellEditCommand(this.host, batch));\n }\n }\n\n /** Reverts the most recent command. No-op when the undo stack is empty. */\n public undo(): void {\n const command = this.#undo.pop();\n if (!command) return;\n command.revert();\n this.#redo.push(command);\n this.#afterReplay(command, 'undo');\n }\n\n /** Re-applies the most recently undone command. No-op when nothing to redo. */\n public redo(): void {\n const command = this.#redo.pop();\n if (!command) return;\n command.apply();\n this.#undo.push(command);\n this.#afterReplay(command, 'redo');\n }\n\n /** Clears both stacks (e.g. after a data reset). */\n public clear(): void {\n if (this.#undo.length === 0 && this.#redo.length === 0) return;\n this.#undo = [];\n this.#redo = [];\n this.#batch = null;\n this.#emitChanged();\n }\n\n #push(command: EditCommand): void {\n this.#undo.push(command);\n // A fresh edit invalidates the redo branch.\n this.#redo = [];\n // Evict the oldest commands once the stack exceeds its cap.\n while (this.#undo.length > this.stackSize) {\n this.#undo.shift();\n }\n this.#emitChanged();\n }\n\n #afterReplay(command: EditCommand, kind: 'undo' | 'redo'): void {\n this.host.requestUpdate(PIPELINE);\n const count = command instanceof CellEditCommand ? command.changes.length : 1;\n const key =\n kind === 'undo'\n ? count === 1\n ? 'announce.undoOne'\n : 'announce.undoMany'\n : count === 1\n ? 'announce.redoOne'\n : 'announce.redoMany';\n this.host.announce(this.host.localize(key, { count }));\n this.#emitChanged();\n }\n\n #emitChanged(): void {\n this.host.emitEvent('historyChanged', {\n detail: { canUndo: this.canUndo, canRedo: this.canRedo },\n });\n }\n}\n"]}
1
+ {"version":3,"file":"history.js","sourceRoot":"","sources":["../../src/controllers/history.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ReactiveController } from 'lit';\nimport { PIPELINE } from '../internal/constants.js';\nimport type { GridHost, Keys } from '../internal/types.js';\n\n/** Default number of commands retained on the undo stack. */\nconst DEFAULT_STACK_SIZE = 100;\n\n/**\n * A single recorded cell mutation: the record reference, its column, and the\n * before / after values. `rowIndex` is the view-relative index at record time;\n * replay reports the record's index at replay time instead (the live record\n * reference is authoritative).\n */\nexport interface CellChange<T extends object> {\n record: T;\n key: Keys<T>;\n rowIndex: number;\n oldValue: unknown;\n newValue: unknown;\n}\n\n/**\n * A reversible unit of work on the history stacks.\n *\n * @remarks\n * `apply` re-performs the change (redo); `revert` undoes it. Cell-data edits use\n * {@link CellEditCommand}; the interface is deliberately open so later command\n * kinds (e.g. view-state) can join the same stacks.\n */\nexport interface EditCommand {\n apply(): void;\n revert(): void;\n /** The record the command acts on: history is cleared once a reload drops it. */\n readonly record?: object;\n /**\n * For a command staged with a row edit: the value to stage for its cell,\n * re-read from the row's staged values just before the row commits, so\n * validation judges what the command will leave there.\n */\n stagedValue?(staged: ReadonlyMap<PropertyKey, unknown>): unknown;\n}\n\n/**\n * One or more cell mutations that undo / redo as a single step. A single-cell\n * edit holds one change; a row-mode commit or a bulk paste / fill holds many so\n * one Ctrl+Z reverts the whole batch. `commands` are other commands pushed in\n * the same batch (a feature's state riding with the edit, such as a formula):\n * they replay after the cell values in both directions, so a value write can\n * never undo the state they restore.\n */\nexport class CellEditCommand<T extends object> implements EditCommand {\n constructor(\n private host: GridHost<T>,\n public readonly changes: ReadonlyArray<CellChange<T>>,\n public readonly commands: ReadonlyArray<EditCommand> = []\n ) {}\n\n public apply(): void {\n const indexOf = viewIndexer(this.host.pageItems, this.changes.length);\n for (const change of this.changes) {\n this.#write(change, change.newValue, indexOf);\n }\n for (const command of this.commands) command.apply();\n }\n\n public revert(): void {\n const indexOf = viewIndexer(this.host.pageItems, this.changes.length);\n // Revert in reverse so overlapping writes to the same cell unwind cleanly.\n for (let i = this.changes.length - 1; i >= 0; i -= 1) {\n this.#write(this.changes[i], this.changes[i].oldValue, indexOf);\n }\n for (let i = this.commands.length - 1; i >= 0; i -= 1) this.commands[i].revert();\n }\n\n #write(change: CellChange<T>, value: unknown, indexOf: (record: T) => number): void {\n (change.record as Record<string, unknown>)[change.key as string] = value;\n // Re-emit `cellValueChanged` (never the cancellable `cellValueChanging`) so\n // data-bound consumers stay in sync. Validation and history recording are\n // intentionally skipped on replay. The index is where the record is now:\n // -1 when it is pinned or on another page.\n this.host.emitEvent('cellValueChanged', {\n detail: { key: change.key, rowIndex: indexOf(change.record), data: change.record, value },\n });\n }\n}\n\n/**\n * Returns a lookup of a record's index in `items` (by reference, `-1` when\n * absent). For more than one lookup it indexes `items` once, so a large batch\n * costs one pass instead of one scan per record.\n */\nexport function viewIndexer<T>(items: ReadonlyArray<T>, lookups: number): (record: T) => number {\n if (lookups < 2) return (record) => items.indexOf(record);\n let index: Map<T, number> | undefined;\n return (record) => {\n if (!index) {\n index = new Map();\n // Backwards, so a record listed twice resolves to its first index.\n for (let i = items.length - 1; i >= 0; i -= 1) index.set(items[i], i);\n }\n return index.get(record) ?? -1;\n };\n}\n\n/**\n * Reactive controller owning the undo / redo stacks for cell-data edits.\n *\n * @remarks\n * Recording is driven from the {@link EditingController.applyCellEdit} choke\n * point: every successful write calls {@link record}. Single edits push one\n * command; {@link beginBatch} / {@link endBatch} coalesce a row-mode commit or a\n * bulk paste / fill into one command. Disabled unless\n * `editing.history.enabled` is set, so the community grid pays nothing.\n */\nexport class HistoryController<T extends object> implements ReactiveController {\n #undo: EditCommand[] = [];\n #redo: EditCommand[] = [];\n /** Changes recorded by the open batch, pushed as one command when it closes. */\n #batch: CellChange<T>[] = [];\n /** Commands {@link push}ed while the batch is open, replayed after its changes. */\n #commands: EditCommand[] = [];\n /** Open {@link beginBatch} calls; only the outermost {@link endBatch} commits. */\n #depth = 0;\n\n constructor(protected host: GridHost<T>) {\n this.host.addController(this);\n }\n\n public hostConnected(): void {}\n\n /** Whether undo / redo tracking is enabled. */\n public get enabled(): boolean {\n return Boolean(this.host.editing?.history?.enabled);\n }\n\n /** Maximum number of commands retained on the undo stack. */\n public get stackSize(): number {\n const size = this.host.editing?.history?.stackSize;\n return typeof size === 'number' && size > 0 ? size : DEFAULT_STACK_SIZE;\n }\n\n /** Whether there is at least one command to undo. */\n public get canUndo(): boolean {\n return this.#undo.length > 0;\n }\n\n /** Whether there is at least one command to redo. */\n public get canRedo(): boolean {\n return this.#redo.length > 0;\n }\n\n /**\n * Records a single cell mutation. Routed to the open batch when one is active,\n * otherwise pushed immediately as a standalone command. No-op when history is\n * disabled.\n */\n public record(change: CellChange<T>): void {\n if (!this.enabled) return;\n if (this.#depth > 0) {\n this.#batch.push(change);\n return;\n }\n this.#push(new CellEditCommand(this.host, [change]));\n }\n\n /**\n * Records a command that is not a plain cell write (its first `apply` has\n * already run). Joins the open batch, replaying after the batch's cell\n * changes, otherwise pushed as its own step. No-op when history is disabled.\n */\n public push(command: EditCommand): void {\n if (!this.enabled) return;\n if (this.#depth > 0) this.#commands.push(command);\n else this.#push(command);\n }\n\n /**\n * Opens a batch so subsequent {@link record} calls coalesce into one command.\n * Nested calls are flattened: a batch opened while another is open (a\n * `cellValueChanged` listener that calls `applyEdits`, say) joins it, and\n * only the outermost {@link endBatch} commits. Counted even while history\n * is disabled, so every begin pairs with its end.\n */\n public beginBatch(): void {\n this.#depth += 1;\n }\n\n /**\n * Closes the innermost open batch. Closing the outermost pushes everything\n * it captured as a single command.\n */\n public endBatch(): void {\n if (this.#depth === 0 || --this.#depth > 0) return;\n const batch = this.#batch;\n const commands = this.#commands;\n this.#batch = [];\n this.#commands = [];\n if (batch.length > 0 || commands.length > 0) {\n this.#push(new CellEditCommand(this.host, batch, commands));\n }\n }\n\n /** Reverts the most recent command. No-op when the undo stack is empty. */\n public undo(): void {\n const command = this.#undo.pop();\n if (!command) return;\n command.revert();\n this.#redo.push(command);\n this.#afterReplay(command, 'undo');\n }\n\n /** Re-applies the most recently undone command. No-op when nothing to redo. */\n public redo(): void {\n const command = this.#redo.pop();\n if (!command) return;\n command.apply();\n this.#undo.push(command);\n this.#afterReplay(command, 'redo');\n }\n\n /** Clears both stacks, and the changes an open batch has captured so far. */\n public clear(): void {\n this.#batch = [];\n this.#commands = [];\n if (this.#undo.length === 0 && this.#redo.length === 0) return;\n this.#undo = [];\n this.#redo = [];\n this.#emitChanged();\n }\n\n /**\n * Clears both stacks when the grid's data was replaced and a recorded change\n * (or a command's {@link EditCommand.record}) names a record that `records`\n * no longer yields (a reload with new objects).\n * Replaying onto such a detached record would change nothing on screen while\n * still reporting the edit. New data that keeps the edited records (an\n * append, a re-sorted copy) keeps its history. `records` is read only while\n * a stack has entries, so the caller may pass an expensive source.\n */\n public reconcile(records: () => Iterable<T>): void {\n if (this.#undo.length === 0 && this.#redo.length === 0) return;\n const live = new Set(records());\n const stale = (command: EditCommand): boolean =>\n (command.record !== undefined && !live.has(command.record as T)) ||\n (command instanceof CellEditCommand &&\n (command.changes.some((change: CellChange<T>) => !live.has(change.record)) ||\n command.commands.some(stale)));\n if (this.#undo.some(stale) || this.#redo.some(stale)) this.clear();\n }\n\n #push(command: EditCommand): void {\n this.#undo.push(command);\n // A fresh edit invalidates the redo branch.\n this.#redo = [];\n // Evict the oldest commands once the stack exceeds its cap.\n while (this.#undo.length > this.stackSize) {\n this.#undo.shift();\n }\n this.#emitChanged();\n }\n\n #afterReplay(command: EditCommand, kind: 'undo' | 'redo'): void {\n this.host.requestUpdate(PIPELINE);\n const count = command instanceof CellEditCommand ? command.changes.length || 1 : 1;\n const key =\n kind === 'undo'\n ? count === 1\n ? 'announce.undoOne'\n : 'announce.undoMany'\n : count === 1\n ? 'announce.redoOne'\n : 'announce.redoMany';\n this.host.announce(this.host.localize(key, { count }));\n this.#emitChanged();\n }\n\n #emitChanged(): void {\n this.host.emitEvent('historyChanged', {\n detail: { canUndo: this.canUndo, canRedo: this.canRedo },\n });\n }\n}\n"]}
@@ -1,16 +1,18 @@
1
1
  import type { ReactiveController } from 'lit';
2
+ import type ApexGridRow from '../components/row.js';
3
+ import { type ScrollInsets } from '../components/virtualizer.js';
2
4
  import type { ActiveNode, ColumnConfiguration, GridHost, Keys } from '../internal/types.js';
3
5
  export declare class NavigationController<T extends object> implements ReactiveController {
4
6
  #private;
5
7
  protected host: GridHost<T>;
6
8
  protected handlers: Map<string, (() => void) | (() => void) | (() => void) | (() => void) | (() => void) | (() => void) | (() => void) | (() => void) | (() => void)>;
7
9
  protected get virtualizer(): import("../components/virtualizer.js").default;
8
- protected state: Map<import("../internal/types.js").NavigationState, ActiveNode<any>>;
9
- protected _active: Readonly<ActiveNode<any>>;
10
- protected get nextNode(): {
11
- column: keyof T;
12
- row: number;
13
- };
10
+ /**
11
+ * The active cell of this grid. Instance state on purpose: every grid on a
12
+ * page, including one nested in another grid's detail panel, keeps its own.
13
+ */
14
+ protected _active: ActiveNode<T>;
15
+ protected get nextNode(): ActiveNode<T>;
14
16
  protected get columns(): Array<{
15
17
  key: Keys<T>;
16
18
  hidden?: boolean;
@@ -20,21 +22,72 @@ export declare class NavigationController<T extends object> implements ReactiveC
20
22
  hidden?: boolean;
21
23
  }[];
22
24
  protected get firstColumn(): Keys<T>;
25
+ /** Whether there is any cell to move to: a body row and a visible column. */
26
+ protected get navigable(): boolean;
23
27
  protected getPreviousColumn(key: Keys<T>): Keys<T>;
24
28
  protected getNextColumn(key: Keys<T>): Keys<T>;
29
+ /** The rendered body row at `index`, when the virtualizer has it in the DOM. */
30
+ protected findRow(index: number): ApexGridRow<T> | undefined;
31
+ /**
32
+ * The element that stands for `node` in the DOM: the cell, or for a row a
33
+ * feature module renders full width (a group header, a total row) the
34
+ * row's single focus target, whatever the column.
35
+ */
36
+ protected findCell(node: ActiveNode<T>): HTMLElement | undefined;
25
37
  protected scrollToCell(node: ActiveNode<T>): void;
26
- protected findActiveCell(): import("../components/cell.js").default<T> | undefined;
38
+ /** Scrolls body row `index` into view, rendered or not. */
39
+ protected scrollToRow(index: number): void;
40
+ /**
41
+ * Scrolls the cell at `node` into view clear of the sticky chrome without
42
+ * moving the active cell: the cell itself when it is rendered, else its row
43
+ * first and the cell once the virtualizer has rendered it. For a feature
44
+ * that moves something other than the active cell with the keyboard (range
45
+ * extension), so it scrolls the way arrow navigation does.
46
+ */
47
+ revealCell(node: ActiveNode<T>): void;
48
+ /**
49
+ * Whether ArrowLeft or ArrowRight steps towards the end of the column
50
+ * order: ArrowRight in a left-to-right grid, ArrowLeft in a right-to-left
51
+ * one. The rule every inline arrow key on this grid follows.
52
+ */
53
+ towardsInlineEnd(key: string): boolean;
54
+ /**
55
+ * How much of the scroll container's scrollport the sticky chrome covers
56
+ * (toolbar, headers and the pinned-top band above, the pinned-bottom band
57
+ * and the paginator below, pinned columns on either side). Keyboard scrolls
58
+ * use it as scroll-padding, so a row or cell the keyboard moves to is never
59
+ * parked underneath that chrome. Measured right before each keyboard scroll
60
+ * because any of those bands can change size between key presses. Public
61
+ * so a feature scrolling the body itself (drag auto-scroll) measures the
62
+ * same chrome.
63
+ */
64
+ scrollInsets(): ScrollInsets;
27
65
  /**
28
66
  * Moves real DOM focus to the active cell (the grid body's roving tab stop),
29
67
  * so screen readers track keyboard navigation. Waits for the host render to
30
- * commit the cell's `active` state, then retries once on the next frame to
31
- * cover virtualizer re-materialization after a long jump. No-op while no
32
- * cell is active (the scroll container keeps focus as the entry point).
68
+ * commit the cell's `active` state and, when the cell is not in the DOM yet
69
+ * (a long jump), for the virtualizer to finish laying out the new range.
70
+ * No-op while no cell is active (the scroll container keeps focus as the
71
+ * entry point).
72
+ *
73
+ * @param reveal - also scroll the cell into view once it exists, for jumps
74
+ * whose target row was not rendered when the scroll started.
33
75
  */
34
- focusActiveCell(): void;
76
+ focusActiveCell(reveal?: boolean): void;
35
77
  get active(): ActiveNode<T>;
36
78
  set active(node: ActiveNode<T>);
37
79
  constructor(host: GridHost<T>);
80
+ /**
81
+ * Keeps the active cell pointing at something that exists. Runs before
82
+ * every grid render, so after a data swap, sort, filter, page change or a
83
+ * column hide or reorder the active node is clamped to the last row and the
84
+ * nearest visible column, or cleared when there is nothing left to point
85
+ * at (no rows, no visible columns). Without it a stale node names a cell
86
+ * that is never rendered, which leaves the body with no tab stop.
87
+ */
88
+ hostUpdate(): void;
89
+ /** The active row is one a feature module renders full width (no cells). */
90
+ protected isPresentedRow(index: number): boolean;
38
91
  /** Home moves to the first cell in the row (ARIA grid pattern). */
39
92
  protected home(): void;
40
93
  /** End moves to the last cell in the row (ARIA grid pattern). */
@@ -44,8 +97,9 @@ export declare class NavigationController<T extends object> implements ReactiveC
44
97
  /** Ctrl/Cmd+End moves to the last cell of the grid. */
45
98
  protected ctrlEnd(): void;
46
99
  /**
47
- * Rows per Page Up/Down step, derived from the host viewport and the height
48
- * of a rendered row (fallback: 10 rows when nothing is measurable yet).
100
+ * Rows per Page Up/Down step: the rows that fit in the part of the
101
+ * scrollport the sticky chrome leaves visible, minus one so the row the
102
+ * user was on stays in view. Falls back to 10 when nothing is measurable.
49
103
  */
50
104
  protected pageStep(): number;
51
105
  protected pageUp(): void;
@@ -1,15 +1,25 @@
1
- import { NAVIGATION_STATE, SENTINEL_NODE } from '../internal/constants.js';
1
+ import { revealWithin } from '../components/virtualizer.js';
2
+ import { SENTINEL_NODE } from '../internal/constants.js';
2
3
  import { GRID_ROW_TAG } from '../internal/tags.js';
3
4
  import { getDisplayColumns, isRTL } from '../internal/utils.js';
5
+ const RELATIVE_KEYS = new Set([
6
+ 'ArrowDown',
7
+ 'ArrowUp',
8
+ 'ArrowLeft',
9
+ 'ArrowRight',
10
+ 'Home',
11
+ 'End',
12
+ 'PageUp',
13
+ 'PageDown',
14
+ ]);
4
15
  export class NavigationController {
5
16
  get virtualizer() {
6
17
  return this.host.scrollContainer;
7
18
  }
8
19
  get nextNode() {
9
- const node = this.state.get('current');
10
- return node === SENTINEL_NODE
20
+ return this._active === SENTINEL_NODE
11
21
  ? { column: this.firstColumn, row: 0 }
12
- : { ...node };
22
+ : { ...this._active };
13
23
  }
14
24
  get columns() {
15
25
  return getDisplayColumns(this.host.columns);
@@ -21,40 +31,121 @@ export class NavigationController {
21
31
  const first = this.visibleColumns.at(0);
22
32
  return (first?.key ?? this.host.columns[0]?.key);
23
33
  }
34
+ get navigable() {
35
+ return this.host.pageItems.length > 0 && this.visibleColumns.length > 0;
36
+ }
24
37
  getPreviousColumn(key) {
25
38
  const columns = this.visibleColumns;
26
39
  const idx = columns.findIndex((column) => column.key === key);
27
- return columns[Math.max(idx - 1, 0)].key;
40
+ return columns[Math.max(idx - 1, 0)]?.key ?? key;
28
41
  }
29
42
  getNextColumn(key) {
30
43
  const columns = this.visibleColumns;
31
44
  const idx = columns.findIndex((column) => column.key === key);
32
- return columns[Math.min(idx + 1, columns.length - 1)].key;
45
+ return columns[Math.min(idx + 1, columns.length - 1)]?.key ?? key;
46
+ }
47
+ findRow(index) {
48
+ return Array.from(this.virtualizer.querySelectorAll(GRID_ROW_TAG)).find((row) => row.index === index);
49
+ }
50
+ findCell(node) {
51
+ const row = this.findRow(node.row);
52
+ if (!row)
53
+ return undefined;
54
+ return (row.presentedCell ??
55
+ row.cells.find((cell) => cell.column.key === node.column));
33
56
  }
34
57
  scrollToCell(node) {
35
- const row = Array.from(this.virtualizer.querySelectorAll(GRID_ROW_TAG)).find((row) => row.index === node.row);
36
- if (row) {
37
- row.cells
38
- .find((cell) => cell.column.key === node.column)
39
- ?.scrollIntoView({ block: 'nearest' });
58
+ const cell = this.findCell(node);
59
+ if (cell) {
60
+ revealWithin(this.host, cell, {
61
+ block: 'nearest',
62
+ inline: 'nearest',
63
+ insets: this.scrollInsets(),
64
+ });
40
65
  }
41
66
  }
42
- findActiveCell() {
43
- const row = Array.from(this.virtualizer.querySelectorAll(GRID_ROW_TAG)).find((el) => el.index === this.active.row);
44
- return row?.cells.find((cell) => cell.column.key === this.active.column);
67
+ scrollToRow(index) {
68
+ this.virtualizer
69
+ .element(index)
70
+ ?.scrollIntoView({ block: 'nearest', insets: this.scrollInsets() });
45
71
  }
46
- focusActiveCell() {
47
- const node = this.state.get('current');
48
- if (!node || node === SENTINEL_NODE)
72
+ #revealing;
73
+ revealCell(node) {
74
+ this.#revealing = node;
75
+ if (this.findCell(node)) {
76
+ this.scrollToCell(node);
77
+ return;
78
+ }
79
+ this.scrollToRow(node.row);
80
+ void this.virtualizer.settled().then(() => {
81
+ if (this.#revealing === node)
82
+ this.scrollToCell(node);
83
+ });
84
+ }
85
+ towardsInlineEnd(key) {
86
+ return isRTL(this.host) === (key === 'ArrowLeft');
87
+ }
88
+ scrollInsets() {
89
+ const host = this.host;
90
+ const root = host.shadowRoot;
91
+ if (!root)
92
+ return { top: 0, right: 0, bottom: 0, left: 0 };
93
+ const box = host.getBoundingClientRect();
94
+ const top = box.top + host.clientTop;
95
+ const left = box.left + host.clientLeft;
96
+ const bottom = top + host.clientHeight;
97
+ const right = left + host.clientWidth;
98
+ let insetTop = 0;
99
+ for (const el of root.querySelectorAll('apex-grid-header-row, [part~="pinned-top"]')) {
100
+ insetTop = Math.max(insetTop, el.getBoundingClientRect().bottom - top);
101
+ }
102
+ let insetBottom = 0;
103
+ for (const el of root.querySelectorAll('apex-grid-paginator, [part~="pinned-bottom"]')) {
104
+ insetBottom = Math.max(insetBottom, bottom - el.getBoundingClientRect().top);
105
+ }
106
+ let insetLeft = 0;
107
+ let insetRight = 0;
108
+ const headerRow = root.querySelector('apex-grid-header-row');
109
+ const pinned = headerRow?.shadowRoot?.querySelectorAll('[data-pinned="start"], [data-pinned="end"]');
110
+ if (pinned?.length) {
111
+ const rtl = isRTL(host);
112
+ for (const el of pinned) {
113
+ const rect = el.getBoundingClientRect();
114
+ const atLeft = (el.getAttribute('data-pinned') === 'start') !== rtl;
115
+ if (atLeft)
116
+ insetLeft = Math.max(insetLeft, rect.right - left);
117
+ else
118
+ insetRight = Math.max(insetRight, right - rect.left);
119
+ }
120
+ }
121
+ const clamp = (inset) => Math.max(0, Math.round(inset));
122
+ return {
123
+ top: clamp(insetTop),
124
+ right: clamp(insetRight),
125
+ bottom: clamp(insetBottom),
126
+ left: clamp(insetLeft),
127
+ };
128
+ }
129
+ focusActiveCell(reveal = false) {
130
+ const node = this._active;
131
+ if (node === SENTINEL_NODE)
49
132
  return;
50
133
  void (async () => {
51
134
  await this.host.updateComplete;
52
- let cell = this.findActiveCell();
135
+ let cell = this.findCell(node);
136
+ if (!cell) {
137
+ await this.virtualizer.settled();
138
+ cell = this.findCell(node);
139
+ }
53
140
  if (!cell) {
54
141
  await new Promise(requestAnimationFrame);
55
- cell = this.findActiveCell();
142
+ cell = this.findCell(node);
56
143
  }
57
- cell?.focus({ preventScroll: true });
144
+ if (!cell || this._active !== node)
145
+ return;
146
+ if (reveal)
147
+ this.scrollToCell(node);
148
+ cell.focus({ preventScroll: true });
58
149
  })();
59
150
  }
60
151
  get active() {
@@ -62,8 +153,6 @@ export class NavigationController {
62
153
  }
63
154
  set active(node) {
64
155
  this._active = node;
65
- this.state.set('previous', this._active);
66
- this.state.set('current', node);
67
156
  this.host.requestUpdate();
68
157
  }
69
158
  constructor(host) {
@@ -79,10 +168,46 @@ export class NavigationController {
79
168
  PageDown: this.pageDown,
80
169
  ' ': this.toggleSelection,
81
170
  }));
82
- this.state = NAVIGATION_STATE;
83
171
  this._active = SENTINEL_NODE;
172
+ this.#revealing = null;
84
173
  this.host.addController(this);
85
174
  }
175
+ hostUpdate() {
176
+ const node = this._active;
177
+ if (node === SENTINEL_NODE)
178
+ return;
179
+ const count = this.host.pageItems.length;
180
+ if (!count || !this.visibleColumns.length) {
181
+ this._active = SENTINEL_NODE;
182
+ return;
183
+ }
184
+ const row = Math.min(node.row, count - 1);
185
+ const column = this.#nearestVisibleColumn(node.column);
186
+ if (row !== node.row || column !== node.column) {
187
+ this._active = { column, row };
188
+ }
189
+ }
190
+ #nearestVisibleColumn(key) {
191
+ const columns = this.columns;
192
+ const index = columns.findIndex((column) => column.key === key);
193
+ if (index < 0)
194
+ return this.firstColumn;
195
+ for (let distance = 0; distance < columns.length; distance++) {
196
+ for (const at of [index + distance, index - distance]) {
197
+ const column = columns[at];
198
+ if (column && !column.hidden)
199
+ return column.key;
200
+ }
201
+ }
202
+ return this.firstColumn;
203
+ }
204
+ isPresentedRow(index) {
205
+ const data = this.host.pageItems[index];
206
+ if (!data)
207
+ return false;
208
+ const state = this.host.stateController;
209
+ return state.presentRow(data, { columns: this.host.columns, rowIndex: index }) !== null;
210
+ }
86
211
  home() {
87
212
  this.active = Object.assign(this.nextNode, { column: this.firstColumn });
88
213
  this.scrollToCell(this.active);
@@ -94,74 +219,75 @@ export class NavigationController {
94
219
  }
95
220
  ctrlHome() {
96
221
  this.active = { column: this.firstColumn, row: 0 };
97
- this.virtualizer.element(0)?.scrollIntoView({ block: 'nearest' });
98
- this.scrollToCell(this.active);
222
+ this.scrollToRow(0);
99
223
  }
100
224
  ctrlEnd() {
101
- const row = Math.max(0, this.host.pageItems.length - 1);
225
+ const row = this.host.pageItems.length - 1;
102
226
  const last = this.visibleColumns.at(-1)?.key ?? this.firstColumn;
103
227
  this.active = { column: last, row };
104
- this.virtualizer.element(row)?.scrollIntoView({ block: 'nearest' });
105
- this.scrollToCell(this.active);
228
+ this.scrollToRow(row);
106
229
  }
107
230
  pageStep() {
108
- const rowEl = this.virtualizer.element(this.nextNode.row);
109
- const rowHeight = rowEl?.offsetHeight || 0;
110
- const viewport = this.host.clientHeight || 0;
111
- if (!rowHeight || !viewport)
231
+ const host = this.host;
232
+ let rowHeight = 0;
233
+ for (const row of this.virtualizer.querySelectorAll(GRID_ROW_TAG)) {
234
+ const height = row.offsetHeight;
235
+ if (height > 0 && (!rowHeight || height < rowHeight))
236
+ rowHeight = height;
237
+ }
238
+ const { top, bottom } = this.scrollInsets();
239
+ const viewport = host.clientHeight - top - bottom;
240
+ if (!rowHeight || viewport <= 0)
112
241
  return 10;
113
- const rows = Math.floor(viewport / rowHeight) - 1;
114
- return rows > 0 ? rows : 10;
242
+ return Math.max(1, Math.floor(viewport / rowHeight) - 1);
115
243
  }
116
244
  pageUp() {
117
245
  const next = this.nextNode;
118
246
  this.active = Object.assign(next, { row: Math.max(0, next.row - this.pageStep()) });
119
- this.virtualizer.element(this.active.row)?.scrollIntoView({ block: 'nearest' });
247
+ this.scrollToRow(this.active.row);
120
248
  }
121
249
  pageDown() {
122
250
  const next = this.nextNode;
123
251
  this.active = Object.assign(next, {
124
252
  row: Math.min(this.host.pageItems.length - 1, next.row + this.pageStep()),
125
253
  });
126
- this.virtualizer.element(this.active.row)?.scrollIntoView({ block: 'nearest' });
254
+ this.scrollToRow(this.active.row);
127
255
  }
128
256
  arrowDown() {
129
257
  const next = this.nextNode;
130
258
  this.active = Object.assign(next, {
131
259
  row: Math.min(next.row + 1, this.host.pageItems.length - 1),
132
260
  });
133
- this.virtualizer.element(next.row)?.scrollIntoView({ block: 'nearest' });
261
+ this.scrollToRow(next.row);
134
262
  }
135
263
  arrowUp() {
136
264
  const next = this.nextNode;
137
265
  this.active = Object.assign(next, { row: Math.max(0, next.row - 1) });
138
- this.virtualizer.element(next.row)?.scrollIntoView({ block: 'nearest' });
266
+ this.scrollToRow(next.row);
139
267
  }
140
268
  arrowLeft() {
141
- this.#moveInline('left');
269
+ this.#moveInline('ArrowLeft');
142
270
  }
143
271
  arrowRight() {
144
- this.#moveInline('right');
272
+ this.#moveInline('ArrowRight');
145
273
  }
146
- #moveInline(direction) {
147
- const towardsEnd = isRTL(this.host) === (direction === 'left');
274
+ #moveInline(key) {
148
275
  const next = this.nextNode;
149
- const column = towardsEnd
276
+ const column = this.towardsInlineEnd(key)
150
277
  ? this.getNextColumn(next.column)
151
278
  : this.getPreviousColumn(next.column);
152
279
  this.active = Object.assign(next, { column });
153
280
  this.scrollToCell(this.active);
154
281
  }
155
282
  toggleSelection() {
156
- const data = this.host.pageItems[this.active.row];
157
- if (!data)
283
+ const data = this.host.pageItems[this._active.row];
284
+ if (!data || this.isPresentedRow(this._active.row))
158
285
  return;
159
286
  void this.host.toggleRowSelection(data);
160
287
  }
161
288
  hostConnected() { }
162
289
  hostDisconnected() {
163
290
  this.active = SENTINEL_NODE;
164
- this.state = NAVIGATION_STATE;
165
291
  }
166
292
  get editing() {
167
293
  return this.host.stateController.editing;
@@ -171,8 +297,7 @@ export class NavigationController {
171
297
  return false;
172
298
  if (event.ctrlKey || event.metaKey || event.altKey)
173
299
  return false;
174
- const node = this.state.get('current');
175
- if (!node || node === SENTINEL_NODE)
300
+ if (this._active === SENTINEL_NODE || this.isPresentedRow(this._active.row))
176
301
  return false;
177
302
  const editing = this.editing;
178
303
  if (!editing?.enabled)
@@ -199,7 +324,7 @@ export class NavigationController {
199
324
  const next = reorder.moveGrabbed(event.key === 'ArrowDown' ? 1 : -1);
200
325
  if (next >= 0) {
201
326
  this.active = Object.assign(this.nextNode, { row: next });
202
- this.virtualizer.element(next)?.scrollIntoView({ block: 'nearest' });
327
+ this.scrollToRow(next);
203
328
  }
204
329
  return true;
205
330
  }
@@ -216,7 +341,7 @@ export class NavigationController {
216
341
  return false;
217
342
  }
218
343
  }
219
- if (event.key === ' ' && this.active.row >= 0) {
344
+ if (event.key === ' ' && this.active.row >= 0 && !this.isPresentedRow(this.active.row)) {
220
345
  event.preventDefault();
221
346
  return reorder.grab(this.active.row);
222
347
  }
@@ -239,18 +364,33 @@ export class NavigationController {
239
364
  }
240
365
  if (event.key === 'Home' || event.key === 'End') {
241
366
  event.preventDefault();
367
+ if (!this.navigable)
368
+ return;
242
369
  if (event.key === 'Home') {
243
370
  this.ctrlHome();
244
371
  }
245
372
  else {
246
373
  this.ctrlEnd();
247
374
  }
248
- this.focusActiveCell();
375
+ this.focusActiveCell(true);
249
376
  return;
250
377
  }
251
378
  }
252
379
  if (this.handleEditKey(event))
253
380
  return;
381
+ if (RELATIVE_KEYS.has(event.key)) {
382
+ event.preventDefault();
383
+ if (!this.navigable)
384
+ return;
385
+ if (this._active === SENTINEL_NODE) {
386
+ this.active = { column: this.firstColumn, row: 0 };
387
+ if (event.key !== 'End' && event.key !== 'PageDown') {
388
+ this.scrollToRow(0);
389
+ this.focusActiveCell(true);
390
+ return;
391
+ }
392
+ }
393
+ }
254
394
  if (this.handlers.has(event.key)) {
255
395
  event.preventDefault();
256
396
  this.handlers.get(event.key).call(this);