tosijs-ui 1.5.13 → 1.5.16

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.
@@ -35,6 +35,7 @@ export declare class TosiTable extends WebComponent {
35
35
  overflow: string;
36
36
  height: string;
37
37
  overscrollBehavior: string;
38
+ alignContent: string;
38
39
  };
39
40
  ':host .th, :host .td': {
40
41
  overflow: string;
@@ -129,6 +130,9 @@ export declare class TosiTable extends WebComponent {
129
130
  private selectBinding;
130
131
  maxVisibleRows: number;
131
132
  private _grid;
133
+ private pinnedItemToCells;
134
+ private pinnedCellToItem;
135
+ private resolvePinnedItem;
132
136
  get value(): TableData;
133
137
  set value(data: TableData);
134
138
  private rowData;
@@ -96,8 +96,17 @@ test('table renders with data', () => {
96
96
  expect(table.array.length).toBeGreaterThan(0)
97
97
  })
98
98
 
99
- test('row selection via data model', () => {
100
- const items = table.array
99
+ test('row selection: data model + aria-selected on every cell (incl. custom dataCell)', async () => {
100
+ // Wait for listBinding to stamp DOM cells for the visible window
101
+ const items = table.visibleRows
102
+ await new Promise(resolve => {
103
+ const check = () => {
104
+ if (table.getCells(items[0]) && table.getCells(items[1])) return resolve()
105
+ setTimeout(check, 100)
106
+ }
107
+ check()
108
+ })
109
+
101
110
  table.deSelect()
102
111
  table.selectRow(items[0])
103
112
  table.selectRow(items[1])
@@ -107,11 +116,27 @@ test('row selection via data model', () => {
107
116
  expect(items[1][table.selectedKey]).toBe(true)
108
117
  expect(table.selectedRows.length).toBe(2)
109
118
 
110
- // Deselect and verify data model
119
+ // DOM: every cell of a selected row has aria-selected, including the
120
+ // custom-rendered `name` column (regression test for custom cells being
121
+ // skipped by selectBinding).
122
+ const cells0 = table.getCells(items[0])
123
+ const cells1 = table.getCells(items[1])
124
+ expect(cells0.length).toBe(table.visibleColumns.length)
125
+ expect(cells1.length).toBe(table.visibleColumns.length)
126
+ for (const c of cells0) expect(c.hasAttribute('aria-selected')).toBe(true)
127
+ for (const c of cells1) expect(c.hasAttribute('aria-selected')).toBe(true)
128
+ // The `name` column (index 1) uses a dataCell input — confirm the custom
129
+ // element is the actual cell and that it carries aria-selected.
130
+ expect(cells0[1].tagName).toBe('INPUT')
131
+ expect(cells0[1].hasAttribute('aria-selected')).toBe(true)
132
+
133
+ // Deselect and verify both data model and DOM clear
111
134
  table.deSelect()
112
135
  expect(table.selectedRows.length).toBe(0)
113
136
  expect(items[0][table.selectedKey]).not.toBe(true)
114
137
  expect(items[1][table.selectedKey]).not.toBe(true)
138
+ for (const c of cells0) expect(c.hasAttribute('aria-selected')).toBe(false)
139
+ for (const c of cells1) expect(c.hasAttribute('aria-selected')).toBe(false)
115
140
  })
116
141
 
117
142
  test('getCells and getItem', async () => {
@@ -291,6 +316,54 @@ preview.append(table)
291
316
  font-variant-numeric: tabular-nums;
292
317
  }
293
318
  ```
319
+ ```test
320
+ const tables = document.querySelectorAll('tosi-table')
321
+ const table = tables[tables.length - 1]
322
+ await new Promise(resolve => {
323
+ const check = () => {
324
+ if (table.querySelector('.pinned-bottom')) return resolve()
325
+ setTimeout(check, 100)
326
+ }
327
+ check()
328
+ })
329
+
330
+ test('pinned row honours dataCell, rowRendered, getCells, getItem, selection', () => {
331
+ const totals = table.array[table.array.length - 1]
332
+ const pinnedCells = Array.from(table.querySelectorAll('.pinned-bottom'))
333
+
334
+ // Sanity: number of pinned cells equals number of visible columns
335
+ expect(pinnedCells.length).toBe(table.visibleColumns.length)
336
+
337
+ // dataCell honoured: numeric columns kept their `num-cell` class, and the
338
+ // _actions column rendered its <button>
339
+ const numCells = pinnedCells.filter(c => c.classList.contains('num-cell'))
340
+ expect(numCells.length).toBeGreaterThan(0)
341
+ expect(pinnedCells.some(c => c.tagName === 'BUTTON')).toBe(true)
342
+
343
+ // rowRendered fired: totals row is negative on average, so all cells of
344
+ // this row should carry `row-negative`
345
+ const total = Object.keys(totals)
346
+ .filter(k => typeof totals[k] === 'number')
347
+ .reduce((s, k) => s + totals[k], 0)
348
+ if (total < 0) {
349
+ expect(pinnedCells.every(c => c.classList.contains('row-negative'))).toBe(true)
350
+ }
351
+
352
+ // getCells / getItem round-trip works for pinned items
353
+ const cellsForTotals = table.getCells(totals)
354
+ expect(cellsForTotals?.length).toBe(table.visibleColumns.length)
355
+ expect(table.getItem(cellsForTotals[0])).toBe(totals)
356
+
357
+ // Selection on a pinned row applies aria-selected to every cell
358
+ table.multiple = true
359
+ table.deSelect()
360
+ table.selectRow(totals)
361
+ expect(cellsForTotals.every(c => c.hasAttribute('aria-selected'))).toBe(true)
362
+
363
+ table.deSelect()
364
+ expect(cellsForTotals.some(c => c.hasAttribute('aria-selected'))).toBe(false)
365
+ })
366
+ ```
294
367
 
295
368
  ## Selection
296
369
 
@@ -466,6 +539,7 @@ export class TosiTable extends WebComponent {
466
539
  overflow: 'auto',
467
540
  height: '100%',
468
541
  overscrollBehavior: 'none',
542
+ alignContent: 'start',
469
543
  },
470
544
  ':host .th, :host .td': {
471
545
  overflow: 'hidden',
@@ -566,6 +640,14 @@ export class TosiTable extends WebComponent {
566
640
  };
567
641
  maxVisibleRows = 10000;
568
642
  _grid = null;
643
+ // Pinned rows live outside the listBinding's virtual window, so we keep a
644
+ // side-table for getCells, getItem, and click-to-select to traverse.
645
+ pinnedItemToCells = new Map();
646
+ pinnedCellToItem = new WeakMap();
647
+ resolvePinnedItem(target) {
648
+ const cell = target.closest('.pinned-top, .pinned-bottom');
649
+ return cell ? this.pinnedCellToItem.get(cell) : undefined;
650
+ }
569
651
  get value() {
570
652
  return {
571
653
  array: this.array,
@@ -756,29 +838,54 @@ export class TosiTable extends WebComponent {
756
838
  Object.assign(cell.style, style);
757
839
  }
758
840
  buildPinnedCells(rows, cols, stickyInfo, pin, rowHeight, startRowIndex) {
759
- const cells = [];
841
+ const allCells = [];
842
+ const selectBindingFn = this.selectBinding;
843
+ const { rowRendered } = this;
760
844
  for (let r = 0; r < rows.length; r++) {
761
845
  const rowItem = rows[r];
762
846
  const offset = pin === 'top'
763
847
  ? (r + 1) * rowHeight + 'px'
764
848
  : (rows.length - 1 - r) * rowHeight + 'px';
849
+ const rowCells = [];
765
850
  for (let c = 0; c < cols.length; c++) {
766
851
  const col = cols[c];
767
852
  const si = stickyInfo[c];
768
- cells.push(span({
769
- class: this.cellClasses(`td pinned-${pin}`, si),
770
- role: 'gridcell',
771
- tabindex: -1,
772
- ariaRowindex: String(startRowIndex + r + 1),
773
- ariaColindex: String(c + 1),
774
- style: this.cellStyle(col, si, {
775
- position: 'sticky',
776
- [pin]: offset,
777
- }),
778
- }, String(rowItem[col.prop] ?? '')));
853
+ const style = this.cellStyle(col, si, {
854
+ position: 'sticky',
855
+ [pin]: offset,
856
+ });
857
+ let cell;
858
+ if (col.dataCell !== undefined) {
859
+ cell = col.dataCell(col);
860
+ this.applyPinnedToCustomCell(cell, c, si, style);
861
+ cell.classList.add(`pinned-${pin}`);
862
+ cell.setAttribute('aria-rowindex', String(startRowIndex + r + 1));
863
+ }
864
+ else {
865
+ cell = span({
866
+ class: this.cellClasses(`td pinned-${pin}`, si),
867
+ role: 'gridcell',
868
+ tabindex: -1,
869
+ ariaRowindex: String(startRowIndex + r + 1),
870
+ ariaColindex: String(c + 1),
871
+ style,
872
+ }, String(rowItem[col.prop] ?? ''));
873
+ }
874
+ // Track cell → item so click-to-select and updateSelectionVisuals
875
+ // can resolve pinned cells (which aren't in the listBinding).
876
+ this.pinnedCellToItem.set(cell, rowItem);
877
+ // Apply any pre-existing selection state.
878
+ selectBindingFn(cell, rowItem);
879
+ rowCells.push(cell);
880
+ allCells.push(cell);
881
+ }
882
+ // Track cells per item so getCells works for pinned rows
883
+ this.pinnedItemToCells.set(tosiValue(rowItem), rowCells);
884
+ if (rowRendered) {
885
+ rowRendered(rowItem, rowCells);
779
886
  }
780
887
  }
781
- return cells;
888
+ return allCells;
782
889
  }
783
890
  getColumn(event) {
784
891
  if (!this._grid)
@@ -865,7 +972,7 @@ export class TosiTable extends WebComponent {
865
972
  if (!this._grid)
866
973
  return;
867
974
  for (const elt of Array.from(this._grid.children)) {
868
- const item = getListItem(elt);
975
+ const item = getListItem(elt) ?? this.pinnedCellToItem.get(elt);
869
976
  if (item != null) {
870
977
  this.selectBinding(elt, item);
871
978
  }
@@ -881,7 +988,7 @@ export class TosiTable extends WebComponent {
881
988
  if (!(target instanceof HTMLElement)) {
882
989
  return;
883
990
  }
884
- const pickedItem = getListItem(target);
991
+ const pickedItem = getListItem(target) ?? this.resolvePinnedItem(target);
885
992
  if (pickedItem == null) {
886
993
  return;
887
994
  }
@@ -1251,16 +1358,20 @@ export class TosiTable extends WebComponent {
1251
1358
  getCells(itemOrCell) {
1252
1359
  if (!this._grid)
1253
1360
  return undefined;
1361
+ const item = itemOrCell instanceof Element ? this.getItem(itemOrCell) : itemOrCell;
1362
+ if (item == null)
1363
+ return undefined;
1364
+ const key = tosiValue(item);
1365
+ const pinned = this.pinnedItemToCells.get(key);
1366
+ if (pinned)
1367
+ return pinned;
1254
1368
  const binding = getListBinding(this._grid);
1255
1369
  if (!binding)
1256
1370
  return undefined;
1257
- const item = itemOrCell instanceof Element ? getListItem(itemOrCell) : itemOrCell;
1258
- if (item == null)
1259
- return undefined;
1260
- return binding.itemToElement.get(tosiValue(item));
1371
+ return binding.itemToElement.get(key);
1261
1372
  }
1262
1373
  getItem(cell) {
1263
- return getListItem(cell);
1374
+ return getListItem(cell) ?? this.resolvePinnedItem(cell);
1264
1375
  }
1265
1376
  draggedColumn;
1266
1377
  dropColumn = (event) => {
@@ -1280,6 +1391,7 @@ export class TosiTable extends WebComponent {
1280
1391
  render() {
1281
1392
  super.render();
1282
1393
  this.textContent = '';
1394
+ this.pinnedItemToCells.clear();
1283
1395
  // Prepare data
1284
1396
  const pinnedTopData = this.pinnedTop > 0 ? this._array.slice(0, this.pinnedTop) : [];
1285
1397
  const pinnedBottomData = this.pinnedBottom > 0 ? this._array.slice(-this.pinnedBottom) : [];
@@ -1377,7 +1489,16 @@ export class TosiTable extends WebComponent {
1377
1489
  const customCell = col.dataCell(col);
1378
1490
  this.applyPinnedToCustomCell(customCell, colIndex, si, style);
1379
1491
  if (rowRenderedBinding && colIndex === lastCol) {
1380
- bind(customCell, item, { toDOM: rowRenderedBinding });
1492
+ bind(customCell, item, {
1493
+ toDOM(cell) {
1494
+ if (selectEnabled)
1495
+ selectBindingFn(cell, getListItem(cell));
1496
+ rowRenderedBinding(cell);
1497
+ },
1498
+ });
1499
+ }
1500
+ else if (selectEnabled) {
1501
+ bind(customCell, item, { toDOM: selectBindingFn });
1381
1502
  }
1382
1503
  return customCell;
1383
1504
  }
package/dist/icon-data.js CHANGED
@@ -87,7 +87,7 @@ export default {
87
87
  gitPullRequest: "<svg class=\"stroked\" viewBox=\"0 0 24 24\"><circle cx=\"18\" cy=\"18\" r=\"3\"></circle><circle cx=\"6\" cy=\"6\" r=\"3\"></circle><path d=\"M13 6h3a2 2 0 0 1 2 2v7\"></path><line x1=\"6\" y1=\"9\" x2=\"6\" y2=\"21\"></line></svg>",
88
88
  minimize: "<svg class=\"stroked\" viewBox=\"0 0 24 24\"><path d=\"M8 3v3a2 2 0 0 1-2 2H3m18 0h-3a2 2 0 0 1-2-2V3m0 18v-3a2 2 0 0 1 2-2h3M3 16h3a2 2 0 0 1 2 2v3\"></path></svg>",
89
89
  minusSquare: "<svg class=\"stroked\" viewBox=\"0 0 24 24\"><rect x=\"3\" y=\"3\" width=\"18\" height=\"18\" rx=\"2\" ry=\"2\"></rect><line x1=\"8\" y1=\"12\" x2=\"16\" y2=\"12\"></line></svg>",
90
- settings: "<svg class=\"stroked\" viewBox=\"0 0 24 24\"><circle cx=\"12\" cy=\"12\" r=\"3\"></circle><path d=\"M19.4 15a1.65 1.65 0 0 0 .33 1.82l.6.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.6.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.6.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.6.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z\"></path></svg>",
90
+ settings: "<svg class=\"stroked\" viewBox=\"0 0 24 24\"><g><path style=\"\" d=\"M4.78,6.75 C6.02,7.26,7.26,6.02,6.75,4.78 C6.75,4.78,6.24,3.57,6.24,3.57 C5.93,2.8,6.29,1.92,7.06,1.6 C7.06,1.6,8.14,1.15,8.14,1.15 C8.91,0.83,9.79,1.2,10.11,1.96 C10.11,1.96,10.61,3.18,10.61,3.18 C11.12,4.42,12.88,4.42,13.39,3.18 C13.39,3.18,13.89,1.97,13.89,1.97 C14.21,1.2,15.09,0.83,15.86,1.15 C15.86,1.15,16.94,1.6,16.94,1.6 C17.71,1.92,18.08,2.8,17.76,3.57 C17.76,3.57,17.15,5.03,17.15,5.03 C16.64,6.27,17.88,7.51,19.12,7 C19.12,7,20.51,6.42,20.51,6.42 C21.28,6.1,22.16,6.47,22.47,7.23 C22.47,7.23,22.92,8.32,22.92,8.32 C23.24,9.09,22.88,9.96,22.11,10.28 C22.11,10.28,21.07,10.71,21.07,10.71 C19.83,11.23,19.83,12.98,21.07,13.49 C21.07,13.49,22.04,13.89,22.04,13.89 C22.8,14.21,23.17,15.09,22.85,15.86 C22.85,15.86,22.4,16.94,22.4,16.94 C22.08,17.71,21.2,18.07,20.43,17.76 C20.43,17.76,19.22,17.25,19.22,17.25 C17.98,16.74,16.74,17.98,17.25,19.22 C17.25,19.22,17.76,20.43,17.76,20.43 C18.07,21.2,17.71,22.08,16.94,22.4 C16.94,22.4,15.86,22.85,15.86,22.85 C15.09,23.17,14.21,22.8,13.89,22.04 C13.89,22.04,13.39,20.82,13.39,20.82 C12.88,19.58,11.12,19.58,10.61,20.82 C10.61,20.82,10.11,22.04,10.11,22.04 C9.79,22.8,8.91,23.17,8.14,22.85 C8.14,22.85,7.06,22.4,7.06,22.4 C6.29,22.08,5.93,21.2,6.24,20.44 C6.24,20.44,6.64,19.47,6.64,19.47 C7.16,18.23,5.92,16.99,4.68,17.5 C4.68,17.5,3.64,17.93,3.64,17.93 C2.87,18.25,1.99,17.89,1.67,17.12 C1.67,17.12,1.22,16.04,1.22,16.04 C0.91,15.27,1.27,14.39,2.04,14.07 C2.04,14.07,3.43,13.49,3.43,13.49 C4.67,12.98,4.67,11.23,3.43,10.71 C3.43,10.71,1.96,10.11,1.96,10.11 C1.2,9.79,0.83,8.91,1.15,8.14 C1.15,8.14,1.6,7.06,1.6,7.06 C1.92,6.29,2.8,5.93,3.57,6.24 C3.57,6.24,4.78,6.75,4.78,6.75 z M14.31,11.04 C14.84,12.32,14.23,13.78,12.96,14.31 C11.68,14.84,10.22,14.23,9.69,12.96 C9.16,11.68,9.77,10.22,11.04,9.69 C12.32,9.16,13.78,9.77,14.31,11.04 z\"/></g></svg> ",
91
91
  cloudSnow: "<svg class=\"stroked\" viewBox=\"0 0 24 24\"><path d=\"M20 17.58A5 5 0 0 0 18 8h-1.26A8 8 0 1 0 4 16.25\"></path><line x1=\"8\" y1=\"16\" x2=\"8.01\" y2=\"16\"></line><line x1=\"8\" y1=\"20\" x2=\"8.01\" y2=\"20\"></line><line x1=\"12\" y1=\"18\" x2=\"12.01\" y2=\"18\"></line><line x1=\"12\" y1=\"22\" x2=\"12.01\" y2=\"22\"></line><line x1=\"16\" y1=\"16\" x2=\"16.01\" y2=\"16\"></line><line x1=\"16\" y1=\"20\" x2=\"16.01\" y2=\"20\"></line></svg>",
92
92
  type: "<svg class=\"stroked\" viewBox=\"0 0 24 24\"><polyline points=\"4 7 4 4 20 4 20 7\"></polyline><line x1=\"9\" y1=\"20\" x2=\"15\" y2=\"20\"></line><line x1=\"12\" y1=\"4\" x2=\"12\" y2=\"20\"></line></svg>",
93
93
  archive: "<svg class=\"stroked\" viewBox=\"0 0 24 24\"><polyline points=\"21 8 21 21 3 21 3 8\"></polyline><rect x=\"1\" y=\"3\" width=\"22\" height=\"5\"></rect><line x1=\"10\" y1=\"12\" x2=\"14\" y2=\"12\"></line></svg>",
package/dist/icons.js CHANGED
@@ -490,7 +490,6 @@ a single SVG. `svg2DataUrl()` will render only the base icon and log a
490
490
  console error. Simple suffix transforms and plain icons work normally
491
491
  with `svg2DataUrl`.
492
492
 
493
-
494
493
  ## Missing Icons
495
494
 
496
495
  If you ask for an icon that isn't defined, the `icons` proxy will print a warning to console
@@ -757,7 +756,7 @@ function parseStyleSuffixes(name) {
757
756
  if (rule.prefix instanceof RegExp) {
758
757
  return rule.prefix.test(baseName);
759
758
  }
760
- return baseName.startsWith(rule.prefix) && baseName.length > rule.prefix.length;
759
+ return (baseName.startsWith(rule.prefix) && baseName.length > rule.prefix.length);
761
760
  });
762
761
  if (!matchesRule)
763
762
  return null;