@dxos/lit-grid 0.6.11 → 0.6.12-main.15a606f

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,11 +2,24 @@
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
- import { DxAxisResize, type DxAxisResizeProps, type DxGridAxis } from './types';
9
+ import {
10
+ type AxisMeta,
11
+ type CellIndex,
12
+ type CellValue,
13
+ DxAxisResize,
14
+ DxEditRequest,
15
+ type DxGridAxis,
16
+ DxGridCellsSelect,
17
+ type DxGridMode,
18
+ type DxGridPointer,
19
+ type DxGridPosition,
20
+ type DxGridPositionNullable,
21
+ } from './types';
22
+ import { separator, toCellIndex } from './util';
10
23
 
11
24
  /**
12
25
  * The size in pixels of the gap between cells
@@ -33,11 +46,6 @@ const sizeColMax = 1024;
33
46
  const sizeRowMin = 16;
34
47
  const sizeRowMax = 1024;
35
48
 
36
- /**
37
- * Separator for serializing cell position vectors
38
- */
39
- const separator = ',';
40
-
41
49
  //
42
50
  // A1 notation is the fallback for numbering columns and rows.
43
51
  //
@@ -53,28 +61,30 @@ const rowToA1Notation = (row: number): string => {
53
61
  return `${row + 1}`;
54
62
  };
55
63
 
56
- export type CellValue = {
57
- /**
58
- * The content value
59
- */
60
- value: string;
61
- /**
62
- * If this is a merged cell, the bottomright-most of the range in numeric notation, otherwise undefined.
63
- */
64
- end?: string;
65
- /**
66
- * CSS inline styles to apply to the gridcell element
67
- */
68
- style?: string;
64
+ const closestAction = (target: EventTarget | null): { action: string | null; actionEl: HTMLElement | null } => {
65
+ const actionEl: HTMLElement | null = (target as HTMLElement | null)?.closest('[data-dx-grid-action]') ?? null;
66
+ return { actionEl, action: actionEl?.getAttribute('data-dx-grid-action') ?? null };
69
67
  };
70
68
 
71
- type AxisMeta = {
72
- size: number;
73
- description?: string;
74
- resizeable?: boolean;
69
+ const closestCell = (target: EventTarget | null, actionEl?: HTMLElement | null): DxGridPositionNullable => {
70
+ let cellElement = actionEl;
71
+ if (!cellElement) {
72
+ const { action, actionEl } = closestAction(target);
73
+ if (action === 'cell') {
74
+ cellElement = actionEl as HTMLElement;
75
+ }
76
+ }
77
+ if (cellElement) {
78
+ const col = parseInt(cellElement.getAttribute('aria-colindex') ?? 'never');
79
+ const row = parseInt(cellElement.getAttribute('aria-rowindex') ?? 'never');
80
+ return { col, row };
81
+ } else {
82
+ return null;
83
+ }
75
84
  };
76
85
 
77
- export type DxGridProps = Partial<Pick<DxGrid, 'cells' | 'rows' | 'columns' | 'rowDefault' | 'columnDefault'>>;
86
+ const isSameCell = (a: DxGridPositionNullable, b: DxGridPositionNullable) =>
87
+ a && b && Number.isFinite(a.col) && Number.isFinite(a.row) && a.col === b.col && a.row === b.row;
78
88
 
79
89
  const localChId = (c0: number) => `ch--${c0}`;
80
90
  const localRhId = (r0: number) => `rh--${r0}`;
@@ -83,6 +93,9 @@ const getPage = (axis: string, event: PointerEvent) => (axis === 'col' ? event.p
83
93
 
84
94
  @customElement('dx-grid')
85
95
  export class DxGrid extends LitElement {
96
+ @property({ type: String })
97
+ gridId: string = 'default-grid-id';
98
+
86
99
  @property({ type: Object })
87
100
  rowDefault: AxisMeta = { size: 32 };
88
101
 
@@ -96,53 +109,56 @@ export class DxGrid extends LitElement {
96
109
  columns: Record<string, AxisMeta> = {};
97
110
 
98
111
  @property({ type: Object })
99
- cells: Record<string, CellValue> = {};
112
+ cells: Record<CellIndex, CellValue> = {};
113
+
114
+ @property({ type: String })
115
+ mode: DxGridMode = 'browse';
100
116
 
101
117
  //
102
118
  // `pos`, short for ‘position’, is the position in pixels of the viewport from the origin.
103
119
  //
104
120
 
105
121
  @state()
106
- posInline = 0;
122
+ private posInline = 0;
107
123
 
108
124
  @state()
109
- posBlock = 0;
125
+ private posBlock = 0;
110
126
 
111
127
  //
112
128
  // `size` (when not suffixed with ‘row’ or ‘col’, see above) is the size in pixels of the viewport.
113
129
  //
114
130
 
115
131
  @state()
116
- sizeInline = 0;
132
+ private sizeInline = 0;
117
133
 
118
134
  @state()
119
- sizeBlock = 0;
135
+ private sizeBlock = 0;
120
136
 
121
137
  //
122
138
  // `overscan` is the amount in pixels to offset the grid content due to the number of overscanned columns or rows.
123
139
  //
124
140
 
125
141
  @state()
126
- overscanInline = 0;
142
+ private overscanInline = 0;
127
143
 
128
144
  @state()
129
- overscanBlock = 0;
145
+ private overscanBlock = 0;
130
146
 
131
147
  //
132
148
  // `bin`, not short for anything, is the range in pixels within which virtualization does not need to reassess.
133
149
  //
134
150
 
135
151
  @state()
136
- binInlineMin = 0;
152
+ private binInlineMin = 0;
137
153
 
138
154
  @state()
139
- binInlineMax = this.colSize(0);
155
+ private binInlineMax = this.colSize(0);
140
156
 
141
157
  @state()
142
- binBlockMin = 0;
158
+ private binBlockMin = 0;
143
159
 
144
160
  @state()
145
- binBlockMax = this.rowSize(0);
161
+ private binBlockMax = this.rowSize(0);
146
162
 
147
163
  //
148
164
  // `vis`, short for ‘visible’, is the range in numeric index of the columns or rows which should be rendered within
@@ -150,83 +166,185 @@ export class DxGrid extends LitElement {
150
166
  //
151
167
 
152
168
  @state()
153
- visColMin = 0;
169
+ private visColMin = 0;
154
170
 
155
171
  @state()
156
- visColMax = 1;
172
+ private visColMax = 1;
157
173
 
158
174
  @state()
159
- visRowMin = 0;
175
+ private visRowMin = 0;
160
176
 
161
177
  @state()
162
- visRowMax = 1;
178
+ private visRowMax = 1;
163
179
 
164
180
  //
165
181
  // `template` is the rendered value of `grid-{axis}-template`.
166
182
  //
167
183
  @state()
168
- templateColumns = `${this.colSize(0)}px`;
184
+ private templateColumns = `${this.colSize(0)}px`;
169
185
 
170
186
  @state()
171
- templateRows = `${this.rowSize(0)}px`;
187
+ private templateRows = `${this.rowSize(0)}px`;
172
188
 
173
189
  //
174
- // Resize state and handlers
190
+ // Focus, selection, and resize states
175
191
  //
176
192
 
177
193
  @state()
178
- colSizes: Record<string, number> = {};
194
+ private pointer: DxGridPointer = null;
179
195
 
180
196
  @state()
181
- rowSizes: Record<string, number> = {};
197
+ private colSizes: Record<string, number> = {};
182
198
 
183
199
  @state()
184
- resizing: null | (DxAxisResizeProps & { page: number }) = null;
185
-
186
- handlePointerDown = (event: PointerEvent) => {
187
- const actionEl = (event.target as HTMLElement)?.closest('[data-dx-grid-action]');
188
- const action = actionEl?.getAttribute('data-dx-grid-action');
189
- if (action) {
190
- if (action.startsWith('resize')) {
191
- const [resize, index] = action.split(',');
192
- const [_, axis] = resize.split('-');
193
- this.resizing = {
194
- axis: axis as DxGridAxis,
195
- size: axis === 'col' ? this.colSize(index) : this.rowSize(index),
196
- page: getPage(axis, event),
197
- index,
198
- };
200
+ private rowSizes: Record<string, number> = {};
201
+
202
+ @state()
203
+ private focusActive: boolean = false;
204
+
205
+ @state()
206
+ private focusedCell: DxGridPosition = { col: 0, row: 0 };
207
+
208
+ @state()
209
+ private selectionStart: DxGridPosition = { col: 0, row: 0 };
210
+
211
+ @state()
212
+ private selectionEnd: DxGridPosition = { col: 0, row: 0 };
213
+
214
+ //
215
+ // Primary pointer and keyboard handlers
216
+ //
217
+
218
+ private dispatchEditRequest(initialContent?: string) {
219
+ this.snapPosToFocusedCell();
220
+ // Without deferring, the event dispatches before `focusedCellBox` can get updated bounds of the cell, hence:
221
+ queueMicrotask(() =>
222
+ this.dispatchEvent(
223
+ new DxEditRequest({
224
+ cellIndex: toCellIndex(this.focusedCell),
225
+ cellBox: this.focusedCellBox(),
226
+ initialContent,
227
+ }),
228
+ ),
229
+ );
230
+ }
231
+
232
+ private handlePointerDown = (event: PointerEvent) => {
233
+ if (event.isPrimary) {
234
+ const { action, actionEl } = closestAction(event.target);
235
+ if (action) {
236
+ if (action.startsWith('resize') && this.mode === 'browse') {
237
+ const [resize, index] = action.split(',');
238
+ const [_, axis] = resize.split('-');
239
+ this.pointer = {
240
+ state: 'resizing',
241
+ axis: axis as DxGridAxis,
242
+ size: axis === 'col' ? this.colSize(index) : this.rowSize(index),
243
+ page: getPage(axis, event),
244
+ index,
245
+ };
246
+ } else if (action === 'cell') {
247
+ const cellCoords = closestCell(event.target, actionEl);
248
+ if (cellCoords) {
249
+ this.pointer = { state: 'selecting' };
250
+ this.selectionStart = cellCoords;
251
+ }
252
+ if (this.mode === 'edit') {
253
+ event.preventDefault();
254
+ } else {
255
+ if (this.focusActive && isSameCell(this.focusedCell, cellCoords)) {
256
+ this.dispatchEditRequest();
257
+ }
258
+ }
259
+ }
199
260
  }
200
261
  }
201
262
  };
202
263
 
203
- handlePointerUp = (_event: PointerEvent) => {
204
- if (this.resizing) {
264
+ private handlePointerUp = (event: PointerEvent) => {
265
+ if (this.pointer?.state === 'resizing') {
205
266
  const resizeEvent = new DxAxisResize({
206
- axis: this.resizing.axis,
207
- index: this.resizing.index,
208
- size: this[this.resizing.axis === 'col' ? 'colSize' : 'rowSize'](this.resizing.index),
267
+ axis: this.pointer.axis,
268
+ index: this.pointer.index,
269
+ size: this[this.pointer.axis === 'col' ? 'colSize' : 'rowSize'](this.pointer.index),
209
270
  });
210
271
  this.dispatchEvent(resizeEvent);
211
- this.resizing = null;
272
+ } else {
273
+ const cell = closestCell(event.target);
274
+ if (cell) {
275
+ this.selectionEnd = cell;
276
+ this.dispatchEvent(
277
+ new DxGridCellsSelect({
278
+ start: this.selectionStart,
279
+ end: this.selectionEnd,
280
+ }),
281
+ );
282
+ }
212
283
  }
284
+ this.pointer = null;
213
285
  };
214
286
 
215
- handlePointerMove = (event: PointerEvent) => {
216
- if (this.resizing) {
217
- const delta = getPage(this.resizing.axis, event) - this.resizing.page;
218
- if (this.resizing.axis === 'col') {
219
- const nextSize = Math.max(sizeColMin, Math.min(sizeColMax, this.resizing.size + delta));
220
- this.colSizes = { ...this.colSizes, [this.resizing.index]: nextSize };
287
+ private handlePointerMove = (event: PointerEvent) => {
288
+ if (this.pointer?.state === 'resizing') {
289
+ const delta = getPage(this.pointer.axis, event) - this.pointer.page;
290
+ if (this.pointer.axis === 'col') {
291
+ const nextSize = Math.max(sizeColMin, Math.min(sizeColMax, this.pointer.size + delta));
292
+ this.colSizes = { ...this.colSizes, [this.pointer.index]: nextSize };
221
293
  this.updateVisInline();
222
294
  } else {
223
- const nextSize = Math.max(sizeRowMin, Math.min(sizeRowMax, this.resizing.size + delta));
224
- this.rowSizes = { ...this.rowSizes, [this.resizing.index]: nextSize };
295
+ const nextSize = Math.max(sizeRowMin, Math.min(sizeRowMax, this.pointer.size + delta));
296
+ this.rowSizes = { ...this.rowSizes, [this.pointer.index]: nextSize };
225
297
  this.updateVisBlock();
226
298
  }
299
+ } else if (this.pointer?.state === 'selecting') {
300
+ const cell = closestCell(event.target);
301
+ if (cell && (cell.col !== this.selectionEnd.col || cell.row !== this.selectionEnd.row)) {
302
+ this.selectionEnd = cell;
303
+ }
227
304
  }
228
305
  };
229
306
 
307
+ private handleKeydown(event: KeyboardEvent) {
308
+ if (this.focusActive && this.mode === 'browse') {
309
+ // Adjust state
310
+ switch (event.key) {
311
+ case 'ArrowDown':
312
+ this.focusedCell = { ...this.focusedCell, row: this.focusedCell.row + 1 };
313
+ break;
314
+ case 'ArrowUp':
315
+ this.focusedCell = { ...this.focusedCell, row: Math.max(0, this.focusedCell.row - 1) };
316
+ break;
317
+ case 'ArrowRight':
318
+ this.focusedCell = { ...this.focusedCell, col: this.focusedCell.col + 1 };
319
+ break;
320
+ case 'ArrowLeft':
321
+ this.focusedCell = { ...this.focusedCell, col: Math.max(0, this.focusedCell.col - 1) };
322
+ break;
323
+ }
324
+ // Emit edit request if relevant
325
+ switch (event.key) {
326
+ case 'Enter':
327
+ this.dispatchEditRequest();
328
+ break;
329
+ default:
330
+ if (event.key.length === 1 && event.key.match(/\P{Cc}/u)) {
331
+ this.dispatchEditRequest(event.key);
332
+ }
333
+ break;
334
+ }
335
+ // Handle virtualization & focus consequences
336
+ switch (event.key) {
337
+ case 'ArrowDown':
338
+ case 'ArrowUp':
339
+ case 'ArrowRight':
340
+ case 'ArrowLeft':
341
+ event.preventDefault();
342
+ this.snapPosToFocusedCell();
343
+ break;
344
+ }
345
+ }
346
+ }
347
+
230
348
  //
231
349
  // Accessors
232
350
  //
@@ -239,16 +357,35 @@ export class DxGrid extends LitElement {
239
357
  return this.rowSizes?.[r] ?? this.rowDefault.size;
240
358
  }
241
359
 
242
- private getCell(c: number | string, r: number | string) {
360
+ private cell(c: number | string, r: number | string) {
243
361
  return this.cells[`${c}${separator}${r}`];
244
362
  }
245
363
 
364
+ private focusedCellBox(): DxEditRequest['cellBox'] {
365
+ const cellElement = this.focusedCellElement();
366
+ const cellSize = { inlineSize: this.colSize(this.focusedCell.col), blockSize: this.rowSize(this.focusedCell.row) };
367
+ if (!cellElement) {
368
+ return { insetInlineStart: NaN, insetBlockStart: NaN, ...cellSize };
369
+ }
370
+ const contentElement = cellElement.offsetParent as HTMLElement;
371
+ // Note that storing `offset` in state causes performance issues, so instead the transform is parsed here.
372
+ const [_translate3d, inlineStr, blockStr] = contentElement.style.transform.split(/[()]|px,?\s?/);
373
+ const contentOffsetInline = parseFloat(inlineStr);
374
+ const contentOffsetBlock = parseFloat(blockStr);
375
+ const offsetParent = contentElement.offsetParent as HTMLElement;
376
+ return {
377
+ insetInlineStart: cellElement.offsetLeft + contentOffsetInline + offsetParent.offsetLeft,
378
+ insetBlockStart: cellElement.offsetTop + contentOffsetBlock + offsetParent.offsetTop,
379
+ ...cellSize,
380
+ };
381
+ }
382
+
246
383
  //
247
384
  // Resize & reposition handlers, observer, ref
248
385
  //
249
386
 
250
387
  @state()
251
- observer = new ResizeObserver((entries) => {
388
+ private observer = new ResizeObserver((entries) => {
252
389
  const { inlineSize, blockSize } = entries?.[0]?.contentBoxSize?.[0] ?? {
253
390
  inlineSize: 0,
254
391
  blockSize: 0,
@@ -264,26 +401,28 @@ export class DxGrid extends LitElement {
264
401
  }
265
402
  });
266
403
 
267
- viewportRef: Ref<HTMLDivElement> = createRef();
268
-
269
- handleWheel = ({ deltaX, deltaY }: WheelEvent) => {
270
- this.posInline = Math.max(0, this.posInline + deltaX);
271
- this.posBlock = Math.max(0, this.posBlock + deltaY);
272
- if (
273
- this.posInline >= this.binInlineMin &&
274
- this.posInline < this.binInlineMax &&
275
- this.posBlock >= this.binBlockMin &&
276
- this.posBlock < this.binBlockMax
277
- ) {
278
- // do nothing
279
- } else {
280
- // console.info(
281
- // '[updating bounds]',
282
- // 'wheel',
283
- // [this.binInlineMin, this.posInline, this.binInlineMax],
284
- // [this.binBlockMin, this.posBlock, this.binBlockMax],
285
- // );
286
- this.updateVis();
404
+ private viewportRef: Ref<HTMLDivElement> = createRef();
405
+
406
+ private handleWheel = ({ deltaX, deltaY }: WheelEvent) => {
407
+ if (this.mode === 'browse') {
408
+ this.posInline = Math.max(0, this.posInline + deltaX);
409
+ this.posBlock = Math.max(0, this.posBlock + deltaY);
410
+ if (
411
+ this.posInline >= this.binInlineMin &&
412
+ this.posInline < this.binInlineMax &&
413
+ this.posBlock >= this.binBlockMin &&
414
+ this.posBlock < this.binBlockMax
415
+ ) {
416
+ // do nothing
417
+ } else {
418
+ // console.info(
419
+ // '[updating bounds]',
420
+ // 'wheel',
421
+ // [this.binInlineMin, this.posInline, this.binInlineMax],
422
+ // [this.binBlockMin, this.posBlock, this.binBlockMax],
423
+ // );
424
+ this.updateVis();
425
+ }
287
426
  }
288
427
  };
289
428
 
@@ -362,48 +501,50 @@ export class DxGrid extends LitElement {
362
501
 
363
502
  // Focus handlers
364
503
 
365
- @state()
366
- focusedCell: Record<DxGridAxis, number> = { col: 0, row: 0 };
367
-
368
- @state()
369
- focusActive: boolean = false;
370
-
371
504
  @eventOptions({ capture: true })
372
- handleFocus(event: FocusEvent) {
373
- const target = event.target as HTMLElement;
374
- const action = target.getAttribute('data-dx-grid-action');
375
- if (action === 'cell') {
376
- const c = parseInt(target.getAttribute('aria-colindex') ?? 'never');
377
- const r = parseInt(target.getAttribute('aria-rowindex') ?? 'never');
378
- this.focusedCell = { col: c, row: r };
505
+ private handleFocus(event: FocusEvent) {
506
+ const cellCoords = closestCell(event.target);
507
+ if (cellCoords) {
508
+ this.focusedCell = cellCoords;
379
509
  this.focusActive = true;
380
510
  }
381
511
  }
382
512
 
383
513
  @eventOptions({ capture: true })
384
- handleBlur(event: FocusEvent) {
514
+ private handleBlur(event: FocusEvent) {
385
515
  // Only unset `focusActive` if focus is not moving to an element within the grid.
386
- if (
387
- !event.relatedTarget ||
388
- (event.relatedTarget as HTMLElement).closest('.dx-grid__viewport') !== this.viewportRef.value
389
- ) {
516
+ if (!event.relatedTarget || !(event.relatedTarget as HTMLElement).closest(`[data-grid="${this.gridId}"]`)) {
390
517
  this.focusActive = false;
391
518
  }
392
519
  }
393
520
 
521
+ private focusedCellElement() {
522
+ return this.viewportRef.value?.querySelector(
523
+ `[aria-colindex="${this.focusedCell.col}"][aria-rowindex="${this.focusedCell.row}"]`,
524
+ ) as HTMLElement | null;
525
+ }
526
+
394
527
  /**
395
528
  * Moves focus to the cell with actual focus, otherwise moves focus to the viewport.
396
529
  */
397
- refocus() {
530
+ refocus(increment?: 'col' | 'row', delta: 1 | -1 = 1) {
531
+ switch (increment) {
532
+ case 'row':
533
+ this.focusedCell = { ...this.focusedCell, row: this.focusedCell.row + delta };
534
+ break;
535
+ case 'col':
536
+ this.focusedCell = { ...this.focusedCell, col: this.focusedCell.col + delta };
537
+ }
398
538
  (this.focusedCell.row < this.visRowMin ||
399
539
  this.focusedCell.row > this.visRowMax ||
400
540
  this.focusedCell.col < this.visColMin ||
401
541
  this.focusedCell.col > this.visColMax
402
542
  ? this.viewportRef.value
403
- : (this.viewportRef.value?.querySelector(
404
- `[aria-colindex="${this.focusedCell.col}"][aria-rowindex="${this.focusedCell.row}"]`,
405
- ) as HTMLElement | null)
543
+ : this.focusedCellElement()
406
544
  )?.focus({ preventScroll: true });
545
+ if (increment) {
546
+ this.snapPosToFocusedCell();
547
+ }
407
548
  }
408
549
 
409
550
  /**
@@ -438,7 +579,7 @@ export class DxGrid extends LitElement {
438
579
  acc += this.colSize(this.visColMin + overscanCol + c0) + gap;
439
580
  return acc;
440
581
  }, 0);
441
- this.posInline = this.binInlineMin + sizeSumCol + gap * 2 - this.sizeInline;
582
+ this.posInline = Math.max(0, this.binInlineMin + sizeSumCol + gap * 2 - this.sizeInline);
442
583
  this.updateVisInline();
443
584
  }
444
585
 
@@ -450,43 +591,12 @@ export class DxGrid extends LitElement {
450
591
  acc += this.rowSize(this.visRowMin + overscanRow + r0) + gap;
451
592
  return acc;
452
593
  }, 0);
453
- this.posBlock = this.binBlockMin + sizeSumRow + gap * 2 - this.sizeBlock;
594
+ this.posBlock = Math.max(0, this.binBlockMin + sizeSumRow + gap * 2 - this.sizeBlock);
454
595
  this.updateVisBlock();
455
596
  }
456
597
  }
457
598
  }
458
599
 
459
- // Keyboard interactions
460
- handleKeydown(event: KeyboardEvent) {
461
- if (this.focusActive) {
462
- // Adjust state
463
- switch (event.key) {
464
- case 'ArrowDown':
465
- this.focusedCell = { ...this.focusedCell, row: this.focusedCell.row + 1 };
466
- break;
467
- case 'ArrowUp':
468
- this.focusedCell = { ...this.focusedCell, row: Math.max(0, this.focusedCell.row - 1) };
469
- break;
470
- case 'ArrowRight':
471
- this.focusedCell = { ...this.focusedCell, col: this.focusedCell.col + 1 };
472
- break;
473
- case 'ArrowLeft':
474
- this.focusedCell = { ...this.focusedCell, col: Math.max(0, this.focusedCell.col - 1) };
475
- break;
476
- }
477
- // Handle virtualization & focus consequences
478
- switch (event.key) {
479
- case 'ArrowDown':
480
- case 'ArrowUp':
481
- case 'ArrowRight':
482
- case 'ArrowLeft':
483
- event.preventDefault();
484
- this.snapPosToFocusedCell();
485
- break;
486
- }
487
- }
488
- }
489
-
490
600
  //
491
601
  // Render and other lifecycle methods
492
602
  //
@@ -494,15 +604,25 @@ export class DxGrid extends LitElement {
494
604
  override render() {
495
605
  const visibleCols = this.visColMax - this.visColMin;
496
606
  const visibleRows = this.visRowMax - this.visRowMin;
497
- const offsetInline = gap + this.binInlineMin - this.posInline - this.overscanInline;
498
- const offsetBlock = gap + this.binBlockMin - this.posBlock - this.overscanBlock;
607
+ const offsetInline = this.binInlineMin - this.posInline - this.overscanInline;
608
+ const offsetBlock = this.binBlockMin - this.posBlock - this.overscanBlock;
609
+
610
+ const selectColMin = Math.min(this.selectionStart.col, this.selectionEnd.col);
611
+ const selectColMax = Math.max(this.selectionStart.col, this.selectionEnd.col);
612
+ const selectRowMin = Math.min(this.selectionStart.row, this.selectionEnd.row);
613
+ const selectRowMax = Math.max(this.selectionStart.row, this.selectionEnd.row);
614
+ const selectVisible = selectColMin !== selectColMax || selectRowMin !== selectRowMax;
499
615
 
500
616
  return html`<div
501
617
  role="none"
502
618
  class="dx-grid"
619
+ data-grid=${this.gridId}
620
+ data-grid-mode=${this.mode}
621
+ ?data-grid-select=${selectVisible}
503
622
  @pointerdown=${this.handlePointerDown}
504
623
  @pointerup=${this.handlePointerUp}
505
624
  @pointermove=${this.handlePointerMove}
625
+ @pointerleave=${this.handlePointerUp}
506
626
  @focus=${this.handleFocus}
507
627
  @blur=${this.handleBlur}
508
628
  @keydown=${this.handleKeydown}
@@ -560,11 +680,17 @@ export class DxGrid extends LitElement {
560
680
  return [...Array(visibleRows)].map((_, r0) => {
561
681
  const c = c0 + this.visColMin;
562
682
  const r = r0 + this.visRowMin;
563
- const cell = this.getCell(c, r);
683
+ const cell = this.cell(c, r);
684
+ const active = this.focusActive && this.focusedCell.col === c && this.focusedCell.row === r;
685
+ const selected = c >= selectColMin && c <= selectColMax && r >= selectRowMin && r <= selectRowMax;
564
686
  return html`<div
565
687
  role="gridcell"
566
688
  tabindex="0"
567
689
  ?inert=${c < 0 || r < 0}
690
+ ?aria-selected=${selected}
691
+ class=${cell || active
692
+ ? (cell?.className ? cell.className + ' ' : '') + (active ? 'dx-grid__cell--active' : '')
693
+ : nothing}
568
694
  aria-rowindex=${r}
569
695
  aria-colindex=${c}
570
696
  data-dx-grid-action="cell"