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
19 changes: 19 additions & 0 deletions .changeset/data-table-sticky-action-header.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
"@object-ui/components": patch
---

fix(data-table): keep the right-pinned action column HEADER sticky on horizontal scroll (objectui#2784)

The row-actions column is pinned to the right edge by injecting `sticky right-0`
into the column's `className`, which reaches both the body cells and the header
cell. Body cells stayed pinned, but the header cell unconditionally appended a
`relative` position utility (it anchors the column-resize handle) — and since
`cn` is `tailwind-merge`, the later `relative` won over the injected `sticky`.
So the "操作" title scrolled away while its body cells stayed frozen.

The header now detects a right-pinned column (its `className` carries
`sticky` + `right-0`), skips `relative` for it (a sticky cell is already its own
positioning context, so the `absolute` resize handle still anchors correctly),
and re-asserts `sticky right-0 z-20` after `col.className` so tailwind-merge
keeps the pin and it stacks above the body's pinned cells (z-10). Left-frozen
columns, the resize handle, and non-pinned columns are unaffected.
18 changes: 17 additions & 1 deletion packages/components/src/renderers/complex/data-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1333,6 +1333,15 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => {
const isDragging = draggedColumn === index;
const isDragOver = dragOverColumn === index;
const isFrozen = frozenColumns > 0 && index < frozenColumns;
// Right-pinned columns (e.g. the auto-pinned row-actions column)
// carry their sticky class via `col.className`. The header cell
// otherwise appends a `relative` position utility below, which —
// because `cn` is tailwind-merge — would win over that `sticky`
// and let the header scroll away while its body cells stay pinned.
// Detect it here so we skip `relative` and re-assert the pin.
const isPinnedRight = typeof col.className === 'string'
&& /\bsticky\b/.test(col.className)
&& /\bright-0\b/.test(col.className);
const frozenOffset = isFrozen
? measuredStickyLefts?.[(selectable ? 1 : 0) + (showRowNumbers ? 1 : 0) + index]
?? columns.slice(0, index).reduce((sum, c, i) => {
Expand All @@ -1355,7 +1364,14 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => {
col.align === 'right' && 'text-right',
col.align === 'center' && 'text-center',
isFit && 'whitespace-nowrap',
'relative group bg-background',
'group bg-background',
// `relative` anchors the resize handle; a sticky cell is
// already its own positioning context, so only add it when
// the column isn't right-pinned (else it clobbers sticky).
!isPinnedRight && 'relative',
// Re-assert the pin AFTER col.className so tailwind-merge
// keeps it, and bump above body pinned cells (z-10).
isPinnedRight && 'sticky right-0 z-20',
isFrozen && 'sticky z-20',
isFrozen && index === frozenColumns - 1 && 'border-r-2 border-border shadow-[2px_0_4px_-2px_rgba(0,0,0,0.1)]',
)}
Expand Down
14 changes: 14 additions & 0 deletions packages/plugin-grid/src/__tests__/row-action-sticky.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,20 @@ describe('row-actions column sticky-right', () => {
expect(container.querySelector('td.right-0')).not.toBeNull(); // the actions column
});

it('pins the actions COLUMN HEADER to the right, not just the body cells', async () => {
// Regression: the header's own `relative` position class (added for the
// resize handle) was clobbering the injected `sticky right-0` via
// tailwind-merge, so the title scrolled away while its cells stayed pinned.
renderGrid();
await waitFor(() => expect(screen.getAllByTestId('row-action-inline-open').length).toBeGreaterThan(0));
const headers = Array.from(document.querySelectorAll('th'));
const actionsHeader = headers[headers.length - 1];
expect(actionsHeader).toBeTruthy();
expect(actionsHeader.className).toContain('sticky');
expect(actionsHeader.className).toContain('right-0');
expect(actionsHeader.className).not.toContain('relative');
});

it('still surfaces the actions inline button (no regression from pinning)', async () => {
renderGrid();
await waitFor(() => expect(screen.getAllByTestId('row-action-inline-open').length).toBe(ROWS.length));
Expand Down
Loading