Skip to content

🐛 Fix #789: rewrite column resize to honor declarative widths#803

Open
gfazioli wants to merge 8 commits into
icflorescu:nextfrom
gfazioli:fix/789-resize-rewrite
Open

🐛 Fix #789: rewrite column resize to honor declarative widths#803
gfazioli wants to merge 8 commits into
icflorescu:nextfrom
gfazioli:fix/789-resize-rewrite

Conversation

@gfazioli
Copy link
Copy Markdown
Contributor

@gfazioli gfazioli commented May 4, 2026

Summary

Closes #789.

Likely also closes:

  • Resizable columns render with equal width instead of auto-fitting content #736Resizable columns render with equal width instead of auto-fitting content. The previous code forced table-layout: fixed as soon as any column had resizable: true, which gave every column an equal share of the table width. This PR keeps auto layout until the first resize, so resizable columns render at their natural content width by default.
  • Column cannot be resized after being dynamically added #694Column cannot be resized after being dynamically added. The old default for new columns was the empty string '', which broke later resize attempts. The new realign effect always seeds new columns with column.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):

Background

The previous implementation forced table-layout: fixed on every render whenever any column was resizable. That broke the documented width: '0%' recipe on actions columns: the cell rendered at 0 px and the title was clipped by overflow: hidden. The bug was introduced in #711.

This PR adopts 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 and switch to table-layout: fixed so pixel widths are honored strictly during the drag and afterwards.
  • 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 dirtyRef flag and !isResizing, so the write happens once at the end of a drag rather than on every frame.
  • isLocked and the locked tableWidth are derived from the visibility-aware effectiveColumns; 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

  • Latent bug in useDataTableColumns.effectiveColumns: column widths are now applied even when no storeColumnsKey is provided. Previously, without a key, columnsOrder was undefined and the width-application path was skipped, so resize state never reached cells via React.
  • DataTable now consumes dragToggle.effectiveColumns directly. Before this PR it used its own local effectiveColumns (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 a useEffect to drop the setState-during-render warning.
  • Honors getInitialValueInEffect: the resize hook used to hardcode the synchronous-hydration mode on the underlying useLocalStorage while bailing out of the hydration effect when callers passed getInitialValueInEffect={false} at the API level. Both paths now apply persisted widths.
  • Realigns on columns change: a dedicated effect drops orphan width entries for removed accessors and seeds defaults for newly introduced columns.

Files touched

  • package/DataTable.tsx — drop fixedLayoutEnabled state; consume enriched columns from the hook; apply tableLayout: 'fixed' and width: tableWidth only when locked.
  • package/DataTable.css — remove the unconditional table-layout: fixed rule and obsolete compensation rules.
  • package/DataTableResizableHeaderHandle.tsx — call beginResize() / endResize(), drive resize via state on every mousemove.
  • package/DataTableColumns.context.ts + DataTableDragToggleProvider.tsx — expose beginResize / endResize through context.
  • package/hooks/useDataTableColumnResize.ts — rewrite around lazy snapshot + state-driven updates; honors getInitialValueInEffect; realigns on columns change; persistence skipped while a drag is in flight.
  • package/hooks/useDataTableColumns.ts — apply widths regardless of order; derive isLocked and tableWidth from effectiveColumns.
  • package/hooks/useDataTableColumnReorder.ts — move side-effect into useEffect.

Test plan

  • pnpm lint clean (typecheck + eslint)
  • pnpm build:package clean
  • pnpm build:docs clean
  • /examples/row-actions-cell (with resizable: true added on the other columns to reproduce Actions column width: 0% is causing wrong initial column width when any column is resizable #789): the actions column with width: '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 to auto.
  • Persistence: resize → reload → pixel widths are still applied (uses 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, ::after shadow not clipped.
  • Drag traffic: 20 simulated mousemove events during a drag produce 0 localStorage.setItem calls; mouseup produces exactly 1.
  • No Cannot update a component while rendering... warning anywhere.

Notes for reviewers

  • Public API of useDataTableColumnResize keeps measureAndSetColumnWidths as a no-op for backwards compatibility (measurement is now lazy and triggered from the handle).
  • The useColumnResize standalone hook in package/hooks/useColumnResize.ts was not changed — it isn't used internally and its surface is preserved for any external consumers.

icflorescu and others added 7 commits December 5, 2025 17:41
Add full RTL support, update deps, bump version to release 9.3.9
Update GitHub workflow action versions
Release 8.3.11 to fix regression in 8.3.10
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.
Copilot AI review requested due to automatic review settings May 4, 2026 13:59
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 useDataTableColumnResize around lazy width snapshotting, locked fixed-layout mode, and effect-based persistence.
  • Updates DataTable and 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.

Comment thread package/hooks/useDataTableColumnResize.ts Outdated
Comment thread package/hooks/useDataTableColumnResize.ts Outdated
Comment thread package/hooks/useDataTableColumnResize.ts Outdated
Comment thread package/hooks/useDataTableColumnResize.ts Outdated
Comment thread package/hooks/useDataTableColumnResize.ts Outdated
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants