@dxos/lit-grid 0.8.4-main.84f28bd → 0.8.4-main.8baae0fced

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 (48) hide show
  1. package/LICENSE +102 -5
  2. package/dist/src/defs.d.ts +1 -0
  3. package/dist/src/defs.d.ts.map +1 -1
  4. package/dist/src/defs.js +1 -0
  5. package/dist/src/defs.js.map +1 -1
  6. package/dist/src/dx-grid-axis-resize-handle.d.ts.map +1 -1
  7. package/dist/src/dx-grid-axis-resize-handle.js +3 -5
  8. package/dist/src/dx-grid-axis-resize-handle.js.map +1 -1
  9. package/dist/src/dx-grid-multiselect-cell.d.ts.map +1 -1
  10. package/dist/src/dx-grid-multiselect-cell.js +2 -1
  11. package/dist/src/dx-grid-multiselect-cell.js.map +1 -1
  12. package/dist/src/dx-grid.d.ts +14 -9
  13. package/dist/src/dx-grid.d.ts.map +1 -1
  14. package/dist/src/dx-grid.js +207 -170
  15. package/dist/src/dx-grid.js.map +1 -1
  16. package/dist/src/dx-grid.lit-stories.d.ts +12 -15
  17. package/dist/src/dx-grid.lit-stories.d.ts.map +1 -1
  18. package/dist/src/dx-grid.lit-stories.js +20 -23
  19. package/dist/src/dx-grid.lit-stories.js.map +1 -1
  20. package/dist/src/playwright/dx-grid.spec.js +7 -6
  21. package/dist/src/playwright/dx-grid.spec.js.map +1 -1
  22. package/dist/src/playwright/playwright.config.d.ts.map +1 -1
  23. package/dist/src/playwright/playwright.config.js +1 -1
  24. package/dist/src/playwright/playwright.config.js.map +1 -1
  25. package/dist/src/testing/dx-grid-manager.d.ts.map +1 -1
  26. package/dist/src/testing/dx-grid-manager.js.map +1 -1
  27. package/dist/src/types.d.ts +10 -1
  28. package/dist/src/types.d.ts.map +1 -1
  29. package/dist/src/types.js.map +1 -1
  30. package/dist/src/util.d.ts +12 -7
  31. package/dist/src/util.d.ts.map +1 -1
  32. package/dist/src/util.js +15 -13
  33. package/dist/src/util.js.map +1 -1
  34. package/dist/tsconfig.tsbuildinfo +1 -1
  35. package/package.json +17 -14
  36. package/src/defs.ts +1 -0
  37. package/src/dx-grid-axis-resize-handle.pcss +9 -3
  38. package/src/dx-grid-axis-resize-handle.ts +1 -1
  39. package/src/dx-grid-multiselect-cell.pcss +7 -5
  40. package/src/dx-grid-multiselect-cell.ts +2 -1
  41. package/src/dx-grid.lit-stories.ts +3 -5
  42. package/src/dx-grid.pcss +86 -44
  43. package/src/dx-grid.ts +203 -118
  44. package/src/playwright/dx-grid.spec.ts +8 -7
  45. package/src/playwright/playwright.config.ts +1 -1
  46. package/src/testing/dx-grid-manager.ts +1 -1
  47. package/src/types.ts +11 -0
  48. package/src/util.ts +17 -11
package/src/dx-grid.ts CHANGED
@@ -2,75 +2,77 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
+ import './dx-grid-axis-resize-handle';
6
+
5
7
  import { LitElement, html, nothing } from 'lit';
6
- import { customElement, state, property } from 'lit/decorators.js';
7
- import { ref, createRef, type Ref } from 'lit/directives/ref.js';
8
+ import { customElement, property, state } from 'lit/decorators.js';
9
+ import { type Ref, createRef, ref } from 'lit/directives/ref.js';
8
10
  import { styleMap } from 'lit/directives/style-map.js';
9
- import { unsafeStatic, html as staticHtml } from 'lit/static-html.js';
11
+ import { html as staticHtml, unsafeStatic } from 'lit/static-html.js';
10
12
 
11
- import { defaultColSize, defaultRowSize } from './defs';
12
- // eslint-disable-next-line unused-imports/no-unused-imports
13
- import './dx-grid-axis-resize-handle';
13
+ import { defaultColSize, defaultRowSize, focusUnfurlDefault } from './defs';
14
14
  import {
15
- type DxGridAxisMetaProps,
16
- type DxGridAxisSizes,
17
- type DxGridPlaneCellIndex,
18
- type DxGridCellValue,
19
15
  DxAxisResize,
20
16
  type DxAxisResizeInternal,
21
17
  DxEditRequest,
18
+ type DxGridAnnotatedPanEvent,
19
+ type DxGridAxis,
22
20
  type DxGridAxisMeta,
21
+ type DxGridAxisMetaProps,
22
+ type DxGridAxisSizes,
23
+ type DxGridCellValue,
23
24
  type DxGridCells,
24
25
  DxGridCellsSelect,
25
26
  type DxGridFixedPlane,
27
+ type DxGridFocusIndicatorVariant,
26
28
  type DxGridFrozenAxes,
27
29
  type DxGridFrozenColsPlane,
28
30
  type DxGridFrozenPlane,
29
31
  type DxGridFrozenRowsPlane,
30
32
  type DxGridMode,
33
+ type DxGridOverscroll,
31
34
  type DxGridPlane,
35
+ type DxGridPlaneCellIndex,
32
36
  type DxGridPlaneCells,
33
37
  type DxGridPlaneRange,
34
38
  type DxGridPlaneRecord,
35
39
  type DxGridPointer,
36
40
  type DxGridPosition,
37
- type DxGridAxis,
38
- type DxGridSelectionProps,
39
- type DxGridAnnotatedPanEvent,
40
41
  type DxGridRange,
42
+ type DxGridSelectionProps,
41
43
  separator,
42
44
  } from './types';
43
45
  import {
44
- toCellIndex,
45
- gap,
46
- resizeTolerance,
47
- sizeColMin,
48
- sizeColMax,
49
- sizeRowMin,
50
- sizeRowMax,
51
- shouldSelect,
52
- selectionProps,
53
46
  cellSelected,
54
47
  closestAction,
55
48
  closestCell,
56
- targetIsPlane,
57
- resolveRowPlane,
49
+ gap,
50
+ isReadonly,
51
+ isSameCell,
52
+ resizeTolerance,
58
53
  resolveColPlane,
59
54
  resolveFrozenPlane,
60
- isSameCell,
61
- isReadonly,
55
+ resolveRowPlane,
56
+ selectionProps,
57
+ shouldSelect,
58
+ sizeColMax,
59
+ sizeColMin,
60
+ sizeRowMax,
61
+ sizeRowMin,
62
+ targetIsPlane,
63
+ toCellIndex,
62
64
  } from './util';
63
65
 
64
66
  @customElement('dx-grid')
65
67
  export class DxGrid extends LitElement {
66
68
  constructor() {
67
69
  super();
68
- // Wheel, top-level and element-level
70
+ // Wheel, top-level and element-level.
69
71
  document.defaultView?.addEventListener('wheel', this.handleTopLevelWheel, { passive: false });
70
72
  this.addEventListener('wheel', this.handleWheel);
71
- // Custom event(s)
73
+ // Custom event(s).
72
74
  this.addEventListener('dx-axis-resize-internal', this.handleAxisResizeInternal as EventListener);
73
- // Standard events
75
+ // Standard events.
74
76
  this.addEventListener('pointerdown', this.handlePointerDown);
75
77
  this.addEventListener('pointermove', this.handlePointerMove);
76
78
  this.addEventListener('pointerup', this.handlePointerUp);
@@ -84,12 +86,12 @@ export class DxGrid extends LitElement {
84
86
  gridId: string = 'default-grid-id';
85
87
 
86
88
  @property({ type: Object })
87
- rowDefault: DxGridPlaneRecord<DxGridFrozenRowsPlane, DxGridAxisMetaProps> = {
89
+ rowDefault: Partial<DxGridPlaneRecord<DxGridFrozenRowsPlane, Partial<DxGridAxisMetaProps>>> = {
88
90
  grid: { size: defaultRowSize },
89
91
  };
90
92
 
91
93
  @property({ type: Object })
92
- columnDefault: DxGridPlaneRecord<DxGridFrozenColsPlane, DxGridAxisMetaProps> = {
94
+ columnDefault: Partial<DxGridPlaneRecord<DxGridFrozenColsPlane, Partial<DxGridAxisMetaProps>>> = {
93
95
  grid: { size: defaultColSize },
94
96
  };
95
97
 
@@ -115,11 +117,14 @@ export class DxGrid extends LitElement {
115
117
  frozen: DxGridFrozenAxes = {};
116
118
 
117
119
  @property({ type: String })
118
- overscroll: 'inline' | 'block' | 'trap' | undefined = undefined;
120
+ overscroll: DxGridOverscroll = undefined;
119
121
 
120
122
  @property({ type: String })
121
123
  activeRefs = '';
122
124
 
125
+ @property({ type: String })
126
+ focusIndicatorVariant: DxGridFocusIndicatorVariant = 'sheet';
127
+
123
128
  /**
124
129
  * When this function is defined, it is used first to try to get a value for a cell,
125
130
  * and otherwise will fall back to `cells`.
@@ -195,6 +200,7 @@ export class DxGrid extends LitElement {
195
200
  //
196
201
  // `template` is the rendered value of `grid-{axis}-template`.
197
202
  //
203
+
198
204
  @state()
199
205
  private templateGridColumns = '0';
200
206
 
@@ -213,6 +219,16 @@ export class DxGrid extends LitElement {
213
219
  @state()
214
220
  private templatefrozenRowsEnd = '';
215
221
 
222
+ //
223
+ // `frozen…Size` is used to measure space available for the non-fixed planes
224
+ //
225
+
226
+ @state()
227
+ private frozenColsSize = 0;
228
+
229
+ @state()
230
+ private frozenRowsSize = 0;
231
+
216
232
  //
217
233
  // Focus, selection, and resize states
218
234
  //
@@ -308,7 +324,7 @@ export class DxGrid extends LitElement {
308
324
  this.dispatchSelectionChange();
309
325
  }
310
326
  if (this.mode === 'edit-select') {
311
- // Prevent focus moving when editing while selection is possible
327
+ // Prevent focus moving when editing while selection is possible.
312
328
  event.preventDefault();
313
329
  } else if (this.focusActive && isSameCell(this.focusedCell, cellCoords)) {
314
330
  this.dispatchEditRequest();
@@ -416,15 +432,13 @@ export class DxGrid extends LitElement {
416
432
  }
417
433
 
418
434
  private moveFocusIntoPlane(plane: DxGridPlane): void {
419
- if (this.focusedCell.plane !== plane) {
420
- const colPlane = resolveColPlane(plane);
421
- const rowPlane = resolveRowPlane(plane);
422
- this.focusedCell = {
423
- plane,
424
- col: colPlane === 'grid' ? this.visColMin : 0,
425
- row: rowPlane === 'grid' ? this.visRowMin : 0,
426
- };
427
- }
435
+ const colPlane = resolveColPlane(plane);
436
+ const rowPlane = resolveRowPlane(plane);
437
+ this.focusedCell = {
438
+ plane,
439
+ col: colPlane === 'grid' ? this.visColMin : 0,
440
+ row: rowPlane === 'grid' ? this.visRowMin : 0,
441
+ };
428
442
  this.focusedCellElement()?.focus({ preventScroll: true });
429
443
  }
430
444
 
@@ -481,6 +495,13 @@ export class DxGrid extends LitElement {
481
495
  event.preventDefault();
482
496
  this.dispatchEditRequest();
483
497
  break;
498
+ case 'Backspace':
499
+ case 'Delete':
500
+ if (!event.defaultPrevented) {
501
+ event.preventDefault();
502
+ this.dispatchEditRequest('');
503
+ }
504
+ break;
484
505
  default:
485
506
  if (event.key.length === 1 && event.key.match(/\P{Cc}/u) && !(event.metaKey || event.ctrlKey)) {
486
507
  this.dispatchEditRequest(event.key);
@@ -574,7 +595,7 @@ export class DxGrid extends LitElement {
574
595
  }
575
596
 
576
597
  //
577
- // Resize & reposition handlers, observer, ref
598
+ // Resize & reposition handlers, observer, ref.
578
599
  //
579
600
 
580
601
  @state()
@@ -584,19 +605,18 @@ export class DxGrid extends LitElement {
584
605
  blockSize: 0,
585
606
  };
586
607
  if (
587
- Math.abs(inlineSize - this.sizeInline) > resizeTolerance ||
588
- Math.abs(blockSize - this.sizeBlock) > resizeTolerance
608
+ Math.abs(inlineSize - this.frozenColsSize - this.sizeInline) > resizeTolerance ||
609
+ Math.abs(blockSize - this.frozenRowsSize - this.sizeBlock) > resizeTolerance
589
610
  ) {
590
611
  // console.info('[updating bounds]', 'resize', [inlineSize - this.sizeInline, blockSize - this.sizeBlock]);
591
- this.sizeInline = inlineSize;
592
- this.sizeBlock = blockSize;
612
+ this.sizeInline = inlineSize - this.frozenColsSize;
613
+ this.sizeBlock = blockSize - this.frozenRowsSize;
593
614
  this.updateVis();
594
615
  queueMicrotask(() => this.updatePos());
595
616
  }
596
617
  });
597
618
 
598
619
  private gridRef: Ref<HTMLDivElement> = createRef();
599
- private viewportRef: Ref<HTMLDivElement> = createRef();
600
620
 
601
621
  private maybeUpdateVisInline = () => {
602
622
  if (this.posInline < this.binInlineMin || this.posInline >= this.binInlineMax) {
@@ -675,7 +695,7 @@ export class DxGrid extends LitElement {
675
695
  };
676
696
 
677
697
  private updateVisInline(): void {
678
- // todo: avoid starting from zero
698
+ // todo: avoid starting from zero.
679
699
  let axisCursor = 0;
680
700
  let pxCursor = this.colSize(axisCursor, 'grid');
681
701
 
@@ -721,10 +741,19 @@ export class DxGrid extends LitElement {
721
741
  this.templatefrozenColsEnd = [...Array(this.frozen.frozenColsEnd ?? 0)]
722
742
  .map((_, c0) => `${this.colSize(c0, 'frozenColsEnd')}px`)
723
743
  .join(' ');
744
+
745
+ this.frozenColsSize =
746
+ [...Array(this.frozen.frozenColsStart ?? 0)].reduce(
747
+ (sum, _, c0) => sum + this.colSize(c0, 'frozenColsStart'),
748
+ 0,
749
+ ) +
750
+ gap * Math.max(0, this.frozen.frozenColsStart ?? 0 - 1) +
751
+ [...Array(this.frozen.frozenColsEnd ?? 0)].reduce((sum, _, c0) => sum + this.colSize(c0, 'frozenColsEnd'), 0) +
752
+ gap * Math.max(0, this.frozen.frozenColsEnd ?? 0 - 1);
724
753
  }
725
754
 
726
755
  private updateVisBlock(): void {
727
- // todo: avoid starting from zero
756
+ // todo: avoid starting from zero.
728
757
  let axisCursor = 0;
729
758
  let pxCursor = this.rowSize(axisCursor, 'grid');
730
759
 
@@ -770,6 +799,15 @@ export class DxGrid extends LitElement {
770
799
  this.templatefrozenRowsEnd = [...Array(this.frozen.frozenRowsEnd ?? 0)]
771
800
  .map((_, r0) => `${this.rowSize(r0, 'frozenRowsEnd')}px`)
772
801
  .join(' ');
802
+
803
+ this.frozenRowsSize =
804
+ [...Array(this.frozen.frozenRowsStart ?? 0)].reduce(
805
+ (sum, _, r0) => sum + this.rowSize(r0, 'frozenRowsStart'),
806
+ 0,
807
+ ) +
808
+ gap * Math.max(0, this.frozen.frozenRowsStart ?? 0 - 1) +
809
+ [...Array(this.frozen.frozenRowsEnd ?? 0)].reduce((sum, _, r0) => sum + this.rowSize(r0, 'frozenRowsEnd'), 0) +
810
+ gap * Math.max(0, this.frozen.frozenRowsEnd ?? 0 - 1);
773
811
  }
774
812
 
775
813
  private updateVis(): void {
@@ -991,7 +1029,7 @@ export class DxGrid extends LitElement {
991
1029
  }
992
1030
 
993
1031
  /**
994
- * Updates `pos` so that a cell in focus is fully within the viewport
1032
+ * Updates `pos` so that a cell in focus is fully within the viewport.
995
1033
  */
996
1034
  snapPosToFocusedCell(): void {
997
1035
  const outOfVis = this.focusedCellOutOfVis();
@@ -1068,16 +1106,40 @@ export class DxGrid extends LitElement {
1068
1106
  : !!(this.rows[plane]?.[index]?.resizeable ?? this.rowDefault[plane as DxGridFrozenRowsPlane]?.resizeable);
1069
1107
  }
1070
1108
 
1109
+ private clampAxisSize(
1110
+ plane: 'grid' | DxGridFrozenPlane,
1111
+ axis: DxGridAxis,
1112
+ index: number | string,
1113
+ requestedSize: number,
1114
+ ): number {
1115
+ const minSize =
1116
+ axis === 'col'
1117
+ ? (this.columns[plane]?.[index]?.minSize ??
1118
+ this.columnDefault[plane as DxGridFrozenColsPlane]?.minSize ??
1119
+ sizeColMin)
1120
+ : (this.rows[plane]?.[index]?.minSize ??
1121
+ this.rowDefault[plane as DxGridFrozenRowsPlane]?.minSize ??
1122
+ sizeRowMin);
1123
+ const maxSize =
1124
+ axis === 'col'
1125
+ ? (this.columns[plane]?.[index]?.maxSize ??
1126
+ this.columnDefault[plane as DxGridFrozenColsPlane]?.maxSize ??
1127
+ sizeColMax)
1128
+ : (this.rows[plane]?.[index]?.maxSize ??
1129
+ this.rowDefault[plane as DxGridFrozenRowsPlane]?.maxSize ??
1130
+ sizeRowMax);
1131
+ return Math.max(minSize, Math.min(maxSize, requestedSize));
1132
+ }
1133
+
1071
1134
  private handleAxisResizeInternal(event: DxAxisResizeInternal): void {
1072
1135
  event.stopPropagation();
1073
1136
  const { plane, axis, delta, size, index, state } = event;
1137
+ const nextSize = this.clampAxisSize(plane, axis, index, size + delta);
1074
1138
  if (axis === 'col') {
1075
- const nextSize = Math.max(sizeColMin, Math.min(sizeColMax, size + delta));
1076
1139
  this.colSizes = { ...this.colSizes, [plane]: { ...this.colSizes[plane], [index]: nextSize } };
1077
1140
  this.updateVisInline();
1078
1141
  this.updateIntrinsicInlineSize();
1079
1142
  } else {
1080
- const nextSize = Math.max(sizeRowMin, Math.min(sizeRowMax, size + delta));
1081
1143
  this.rowSizes = { ...this.colSizes, [plane]: { ...this.rowSizes[plane], [index]: nextSize } };
1082
1144
  this.updateVisBlock();
1083
1145
  this.updateIntrinsicBlockSize();
@@ -1095,7 +1157,7 @@ export class DxGrid extends LitElement {
1095
1157
  }
1096
1158
 
1097
1159
  //
1098
- // Render and other lifecycle methods
1160
+ // Render and other lifecycle methods.
1099
1161
  //
1100
1162
 
1101
1163
  // TODO(thure): This is for rendering presentational objects superimposed onto the canonical grid (e.g. DnD drop line for #8108).
@@ -1178,7 +1240,7 @@ export class DxGrid extends LitElement {
1178
1240
  ) {
1179
1241
  const rowPlane = resolveRowPlane(plane) as DxGridFrozenPlane;
1180
1242
  const rows = this.frozen[rowPlane];
1181
- return (rows ?? 0) > 0
1243
+ return (rows ?? 0) > 0 && this.limitColumns > 0
1182
1244
  ? html`<div
1183
1245
  role="none"
1184
1246
  class="dx-grid__plane--frozen-row"
@@ -1212,7 +1274,7 @@ export class DxGrid extends LitElement {
1212
1274
  ) {
1213
1275
  const colPlane = resolveColPlane(plane) as DxGridFrozenPlane;
1214
1276
  const cols = this.frozen[colPlane];
1215
- return (cols ?? 0) > 0
1277
+ return (cols ?? 0) > 0 && this.limitRows > 0
1216
1278
  ? html`<div
1217
1279
  role="none"
1218
1280
  class="dx-grid__plane--frozen-col"
@@ -1238,6 +1300,40 @@ export class DxGrid extends LitElement {
1238
1300
  : null;
1239
1301
  }
1240
1302
 
1303
+ private renderMainGrid(
1304
+ visibleCols: number,
1305
+ visibleRows: number,
1306
+ offsetInline: number,
1307
+ offsetBlock: number,
1308
+ selection: DxGridSelectionProps,
1309
+ ) {
1310
+ return this.limitRows > 0 && this.limitColumns > 0
1311
+ ? html`<div
1312
+ role="grid"
1313
+ class="dx-grid__plane--grid"
1314
+ tabindex="0"
1315
+ data-dx-grid-plane="grid"
1316
+ data-dx-grid-plane-row="1"
1317
+ data-dx-grid-plane-col="1"
1318
+ >
1319
+ <div
1320
+ role="none"
1321
+ class="dx-grid__plane--grid__content"
1322
+ style="transform:translate3d(${offsetInline}px,${offsetBlock}px,0);grid-template-columns:${this
1323
+ .templateGridColumns};grid-template-rows:${this.templateGridRows};"
1324
+ >
1325
+ ${[...Array(visibleRows)].map((_, r0) => {
1326
+ return [...Array(visibleCols)].map((_, c0) => {
1327
+ const c = c0 + this.visColMin;
1328
+ const r = r0 + this.visRowMin;
1329
+ return this.renderCell(c, r, 'grid', cellSelected(c, r, 'grid', selection), c0, r0);
1330
+ });
1331
+ })}
1332
+ </div>
1333
+ </div>`
1334
+ : null;
1335
+ }
1336
+
1241
1337
  private cellReadonly(col: number, row: number, plane: DxGridPlane): boolean {
1242
1338
  const colPlane = resolveColPlane(plane);
1243
1339
  const rowPlane = resolveRowPlane(plane);
@@ -1255,6 +1351,23 @@ export class DxGrid extends LitElement {
1255
1351
  return isReadonly(colReadOnly) || isReadonly(rowReadOnly);
1256
1352
  }
1257
1353
 
1354
+ private cellFocusUnfurl(col: number, row: number, plane: DxGridPlane): boolean {
1355
+ const colPlane = resolveColPlane(plane);
1356
+ const rowPlane = resolveRowPlane(plane);
1357
+
1358
+ // Check cell-specific setting first.
1359
+ const cellUnfurl = this.cell(col, row, plane)?.focusUnfurl;
1360
+ if (cellUnfurl !== undefined) {
1361
+ return cellUnfurl;
1362
+ }
1363
+
1364
+ // Check column/row defaults.
1365
+ const colUnfurl = this.columns?.[colPlane]?.[col]?.focusUnfurl ?? this.columnDefault?.[colPlane]?.focusUnfurl;
1366
+ const rowUnfurl = this.rows?.[rowPlane]?.[row]?.focusUnfurl ?? this.rowDefault?.[rowPlane]?.focusUnfurl;
1367
+
1368
+ return colUnfurl ?? rowUnfurl ?? focusUnfurlDefault;
1369
+ }
1370
+
1258
1371
  /**
1259
1372
  * Determines if the cell's text content should be selectable based on its readonly value.
1260
1373
  * @returns true if the cells text content is selectable, false otherwise.
@@ -1317,6 +1430,7 @@ export class DxGrid extends LitElement {
1317
1430
  const cell = this.cell(col, row, plane);
1318
1431
  const active = this.cellActive(col, row, plane);
1319
1432
  const readonly = this.cellReadonly(col, row, plane);
1433
+ const focusUnfurl = this.cellFocusUnfurl(col, row, plane);
1320
1434
  const textSelectable = this.cellTextSelectable(col, row, plane);
1321
1435
  const resizeIndex = cell?.resizeHandle ? (cell.resizeHandle === 'col' ? col : row) : undefined;
1322
1436
  const resizePlane = cell?.resizeHandle ? resolveFrozenPlane(cell.resizeHandle, plane) : undefined;
@@ -1328,16 +1442,16 @@ export class DxGrid extends LitElement {
1328
1442
  aria-readonly=${readonly ? 'true' : nothing}
1329
1443
  class=${cell?.className ?? nothing}
1330
1444
  data-refs=${cell?.dataRefs ?? nothing}
1445
+ data-focus-unfurl=${focusUnfurl ? nothing : 'false'}
1331
1446
  ?data-dx-active=${active}
1332
1447
  data-text-selectable=${textSelectable ? 'true' : 'false'}
1333
1448
  data-dx-grid-action="cell"
1334
1449
  aria-colindex=${col}
1335
1450
  aria-rowindex=${row}
1451
+ data-testid=${`${plane}.${col}.${row}`}
1336
1452
  style="grid-column:${visCol + 1};grid-row:${visRow + 1}"
1337
1453
  >
1338
- <div role="none" class="dx-grid__cell__content">
1339
- ${this.mode !== 'browse' && active ? null : cell?.value}${this.mode !== 'browse' && active ? null : accessory}
1340
- </div>
1454
+ <div role="none" class="dx-grid__cell__content">${cell?.value}${accessory}</div>
1341
1455
  ${cell?.resizeHandle &&
1342
1456
  this.mode === 'browse' &&
1343
1457
  this.axisResizeable(resizePlane!, cell.resizeHandle, resizeIndex!)
@@ -1363,22 +1477,31 @@ export class DxGrid extends LitElement {
1363
1477
  .split(' ')
1364
1478
  .filter((value) => value)
1365
1479
  .map(
1366
- // TODO(burdon): Consistent camelCase?
1367
- (activeRef) =>
1368
- `[data-refs~="${activeRef}"] { background: var(--dx-grid-commented-active, var(--dx-gridCommentedActive)) !important; }`,
1480
+ (activeRef) => `[data-refs~="${activeRef}"] { background: var(--color-grid-comment-active) !important; }`,
1369
1481
  )
1370
1482
  .join('\n')}
1371
1483
  </style>
1372
1484
  <div
1373
1485
  role="none"
1374
1486
  class="dx-grid"
1487
+ data-arrow-keys="all"
1375
1488
  style=${styleMap({
1376
- 'grid-template-columns': `${this.templatefrozenColsStart ? 'min-content ' : ''}minmax(0, ${
1377
- Number.isFinite(this.limitColumns) ? `${Math.max(0, this.intrinsicInlineSize)}px` : '1fr'
1378
- })${this.templatefrozenColsEnd ? ' min-content' : ''}`,
1379
- 'grid-template-rows': `${this.templatefrozenRowsStart ? 'min-content ' : ''}minmax(0, ${
1380
- Number.isFinite(this.limitRows) ? `${Math.max(0, this.intrinsicBlockSize)}px` : '1fr'
1381
- })${this.templatefrozenRowsEnd ? ' min-content' : ''}`,
1489
+ 'grid-template-columns': [
1490
+ this.templatefrozenColsStart ? 'min-content' : false,
1491
+ this.limitColumns > 0 &&
1492
+ `minmax(0, ${Number.isFinite(this.limitColumns) ? `${Math.max(0, this.intrinsicInlineSize)}px` : '1fr'})`,
1493
+ this.templatefrozenColsEnd ? 'min-content' : false,
1494
+ ]
1495
+ .filter(Boolean)
1496
+ .join(' '),
1497
+ 'grid-template-rows': [
1498
+ this.templatefrozenRowsStart ? 'min-content' : false,
1499
+ this.limitRows > 0 &&
1500
+ `minmax(0, ${Number.isFinite(this.limitRows) ? `${Math.max(0, this.intrinsicBlockSize)}px` : '1fr'})`,
1501
+ this.templatefrozenRowsEnd ? ' min-content' : false,
1502
+ ]
1503
+ .filter(Boolean)
1504
+ .join(' '),
1382
1505
  '--dx-grid-content-inline-size': Number.isFinite(this.limitColumns)
1383
1506
  ? `${Math.max(0, this.totalIntrinsicInlineSize)}px`
1384
1507
  : 'max-content',
@@ -1388,6 +1511,7 @@ export class DxGrid extends LitElement {
1388
1511
  })}
1389
1512
  data-grid=${this.gridId}
1390
1513
  data-grid-mode=${this.mode}
1514
+ data-grid-focus-indicator-variant=${this.focusIndicatorVariant}
1391
1515
  ?data-grid-select=${selection.visible}
1392
1516
  ${ref(this.gridRef)}
1393
1517
  >
@@ -1402,30 +1526,7 @@ export class DxGrid extends LitElement {
1402
1526
  offsetBlock,
1403
1527
  selection,
1404
1528
  )}
1405
- <div
1406
- role="grid"
1407
- class="dx-grid__plane--grid"
1408
- tabindex="0"
1409
- data-dx-grid-plane="grid"
1410
- data-dx-grid-plane-row="1"
1411
- data-dx-grid-plane-col="1"
1412
- ${ref(this.viewportRef)}
1413
- >
1414
- <div
1415
- role="none"
1416
- class="dx-grid__plane--grid__content"
1417
- style="transform:translate3d(${offsetInline}px,${offsetBlock}px,0);grid-template-columns:${this
1418
- .templateGridColumns};grid-template-rows:${this.templateGridRows};"
1419
- >
1420
- ${[...Array(visibleRows)].map((_, r0) => {
1421
- return [...Array(visibleCols)].map((_, c0) => {
1422
- const c = c0 + this.visColMin;
1423
- const r = r0 + this.visRowMin;
1424
- return this.renderCell(c, r, 'grid', cellSelected(c, r, 'grid', selection), c0, r0);
1425
- });
1426
- })}
1427
- </div>
1428
- </div>
1529
+ ${this.renderMainGrid(visibleCols, visibleRows, offsetInline, offsetBlock, selection)}
1429
1530
  ${this.renderFrozenColumns('frozenColsEnd', visibleRows, offsetBlock, selection)}${this.renderFixed(
1430
1531
  'fixedEndStart',
1431
1532
  selection,
@@ -1439,37 +1540,19 @@ export class DxGrid extends LitElement {
1439
1540
  private updateIntrinsicInlineSize(): void {
1440
1541
  this.intrinsicInlineSize = Number.isFinite(this.limitColumns)
1441
1542
  ? [...Array(this.limitColumns)].reduce((acc, _, c0) => acc + this.colSize(c0, 'grid'), 0) +
1442
- gap * (this.limitColumns - 1)
1543
+ gap * Math.max(0, this.limitColumns - 1)
1443
1544
  : Infinity;
1444
1545
  this.totalIntrinsicInlineSize =
1445
- this.intrinsicInlineSize +
1446
- (Number.isFinite(this.frozen.frozenColsStart)
1447
- ? [...Array(this.frozen.frozenColsStart)].reduce(
1448
- (acc, _, c0) => acc + gap + this.colSize(c0, 'frozenColsStart'),
1449
- 0,
1450
- )
1451
- : 0) +
1452
- (Number.isFinite(this.frozen.frozenColsEnd)
1453
- ? [...Array(this.frozen.frozenColsEnd)].reduce((acc, _, c0) => acc + gap + this.colSize(c0, 'frozenColsEnd'), 0)
1454
- : 0);
1546
+ this.limitColumns > 0 ? this.intrinsicInlineSize + this.frozenColsSize : this.frozenColsSize - gap;
1455
1547
  }
1456
1548
 
1457
1549
  private updateIntrinsicBlockSize(): void {
1458
1550
  this.intrinsicBlockSize = Number.isFinite(this.limitRows)
1459
1551
  ? [...Array(this.limitRows)].reduce((acc, _, r0) => acc + this.rowSize(r0, 'grid'), 0) +
1460
- gap * (this.limitRows - 1)
1552
+ gap * Math.max(0, this.limitRows - 1)
1461
1553
  : Infinity;
1462
1554
  this.totalIntrinsicBlockSize =
1463
- this.intrinsicBlockSize +
1464
- (Number.isFinite(this.frozen.frozenRowsStart)
1465
- ? [...Array(this.frozen.frozenRowsStart)].reduce(
1466
- (acc, _, r0) => acc + gap + this.rowSize(r0, 'frozenRowsStart'),
1467
- 0,
1468
- )
1469
- : 0) +
1470
- (Number.isFinite(this.frozen.frozenRowsEnd)
1471
- ? [...Array(this.frozen.frozenRowsEnd)].reduce((acc, _, r0) => acc + gap + this.rowSize(r0, 'frozenRowsEnd'), 0)
1472
- : 0);
1555
+ this.limitRows > 0 ? this.intrinsicBlockSize + this.frozenRowsSize : this.frozenRowsSize - gap;
1473
1556
  }
1474
1557
 
1475
1558
  private updateIntrinsicSizes(): void {
@@ -1517,7 +1600,7 @@ export class DxGrid extends LitElement {
1517
1600
  if (this.getCells) {
1518
1601
  this.updateCells(true);
1519
1602
  }
1520
- this.observer.observe(this.viewportRef.value!);
1603
+ this.observer.observe(this.gridRef.value!);
1521
1604
  this.computeColSizes();
1522
1605
  this.computeRowSizes();
1523
1606
  this.updateIntrinsicSizes();
@@ -1580,8 +1663,9 @@ export class DxGrid extends LitElement {
1580
1663
  }
1581
1664
  }
1582
1665
 
1583
- public updateIfWithinBounds({ col, row }: { col: number; row: number }): boolean {
1666
+ public updateIfWithinBounds({ col, row }: { col: number; row: number }, includeFixed?: boolean): boolean {
1584
1667
  if (col >= this.visColMin && col <= this.visColMax && row >= this.visRowMin && row <= this.visRowMax) {
1668
+ this.updateCells(includeFixed);
1585
1669
  this.requestUpdate();
1586
1670
  return true;
1587
1671
  }
@@ -1590,8 +1674,8 @@ export class DxGrid extends LitElement {
1590
1674
 
1591
1675
  override disconnectedCallback(): void {
1592
1676
  super.disconnectedCallback();
1593
- if (this.viewportRef.value) {
1594
- this.observer.unobserve(this.viewportRef.value);
1677
+ if (this.gridRef.value) {
1678
+ this.observer.unobserve(this.gridRef.value);
1595
1679
  }
1596
1680
  document.defaultView?.removeEventListener('wheel', this.handleTopLevelWheel);
1597
1681
  }
@@ -1609,6 +1693,7 @@ export {
1609
1693
  parseCellIndex,
1610
1694
  toPlaneCellIndex,
1611
1695
  cellQuery,
1696
+ accessoryHandlesPointerdownAttrs,
1612
1697
  } from './util';
1613
1698
 
1614
1699
  export const commentedClassName = 'dx-grid__cell--commented';
@@ -2,7 +2,7 @@
2
2
  // Copyright 2021 DXOS.org
3
3
  //
4
4
 
5
- import { expect, test, type Page } from '@playwright/test';
5
+ import { type Page, expect, test } from '@playwright/test';
6
6
 
7
7
  import { setupPage, storybookUrl } from '@dxos/test-utils/playwright';
8
8
 
@@ -12,8 +12,9 @@ import { toPlaneCellIndex } from '../util';
12
12
 
13
13
  const gridPlaneCellSize = 31;
14
14
  const gap = 1;
15
- const nCols = 9;
16
- const nRows = 7;
15
+ // The spec viewport fits one extra column/row beyond the nominal grid plane size.
16
+ const nCols = 10;
17
+ const nRows = 8;
17
18
 
18
19
  test.describe('dx-grid', () => {
19
20
  let page: Page;
@@ -23,8 +24,8 @@ test.describe('dx-grid', () => {
23
24
  const setup = await setupPage(browser, {
24
25
  url: storybookUrl('dx-grid--spec', 9002),
25
26
  viewportSize: {
26
- width: (gridPlaneCellSize + gap) * (nCols + 1.5),
27
- height: (gridPlaneCellSize + gap) * (nRows + 1.5),
27
+ width: (gridPlaneCellSize + gap) * (nCols + 0.5),
28
+ height: (gridPlaneCellSize + gap) * (nRows + 0.5),
28
29
  }, // 336 x 272
29
30
  });
30
31
  page = setup.page;
@@ -46,8 +47,8 @@ test.describe('dx-grid', () => {
46
47
  // Now pan by wheel to the center point of the cell one right and one down from the origin cell of the grid plane.
47
48
  await grid.panByWheel(gridPlaneCellSize * 1.5 + gap, gridPlaneCellSize * 1.5 + gap);
48
49
 
49
- // Confirm that the grid plane is showing only the cells that would be visible.
50
- await grid.expectVirtualizationResult(nCols + 1, nRows + 1, 1, 1);
50
+ // Pan shifts the viewport by one cell; visible cell counts stay the same.
51
+ await grid.expectVirtualizationResult(nCols, nRows, 1, 1);
51
52
  });
52
53
 
53
54
  test('mouse access', async () => {
@@ -10,7 +10,7 @@ export default defineConfig({
10
10
  ...e2ePreset(import.meta.dirname),
11
11
  // TODO(wittjosiah): Avoid hard-coding ports.
12
12
  webServer: {
13
- command: 'moon run lit-storybook:serve-e2e -- --port=9002',
13
+ command: 'pnpm storybook dev --ci --quiet --port=9002 --config-dir=.storybook',
14
14
  port: 9002,
15
15
  reuseExistingServer: false,
16
16
  },
@@ -2,7 +2,7 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { expect, type Locator, type Page } from '@playwright/test';
5
+ import { type Locator, type Page, expect } from '@playwright/test';
6
6
 
7
7
  import type { DxGridPlanePosition } from '../types';
8
8