@dxos/lit-grid 0.14.0 → 0.15.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/lit-grid",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "description": "A grid Web Component using Lit",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
@@ -39,8 +39,8 @@
39
39
  "lit": "^3.3.3"
40
40
  },
41
41
  "devDependencies": {
42
- "@dxos/random": "0.14.0",
43
- "@dxos/test-utils": "0.14.0"
42
+ "@dxos/random": "0.15.0",
43
+ "@dxos/test-utils": "0.15.0"
44
44
  },
45
45
  "publishConfig": {
46
46
  "access": "public"
package/src/dx-grid.pcss CHANGED
@@ -61,7 +61,7 @@
61
61
  position: absolute;
62
62
  inset: 0;
63
63
  pointer-events: none;
64
- border: var(--dx-grid-focus-indicator-width) solid var(--color-focus-ring);
64
+ border: var(--dx-grid-focus-indicator-width) solid var(--color-grid-focus-ring);
65
65
  }
66
66
  }
67
67
 
@@ -158,9 +158,47 @@
158
158
  [role='columnheader'],
159
159
  [role='rowheader'] {
160
160
  &[aria-selected] {
161
- --grid-cell-surface: var(--color-grid-selection-surface);
161
+ /* The selection tint is translucent, so it is layered over the grid surface rather than the host behind it. */
162
+ --grid-cell-surface:
163
+ linear-gradient(var(--color-grid-selection-surface), var(--color-grid-selection-surface)),
164
+ var(--color-grid-surface);
162
165
  --grid-cell-fg: var(--color-grid-selection-fg);
163
166
 
167
+ /*
168
+ * Outline the selected range: each edge cell draws its outer sides. Like the focus ring it reaches
169
+ * back over the neighbours' right/bottom lines, so the outline sits on grid lines on all four sides.
170
+ */
171
+ &[data-selection-edge]::before {
172
+ content: '';
173
+ position: absolute;
174
+ inset-inline-start: calc(-1 * var(--dx-grid-focus-indicator-width));
175
+ inset-block-start: calc(-1 * var(--dx-grid-focus-indicator-width));
176
+ inset-inline-end: 0;
177
+ inset-block-end: 0;
178
+ z-index: 1;
179
+ pointer-events: none;
180
+ border: 0 solid var(--color-grid-focus-ring);
181
+ }
182
+ &[data-selection-edge~='top']::before {
183
+ border-block-start-width: var(--dx-grid-focus-indicator-width);
184
+ }
185
+ &[data-selection-edge~='bottom']::before {
186
+ border-block-end-width: var(--dx-grid-focus-indicator-width);
187
+ }
188
+ &[data-selection-edge~='start']::before {
189
+ border-inline-start-width: var(--dx-grid-focus-indicator-width);
190
+ }
191
+ &[data-selection-edge~='end']::before {
192
+ border-inline-end-width: var(--dx-grid-focus-indicator-width);
193
+ }
194
+ /* The plane clips at its first column and row, where there is no neighbour line to reach over. */
195
+ &[aria-colindex='0']::before {
196
+ inset-inline-start: 0;
197
+ }
198
+ &[aria-rowindex='0']::before {
199
+ inset-block-start: 0;
200
+ }
201
+
164
202
  &:focus:not([aria-readonly='true']),
165
203
  &:focus-visible,
166
204
  &:focus-within {
@@ -203,10 +241,23 @@
203
241
  * border) exceeds the cell track size by even a fraction of a
204
242
  * pixel, pushing the focus ring's bottom edge below the grid line.
205
243
  */
206
- min-inline-size: 100%;
244
+ min-inline-size: calc(100% + var(--dx-grid-focus-indicator-width));
207
245
  inline-size: min-content;
208
246
  max-inline-size: min(60ch, 90vw);
209
247
 
248
+ /*
249
+ * A cell draws only its right/bottom lines; its left/top lines belong to its neighbours.
250
+ * Reaching back over them puts all four ring edges on grid lines; the extra start padding
251
+ * keeps the text where it sits unfocused.
252
+ */
253
+ inset-inline-start: calc(-1 * var(--dx-grid-focus-indicator-width));
254
+ inset-block-start: calc(-1 * var(--dx-grid-focus-indicator-width));
255
+ block-size: calc(100% + var(--dx-grid-focus-indicator-width));
256
+ padding-inline-start: calc(
257
+ var(--dx-grid-cell-content-padding-inline) + var(--dx-grid-focus-indicator-width)
258
+ );
259
+ padding-block-start: calc(var(--dx-grid-cell-content-padding-block) + var(--dx-grid-focus-indicator-width));
260
+
210
261
  overflow: auto;
211
262
  scrollbar-width: thin;
212
263
  overscroll-behavior: contain;
@@ -217,14 +268,27 @@
217
268
  color: var(--grid-cell-fg);
218
269
  }
219
270
 
271
+ /* The plane clips at its first column and row, where there is no neighbour line to reach over. */
272
+ &[aria-colindex='0'] > .dx-grid__cell__content {
273
+ inset-inline-start: 0;
274
+ min-inline-size: 100%;
275
+ padding-inline-start: var(--dx-grid-cell-content-padding-inline);
276
+ }
277
+
278
+ &[aria-rowindex='0'] > .dx-grid__cell__content {
279
+ inset-block-start: 0;
280
+ block-size: 100%;
281
+ padding-block-start: var(--dx-grid-cell-content-padding-block);
282
+ }
283
+
220
284
  &:not([aria-readonly='true']) > .dx-grid__cell__content {
221
- border-color: var(--color-focus-ring);
285
+ border-color: var(--color-grid-focus-ring);
222
286
  }
223
287
  }
224
288
  }
225
289
 
226
290
  &:focus-visible > .dx-grid__cell__content {
227
- border-color: var(--color-focus-ring);
291
+ border-color: var(--color-grid-focus-ring);
228
292
  }
229
293
  }
230
294
  }
@@ -236,8 +300,11 @@
236
300
  overflow-wrap: break-word;
237
301
 
238
302
  background: var(--dx-grid-base, var(--color-base-surface));
239
- border: var(--dx-grid-focus-indicator-width) solid var(--color-focus-ring);
303
+ border: var(--dx-grid-focus-indicator-width) solid var(--color-grid-focus-ring);
240
304
  border-radius: 2px;
305
+ /* Matches the focused cell: the box reaches over the neighbours' lines, the text stays put. */
306
+ padding-inline-start: var(--dx-grid-focus-indicator-width);
307
+ padding-block-start: var(--dx-grid-focus-indicator-width);
241
308
 
242
309
  --dx-grid-cell-editor-max-block-size: min(12em, 50vh);
243
310
  --dx-grid-cell-editor-max-inline-size: min(60ch, 90vw);
@@ -262,7 +329,7 @@
262
329
  [role='columnheader'],
263
330
  [role='rowheader'] {
264
331
  &[data-dx-active]:not([aria-readonly='true']) > .dx-grid__cell__content {
265
- border-color: var(--color-focus-ring);
332
+ border-color: var(--color-grid-focus-ring);
266
333
  }
267
334
  }
268
335
  }
package/src/dx-grid.ts CHANGED
@@ -44,6 +44,7 @@ import {
44
44
  } from './types.ts';
45
45
  import {
46
46
  cellSelected,
47
+ cellSelectionEdges,
47
48
  closestAction,
48
49
  closestCell,
49
50
  gap,
@@ -1225,7 +1226,7 @@ export class DxGrid extends LitElement {
1225
1226
  >
1226
1227
  ${[...Array(rows)].map((_, r) => {
1227
1228
  return [...Array(cols)].map((_, c) => {
1228
- return this.renderCell(c, r, plane, cellSelected(c, r, plane, selection));
1229
+ return this.renderCell(c, r, plane, selection);
1229
1230
  });
1230
1231
  })}
1231
1232
  </div>`
@@ -1259,7 +1260,7 @@ export class DxGrid extends LitElement {
1259
1260
  ${[...Array(rows)].map((_, r) => {
1260
1261
  return [...Array(visibleCols)].map((_, c0) => {
1261
1262
  const c = this.visColMin + c0;
1262
- return this.renderCell(c, r, plane, cellSelected(c, r, plane, selection), c0, r);
1263
+ return this.renderCell(c, r, plane, selection, c0, r);
1263
1264
  });
1264
1265
  })}
1265
1266
  </div>
@@ -1294,7 +1295,7 @@ export class DxGrid extends LitElement {
1294
1295
  ${[...Array(visibleRows)].map((_, r0) => {
1295
1296
  return [...Array(cols)].map((_, c) => {
1296
1297
  const r = this.visRowMin + r0;
1297
- return this.renderCell(c, r, plane, cellSelected(c, r, plane, selection), c, r0);
1298
+ return this.renderCell(c, r, plane, selection, c, r0);
1298
1299
  });
1299
1300
  })}
1300
1301
  </div>
@@ -1329,7 +1330,7 @@ export class DxGrid extends LitElement {
1329
1330
  return [...Array(visibleCols)].map((_, c0) => {
1330
1331
  const c = c0 + this.visColMin;
1331
1332
  const r = r0 + this.visRowMin;
1332
- return this.renderCell(c, r, 'grid', cellSelected(c, r, 'grid', selection), c0, r0);
1333
+ return this.renderCell(c, r, 'grid', selection, c0, r0);
1333
1334
  });
1334
1335
  })}
1335
1336
  </div>
@@ -1429,7 +1430,16 @@ export class DxGrid extends LitElement {
1429
1430
  return { deltaX, deltaY };
1430
1431
  }
1431
1432
 
1432
- private renderCell(col: number, row: number, plane: DxGridPlane, selected?: boolean, visCol = col, visRow = row) {
1433
+ private renderCell(
1434
+ col: number,
1435
+ row: number,
1436
+ plane: DxGridPlane,
1437
+ selection?: DxGridSelectionProps,
1438
+ visCol = col,
1439
+ visRow = row,
1440
+ ) {
1441
+ const selected = selection ? cellSelected(col, row, plane, selection) : false;
1442
+ const selectionEdge = selection ? cellSelectionEdges(col, row, plane, selection) : undefined;
1433
1443
  const cell = this.cell(col, row, plane);
1434
1444
  const active = this.cellActive(col, row, plane);
1435
1445
  const readonly = this.cellReadonly(col, row, plane);
@@ -1442,6 +1452,7 @@ export class DxGrid extends LitElement {
1442
1452
  role="gridcell"
1443
1453
  tabindex="0"
1444
1454
  aria-selected=${selected ? 'true' : nothing}
1455
+ data-selection-edge=${selectionEdge ?? nothing}
1445
1456
  aria-readonly=${readonly ? 'true' : nothing}
1446
1457
  class=${cell?.className ?? nothing}
1447
1458
  data-refs=${cell?.dataRefs ?? nothing}
package/src/util.ts CHANGED
@@ -120,6 +120,29 @@ export const cellSelected = (
120
120
  );
121
121
  };
122
122
 
123
+ /**
124
+ * The sides of a multi-cell selection that `col`/`row` sits on (`top`, `bottom`, `start`, `end`), space-separated
125
+ * for an attribute's `~=` selector; undefined for cells inside it, outside it, or a single-cell selection.
126
+ */
127
+ export const cellSelectionEdges = (
128
+ col: number,
129
+ row: number,
130
+ plane: DxGridPlane,
131
+ selection: DxGridSelectionProps,
132
+ ): string | undefined => {
133
+ if (!selection.visible || !cellSelected(col, row, plane, selection)) {
134
+ return undefined;
135
+ }
136
+
137
+ const edges = [
138
+ row === selection.rowMin && 'top',
139
+ row === selection.rowMax && 'bottom',
140
+ col === selection.colMin && 'start',
141
+ col === selection.colMax && 'end',
142
+ ].filter((edge) => edge !== false);
143
+ return edges.length > 0 ? edges.join(' ') : undefined;
144
+ };
145
+
123
146
  export const closestAction = (target: EventTarget | null): { action: string | null; actionEl: HTMLElement | null } => {
124
147
  const actionEl: HTMLElement | null = (target as HTMLElement | null)?.closest('[data-dx-grid-action]') ?? null;
125
148
  return { actionEl, action: actionEl?.getAttribute('data-dx-grid-action') ?? null };