tosijs-ui 1.5.14 → 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.
@@ -130,6 +130,9 @@ export declare class TosiTable extends WebComponent {
130
130
  private selectBinding;
131
131
  maxVisibleRows: number;
132
132
  private _grid;
133
+ private pinnedItemToCells;
134
+ private pinnedCellToItem;
135
+ private resolvePinnedItem;
133
136
  get value(): TableData;
134
137
  set value(data: TableData);
135
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
 
@@ -567,6 +640,14 @@ export class TosiTable extends WebComponent {
567
640
  };
568
641
  maxVisibleRows = 10000;
569
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
+ }
570
651
  get value() {
571
652
  return {
572
653
  array: this.array,
@@ -757,29 +838,54 @@ export class TosiTable extends WebComponent {
757
838
  Object.assign(cell.style, style);
758
839
  }
759
840
  buildPinnedCells(rows, cols, stickyInfo, pin, rowHeight, startRowIndex) {
760
- const cells = [];
841
+ const allCells = [];
842
+ const selectBindingFn = this.selectBinding;
843
+ const { rowRendered } = this;
761
844
  for (let r = 0; r < rows.length; r++) {
762
845
  const rowItem = rows[r];
763
846
  const offset = pin === 'top'
764
847
  ? (r + 1) * rowHeight + 'px'
765
848
  : (rows.length - 1 - r) * rowHeight + 'px';
849
+ const rowCells = [];
766
850
  for (let c = 0; c < cols.length; c++) {
767
851
  const col = cols[c];
768
852
  const si = stickyInfo[c];
769
- cells.push(span({
770
- class: this.cellClasses(`td pinned-${pin}`, si),
771
- role: 'gridcell',
772
- tabindex: -1,
773
- ariaRowindex: String(startRowIndex + r + 1),
774
- ariaColindex: String(c + 1),
775
- style: this.cellStyle(col, si, {
776
- position: 'sticky',
777
- [pin]: offset,
778
- }),
779
- }, 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);
780
886
  }
781
887
  }
782
- return cells;
888
+ return allCells;
783
889
  }
784
890
  getColumn(event) {
785
891
  if (!this._grid)
@@ -866,7 +972,7 @@ export class TosiTable extends WebComponent {
866
972
  if (!this._grid)
867
973
  return;
868
974
  for (const elt of Array.from(this._grid.children)) {
869
- const item = getListItem(elt);
975
+ const item = getListItem(elt) ?? this.pinnedCellToItem.get(elt);
870
976
  if (item != null) {
871
977
  this.selectBinding(elt, item);
872
978
  }
@@ -882,7 +988,7 @@ export class TosiTable extends WebComponent {
882
988
  if (!(target instanceof HTMLElement)) {
883
989
  return;
884
990
  }
885
- const pickedItem = getListItem(target);
991
+ const pickedItem = getListItem(target) ?? this.resolvePinnedItem(target);
886
992
  if (pickedItem == null) {
887
993
  return;
888
994
  }
@@ -1252,16 +1358,20 @@ export class TosiTable extends WebComponent {
1252
1358
  getCells(itemOrCell) {
1253
1359
  if (!this._grid)
1254
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;
1255
1368
  const binding = getListBinding(this._grid);
1256
1369
  if (!binding)
1257
1370
  return undefined;
1258
- const item = itemOrCell instanceof Element ? getListItem(itemOrCell) : itemOrCell;
1259
- if (item == null)
1260
- return undefined;
1261
- return binding.itemToElement.get(tosiValue(item));
1371
+ return binding.itemToElement.get(key);
1262
1372
  }
1263
1373
  getItem(cell) {
1264
- return getListItem(cell);
1374
+ return getListItem(cell) ?? this.resolvePinnedItem(cell);
1265
1375
  }
1266
1376
  draggedColumn;
1267
1377
  dropColumn = (event) => {
@@ -1281,6 +1391,7 @@ export class TosiTable extends WebComponent {
1281
1391
  render() {
1282
1392
  super.render();
1283
1393
  this.textContent = '';
1394
+ this.pinnedItemToCells.clear();
1284
1395
  // Prepare data
1285
1396
  const pinnedTopData = this.pinnedTop > 0 ? this._array.slice(0, this.pinnedTop) : [];
1286
1397
  const pinnedBottomData = this.pinnedBottom > 0 ? this._array.slice(-this.pinnedBottom) : [];
@@ -1378,7 +1489,16 @@ export class TosiTable extends WebComponent {
1378
1489
  const customCell = col.dataCell(col);
1379
1490
  this.applyPinnedToCustomCell(customCell, colIndex, si, style);
1380
1491
  if (rowRenderedBinding && colIndex === lastCol) {
1381
- 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 });
1382
1502
  }
1383
1503
  return customCell;
1384
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;