@indico-data/design-system 2.48.0 → 2.49.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.
Files changed (31) hide show
  1. package/lib/components/table/Table.stories.d.ts +1 -0
  2. package/lib/components/table/components/HorizontalStickyHeader.d.ts +10 -0
  3. package/lib/components/table/components/__tests__/HorizontalStickyHeader.test.d.ts +1 -0
  4. package/lib/components/table/components/helpers.d.ts +6 -0
  5. package/lib/components/table/hooks/usePinnedColumnsManager.d.ts +8 -0
  6. package/lib/components/table/sampleData.d.ts +4 -0
  7. package/lib/components/table/types.d.ts +11 -1
  8. package/lib/components/table/utils/processColumns.d.ts +2 -0
  9. package/lib/index.css +28 -9
  10. package/lib/index.d.ts +12 -1
  11. package/lib/index.esm.css +28 -9
  12. package/lib/index.esm.js +238 -2
  13. package/lib/index.esm.js.map +1 -1
  14. package/lib/index.js +238 -2
  15. package/lib/index.js.map +1 -1
  16. package/lib/utils/getPreviousHeadersWidth.d.ts +1 -0
  17. package/package.json +1 -1
  18. package/src/components/table/Table.mdx +134 -0
  19. package/src/components/table/Table.stories.tsx +71 -2
  20. package/src/components/table/Table.tsx +16 -1
  21. package/src/components/table/components/HorizontalStickyHeader.tsx +57 -0
  22. package/src/components/table/components/__tests__/HorizontalStickyHeader.test.tsx +104 -0
  23. package/src/components/table/components/helpers.ts +90 -0
  24. package/src/components/table/hooks/usePinnedColumnsManager.ts +146 -0
  25. package/src/components/table/{sampleData.ts → sampleData.tsx} +156 -1
  26. package/src/components/table/styles/Table.scss +32 -15
  27. package/src/components/table/styles/_variables.scss +2 -0
  28. package/src/components/table/types.ts +13 -1
  29. package/src/components/table/utils/processColumns.tsx +35 -0
  30. package/src/setup/setupTests.ts +8 -0
  31. package/src/utils/getPreviousHeadersWidth.ts +12 -0
package/lib/index.js CHANGED
@@ -2628,6 +2628,16 @@ function __rest(s, e) {
2628
2628
  return t;
2629
2629
  }
2630
2630
 
2631
+ function __awaiter(thisArg, _arguments, P, generator) {
2632
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
2633
+ return new (P || (P = Promise))(function (resolve, reject) {
2634
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
2635
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
2636
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
2637
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
2638
+ });
2639
+ }
2640
+
2631
2641
  typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
2632
2642
  var e = new Error(message);
2633
2643
  return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
@@ -7281,8 +7291,234 @@ const TablePagination = ({ rowsPerPage, rowCount, onChangePage, currentPage, tot
7281
7291
  return (jsxRuntime.jsx("div", { className: "table__pagination", children: jsxRuntime.jsxs(Row, { align: "center", justify: "between", children: [jsxRuntime.jsx(Col, { xs: "content", children: totalEntriesText && (jsxRuntime.jsx("span", { "data-testid": "table-pagination-total-entries", className: "table__pagination-total-entries", children: totalEntriesText })) }), jsxRuntime.jsx(Col, { xs: "content", children: jsxRuntime.jsx(Pagination, { "data-testid": "table-pagination-component", totalPages: totalPages, currentPage: currentPage, onChange: (page) => onChangePage(page, rowsPerPage) }) })] }) }));
7282
7292
  };
7283
7293
 
7294
+ // Gets the width of the previous pinned columns
7295
+ const getPreviousHeadersWidth = (position, pinnedColumnIds) => {
7296
+ let totalWidth = 0;
7297
+ // Add checkbox column width if it's pinned
7298
+ if (pinnedColumnIds.includes('checkbox-column')) {
7299
+ const checkboxCell = document.querySelector('.rdt_TableCol:not([data-column-id])');
7300
+ if (checkboxCell) {
7301
+ totalWidth += checkboxCell.offsetWidth;
7302
+ }
7303
+ }
7304
+ // Add widths of other pinned columns before this position
7305
+ const previousHeaders = Array.from({ length: position }, (_, i) => {
7306
+ const header = document.querySelector(`[data-column-id="sticky-column-${i}"]`);
7307
+ if (header && pinnedColumnIds.includes(`sticky-column-${i}`)) {
7308
+ return header;
7309
+ }
7310
+ return null;
7311
+ }).filter((header) => header !== null);
7312
+ // Calculate base width from previous columns
7313
+ totalWidth = previousHeaders.reduce((acc, header) => {
7314
+ return acc + header.offsetWidth;
7315
+ }, totalWidth);
7316
+ // Leave this for if we ever try to fix the auto width columns
7317
+ // There is a bug where borders cause the offset to be wrong, this keeps it in sync.
7318
+ // Add offset that increases by 1 every two columns after index 1
7319
+ // if (position >= 2) {
7320
+ // const additionalOffset = Math.floor((position - 2) / 2) + 1;
7321
+ // totalWidth += additionalOffset;
7322
+ // }
7323
+ return totalWidth;
7324
+ };
7325
+ // Applies sticky styles to the column header
7326
+ const applyStickyStylesToTableHeader = (position, left) => __awaiter(void 0, void 0, void 0, function* () {
7327
+ const header = document.querySelector(`[data-column-id="sticky-column-${position}"]`);
7328
+ if (header) {
7329
+ header.style.position = 'sticky';
7330
+ header.style.left = `${left}px`;
7331
+ header.style.zIndex = '3';
7332
+ header.style.backgroundColor =
7333
+ 'var(--pf-table-pinned-column-background-color)';
7334
+ }
7335
+ });
7336
+ // Sorts the pinned columns so that any column that is pinned comes before any column that is not.
7337
+ const sortPinnedColumns = (columns, pinnedColumnIds) => {
7338
+ return [...columns].sort((a, b) => {
7339
+ const aIsPinned = pinnedColumnIds.includes(a.id);
7340
+ const bIsPinned = pinnedColumnIds.includes(b.id);
7341
+ if (aIsPinned && !bIsPinned)
7342
+ return -1; // a comes first
7343
+ if (!aIsPinned && bIsPinned)
7344
+ return 1; // b comes first
7345
+ return 0; // maintain relative order for columns with same pinned state
7346
+ });
7347
+ };
7348
+ // Gets the styles for the pinned columns
7349
+ const getPinnedColumnStyles = (isPinned, index, pinnedColumnIds) => {
7350
+ return isPinned
7351
+ ? {
7352
+ position: 'sticky',
7353
+ left: `${getPreviousHeadersWidth(index, pinnedColumnIds)}px`,
7354
+ zIndex: 3,
7355
+ backgroundColor: 'var(--pf-table-pinned-column-background-color)',
7356
+ }
7357
+ : {
7358
+ position: undefined,
7359
+ left: undefined,
7360
+ zIndex: undefined,
7361
+ backgroundColor: undefined,
7362
+ };
7363
+ };
7364
+ const clearStickyStyles = (header) => {
7365
+ header.style.position = '';
7366
+ header.style.left = '';
7367
+ header.style.zIndex = '';
7368
+ header.style.backgroundColor = '';
7369
+ };
7370
+
7371
+ const HorizontalStickyHeader = ({ children, position, onPinColumn, isPinned = false, pinnedColumnIds, }) => {
7372
+ React.useEffect(() => {
7373
+ const calculateWidth = () => __awaiter(void 0, void 0, void 0, function* () {
7374
+ yield new Promise((resolve) => setTimeout(resolve, 0));
7375
+ const header = document.querySelector(`[data-column-id="sticky-column-${position}"]`);
7376
+ if (header) {
7377
+ if (isPinned) {
7378
+ const width = getPreviousHeadersWidth(position, pinnedColumnIds);
7379
+ yield applyStickyStylesToTableHeader(position, width);
7380
+ }
7381
+ else {
7382
+ clearStickyStyles(header);
7383
+ }
7384
+ }
7385
+ });
7386
+ calculateWidth();
7387
+ }, [position, isPinned, pinnedColumnIds]);
7388
+ return (jsxRuntime.jsxs("div", { className: "table__header-cell", "data-testid": `sticky-column-${position}`, children: [jsxRuntime.jsx(Button$1, { "data-testid": `sticky-header-pin-button-${position}`, variant: "link", size: "sm", iconLeft: "pin", onClick: onPinColumn, ariaLabel: isPinned ? 'Unpin column' : 'Pin column', className: `table__column--${isPinned ? 'is-pinned' : 'is-not-pinned'} table__column__pin-action` }), jsxRuntime.jsx("div", { className: "table__header-content", children: children })] }));
7389
+ };
7390
+
7391
+ const processColumns = (columns, pinnedColumnIds, togglePinnedColumn) => {
7392
+ return columns.map((column, index) => {
7393
+ const dataColumnId = `sticky-column-${index}`;
7394
+ const isPinned = pinnedColumnIds.includes(dataColumnId);
7395
+ const headerContent = column.isPinned !== undefined ? (jsxRuntime.jsx(HorizontalStickyHeader, { position: index, isPinned: isPinned, onPinColumn: () => togglePinnedColumn(column.id), pinnedColumnIds: pinnedColumnIds, children: column.name })) : (jsxRuntime.jsx(jsxRuntime.Fragment, { children: column.name }));
7396
+ return Object.assign(Object.assign({}, column), { name: headerContent, id: dataColumnId, style: getPinnedColumnStyles(isPinned, index, pinnedColumnIds) });
7397
+ });
7398
+ };
7399
+
7400
+ /**
7401
+ * Hook to manage pinned columns in a table
7402
+ * Handles initialization, toggling, positioning and resizing of pinned columns
7403
+ */
7404
+ const usePinnedColumnsManager = (columns, canPinColumns, onPinnedColumnsChange) => {
7405
+ const pinnedColumnIds = columns.filter((column) => column.isPinned).map((column) => column.id);
7406
+ // `dataColumnIds` is the list of IDs used as `data-column-id` attributes on the table headers and cells
7407
+ const dataColumnIds = React.useMemo(() => {
7408
+ const ids = columns
7409
+ .map((column, index) => (column.isPinned ? `sticky-column-${index}` : null))
7410
+ .filter((id) => id !== null);
7411
+ return ids.length > 0 ? ['checkbox-column', ...ids] : ids;
7412
+ }, [columns]);
7413
+ // Toggle individual column pin state
7414
+ const togglePinnedColumn = React.useCallback((columnId) => {
7415
+ const prevPinnedColumns = pinnedColumnIds;
7416
+ // Handle unpinning
7417
+ if (prevPinnedColumns.some((id) => id === columnId)) {
7418
+ onPinnedColumnsChange === null || onPinnedColumnsChange === void 0 ? void 0 : onPinnedColumnsChange(prevPinnedColumns.filter((id) => id !== columnId));
7419
+ }
7420
+ else {
7421
+ onPinnedColumnsChange === null || onPinnedColumnsChange === void 0 ? void 0 : onPinnedColumnsChange(prevPinnedColumns.concat(columnId));
7422
+ }
7423
+ }, [pinnedColumnIds, onPinnedColumnsChange]);
7424
+ // Handle resize events and recalculate pinned column positions
7425
+ React.useEffect(() => {
7426
+ if (!canPinColumns)
7427
+ return;
7428
+ const recalculatePositions = () => {
7429
+ // Reset all column styles and remove last-pinned-column class
7430
+ const allCells = document.querySelectorAll('.rdt_TableCol, .rdt_TableCell');
7431
+ allCells.forEach((cell) => {
7432
+ cell.style.position = '';
7433
+ cell.style.left = '';
7434
+ cell.style.zIndex = '';
7435
+ cell.style.backgroundColor = '';
7436
+ cell.style.borderRight = '';
7437
+ });
7438
+ // Apply styles to pinned columns
7439
+ dataColumnIds.forEach((column, index) => {
7440
+ const isLastPinnedColumn = index === dataColumnIds.length - 1;
7441
+ if (column === 'checkbox-column') {
7442
+ // Handle header checkbox
7443
+ const headerCheckbox = document.querySelector('.rdt_TableCol:not([data-column-id])');
7444
+ if (headerCheckbox) {
7445
+ headerCheckbox.style.position = 'sticky';
7446
+ headerCheckbox.style.left = '0';
7447
+ headerCheckbox.style.zIndex = '4';
7448
+ headerCheckbox.style.backgroundColor =
7449
+ 'var(--pf-table-background-color)';
7450
+ }
7451
+ // Handle cell checkboxes
7452
+ const cellCheckboxes = document.querySelectorAll('.rdt_TableCell:first-child');
7453
+ cellCheckboxes.forEach((cell) => {
7454
+ cell.style.position = 'sticky';
7455
+ cell.style.left = '0';
7456
+ cell.style.zIndex = '2';
7457
+ cell.style.backgroundColor =
7458
+ 'var(--pf-table-pinned-column-background-color)';
7459
+ if (isLastPinnedColumn) {
7460
+ cell.style.borderRight =
7461
+ `2px solid var(--pf-table-pinned-column-border-color)`;
7462
+ }
7463
+ });
7464
+ }
7465
+ else {
7466
+ const columnIndex = parseInt(column.split('-')[2]);
7467
+ const left = getPreviousHeadersWidth(columnIndex, dataColumnIds);
7468
+ // Headers
7469
+ const headers = document.querySelectorAll(`.rdt_TableCol[data-column-id="sticky-column-${columnIndex}"]`);
7470
+ headers.forEach((header) => {
7471
+ header.style.position = 'sticky';
7472
+ header.style.left = `${left}px`;
7473
+ header.style.zIndex = '2';
7474
+ header.style.backgroundColor =
7475
+ 'var(--pf-table-pinned-column-background-color)';
7476
+ if (isLastPinnedColumn) {
7477
+ header.style.borderRight =
7478
+ `2px solid var(--pf-table-pinned-column-border-color)`;
7479
+ }
7480
+ });
7481
+ // Cells
7482
+ const cells = document.querySelectorAll(`.rdt_TableCell[data-column-id="sticky-column-${columnIndex}"]`);
7483
+ cells.forEach((cell) => {
7484
+ cell.style.position = 'sticky';
7485
+ cell.style.left = `${left}px`;
7486
+ cell.style.zIndex = '2';
7487
+ cell.style.backgroundColor =
7488
+ 'var(--pf-table-pinned-column-background-color)';
7489
+ if (isLastPinnedColumn) {
7490
+ cell.style.borderRight =
7491
+ `2px solid var(--pf-table-pinned-column-border-color)`;
7492
+ }
7493
+ });
7494
+ }
7495
+ });
7496
+ };
7497
+ // Set up resize observers
7498
+ const table = document.querySelector('.rdt_Table');
7499
+ const resizeObserver = new ResizeObserver(recalculatePositions);
7500
+ if (table) {
7501
+ resizeObserver.observe(table);
7502
+ }
7503
+ window.addEventListener('resize', recalculatePositions);
7504
+ return () => {
7505
+ resizeObserver.disconnect();
7506
+ window.removeEventListener('resize', recalculatePositions);
7507
+ };
7508
+ }, [canPinColumns, dataColumnIds]);
7509
+ // Process columns for rendering with pin state
7510
+ const columnsWithPinning = canPinColumns
7511
+ ? sortPinnedColumns(processColumns(columns, dataColumnIds, togglePinnedColumn), dataColumnIds)
7512
+ : columns;
7513
+ return {
7514
+ columnsWithPinning, // Columns with pin state and handlers applied
7515
+ };
7516
+ };
7517
+
7284
7518
  const Table = (props) => {
7285
- const { responsive = true, direction = 'auto', keyField = 'id', striped = false, noDataComponent = 'built-in', isDisabled, isLoading, isFullHeight = false, subHeaderAlign = 'left', className, paginationTotalRows, totalEntriesText } = props, rest = __rest(props, ["responsive", "direction", "keyField", "striped", "noDataComponent", "isDisabled", "isLoading", "isFullHeight", "subHeaderAlign", "className", "paginationTotalRows", "totalEntriesText"]);
7519
+ const { responsive = true, direction = 'auto', keyField = 'id', striped = false, noDataComponent = 'built-in', isDisabled, isLoading, isFullHeight = false, subHeaderAlign = 'left', className, paginationTotalRows, totalEntriesText, data, columns: initialColumns, canPinColumns = false, onPinnedColumnsChange } = props, rest = __rest(props, ["responsive", "direction", "keyField", "striped", "noDataComponent", "isDisabled", "isLoading", "isFullHeight", "subHeaderAlign", "className", "paginationTotalRows", "totalEntriesText", "data", "columns", "canPinColumns", "onPinnedColumnsChange"]);
7520
+ // Turns on/off column pinning.
7521
+ const { columnsWithPinning } = usePinnedColumnsManager(initialColumns, canPinColumns, onPinnedColumnsChange);
7286
7522
  const combinedClassName = classNames(className, {
7287
7523
  'table--striped': striped,
7288
7524
  'table-body': true,
@@ -7290,7 +7526,7 @@ const Table = (props) => {
7290
7526
  const tableWrapperClassName = classNames('table', {
7291
7527
  'table--full-height': isFullHeight,
7292
7528
  });
7293
- return (jsxRuntime.jsx("div", { className: tableWrapperClassName, "data-testid": "table", children: jsxRuntime.jsx(Xe, Object.assign({ responsive: responsive, direction: direction, subHeaderAlign: subHeaderAlign, keyField: keyField, striped: striped, className: combinedClassName, disabled: isDisabled, noDataComponent: noDataComponent, progressPending: isLoading, progressComponent: jsxRuntime.jsx(LoadingComponent, {}), pagination: true, paginationComponent: (props) => (jsxRuntime.jsx(TablePagination, Object.assign({}, props, { totalEntriesText: totalEntriesText }))), paginationTotalRows: paginationTotalRows }, rest)) }));
7529
+ return (jsxRuntime.jsx("div", { className: tableWrapperClassName, "data-testid": "table", children: jsxRuntime.jsx(Xe, Object.assign({ data: data, columns: columnsWithPinning, responsive: responsive, direction: direction, subHeaderAlign: subHeaderAlign, keyField: keyField, striped: striped, className: combinedClassName, disabled: isDisabled, noDataComponent: noDataComponent, progressPending: isLoading, progressComponent: jsxRuntime.jsx(LoadingComponent, {}), pagination: true, paginationComponent: (props) => (jsxRuntime.jsx(TablePagination, Object.assign({}, props, { totalEntriesText: totalEntriesText }))), paginationTotalRows: paginationTotalRows, highlightOnHover: true, pointerOnHover: true }, rest)) }));
7294
7530
  };
7295
7531
 
7296
7532
  const Radio = React__namespace.default.forwardRef((_a, ref) => {