Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,45 @@

No changes yet.

## 4.5.0 - 2026-08-11

Version 4.5.0 improves fixed-region scrolling and fill-layout sizing without
removing or renaming any public prop, type, or package entrypoint.

### Features

- Added `scrollbarVisibility` for controlling automatic, persistent,
scroll-only, or hover-only data-table and card scrollbars. Use `"always"`
for data tables that must keep both scrollbars visible.

### Fixes

- Fixed the horizontal and vertical scrollbars and their corner to stack above
fixed left/right columns and top/bottom-pinned rows when both axes overflow.
- Fixed `layoutMode="fill"` to preserve every currently visible fixed-width
data column and place an internal inert flexible spacer before right-pinned
actions. Spacer presence now follows controlled and responsive
column-visibility changes, including grouped columns.
- Preserved horizontal overflow when visible fixed-width columns exceed the
viewport instead of compressing those columns, in both standard and virtual
table adapters.

### Compatibility

- Consumers do not need to define, order, pin, render, export, or navigate an
`__spacer__` column. The reserved column is created and managed internally.
- A genuine flexible visible data column continues to consume unused width and
prevents the internal spacer from being added.

### Validation

- 317 unit/integration tests across the shadcn, HeroUI, The Gridcn, and virtual
adapters
- lint, package/demo typechecks, deterministic package/demo builds, public API
snapshot, packed-consumer build, bundle budgets, and dependency audit
- browser regressions for dual-axis scrollbar stacking, fixed left/right
columns, a bottom-pinned row, fill-space allocation, and overflow retention

## 4.4.0 - 2026-08-09

Version 4.4.0 completes the core Phase 2–4 modernization work with additive,
Expand Down
24 changes: 17 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,15 @@ integrations. See
- toolbar search now filters client-side tables by default; disable with `manualFiltering` or `enableToolbarQueryFiltering={false}`
- column filters, CSV export, row expansion, density, column pinning/reordering, selection policies, labels, and column preference persistence were added

## 4.0.0 Compatibility
## 4.5.0 Compatibility

Version 4.0.0 removes no public prop, type, or package entrypoint. It is a
compatibility-first major that releases the accumulated state, persistence,
quality, dependency, accessibility, and package-splitting work after 3.0.9.
The previously planned API cleanup is deferred to a future 5.0 release.
Version 4.5.0 adds optional scrollbar visibility control and corrects
fixed-width `layoutMode="fill"` tables without removing any public API. Set
`scrollbarVisibility="always"` when persistent horizontal and vertical
scrollbars are required. When every currently visible data column has a fixed
width, fill layout now keeps those widths and automatically places a
transparent flexible spacer before a right-pinned actions column. Consumers
must not create or reference the reserved `__spacer__` column.

## 4.4.0 Compatibility

Expand All @@ -62,14 +65,21 @@ cell ranges, clipboard/paste, enhanced data operations, auto page sizing,
print/fullscreen controls, and error overlays are individually opt-in. No
public prop, type, or entrypoint was removed.

## 4.0.0 Compatibility

Version 4.0.0 removes no public prop, type, or package entrypoint. It is a
compatibility-first major that releases the accumulated state, persistence,
quality, dependency, accessibility, and package-splitting work after 3.0.9.
The previously planned API cleanup is deferred to a future 5.0 release.

## Installation

```bash
pnpm add github:Dastari/data-table-pro#v4.4.0
pnpm add github:Dastari/data-table-pro#v4.5.0
```

This package is installed from GitHub refs. It is not published to npm.
Release tags such as `v4.4.0` include committed `dist/` output, so consumers
Release tags such as `v4.5.0` include committed `dist/` output, so consumers
do not need to allow package build scripts during install.

Peer dependencies:
Expand Down
42 changes: 24 additions & 18 deletions api-snapshots/public-api.md

Large diffs are not rendered by default.

187 changes: 187 additions & 0 deletions browser-tests/data-table.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,76 @@ test("responsive behavior follows table container boundaries", async ({
await expect(header("location")).toHaveCount(0);
});

test("fill layout gives spare width to the internal spacer and preserves overflow", async ({
page,
}) => {
await page.setViewportSize({ width: 1700, height: 900 });
await page.goto("/");
await hideContentSizedCards(page);

const table = page.locator('[data-dtp-slot="data-table-root"]').first();
await table.evaluate((element) => {
element.style.flex = "none";
element.style.width = "1500px";
element.style.height = "520px";
});

const readLayout = () =>
table.evaluate((element) => {
const scrollArea = element.querySelector<HTMLElement>(
'[data-slot="scroll-area"]',
);
const viewport = element.querySelector<HTMLElement>(
'[data-slot="scroll-area-viewport"]',
);
const headers = Array.from(
element.querySelectorAll<HTMLElement>(
"thead tr:last-child [data-column-id]",
),
);
const name = headers.find(
(header) => header.dataset.columnId === "name",
);
const spacer = headers.find(
(header) => header.dataset.columnId === "__spacer__",
);
const actions = headers.find(
(header) => header.dataset.columnId === "__actions__",
);
if (!scrollArea || !viewport || !name || !spacer || !actions) {
throw new Error("Expected the fill-layout regression columns");
}
return {
actionIndex: headers.indexOf(actions),
clientWidth: viewport.clientWidth,
nameWidth: name.getBoundingClientRect().width,
scrollWidth: viewport.scrollWidth,
spacerIndex: headers.indexOf(spacer),
spacerWidth: spacer.getBoundingClientRect().width,
width: scrollArea.getBoundingClientRect().width,
};
});

await expect.poll(async () => (await readLayout()).spacerWidth).toBeGreaterThan(0);
const wideLayout = await readLayout();
expect(wideLayout.nameWidth).toBeCloseTo(220, 0);
expect(wideLayout.spacerIndex).toBe(wideLayout.actionIndex - 1);
expect(wideLayout.scrollWidth).toBeLessThanOrEqual(
wideLayout.clientWidth + 1,
);

await table.evaluate((element) => {
element.style.width = "720px";
});
await expect(table).toHaveCSS("width", "720px");
await expect.poll(async () => (await readLayout()).scrollWidth).toBeGreaterThan(720);

const narrowLayout = await readLayout();
expect(narrowLayout.nameWidth).toBeCloseTo(220, 0);
expect(narrowLayout.spacerIndex).toBe(narrowLayout.actionIndex - 1);
expect(narrowLayout.scrollWidth).toBeGreaterThan(narrowLayout.clientWidth);
});

test("interactive grid navigation follows the rendered cell geometry", async ({
page,
}) => {
Expand Down Expand Up @@ -242,3 +312,120 @@ test("interactive grid selects a pointer-dragged cell range", async ({ page }) =
await expect(firstCell).toHaveAttribute("aria-selected", "true");
await expect(fourthCell).toHaveAttribute("aria-selected", "true");
});

test("persistent scrollbars stack above fixed columns and a bottom-pinned row", async ({
page,
}) => {
await page.goto("/?scrollbar-regression=1");
await hideContentSizedCards(page);

const table = page.locator('[data-dtp-slot="data-table-root"]').first();
await table.evaluate((element) => {
element.style.flex = "none";
element.style.width = "720px";
element.style.height = "520px";
});

const scrollArea = table.locator('[data-slot="scroll-area"]').first();
const viewport = scrollArea.locator('[data-slot="scroll-area-viewport"]');
const horizontalScrollbar = scrollArea.locator(
'[data-slot="scroll-area-scrollbar"][data-orientation="horizontal"]',
);
const verticalScrollbar = scrollArea.locator(
'[data-slot="scroll-area-scrollbar"][data-orientation="vertical"]',
);
const corner = scrollArea.locator('[data-slot="scroll-area-corner"]');
const fixedLeftHeader = table.locator(
'thead [data-column-id="name"]',
);
const fixedActionsHeader = table.locator(
'thead [data-column-id="__actions__"]',
);
const bottomPinnedRow = table.locator(
'[data-dtp-slot="data-table-pinned-row"][data-row-pinned="bottom"]',
);

await expect(fixedLeftHeader).toBeVisible();
await expect(fixedActionsHeader).toBeVisible();
await expect(bottomPinnedRow).toBeVisible();
await expect(horizontalScrollbar).toBeVisible();
await expect(verticalScrollbar).toBeVisible();
await expect(corner).toBeVisible();

const geometry = await viewport.evaluate((element) => ({
clientHeight: element.clientHeight,
clientWidth: element.clientWidth,
scrollHeight: element.scrollHeight,
scrollWidth: element.scrollWidth,
}));
expect(geometry.scrollWidth).toBeGreaterThan(geometry.clientWidth);
expect(geometry.scrollHeight).toBeGreaterThan(geometry.clientHeight);

await viewport.evaluate((element) => {
element.scrollTop = element.scrollHeight;
});

const stacking = await scrollArea.evaluate((element) => {
const horizontal = element.querySelector<HTMLElement>(
'[data-slot="scroll-area-scrollbar"][data-orientation="horizontal"]',
);
const vertical = element.querySelector<HTMLElement>(
'[data-slot="scroll-area-scrollbar"][data-orientation="vertical"]',
);
const cornerElement = element.querySelector<HTMLElement>(
'[data-slot="scroll-area-corner"]',
);
if (!horizontal || !vertical || !cornerElement) {
throw new Error("Expected both scrollbars and their corner");
}

const topSlotAt = (x: number, y: number) =>
document
.elementFromPoint(x, y)
?.closest<HTMLElement>(
'[data-slot="scroll-area-scrollbar"], [data-slot="scroll-area-corner"]',
)
?.dataset.slot;
const horizontalRect = horizontal.getBoundingClientRect();
const verticalRect = vertical.getBoundingClientRect();
const cornerRect = cornerElement.getBoundingClientRect();
const fixedLeftRect = element
.querySelector<HTMLElement>('thead [data-column-id="name"]')
?.getBoundingClientRect();
const bottomPinnedRect = element
.querySelector<HTMLElement>(
'[data-dtp-slot="data-table-pinned-row"][data-row-pinned="bottom"]',
)
?.getBoundingClientRect();
if (!fixedLeftRect || !bottomPinnedRect) {
throw new Error("Expected fixed and pinned regression regions");
}

return {
horizontalZIndex: getComputedStyle(horizontal).zIndex,
verticalZIndex: getComputedStyle(vertical).zIndex,
cornerZIndex: getComputedStyle(cornerElement).zIndex,
overFixedLeft: topSlotAt(
fixedLeftRect.left + fixedLeftRect.width / 2,
horizontalRect.top + horizontalRect.height / 2,
),
overFixedActions: topSlotAt(
verticalRect.left + verticalRect.width / 2,
bottomPinnedRect.top + bottomPinnedRect.height / 2,
),
atCorner: topSlotAt(
cornerRect.left + cornerRect.width / 2,
cornerRect.top + cornerRect.height / 2,
),
};
});

expect(stacking).toEqual({
horizontalZIndex: "100",
verticalZIndex: "100",
cornerZIndex: "100",
overFixedLeft: "scroll-area-scrollbar",
overFixedActions: "scroll-area-scrollbar",
atCorner: "scroll-area-corner",
});
});
11 changes: 11 additions & 0 deletions demo/src/DemoApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,9 @@ const statuses: Array<Employee["status"]> = [
const priorities: Array<Employee["priority"]> = ["low", "medium", "high"];

export function DemoApp() {
const [scrollbarRegressionFixture] = React.useState(() =>
new URLSearchParams(window.location.search).has("scrollbar-regression"),
);
const [adapter, setAdapter] = React.useState<AdapterKey>("shadcn");
const [theme, setTheme] = React.useState<ThemeKey>("light");
const [rows, setRows] = React.useState(() => generateEmployees(96));
Expand Down Expand Up @@ -521,6 +524,14 @@ export function DemoApp() {
enableDensityToggle
enableColumnPinning
enableColumnReordering
rowPinning={
scrollbarRegressionFixture
? { top: [], bottom: ["emp-001"] }
: undefined
}
scrollbarVisibility={
scrollbarRegressionFixture ? "always" : undefined
}
columnPrefsKey={`demo-${adapter}`}
labels={{
exportCsv: "Download CSV",
Expand Down
2 changes: 1 addition & 1 deletion dist/adapter-virtual.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@ import { D as DataTableUiKit } from './ui-kit-C6Z8X6oi.js';
export { a as DataTableUiClassNames } from './ui-kit-C6Z8X6oi.js';
import '@tanstack/react-table';

declare function createVirtualDataTable(ui: DataTableUiKit): <TData>({ columns, data, getRowId, children, title, description, toolbarQueryValue, onToolbarQueryValueChange, toolbarQueryPlaceholder, toolbarQueryDebounceMs, manualFiltering, enableToolbarQueryFiltering, globalFilterFn, columnFilters, onColumnFiltersChange, enableColumnFilters, customToolbar, compactToolbar, rowsPerPageOptions, totalRowCount, hasNextPage, sorting, onSortingChange, manualSorting, grouping, onGroupingChange, manualGrouping, enableGrouping, groupedColumnMode, aggregationFns, pageIndex, pageSize, autoPageSize, onPageIndexChange, onPageSizeChange, pageCount, manualPagination, rowSelection, onRowSelectionChange, enableRowSelection, enableMultiRowSelection, enableSubRowSelection, getRowCanSelect, rowSelectionSelectAllScope, expanded, onExpandedChange, getSubRows, manualExpanding, paginateExpandedRows, filterFromLeafRows, maxLeafRowFilterDepth, detailPanel, getRowCanExpand, renderExpandedRow, columnOrder, onColumnOrderChange, enableColumnReordering, columnGroupHeaderHeight, columnPinning, onColumnPinningChange, enableColumnPinning, rowPinning, onRowPinningChange, enableRowPinning, keepPinnedRows, toolbarActions, selectionActions, rowActions, csvExport, clipboard, enableCellSelection, cellSelection, defaultCellSelection, onCellSelectionChange, gridCommands, density, onDensityChange, enableDensityToggle, columnPrefsKey, persistence, savedViews, toolbarDataOperations, initialState, state: unifiedState, onStateChange, apiRef, labels, summaryRows, cardRenderer, cardSizing, cardGridClassName, cardClassName, viewMode, onViewModeChange, enableViewToggle, enablePrint, enableFullscreen, emptyState, stateOverlay, isLoading, loadingRowCount, getRowLoadingState, hiddenRows, showHiddenRows, onShowHiddenRowsChange, infiniteScroll, editableRows, columnVisibility, onColumnVisibilityChange, columnSizing, onColumnSizingChange, enableColumnResizing, columnResizeMode, layoutMode, stickyHeader, showFooter, showToolbar, dir, flexGrow, toolbarVisibility, className, tableClassName, tableContainerClassName, stripedRows, getRowClassName, onRowClick, onActionError, dragAndDrop, fileUpload, virtualization, accessibility, interactiveGrid, }: DataTableProps<TData>) => React.JSX.Element;
declare function createVirtualDataTable(ui: DataTableUiKit): <TData>({ columns, data, getRowId, children, title, description, toolbarQueryValue, onToolbarQueryValueChange, toolbarQueryPlaceholder, toolbarQueryDebounceMs, manualFiltering, enableToolbarQueryFiltering, globalFilterFn, columnFilters, onColumnFiltersChange, enableColumnFilters, customToolbar, compactToolbar, rowsPerPageOptions, totalRowCount, hasNextPage, sorting, onSortingChange, manualSorting, grouping, onGroupingChange, manualGrouping, enableGrouping, groupedColumnMode, aggregationFns, pageIndex, pageSize, autoPageSize, onPageIndexChange, onPageSizeChange, pageCount, manualPagination, rowSelection, onRowSelectionChange, enableRowSelection, enableMultiRowSelection, enableSubRowSelection, getRowCanSelect, rowSelectionSelectAllScope, expanded, onExpandedChange, getSubRows, manualExpanding, paginateExpandedRows, filterFromLeafRows, maxLeafRowFilterDepth, detailPanel, getRowCanExpand, renderExpandedRow, columnOrder, onColumnOrderChange, enableColumnReordering, columnGroupHeaderHeight, columnPinning, onColumnPinningChange, enableColumnPinning, rowPinning, onRowPinningChange, enableRowPinning, keepPinnedRows, toolbarActions, selectionActions, rowActions, csvExport, clipboard, enableCellSelection, cellSelection, defaultCellSelection, onCellSelectionChange, gridCommands, density, onDensityChange, enableDensityToggle, columnPrefsKey, persistence, savedViews, toolbarDataOperations, initialState, state: unifiedState, onStateChange, apiRef, labels, summaryRows, cardRenderer, cardSizing, cardGridClassName, cardClassName, viewMode, onViewModeChange, enableViewToggle, enablePrint, enableFullscreen, emptyState, stateOverlay, isLoading, loadingRowCount, getRowLoadingState, hiddenRows, showHiddenRows, onShowHiddenRowsChange, infiniteScroll, editableRows, columnVisibility, onColumnVisibilityChange, columnSizing, onColumnSizingChange, enableColumnResizing, columnResizeMode, layoutMode, stickyHeader, scrollbarVisibility, showFooter, showToolbar, dir, flexGrow, toolbarVisibility, className, tableClassName, tableContainerClassName, stripedRows, getRowClassName, onRowClick, onActionError, dragAndDrop, fileUpload, virtualization, accessibility, interactiveGrid, }: DataTableProps<TData>) => React.JSX.Element;

export { DataTableUiKit, createVirtualDataTable };
2 changes: 1 addition & 1 deletion dist/adapter-virtual.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading