From a71986874e048711526dc513def77357674088c8 Mon Sep 17 00:00:00 2001 From: vpolessky Date: Fri, 15 May 2026 13:35:43 +0200 Subject: [PATCH 1/2] feat(design-system): table scrollbar adjustment [AR-54032] --- .changeset/swift-stamps-pull.md | 5 ++ .../ds-table-infinite-scroll.browser.test.tsx | 2 +- .../ds-table-virtualized.browser.test.tsx | 4 +- .../ds-table-body-virtualized.module.scss | 22 ++++- .../ds-table-body-virtualized.tsx | 25 +++--- .../ds-table-body-virtualized.types.ts | 7 +- .../ds-table-header.module.scss | 6 +- .../ds-table-header/ds-table-header.tsx | 2 +- .../ds-table-row-virtualized.tsx | 2 +- .../ds-table-row-virtualized.types.ts | 2 +- .../ds-table-row/ds-table-row.module.scss | 1 + .../components/ds-table/ds-table.module.scss | 84 +++++++++++++++++-- .../src/components/ds-table/ds-table.tsx | 9 +- .../stories/ds-table.stories.module.scss | 5 ++ .../ds-table/stories/ds-table.stories.tsx | 48 +++++++++++ .../components/ds-table/utils/column-size.ts | 17 ++-- .../design-system/src/styles/_scrollbars.scss | 37 ++++++++ 17 files changed, 227 insertions(+), 51 deletions(-) create mode 100644 .changeset/swift-stamps-pull.md diff --git a/.changeset/swift-stamps-pull.md b/.changeset/swift-stamps-pull.md new file mode 100644 index 000000000..846e336df --- /dev/null +++ b/.changeset/swift-stamps-pull.md @@ -0,0 +1,5 @@ +--- +'@drivenets/design-system': patch +--- + +Update `DsTable` scroll, use thin scrollbar for header diff --git a/packages/design-system/src/components/ds-table/__tests__/ds-table-infinite-scroll.browser.test.tsx b/packages/design-system/src/components/ds-table/__tests__/ds-table-infinite-scroll.browser.test.tsx index 9af01f151..8c19e9063 100644 --- a/packages/design-system/src/components/ds-table/__tests__/ds-table-infinite-scroll.browser.test.tsx +++ b/packages/design-system/src/components/ds-table/__tests__/ds-table-infinite-scroll.browser.test.tsx @@ -18,7 +18,7 @@ function generateTestData(count: number): Person[] { } const getScrollContainer = (): HTMLElement => { - const el = document.querySelector('[class*="virtualizedContainer"]'); + const el = document.querySelector('[class*="virtualizedContainer"] tbody'); if (!el) { throw new Error('Expected virtualized scroll container'); } diff --git a/packages/design-system/src/components/ds-table/__tests__/ds-table-virtualized.browser.test.tsx b/packages/design-system/src/components/ds-table/__tests__/ds-table-virtualized.browser.test.tsx index ddcdea5a9..bb9ba52e4 100644 --- a/packages/design-system/src/components/ds-table/__tests__/ds-table-virtualized.browser.test.tsx +++ b/packages/design-system/src/components/ds-table/__tests__/ds-table-virtualized.browser.test.tsx @@ -49,7 +49,7 @@ describe('DsTable Virtualized', () => { await page.elementLocator(firstRowRoot).click(); await expect.element(firstRowCheckboxInput).toBeChecked(); - const scrollContainer = document.querySelector('[class*="virtualizedContainer"]'); + const scrollContainer = document.querySelector('[class*="virtualizedContainer"] tbody'); if (scrollContainer) { scrollContainer.scrollTop = scrollContainer.scrollHeight; @@ -88,7 +88,7 @@ describe('DsTable Virtualized', () => { await page.getByRole('button', { name: 'chevron_right' }).nth(0).click(); await expect.element(page.getByText('Expanded: First1')).toBeVisible(); - const scrollContainer = document.querySelector('[class*="virtualizedContainer"]'); + const scrollContainer = document.querySelector('[class*="virtualizedContainer"] tbody'); if (scrollContainer) { scrollContainer.scrollTop = scrollContainer.scrollHeight; diff --git a/packages/design-system/src/components/ds-table/components/ds-table-body-virtualized/ds-table-body-virtualized.module.scss b/packages/design-system/src/components/ds-table/components/ds-table-body-virtualized/ds-table-body-virtualized.module.scss index d3ffcfc6f..7ad927ec4 100644 --- a/packages/design-system/src/components/ds-table/components/ds-table-body-virtualized/ds-table-body-virtualized.module.scss +++ b/packages/design-system/src/components/ds-table/components/ds-table-body-virtualized/ds-table-body-virtualized.module.scss @@ -1,9 +1,27 @@ +@use '../../../../styles/scrollbars' as scrollbars; + .body { - display: grid; + display: block; + position: relative; + overflow-y: auto; + flex: 1; + min-height: 0; + + @include scrollbars.scrollbar-on-hover; +} + +// 1px row pinned at `top: ` (set inline) so absolutely-positioned +// rows extend the parent's scrollable overflow area. +.sentinel { + position: absolute; + left: 0; + width: 1px; + height: 1px; + pointer-events: none; + visibility: hidden; } .emptyState { position: absolute; inset: 0; - top: var(--ds-table-header-height); } diff --git a/packages/design-system/src/components/ds-table/components/ds-table-body-virtualized/ds-table-body-virtualized.tsx b/packages/design-system/src/components/ds-table/components/ds-table-body-virtualized/ds-table-body-virtualized.tsx index 059e5cafe..99e9bee11 100644 --- a/packages/design-system/src/components/ds-table/components/ds-table-body-virtualized/ds-table-body-virtualized.tsx +++ b/packages/design-system/src/components/ds-table/components/ds-table-body-virtualized/ds-table-body-virtualized.tsx @@ -10,7 +10,6 @@ import { useInfiniteScroll } from './use-infinite-scroll'; export const DsTableBodyVirtualized = ({ table, - tableContainerRef, emptyState, estimateSize, overscan, @@ -20,6 +19,7 @@ export const DsTableBodyVirtualized = ({ }: DsTableBodyVirtualizedProps) => { const rowsMapRef = useRef(new Map()); const rowHeightsMapRef = useRef(new Map()); + const tbodyRef = useRef(null); const { rows } = table.getRowModel(); @@ -32,16 +32,16 @@ export const DsTableBodyVirtualized = ({ return item ? `${item.row.id}${item.isExpandedRowContent ? '-expanded-content' : ''}` : String(index); }; - const { loadDataIfNeeded } = useInfiniteScroll(tableContainerRef, rows.length, infiniteScroll); + const { loadDataIfNeeded } = useInfiniteScroll(tbodyRef, rows.length, infiniteScroll); - const rowVirtualizer = useVirtualizer({ + const rowVirtualizer = useVirtualizer({ count: rowsAndExpandedRowContent.length, estimateSize: (index) => { const cachedHeight = rowHeightsMapRef.current.get(getItemKey(index)); return cachedHeight || estimateSize; }, // estimate row height for accurate scrollbar dragging getItemKey, - getScrollElement: () => tableContainerRef.current, + getScrollElement: () => tbodyRef.current, // measure dynamic row height, except in firefox because it measures table border height incorrectly measureElement: typeof window !== 'undefined' && navigator.userAgent.indexOf('Firefox') === -1 @@ -86,6 +86,7 @@ export const DsTableBodyVirtualized = ({ return ( ({ }; interface DsTableBodyProps { - rowVirtualizer: Virtualizer; + ref: RefObject; + rowVirtualizer: Virtualizer; rowsMapRef: RefObject>; rowHeightsMapRef: RefObject>; rowsAndExpandedRowContent: { @@ -109,6 +111,7 @@ interface DsTableBodyProps { } function DsTableBody({ + ref, rowVirtualizer, rowsMapRef, rowHeightsMapRef, @@ -117,14 +120,10 @@ function DsTableBody({ rowSelection, }: DsTableBodyProps) { const virtualRows = rowVirtualizer.getVirtualItems(); + const totalSize = rowVirtualizer.getTotalSize(); return ( - + {virtualRows.length > 0 ? ( virtualRows.map((virtualRow) => { const row = rowsAndExpandedRowContent[virtualRow.index]; @@ -150,6 +149,10 @@ function DsTableBody({ {emptyState || EMPTY_TABLE_STATE_TEXT} )} + + + + ); } diff --git a/packages/design-system/src/components/ds-table/components/ds-table-body-virtualized/ds-table-body-virtualized.types.ts b/packages/design-system/src/components/ds-table/components/ds-table-body-virtualized/ds-table-body-virtualized.types.ts index 1bd5566d5..15db26800 100644 --- a/packages/design-system/src/components/ds-table/components/ds-table-body-virtualized/ds-table-body-virtualized.types.ts +++ b/packages/design-system/src/components/ds-table/components/ds-table-body-virtualized/ds-table-body-virtualized.types.ts @@ -1,4 +1,4 @@ -import { type RefObject, type ReactNode } from 'react'; +import { type ReactNode } from 'react'; import { type RowSelectionState, type Table } from '@tanstack/react-table'; export interface DsTableBodyVirtualizedProps { @@ -7,11 +7,6 @@ export interface DsTableBodyVirtualizedProps { * and column state. */ table: Table; - /** - * Ref to the scrollable container that hosts the virtualized rows. Required by - * the virtualizer to measure viewport size and listen for scroll events. - */ - tableContainerRef: RefObject; /** * Optional content rendered in place of rows when the table has no data. */ diff --git a/packages/design-system/src/components/ds-table/components/ds-table-header/ds-table-header.module.scss b/packages/design-system/src/components/ds-table/components/ds-table-header/ds-table-header.module.scss index 33731c3d7..9dba76d79 100644 --- a/packages/design-system/src/components/ds-table/components/ds-table-header/ds-table-header.module.scss +++ b/packages/design-system/src/components/ds-table/components/ds-table-header/ds-table-header.module.scss @@ -2,14 +2,11 @@ @use '../../styles/variables' as vars; .stickyHeader { - position: sticky; - top: 0; - z-index: 10; background-color: var(--background); } .virtualizedHeader { - display: grid; + display: block; } .headerRow { @@ -61,6 +58,7 @@ .selectHeaderCell { padding: 0; + justify-content: center; } .headerSortContainer { diff --git a/packages/design-system/src/components/ds-table/components/ds-table-header/ds-table-header.tsx b/packages/design-system/src/components/ds-table/components/ds-table-header/ds-table-header.tsx index 0471feb7e..c0743d870 100644 --- a/packages/design-system/src/components/ds-table/components/ds-table-header/ds-table-header.tsx +++ b/packages/design-system/src/components/ds-table/components/ds-table-header/ds-table-header.tsx @@ -30,7 +30,7 @@ const DsTableHeader = ({ table }: DsTableHeaderProps) => { Order )} {headerGroup.headers.map((header) => { - const headerStyle = getColumnSizeStyle(header.column.getSize(), virtualized); + const headerStyle = getColumnSizeStyle(header.column.getSize()); const canSort = header.column.getCanSort(); const isSelectColumn = header.column.id === SELECT_COLUMN_ID; diff --git a/packages/design-system/src/components/ds-table/components/ds-table-row-virtualized/ds-table-row-virtualized.tsx b/packages/design-system/src/components/ds-table/components/ds-table-row-virtualized/ds-table-row-virtualized.tsx index c9764ca2a..09dea7d5e 100644 --- a/packages/design-system/src/components/ds-table/components/ds-table-row-virtualized/ds-table-row-virtualized.tsx +++ b/packages/design-system/src/components/ds-table/components/ds-table-row-virtualized/ds-table-row-virtualized.tsx @@ -67,7 +67,7 @@ export const DsTableRowVirtualized = ({ <> {row.getVisibleCells().map((cell, idx) => { const isLastColumn = idx === row.getVisibleCells().length - 1; - const cellStyle = getColumnSizeStyle(cell.column.getSize(), true); + const cellStyle = getColumnSizeStyle(cell.column.getSize()); return ( { /** * TanStack virtualizer driving the scroll window for the table body. */ - rowVirtualizer: Virtualizer; + rowVirtualizer: Virtualizer; /** * Virtual item descriptor for this row (index, start, size) from the virtualizer. */ diff --git a/packages/design-system/src/components/ds-table/components/ds-table-row/ds-table-row.module.scss b/packages/design-system/src/components/ds-table/components/ds-table-row/ds-table-row.module.scss index 6af06433b..974977749 100644 --- a/packages/design-system/src/components/ds-table/components/ds-table-row/ds-table-row.module.scss +++ b/packages/design-system/src/components/ds-table/components/ds-table-row/ds-table-row.module.scss @@ -80,4 +80,5 @@ .selectableCell { padding: 0; text-align: center; + justify-content: center; } diff --git a/packages/design-system/src/components/ds-table/ds-table.module.scss b/packages/design-system/src/components/ds-table/ds-table.module.scss index eed577967..33176bf5a 100644 --- a/packages/design-system/src/components/ds-table/ds-table.module.scss +++ b/packages/design-system/src/components/ds-table/ds-table.module.scss @@ -1,5 +1,6 @@ @use '../../styles/typography'; @use '../../styles/utilities' as utils; +@use '../../styles/scrollbars' as scrollbars; @use './styles/variables' as vars; :root { @@ -21,7 +22,48 @@ $bulk-actions-height: 60px; .dataTableContainer { max-height: 100%; - overflow: auto; + overflow-x: auto; + overflow-y: hidden; + display: flex; + flex-direction: column; + + @include scrollbars.scrollbar-on-hover; + + > table { + display: flex; + flex-direction: column; + flex: 1; + min-height: 0; + width: max-content; + min-width: 100%; + } + + thead { + display: block; + flex-shrink: 0; + padding-right: scrollbars.$scrollbar-thin-size; + } + + tbody { + display: block; + overflow-y: auto; + flex: 1; + min-height: 0; + scrollbar-gutter: stable; + + @include scrollbars.scrollbar-on-hover; + } + + thead tr, + tbody tr { + display: flex; + } + + thead th, + tbody td { + display: flex; + align-items: center; + } } .table { @@ -45,14 +87,44 @@ $bulk-actions-height: 60px; text-align: center; } -.virtualized { - display: grid; -} - .virtualizedContainer { position: relative; height: vars.$virtualized-height; - overflow: auto; + overflow-x: auto; + overflow-y: hidden; + display: flex; + flex-direction: column; + + @include scrollbars.scrollbar-on-hover; + + > table { + display: flex; + flex-direction: column; + flex: 1; + min-height: 0; + width: max-content; + min-width: 100%; + } + + thead { + display: block; + flex-shrink: 0; + padding-right: scrollbars.$scrollbar-thin-size; + } + + tbody { + flex: 1; + min-height: 0; + } + + thead tr { + display: flex; + } + + thead th { + display: flex; + align-items: center; + } } .bulkActionsVisible { diff --git a/packages/design-system/src/components/ds-table/ds-table.tsx b/packages/design-system/src/components/ds-table/ds-table.tsx index 79eea18fb..740fbc5ec 100644 --- a/packages/design-system/src/components/ds-table/ds-table.tsx +++ b/packages/design-system/src/components/ds-table/ds-table.tsx @@ -281,18 +281,11 @@ const DsTable = ({ )} > - +
{virtualized ? ( [] = [ + { accessorKey: 'firstName', header: 'First Name', cell: (info) => info.getValue(), size: 250 }, + { accessorKey: 'lastName', header: 'Last Name', cell: (info) => info.getValue(), size: 250 }, + { accessorKey: 'age', header: 'Age (years)', cell: (info) => info.getValue(), size: 200 }, + { + accessorKey: 'visits', + header: 'Number of Visits', + cell: (info) => info.getValue(), + size: 250, + }, + { + accessorKey: 'status', + header: 'Relationship Status', + cell: (info) => info.getValue(), + size: 250, + }, + { + accessorKey: 'progress', + header: 'Profile Progress', + cell: (info) => `${String(info.getValue())}%`, + size: 250, + }, +]; const meta: Meta> = { title: 'Components/Table', @@ -44,3 +70,25 @@ export const NoBorder: Story = { bordered: false, }, }; + +export const HorizontalScroll: Story = { + parameters: { + docs: { + description: { + story: + 'When columns are wider than the container, the body and header scroll horizontally together while only the body scrolls vertically. The scrollbars are thin and become visible on hover.', + }, + }, + }, + decorators: [ + (Story) => ( +
+ +
+ ), + ], + args: { + data: defaultData, + columns: horizontalScrollColumns, + }, +}; diff --git a/packages/design-system/src/components/ds-table/utils/column-size.ts b/packages/design-system/src/components/ds-table/utils/column-size.ts index 69469d136..edbcf89fa 100644 --- a/packages/design-system/src/components/ds-table/utils/column-size.ts +++ b/packages/design-system/src/components/ds-table/utils/column-size.ts @@ -2,19 +2,20 @@ import type { CSSProperties } from 'react'; import { defaultColumnSizing } from '@tanstack/react-table'; /** - * Generates the appropriate style object for a table column/cell based on its size and virtualization state + * Generates the appropriate style object for a table column/cell based on its size + * + * Custom-sized columns get a fixed `width` plus `minWidth` and `flexShrink: 0` so + * they enforce horizontal overflow on the table container. Default-sized columns + * grow to fill the row evenly via `flex: 1`. * * @param columnSize - The size of the column - * @param virtualized - Whether the table is virtualized - * @returns Style object or undefined if no custom styling is needed + * @returns Style object for the column/cell */ -export const getColumnSizeStyle = (columnSize: number, virtualized?: boolean): CSSProperties | undefined => { +export const getColumnSizeStyle = (columnSize: number): CSSProperties => { const hasCustomSize = columnSize !== defaultColumnSizing.size; if (hasCustomSize) { - return { width: columnSize }; + return { width: columnSize, minWidth: columnSize, flexShrink: 0 }; } - if (virtualized) { - return { flex: 1 }; - } + return { flex: 1, minWidth: 0 }; }; diff --git a/packages/design-system/src/styles/_scrollbars.scss b/packages/design-system/src/styles/_scrollbars.scss index 413d0ded8..25cdd360f 100644 --- a/packages/design-system/src/styles/_scrollbars.scss +++ b/packages/design-system/src/styles/_scrollbars.scss @@ -35,6 +35,43 @@ $scrollbar-thin-size: 4px; } } +@mixin scrollbar-on-hover { + &::-webkit-scrollbar { + width: $scrollbar-thin-size; + height: $scrollbar-thin-size; + background: transparent; + } + + &::-webkit-scrollbar-track { + background: transparent; + } + + &::-webkit-scrollbar-corner { + background: transparent; + } + + &::-webkit-scrollbar-thumb { + background-color: transparent; + border-radius: $scrollbar-border-radius; + transition: background-color 0.2s; + } + + &:hover::-webkit-scrollbar-thumb { + background-color: var(--background-tertiary); + } + + &::-webkit-scrollbar-thumb:hover { + background-color: var(--background-deselected-hover); + } + + scrollbar-width: thin; + scrollbar-color: transparent transparent; + + &:hover { + scrollbar-color: var(--background-tertiary) transparent; + } +} + * { @include scrollbar-base($scrollbar-default-size); @include firefox-scrollbar($scrollbar-default-size); From 122552276b1855a460e87914575c5837e28f7257 Mon Sep 17 00:00:00 2001 From: vpolessky Date: Tue, 19 May 2026 12:55:09 +0200 Subject: [PATCH 2/2] feat(design-system): table scrollbar adjustment [AR-54032] --- .../ds-table-bulk-actions.module.scss | 4 ++-- .../ds-table-cell/ds-table-cell.module.scss | 2 ++ .../ds-table-row/ds-table-row.module.scss | 2 ++ .../components/ds-table/ds-table.module.scss | 20 ++++++++++--------- .../src/components/ds-table/ds-table.tsx | 20 +++++++++---------- 5 files changed, 27 insertions(+), 21 deletions(-) diff --git a/packages/design-system/src/components/ds-table/components/ds-table-bulk-actions/ds-table-bulk-actions.module.scss b/packages/design-system/src/components/ds-table/components/ds-table-bulk-actions/ds-table-bulk-actions.module.scss index 3566de565..232451adf 100644 --- a/packages/design-system/src/components/ds-table/components/ds-table-bulk-actions/ds-table-bulk-actions.module.scss +++ b/packages/design-system/src/components/ds-table/components/ds-table-bulk-actions/ds-table-bulk-actions.module.scss @@ -1,14 +1,14 @@ @use '../../../../styles/typography'; .bulkActionsContainer { - position: fixed; + position: absolute; bottom: 20px; left: 50%; transform: translateX(-50%); background-color: var(--color-dap-gray-050); border-radius: 8px; box-shadow: 0 4px 12px #0445cc66; - z-index: 1000; + z-index: 1; display: flex; align-items: center; border-radius: 8px; diff --git a/packages/design-system/src/components/ds-table/components/ds-table-cell/ds-table-cell.module.scss b/packages/design-system/src/components/ds-table/components/ds-table-cell/ds-table-cell.module.scss index 47eaec869..e09c7d9b4 100644 --- a/packages/design-system/src/components/ds-table/components/ds-table-cell/ds-table-cell.module.scss +++ b/packages/design-system/src/components/ds-table/components/ds-table-cell/ds-table-cell.module.scss @@ -1,6 +1,8 @@ @use '../../../../styles/typography'; .tableCellEllipsis { + flex: 1; + min-width: 0; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; diff --git a/packages/design-system/src/components/ds-table/components/ds-table-row/ds-table-row.module.scss b/packages/design-system/src/components/ds-table/components/ds-table-row/ds-table-row.module.scss index 974977749..6fcbad46c 100644 --- a/packages/design-system/src/components/ds-table/components/ds-table-row/ds-table-row.module.scss +++ b/packages/design-system/src/components/ds-table/components/ds-table-row/ds-table-row.module.scss @@ -50,6 +50,8 @@ background-color: var(--background-secondary); & > td { + flex: 1; + min-width: 0; padding: var(--standard); } diff --git a/packages/design-system/src/components/ds-table/ds-table.module.scss b/packages/design-system/src/components/ds-table/ds-table.module.scss index 33176bf5a..376bf84ec 100644 --- a/packages/design-system/src/components/ds-table/ds-table.module.scss +++ b/packages/design-system/src/components/ds-table/ds-table.module.scss @@ -1,5 +1,4 @@ @use '../../styles/typography'; -@use '../../styles/utilities' as utils; @use '../../styles/scrollbars' as scrollbars; @use './styles/variables' as vars; @@ -10,9 +9,10 @@ --ds-table-header-height: 45px; } -$bulk-actions-height: 60px; +$bulk-actions-reserved-space: 80px; .container { + position: relative; border-radius: vars.$border-radius; border-width: vars.$border-width; border-style: solid; @@ -41,7 +41,6 @@ $bulk-actions-height: 60px; thead { display: block; flex-shrink: 0; - padding-right: scrollbars.$scrollbar-thin-size; } tbody { @@ -49,7 +48,6 @@ $bulk-actions-height: 60px; overflow-y: auto; flex: 1; min-height: 0; - scrollbar-gutter: stable; @include scrollbars.scrollbar-on-hover; } @@ -82,8 +80,17 @@ $bulk-actions-height: 60px; width: 100%; } +.bulkActionsVisible { + tbody { + padding-bottom: $bulk-actions-reserved-space; + } +} + .emptyState { + flex: 1; + min-width: 0; height: vars.$empty-state-height; + justify-content: center; text-align: center; } @@ -109,7 +116,6 @@ $bulk-actions-height: 60px; thead { display: block; flex-shrink: 0; - padding-right: scrollbars.$scrollbar-thin-size; } tbody { @@ -126,7 +132,3 @@ $bulk-actions-height: 60px; align-items: center; } } - -.bulkActionsVisible { - padding-bottom: $bulk-actions-height; -} diff --git a/packages/design-system/src/components/ds-table/ds-table.tsx b/packages/design-system/src/components/ds-table/ds-table.tsx index ae09b9a1b..5fe196a67 100644 --- a/packages/design-system/src/components/ds-table/ds-table.tsx +++ b/packages/design-system/src/components/ds-table/ds-table.tsx @@ -323,17 +323,17 @@ const DsTable = ({ )}
+ {selectable && actions.length > 0 && ( + ({ + ...action, + onClick: () => action.onClick(selectedRows), + }))} + onClearSelection={table.resetRowSelection} + /> + )} - {selectable && actions.length > 0 && ( - ({ - ...action, - onClick: () => action.onClick(selectedRows), - }))} - onClearSelection={table.resetRowSelection} - /> - )} ); };