@dxos/lit-grid 0.6.12-main.5cc132e → 0.6.12-main.78ddbdf

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/dx-grid.ts CHANGED
@@ -2,21 +2,29 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { LitElement, html } from 'lit';
5
+ import { LitElement, html, nothing } from 'lit';
6
6
  import { customElement, state, property, eventOptions } from 'lit/decorators.js';
7
7
  import { ref, createRef, type Ref } from 'lit/directives/ref.js';
8
8
 
9
+ // eslint-disable-next-line unused-imports/no-unused-imports
10
+ import './dx-grid-axis-resize-handle';
9
11
  import {
10
12
  type AxisMeta,
11
13
  type CellIndex,
12
- type CellValue,
13
14
  DxAxisResize,
14
- type DxAxisResizeProps,
15
+ type DxAxisResizeInternal,
15
16
  DxEditRequest,
16
17
  type DxGridAxis,
18
+ type DxGridAxisMeta,
19
+ type DxGridCells,
20
+ DxGridCellsSelect,
17
21
  type DxGridMode,
22
+ type DxGridPointer,
23
+ type DxGridPosition,
18
24
  type DxGridPositionNullable,
25
+ type DxGridRange,
19
26
  } from './types';
27
+ import { separator, toCellIndex } from './util';
20
28
 
21
29
  /**
22
30
  * The size in pixels of the gap between cells
@@ -43,11 +51,6 @@ const sizeColMax = 1024;
43
51
  const sizeRowMin = 16;
44
52
  const sizeRowMax = 1024;
45
53
 
46
- /**
47
- * Separator for serializing cell position vectors
48
- */
49
- const separator = ',';
50
-
51
54
  //
52
55
  // A1 notation is the fallback for numbering columns and rows.
53
56
  //
@@ -68,7 +71,7 @@ const closestAction = (target: EventTarget | null): { action: string | null; act
68
71
  return { actionEl, action: actionEl?.getAttribute('data-dx-grid-action') ?? null };
69
72
  };
70
73
 
71
- const closestCell = (target: EventTarget | null, actionEl?: HTMLElement | null): Record<DxGridAxis, number> | null => {
74
+ const closestCell = (target: EventTarget | null, actionEl?: HTMLElement | null): DxGridPositionNullable => {
72
75
  let cellElement = actionEl;
73
76
  if (!cellElement) {
74
77
  const { action, actionEl } = closestAction(target);
@@ -88,9 +91,6 @@ const closestCell = (target: EventTarget | null, actionEl?: HTMLElement | null):
88
91
  const isSameCell = (a: DxGridPositionNullable, b: DxGridPositionNullable) =>
89
92
  a && b && Number.isFinite(a.col) && Number.isFinite(a.row) && a.col === b.col && a.row === b.row;
90
93
 
91
- const toCellIndex = (cellCoords: Record<DxGridAxis, number>): CellIndex =>
92
- `${cellCoords.col}${separator}${cellCoords.row}`;
93
-
94
94
  const localChId = (c0: number) => `ch--${c0}`;
95
95
  const localRhId = (r0: number) => `rh--${r0}`;
96
96
 
@@ -98,6 +98,22 @@ const getPage = (axis: string, event: PointerEvent) => (axis === 'col' ? event.p
98
98
 
99
99
  @customElement('dx-grid')
100
100
  export class DxGrid extends LitElement {
101
+ constructor() {
102
+ super();
103
+ this.addEventListener('dx-axis-resize-internal', this.handleAxisResizeInternal as EventListener);
104
+ this.addEventListener('wheel', this.handleWheel);
105
+ this.addEventListener('pointerdown', this.handlePointerDown);
106
+ this.addEventListener('pointermove', this.handlePointerMove);
107
+ this.addEventListener('pointerup', this.handlePointerUp);
108
+ this.addEventListener('pointerleave', this.handlePointerUp);
109
+ this.addEventListener('focus', this.handleFocus, { capture: true });
110
+ this.addEventListener('blur', this.handleBlur, { capture: true });
111
+ this.addEventListener('keydown', this.handleKeydown);
112
+ }
113
+
114
+ @property({ type: String })
115
+ gridId: string = 'default-grid-id';
116
+
101
117
  @property({ type: Object })
102
118
  rowDefault: AxisMeta = { size: 32 };
103
119
 
@@ -105,62 +121,71 @@ export class DxGrid extends LitElement {
105
121
  columnDefault: AxisMeta = { size: 180 };
106
122
 
107
123
  @property({ type: Object })
108
- rows: Record<string, AxisMeta> = {};
124
+ rows: DxGridAxisMeta = {};
109
125
 
110
126
  @property({ type: Object })
111
- columns: Record<string, AxisMeta> = {};
127
+ columns: DxGridAxisMeta = {};
112
128
 
113
129
  @property({ type: Object })
114
- cells: Record<CellIndex, CellValue> = {};
130
+ initialCells: DxGridCells = {};
115
131
 
116
132
  @property({ type: String })
117
133
  mode: DxGridMode = 'browse';
118
134
 
135
+ /**
136
+ * When this function is defined, it is used first to try to get a value for a cell, and otherwise will fall back
137
+ * to `cells`.
138
+ */
139
+ getCells: ((nextRange: DxGridRange) => DxGridCells) | null = null;
140
+
141
+ @state()
142
+ private cells: DxGridCells = {};
143
+
119
144
  //
120
145
  // `pos`, short for ‘position’, is the position in pixels of the viewport from the origin.
121
146
  //
122
147
 
123
148
  @state()
124
- posInline = 0;
149
+ private posInline = 0;
125
150
 
126
151
  @state()
127
- posBlock = 0;
152
+ private posBlock = 0;
128
153
 
129
154
  //
130
155
  // `size` (when not suffixed with ‘row’ or ‘col’, see above) is the size in pixels of the viewport.
131
156
  //
132
157
 
133
158
  @state()
134
- sizeInline = 0;
159
+ private sizeInline = 0;
135
160
 
136
161
  @state()
137
- sizeBlock = 0;
162
+ private sizeBlock = 0;
138
163
 
139
164
  //
140
165
  // `overscan` is the amount in pixels to offset the grid content due to the number of overscanned columns or rows.
141
166
  //
142
167
 
143
168
  @state()
144
- overscanInline = 0;
169
+ private overscanInline = 0;
145
170
 
146
171
  @state()
147
- overscanBlock = 0;
172
+ private overscanBlock = 0;
148
173
 
149
174
  //
150
175
  // `bin`, not short for anything, is the range in pixels within which virtualization does not need to reassess.
151
176
  //
152
177
 
153
178
  @state()
154
- binInlineMin = 0;
179
+ private binInlineMin = 0;
155
180
 
156
181
  @state()
157
- binInlineMax = this.colSize(0);
182
+ private binInlineMax = this.colSize(0);
158
183
 
159
184
  @state()
160
- binBlockMin = 0;
185
+ private binBlockMin = 0;
161
186
 
162
187
  @state()
163
- binBlockMax = this.rowSize(0);
188
+ private binBlockMax = this.rowSize(0);
164
189
 
165
190
  //
166
191
  // `vis`, short for ‘visible’, is the range in numeric index of the columns or rows which should be rendered within
@@ -168,97 +193,129 @@ export class DxGrid extends LitElement {
168
193
  //
169
194
 
170
195
  @state()
171
- visColMin = 0;
196
+ private visColMin = 0;
172
197
 
173
198
  @state()
174
- visColMax = 1;
199
+ private visColMax = 1;
175
200
 
176
201
  @state()
177
- visRowMin = 0;
202
+ private visRowMin = 0;
178
203
 
179
204
  @state()
180
- visRowMax = 1;
205
+ private visRowMax = 1;
181
206
 
182
207
  //
183
208
  // `template` is the rendered value of `grid-{axis}-template`.
184
209
  //
185
210
  @state()
186
- templateColumns = `${this.colSize(0)}px`;
211
+ private templateColumns = `${this.colSize(0)}px`;
187
212
 
188
213
  @state()
189
- templateRows = `${this.rowSize(0)}px`;
214
+ private templateRows = `${this.rowSize(0)}px`;
190
215
 
191
216
  //
192
- // Resize state
217
+ // Focus, selection, and resize states
193
218
  //
194
219
 
195
220
  @state()
196
- colSizes: Record<string, number> = {};
221
+ private pointer: DxGridPointer = null;
222
+
223
+ @state()
224
+ private colSizes: Record<string, number> = {};
225
+
226
+ @state()
227
+ private rowSizes: Record<string, number> = {};
228
+
229
+ @state()
230
+ private focusActive: boolean = false;
197
231
 
198
232
  @state()
199
- rowSizes: Record<string, number> = {};
233
+ private focusedCell: DxGridPosition = { col: 0, row: 0 };
200
234
 
201
235
  @state()
202
- resizing: null | (DxAxisResizeProps & { page: number }) = null;
236
+ private selectionStart: DxGridPosition = { col: 0, row: 0 };
237
+
238
+ @state()
239
+ private selectionEnd: DxGridPosition = { col: 0, row: 0 };
203
240
 
204
241
  //
205
242
  // Primary pointer and keyboard handlers
206
243
  //
207
244
 
208
- handlePointerDown = (event: PointerEvent) => {
209
- const { action, actionEl } = closestAction(event.target);
210
- if (action) {
211
- if (action.startsWith('resize') && this.mode === 'browse') {
212
- const [resize, index] = action.split(',');
213
- const [_, axis] = resize.split('-');
214
- this.resizing = {
215
- axis: axis as DxGridAxis,
216
- size: axis === 'col' ? this.colSize(index) : this.rowSize(index),
217
- page: getPage(axis, event),
218
- index,
219
- };
220
- } else if (action === 'cell') {
221
- const cellCoords = closestCell(event.target, actionEl);
222
- if (this.focusActive && isSameCell(this.focusedCell, cellCoords)) {
223
- this.dispatchEvent(
224
- new DxEditRequest({
225
- cellIndex: toCellIndex(cellCoords!),
226
- cellBox: this.focusedCellBox(),
227
- }),
228
- );
245
+ private dispatchEditRequest(initialContent?: string) {
246
+ this.snapPosToFocusedCell();
247
+ // Without deferring, the event dispatches before `focusedCellBox` can get updated bounds of the cell, hence:
248
+ queueMicrotask(() =>
249
+ this.dispatchEvent(
250
+ new DxEditRequest({
251
+ cellIndex: toCellIndex(this.focusedCell),
252
+ cellBox: this.focusedCellBox(),
253
+ initialContent,
254
+ }),
255
+ ),
256
+ );
257
+ }
258
+
259
+ private handlePointerDown = (event: PointerEvent) => {
260
+ if (event.isPrimary) {
261
+ const { action, actionEl } = closestAction(event.target);
262
+ if (action) {
263
+ if (action.startsWith('resize') && this.mode === 'browse') {
264
+ const [resize, index] = action.split(',');
265
+ const [_, axis] = resize.split('-');
266
+ this.pointer = {
267
+ state: 'resizing',
268
+ axis: axis as DxGridAxis,
269
+ size: axis === 'col' ? this.colSize(index) : this.rowSize(index),
270
+ page: getPage(axis, event),
271
+ index,
272
+ };
273
+ } else if (action === 'cell') {
274
+ const cellCoords = closestCell(event.target, actionEl);
275
+ if (cellCoords) {
276
+ this.pointer = { state: 'selecting' };
277
+ this.selectionStart = cellCoords;
278
+ }
279
+ if (this.mode === 'edit') {
280
+ event.preventDefault();
281
+ } else {
282
+ if (this.focusActive && isSameCell(this.focusedCell, cellCoords)) {
283
+ this.dispatchEditRequest();
284
+ }
285
+ }
229
286
  }
230
287
  }
231
288
  }
232
289
  };
233
290
 
234
- handlePointerUp = (_event: PointerEvent) => {
235
- if (this.resizing) {
236
- const resizeEvent = new DxAxisResize({
237
- axis: this.resizing.axis,
238
- index: this.resizing.index,
239
- size: this[this.resizing.axis === 'col' ? 'colSize' : 'rowSize'](this.resizing.index),
240
- });
241
- this.dispatchEvent(resizeEvent);
242
- this.resizing = null;
291
+ private handlePointerUp = (event: PointerEvent) => {
292
+ if (this.pointer?.state === 'resizing') {
293
+ // do nothing, todo: remove
294
+ } else {
295
+ const cell = closestCell(event.target);
296
+ if (cell) {
297
+ this.selectionEnd = cell;
298
+ this.dispatchEvent(
299
+ new DxGridCellsSelect({
300
+ start: this.selectionStart,
301
+ end: this.selectionEnd,
302
+ }),
303
+ );
304
+ }
243
305
  }
306
+ this.pointer = null;
244
307
  };
245
308
 
246
- handlePointerMove = (event: PointerEvent) => {
247
- if (this.resizing) {
248
- const delta = getPage(this.resizing.axis, event) - this.resizing.page;
249
- if (this.resizing.axis === 'col') {
250
- const nextSize = Math.max(sizeColMin, Math.min(sizeColMax, this.resizing.size + delta));
251
- this.colSizes = { ...this.colSizes, [this.resizing.index]: nextSize };
252
- this.updateVisInline();
253
- } else {
254
- const nextSize = Math.max(sizeRowMin, Math.min(sizeRowMax, this.resizing.size + delta));
255
- this.rowSizes = { ...this.rowSizes, [this.resizing.index]: nextSize };
256
- this.updateVisBlock();
309
+ private handlePointerMove = (event: PointerEvent) => {
310
+ if (this.pointer?.state === 'selecting') {
311
+ const cell = closestCell(event.target);
312
+ if (cell && (cell.col !== this.selectionEnd.col || cell.row !== this.selectionEnd.row)) {
313
+ this.selectionEnd = cell;
257
314
  }
258
315
  }
259
316
  };
260
317
 
261
- handleKeydown(event: KeyboardEvent) {
318
+ private handleKeydown(event: KeyboardEvent) {
262
319
  if (this.focusActive && this.mode === 'browse') {
263
320
  // Adjust state
264
321
  switch (event.key) {
@@ -275,15 +332,15 @@ export class DxGrid extends LitElement {
275
332
  this.focusedCell = { ...this.focusedCell, col: Math.max(0, this.focusedCell.col - 1) };
276
333
  break;
277
334
  }
278
- // Emit interactions
335
+ // Emit edit request if relevant
279
336
  switch (event.key) {
280
337
  case 'Enter':
281
- this.dispatchEvent(
282
- new DxEditRequest({
283
- cellIndex: toCellIndex(this.focusedCell),
284
- cellBox: this.focusedCellBox(),
285
- }),
286
- );
338
+ this.dispatchEditRequest();
339
+ break;
340
+ default:
341
+ if (event.key.length === 1 && event.key.match(/\P{Cc}/u)) {
342
+ this.dispatchEditRequest(event.key);
343
+ }
287
344
  break;
288
345
  }
289
346
  // Handle virtualization & focus consequences
@@ -312,7 +369,8 @@ export class DxGrid extends LitElement {
312
369
  }
313
370
 
314
371
  private cell(c: number | string, r: number | string) {
315
- return this.cells[`${c}${separator}${r}`];
372
+ const index: CellIndex = `${c}${separator}${r}`;
373
+ return this.cells[index] ?? this.initialCells[index];
316
374
  }
317
375
 
318
376
  private focusedCellBox(): DxEditRequest['cellBox'] {
@@ -339,7 +397,7 @@ export class DxGrid extends LitElement {
339
397
  //
340
398
 
341
399
  @state()
342
- observer = new ResizeObserver((entries) => {
400
+ private observer = new ResizeObserver((entries) => {
343
401
  const { inlineSize, blockSize } = entries?.[0]?.contentBoxSize?.[0] ?? {
344
402
  inlineSize: 0,
345
403
  blockSize: 0,
@@ -355,28 +413,32 @@ export class DxGrid extends LitElement {
355
413
  }
356
414
  });
357
415
 
358
- viewportRef: Ref<HTMLDivElement> = createRef();
416
+ private viewportRef: Ref<HTMLDivElement> = createRef();
417
+
418
+ private maybeUpdateVis = () => {
419
+ if (
420
+ this.posInline >= this.binInlineMin &&
421
+ this.posInline < this.binInlineMax &&
422
+ this.posBlock >= this.binBlockMin &&
423
+ this.posBlock < this.binBlockMax
424
+ ) {
425
+ // do nothing
426
+ } else {
427
+ // console.info(
428
+ // '[updating bounds]',
429
+ // 'wheel',
430
+ // [this.binInlineMin, this.posInline, this.binInlineMax],
431
+ // [this.binBlockMin, this.posBlock, this.binBlockMax],
432
+ // );
433
+ this.updateVis();
434
+ }
435
+ };
359
436
 
360
- handleWheel = ({ deltaX, deltaY }: WheelEvent) => {
437
+ private handleWheel = ({ deltaX, deltaY }: WheelEvent) => {
361
438
  if (this.mode === 'browse') {
362
439
  this.posInline = Math.max(0, this.posInline + deltaX);
363
440
  this.posBlock = Math.max(0, this.posBlock + deltaY);
364
- if (
365
- this.posInline >= this.binInlineMin &&
366
- this.posInline < this.binInlineMax &&
367
- this.posBlock >= this.binBlockMin &&
368
- this.posBlock < this.binBlockMax
369
- ) {
370
- // do nothing
371
- } else {
372
- // console.info(
373
- // '[updating bounds]',
374
- // 'wheel',
375
- // [this.binInlineMin, this.posInline, this.binInlineMax],
376
- // [this.binBlockMin, this.posBlock, this.binBlockMax],
377
- // );
378
- this.updateVis();
379
- }
441
+ this.maybeUpdateVis();
380
442
  }
381
443
  };
382
444
 
@@ -455,14 +517,8 @@ export class DxGrid extends LitElement {
455
517
 
456
518
  // Focus handlers
457
519
 
458
- @state()
459
- focusedCell: Record<DxGridAxis, number> = { col: 0, row: 0 };
460
-
461
- @state()
462
- focusActive: boolean = false;
463
-
464
520
  @eventOptions({ capture: true })
465
- handleFocus(event: FocusEvent) {
521
+ private handleFocus(event: FocusEvent) {
466
522
  const cellCoords = closestCell(event.target);
467
523
  if (cellCoords) {
468
524
  this.focusedCell = cellCoords;
@@ -471,17 +527,14 @@ export class DxGrid extends LitElement {
471
527
  }
472
528
 
473
529
  @eventOptions({ capture: true })
474
- handleBlur(event: FocusEvent) {
530
+ private handleBlur(event: FocusEvent) {
475
531
  // Only unset `focusActive` if focus is not moving to an element within the grid.
476
- if (
477
- !event.relatedTarget ||
478
- (event.relatedTarget as HTMLElement).closest('.dx-grid__viewport') !== this.viewportRef.value
479
- ) {
532
+ if (!event.relatedTarget || !(event.relatedTarget as HTMLElement).closest(`[data-grid="${this.gridId}"]`)) {
480
533
  this.focusActive = false;
481
534
  }
482
535
  }
483
536
 
484
- focusedCellElement() {
537
+ private focusedCellElement() {
485
538
  return this.viewportRef.value?.querySelector(
486
539
  `[aria-colindex="${this.focusedCell.col}"][aria-rowindex="${this.focusedCell.row}"]`,
487
540
  ) as HTMLElement | null;
@@ -490,13 +543,13 @@ export class DxGrid extends LitElement {
490
543
  /**
491
544
  * Moves focus to the cell with actual focus, otherwise moves focus to the viewport.
492
545
  */
493
- refocus(increment?: 'down' | 'right') {
546
+ refocus(increment?: 'col' | 'row', delta: 1 | -1 = 1) {
494
547
  switch (increment) {
495
- case 'down':
496
- this.focusedCell = { ...this.focusedCell, row: this.focusedCell.row + 1 };
548
+ case 'row':
549
+ this.focusedCell = { ...this.focusedCell, row: this.focusedCell.row + delta };
497
550
  break;
498
- case 'right':
499
- this.focusedCell = { ...this.focusedCell, col: this.focusedCell.col + 1 };
551
+ case 'col':
552
+ this.focusedCell = { ...this.focusedCell, col: this.focusedCell.col + delta };
500
553
  }
501
554
  (this.focusedCell.row < this.visRowMin ||
502
555
  this.focusedCell.row > this.visRowMax ||
@@ -505,6 +558,9 @@ export class DxGrid extends LitElement {
505
558
  ? this.viewportRef.value
506
559
  : this.focusedCellElement()
507
560
  )?.focus({ preventScroll: true });
561
+ if (increment) {
562
+ this.snapPosToFocusedCell();
563
+ }
508
564
  }
509
565
 
510
566
  /**
@@ -539,7 +595,7 @@ export class DxGrid extends LitElement {
539
595
  acc += this.colSize(this.visColMin + overscanCol + c0) + gap;
540
596
  return acc;
541
597
  }, 0);
542
- this.posInline = this.binInlineMin + sizeSumCol + gap * 2 - this.sizeInline;
598
+ this.posInline = Math.max(0, this.binInlineMin + sizeSumCol + gap * 2 - this.sizeInline);
543
599
  this.updateVisInline();
544
600
  }
545
601
 
@@ -551,12 +607,60 @@ export class DxGrid extends LitElement {
551
607
  acc += this.rowSize(this.visRowMin + overscanRow + r0) + gap;
552
608
  return acc;
553
609
  }, 0);
554
- this.posBlock = this.binBlockMin + sizeSumRow + gap * 2 - this.sizeBlock;
610
+ this.posBlock = Math.max(0, this.binBlockMin + sizeSumRow + gap * 2 - this.sizeBlock);
555
611
  this.updateVisBlock();
556
612
  }
557
613
  }
558
614
  }
559
615
 
616
+ //
617
+ // Map scroll DOM methods to virtualized value.
618
+ //
619
+
620
+ override get scrollLeft() {
621
+ return this.posInline;
622
+ }
623
+
624
+ override set scrollLeft(nextValue: number) {
625
+ this.posInline = nextValue;
626
+ this.maybeUpdateVis();
627
+ }
628
+
629
+ override get scrollTop() {
630
+ return this.posBlock;
631
+ }
632
+
633
+ override set scrollTop(nextValue: number) {
634
+ this.posBlock = nextValue;
635
+ this.maybeUpdateVis();
636
+ }
637
+
638
+ //
639
+ // Resize handlers
640
+ //
641
+ private handleAxisResizeInternal(event: DxAxisResizeInternal) {
642
+ event.stopPropagation();
643
+ const { axis, delta, size, index, type } = event;
644
+ if (axis === 'col') {
645
+ const nextSize = Math.max(sizeColMin, Math.min(sizeColMax, size + delta));
646
+ this.colSizes = { ...this.colSizes, [index]: nextSize };
647
+ this.updateVisInline();
648
+ } else {
649
+ const nextSize = Math.max(sizeRowMin, Math.min(sizeRowMax, size + delta));
650
+ this.rowSizes = { ...this.rowSizes, [index]: nextSize };
651
+ this.updateVisBlock();
652
+ }
653
+ if (type === 'dropped') {
654
+ this.dispatchEvent(
655
+ new DxAxisResize({
656
+ axis,
657
+ index,
658
+ size: this[axis === 'col' ? 'colSize' : 'rowSize'](index),
659
+ }),
660
+ );
661
+ }
662
+ }
663
+
560
664
  //
561
665
  // Render and other lifecycle methods
562
666
  //
@@ -564,18 +668,21 @@ export class DxGrid extends LitElement {
564
668
  override render() {
565
669
  const visibleCols = this.visColMax - this.visColMin;
566
670
  const visibleRows = this.visRowMax - this.visRowMin;
567
- const offsetInline = gap + this.binInlineMin - this.posInline - this.overscanInline;
568
- const offsetBlock = gap + this.binBlockMin - this.posBlock - this.overscanBlock;
671
+ const offsetInline = this.binInlineMin - this.posInline - this.overscanInline;
672
+ const offsetBlock = this.binBlockMin - this.posBlock - this.overscanBlock;
673
+
674
+ const selectColMin = Math.min(this.selectionStart.col, this.selectionEnd.col);
675
+ const selectColMax = Math.max(this.selectionStart.col, this.selectionEnd.col);
676
+ const selectRowMin = Math.min(this.selectionStart.row, this.selectionEnd.row);
677
+ const selectRowMax = Math.max(this.selectionStart.row, this.selectionEnd.row);
678
+ const selectVisible = selectColMin !== selectColMax || selectRowMin !== selectRowMax;
569
679
 
570
680
  return html`<div
571
681
  role="none"
572
682
  class="dx-grid"
573
- @pointerdown=${this.handlePointerDown}
574
- @pointerup=${this.handlePointerUp}
575
- @pointermove=${this.handlePointerMove}
576
- @focus=${this.handleFocus}
577
- @blur=${this.handleBlur}
578
- @keydown=${this.handleKeydown}
683
+ data-grid=${this.gridId}
684
+ data-grid-mode=${this.mode}
685
+ ?data-grid-select=${selectVisible}
579
686
  >
580
687
  <div role="none" class="dx-grid__corner"></div>
581
688
  <div role="none" class="dx-grid__columnheader">
@@ -593,9 +700,12 @@ export class DxGrid extends LitElement {
593
700
  >
594
701
  <span id=${localChId(c0)}>${colToA1Notation(c)}</span>
595
702
  ${(this.columns[c]?.resizeable ?? this.columnDefault.resizeable) &&
596
- html`<button class="dx-grid__resize-handle" data-dx-grid-action=${`resize-col,${c}`}>
597
- <span class="sr-only">Resize</span>
598
- </button>`}
703
+ html`<dx-grid-axis-resize-handle
704
+ axis="col"
705
+ index=${c}
706
+ size=${this.colSize(c)}
707
+ @dxaxisresizeinternal=${this.handleAxisResizeInternal}
708
+ ></dx-grid-axis-resize-handle>`}
599
709
  </div>`;
600
710
  })}
601
711
  </div>
@@ -612,9 +722,11 @@ export class DxGrid extends LitElement {
612
722
  return html`<div role="rowheader" ?inert=${r < 0} style="grid-row:${r0 + 1}/${r0 + 2}">
613
723
  <span id=${localRhId(r0)}>${rowToA1Notation(r)}</span>
614
724
  ${(this.rows[r]?.resizeable ?? this.rowDefault.resizeable) &&
615
- html`<button class="dx-grid__resize-handle" data-dx-grid-action=${`resize-row,${r}`}>
616
- <span class="sr-only">Resize</span>
617
- </button>`}
725
+ html`<dx-grid-axis-resize-handle
726
+ axis="row"
727
+ index=${r}
728
+ size=${this.rowSize(r)}
729
+ ></dx-grid-axis-resize-handle>`}
618
730
  </div>`;
619
731
  })}
620
732
  </div>
@@ -631,10 +743,16 @@ export class DxGrid extends LitElement {
631
743
  const c = c0 + this.visColMin;
632
744
  const r = r0 + this.visRowMin;
633
745
  const cell = this.cell(c, r);
746
+ const active = this.focusActive && this.focusedCell.col === c && this.focusedCell.row === r;
747
+ const selected = c >= selectColMin && c <= selectColMax && r >= selectRowMin && r <= selectRowMax;
634
748
  return html`<div
635
749
  role="gridcell"
636
750
  tabindex="0"
637
751
  ?inert=${c < 0 || r < 0}
752
+ ?aria-selected=${selected}
753
+ class=${cell || active
754
+ ? (cell?.className ? cell.className + ' ' : '') + (active ? 'dx-grid__cell--active' : '')
755
+ : nothing}
638
756
  aria-rowindex=${r}
639
757
  aria-colindex=${c}
640
758
  data-dx-grid-action="cell"
@@ -658,6 +776,12 @@ export class DxGrid extends LitElement {
658
776
  }
659
777
 
660
778
  override firstUpdated() {
779
+ if (this.getCells) {
780
+ this.cells = this.getCells({
781
+ start: { col: this.visColMin, row: this.visRowMin },
782
+ end: { col: this.visColMax, row: this.visRowMax },
783
+ });
784
+ }
661
785
  this.observer.observe(this.viewportRef.value!);
662
786
  this.colSizes = Object.entries(this.columns).reduce((acc: Record<string, number>, [colId, colMeta]) => {
663
787
  if (colMeta?.size) {
@@ -673,6 +797,22 @@ export class DxGrid extends LitElement {
673
797
  }, {});
674
798
  }
675
799
 
800
+ override willUpdate(changedProperties: Map<string, any>) {
801
+ if (
802
+ this.getCells &&
803
+ (changedProperties.has('initialCells') ||
804
+ changedProperties.has('visColMin') ||
805
+ changedProperties.has('visColMax') ||
806
+ changedProperties.has('visRowMin') ||
807
+ changedProperties.has('visRowMax'))
808
+ ) {
809
+ this.cells = this.getCells({
810
+ start: { col: this.visColMin, row: this.visRowMin },
811
+ end: { col: this.visColMax, row: this.visRowMax },
812
+ });
813
+ }
814
+ }
815
+
676
816
  override updated(changedProperties: Map<string, any>) {
677
817
  // Update the focused element if there is a change in bounds (otherwise Lit keeps focus on the relative element).
678
818
  if (