🐛 Fix #789: rewrite column resize to honor declarative widths#803
Open
gfazioli wants to merge 8 commits into
Open
🐛 Fix #789: rewrite column resize to honor declarative widths#803gfazioli wants to merge 8 commits into
gfazioli wants to merge 8 commits into
Conversation
Add full RTL support, update deps, bump version to release 9.3.9
Release 8.3.10
Update GitHub workflow action versions
Release 8.3.11 to fix regression in 8.3.10
Update dev deps
Release 8.3.13
The previous implementation forced `table-layout: fixed` on every render whenever any column was resizable, which broke the documented `width: '0%'` trick on actions columns (issue icflorescu#789): the column was rendered at 0px and the title was clipped by `overflow: hidden`. Adopt the lazy-snapshot pattern from `mantine-list-view-table`: - Default to `table-layout: auto` so declarative widths (including `0%`) work as intended. - On the first `mousedown` on a resize handle, snapshot every header cell width into pixels, lock the table total width, and switch to `table-layout: fixed` (state: `isLocked`). - Drive resize updates through React state on every `mousemove` (instead of bypassing React with direct DOM mutation), so widths actually flow through `effectiveColumns` and persist correctly across renders. - Persist to `localStorage` in a dedicated `useEffect` gated by a `dirty` flag, never inside a `setState` updater (fixes the "Cannot update a component while rendering a different component" warning). Also: - Fix latent bug in `useDataTableColumns.effectiveColumns`: widths are now applied even when no `storeColumnsKey` is provided. - Move `useDataTableColumnReorder` alignment side-effect out of render into a `useEffect` to avoid setState-during-render warnings. - `DataTable` now consumes `dragToggle.effectiveColumns` directly so resize widths reach the rendered `<th>` / `<td>` cells.
There was a problem hiding this comment.
Pull request overview
Reworks the package-side column resizing flow so Mantine DataTable starts in table-layout: auto, snapshots pixel widths lazily on first resize, and then drives subsequent width changes through React state instead of direct DOM mutation. This fits the core package/ component implementation and aims to fix the documented width: '0%' actions-column behavior while keeping resize persistence and drag/toggle integrations working.
Changes:
- Rewrites
useDataTableColumnResizearound lazy width snapshotting, locked fixed-layout mode, and effect-based persistence. - Updates
DataTableand the resize handle/context plumbing to consume hook-enriched columns and expose resize lifecycle actions. - Simplifies CSS/layout handling by removing the unconditional fixed-layout rule and only locking layout after a resize starts.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
package/hooks/useDataTableColumns.ts |
Applies width state unconditionally and returns new resize-lock metadata/actions. |
package/hooks/useDataTableColumnResize.ts |
Main resize rewrite: localStorage hydration, lock state, table width tracking, and resize lifecycle. |
package/hooks/useDataTableColumnReorder.ts |
Moves persisted order alignment out of render and into memo/effect flow. |
package/DataTableResizableHeaderHandle.tsx |
Switches drag behavior from direct DOM mutation to state-driven width updates. |
package/DataTableDragToggleProvider.tsx |
Threads beginResize/endResize through the shared columns provider. |
package/DataTableColumns.context.ts |
Extends the columns context contract with resize lifecycle methods. |
package/DataTable.tsx |
Uses hook-enriched columns for rendering and conditionally locks table layout/width. |
package/DataTable.css |
Removes unconditional fixed layout and keeps header overflow rules for resizable tables. |
Five issues raised by the Copilot reviewer, all fixed.
1. Hydration restored `effectiveColumnsWidth` and flipped `isLocked`, but never
recomputed `tableWidth`. After a refresh the table re-entered
`table-layout: fixed` without a locked total, so saved pixel widths got
stretched to fill the viewport.
2. `useLocalStorage` was hardcoded to `getInitialValueInEffect: false`, but the
hydration effect bailed out when callers passed `getInitialValueInEffect={false}`
at the API level, leaving the synchronous-hydration mode unable to apply
persisted widths.
3. `effectiveColumnsWidth` was seeded only on first render. Runtime changes to
the `columns` prop (added/removed columns, declarative width changes) never
realigned the state, so stale entries kept overriding new column definitions.
4. `tableWidth` was captured once at first resize and never recomputed when
columns were toggled visible/hidden, leaving the table locked to a stale
total that no longer matched the visible columns.
5. The persistence effect wrote to `localStorage` on every `mousemove`, since
`dirtyRef` was flipped per drag frame. `localStorage.setItem` is synchronous
and made the new state-driven drag noticeably less smooth.
Fixes:
- `useLocalStorage` now receives the caller's `getInitialValueInEffect` and the
`useState` initializer seeds from `storedColumnsWidth` synchronously when
available, with the hydration effect catching the async case.
- A new realign effect drops orphan entries and adds defaults for newly added
columns whenever the `columns` prop changes.
- Persistence is gated on `!isResizing`, so the localStorage write happens
exactly once at the end of a drag instead of on every frame.
- `isLocked` and `tableWidth` are now derived in `useDataTableColumns` from
the visibility-aware `effectiveColumns`. The lock engages only when every
visible column has a pixel width — mixed states stay in `auto` layout so the
browser keeps auto cells visible — and the total recomputes on every
visibility change.
- `beginResize` lost its `isLocked` early-return and now always snapshots from
the DOM. Snapshotting an already-pixel-locked layout is idempotent.
Verified in browser:
- Resize round-trip persists across reload with the correct table width.
- 20 simulated mousemoves during a drag produce 0 localStorage writes; mouseup
produces exactly 1.
- Initial render with empty storage stays in `auto` layout.
This was referenced May 4, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #789.
Likely also closes:
table-layout: fixedas soon as any column hadresizable: true, which gave every column an equal share of the table width. This PR keepsautolayout until the first resize, so resizable columns render at their natural content width by default.'', which broke later resize attempts. The new realign effect always seeds new columns withcolumn.width ?? 'auto', and'auto'is treated as "no override" so the column gets a normal initial render.Possibly also closes (worth a re-test from the original reporters):
'auto'andisLockedis derived from the current state, so a mixed (some pixel / someauto) snapshot drops the table back toautolayout instead of leaving columns at 0 px.useDataTableColumnscausing unnecessary updates. The setState-during-render inuseDataTableColumnReorder(one of the classic causes of "Maximum update depth exceeded") has been moved into auseEffect.Background
The previous implementation forced
table-layout: fixedon every render whenever any column was resizable. That broke the documentedwidth: '0%'recipe on actions columns: the cell rendered at 0 px and the title was clipped byoverflow: hidden. The bug was introduced in #711.This PR adopts the lazy-snapshot pattern from
mantine-list-view-table:table-layout: autoso declarative widths (including0%) work as intended.mousedownon a resize handle, snapshot every header cell width into pixels and switch totable-layout: fixedso pixel widths are honored strictly during the drag and afterwards.mousemove(instead of bypassing React with direct DOM mutation), so widths actually flow througheffectiveColumnsand persist correctly across renders.localStoragein a dedicateduseEffectgated by adirtyRefflag and!isResizing, so the write happens once at the end of a drag rather than on every frame.isLockedand the lockedtableWidthare derived from the visibility-awareeffectiveColumns; the lock engages only when every visible column has a pixel width, and the total recomputes when a column is hidden/shown.Side fixes picked up along the way
useDataTableColumns.effectiveColumns: column widths are now applied even when nostoreColumnsKeyis provided. Previously, without a key,columnsOrderwas undefined and the width-application path was skipped, so resize state never reached cells via React.DataTablenow consumesdragToggle.effectiveColumnsdirectly. Before this PR it used its own localeffectiveColumns(raw user columns) and the resize widths only reached cells via direct DOM mutation in the handle.useDataTableColumnReorder: column-order alignment moved out of render into auseEffectto drop the setState-during-render warning.getInitialValueInEffect: the resize hook used to hardcode the synchronous-hydration mode on the underlyinguseLocalStoragewhile bailing out of the hydration effect when callers passedgetInitialValueInEffect={false}at the API level. Both paths now apply persisted widths.columnschange: a dedicated effect drops orphan width entries for removed accessors and seeds defaults for newly introduced columns.Files touched
package/DataTable.tsx— dropfixedLayoutEnabledstate; consume enriched columns from the hook; applytableLayout: 'fixed'andwidth: tableWidthonly when locked.package/DataTable.css— remove the unconditionaltable-layout: fixedrule and obsolete compensation rules.package/DataTableResizableHeaderHandle.tsx— callbeginResize()/endResize(), drive resize via state on everymousemove.package/DataTableColumns.context.ts+DataTableDragToggleProvider.tsx— exposebeginResize/endResizethrough context.package/hooks/useDataTableColumnResize.ts— rewrite around lazy snapshot + state-driven updates; honorsgetInitialValueInEffect; realigns oncolumnschange; persistence skipped while a drag is in flight.package/hooks/useDataTableColumns.ts— apply widths regardless of order; deriveisLockedandtableWidthfromeffectiveColumns.package/hooks/useDataTableColumnReorder.ts— move side-effect intouseEffect.Test plan
pnpm lintclean (typecheck + eslint)pnpm build:packagecleanpnpm build:docsclean/examples/row-actions-cell(withresizable: trueadded on the other columns to reproduce Actions columnwidth: 0%is causing wrong initial column width when any column is resizable #789): the actions column withwidth: '0%'shows its title and renders at content width before any resize./examples/column-resizing(basic and complex): drag is smooth and precise; trade between adjacent columns is exact; double-click resets a column toauto.storeColumnsKey); the table re-locks at the correct total width./examples/column-dragging-and-toggling: reorder + visibility toggle still work; no React warnings in the console./examples/pinning-the-first-column(with resizable columns added temporarily): pinned column stays sticky during/after resize,::aftershadow not clipped.mousemoveevents during a drag produce 0localStorage.setItemcalls;mouseupproduces exactly 1.Cannot update a component while rendering...warning anywhere.Notes for reviewers
useDataTableColumnResizekeepsmeasureAndSetColumnWidthsas a no-op for backwards compatibility (measurement is now lazy and triggered from the handle).useColumnResizestandalone hook inpackage/hooks/useColumnResize.tswas not changed — it isn't used internally and its surface is preserved for any external consumers.