feat: pinned-column + sticky-header shadows, scrollProps wrapper, generalized borders#28
Merged
Conversation
Closes #27. API: - Extend `ListViewTableColumn['sticky']` from `boolean` to `boolean | 'left' | 'right'`. `true` is preserved as an alias for `'left'` (full backward compatibility). - New `useStickyShadow` hook (exported alongside the other column hooks) watches horizontal scroll on the closest scrollable ancestor and writes `--lvt-shadow-left-opacity` / `--lvt-shadow-right-opacity` on the table via `requestAnimationFrame`-throttled scroll events. A `ResizeObserver` covers the layout-after-mount cases (Mantine `ScrollArea` viewport, font reflows, column resizing). Rendering: - New `stickyColumnShadow` Styles API selector — a real `<span>` rendered inside the *last* sticky-left and *first* sticky-right header and body cells. Its opacity is driven by the CSS variables above; no React re-renders during scroll. Customize via `--lvt-shadow-color` (default rgba(0,0,0,0.15) light / rgba(0,0,0,0.4) dark) and `--lvt-shadow-width` (default 4px). - Sticky cells switch to `overflow: visible` so the shadow can render outside the cell border. Ellipsis rules move to an inner wrapper for sticky cells; non-sticky cells keep the existing structure. - New `data-sticky-side` (`"left"` / `"right"`) and `data-sticky-shadow` modifiers on header and body cells. Scroll-ancestor detection: - Match any ancestor with `overflow-x !== 'visible'`. Mantine `ScrollArea`'s viewport uses `overflow-x: hidden` while still driving `scrollLeft` programmatically, so an `auto`/`scroll`-only match would fall back to `window` and miss the real scroll target. Tests: 47/47 passing — new suites for the hook (initial mount + disabled short-circuit) and the multi-side pinning (data-sticky-side, shadow attribution, sticky=true backward-compat).
- New `docs/demos/ListViewTable.demo.pinnedColumns.tsx` showing
`sticky: 'left'` and `sticky: 'right'` together inside a
`Table.ScrollContainer` so the gradient shadow is visible during
horizontal scroll.
- New "Pinned Columns" section in `docs.mdx` between "Sticky Header"
and "Row Selection" with code example and a note about the
CSS variables for theming the shadow.
- Update `ListViewTable.styles-api.ts` with:
- the new `stickyColumnShadow` selector
- the four new CSS variables (`--lvt-shadow-color`,
`--lvt-shadow-width`, `--lvt-shadow-left-opacity`,
`--lvt-shadow-right-opacity`)
- new modifiers for `data-sticky-side` (cell + headerCell) and
`data-side` (stickyColumnShadow)
Contributor
There was a problem hiding this comment.
Pull request overview
This PR enhances ListViewTable column pinning by adding support for right-side sticky columns (sticky: 'right') and introducing a scroll-driven gradient shadow cue on the inner edge of pinned columns, with documentation, demos, and tests to validate the behavior.
Changes:
- Extended
column.stickyto supportboolean | 'left' | 'right'(withtruepreserved as'left'). - Added
useStickyShadowhook to update shadow opacity CSS variables based on horizontal scroll (rAF-throttled). - Rendered a new
stickyColumnShadowStyles API slot and updated docs/demos/styles-api accordingly.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| package/src/types.ts | Extends sticky typing and registers new Styles API selector + CSS variables. |
| package/src/ListViewTable.tsx | Implements left/right sticky handling, shadow span rendering, and wires useStickyShadow. |
| package/src/ListViewTable.test.tsx | Adds coverage for sticky side attributes and shadow span rendering rules. |
| package/src/ListViewTable.module.css | Adds .stickyColumnShadow styling and new shadow CSS variables/defaults. |
| package/src/index.ts | Re-exports useStickyShadow and its options type from the package entry. |
| package/src/hooks/use-sticky-shadow.ts | New hook to compute and write --lvt-shadow-*-opacity on scroll/resize/RO updates. |
| package/src/hooks/use-sticky-shadow.test.tsx | Adds basic tests for enabled/disabled behavior and initial variable writes. |
| package/src/hooks/index.ts | Exports the new hook from the hooks barrel. |
| docs/styles-api/ListViewTable.styles-api.ts | Documents new selector and shadow-related CSS variables/modifiers. |
| docs/docs.mdx | Adds “Pinned Columns” docs section describing left/right pinning and shadow theming. |
| docs/demos/ListViewTable.demo.pinnedColumns.tsx | Adds a demo showcasing both-side pinning and horizontal scroll. |
| docs/demos/index.ts | Registers the new pinned columns demo export. |
Mirror mantine-datatable's sticky-column rendering: pinned columns now have a fully opaque background (`var(--mantine-color-body)` light / `var(--mantine-color-dark-7)` dark) so the columns underneath are visually occluded as the table scrolls horizontally. The only edge cue remains the gradient shadow span introduced by this PR. The previous implementation used a `::before` pseudo-element with `backdrop-filter: blur(var(--list-view-sticky-blur, 3px))`, which made the underlying content visible-but-blurred — distracting and a regression versus mantine-datatable's reference behavior. Removed: - The `::before` pseudo-element on `.stickyColumn` and `.stickyHeaderColumn` (with all backdrop-filter rules) - `--list-view-sticky-blur` from `ListViewTableCssVariables`, the varsResolver, and the Styles API docs The variable was unused outside the removed blur rule, so the cleanup does not break any other styling path.
…xtension Two issues addressed: 1. Right-pinned cells were also receiving the legacy `left: 0` from the `.stickyColumn` / `.stickyHeaderColumn` class while the renderer set `right: 0` inline. With both `left: 0` and `right: 0` simultaneously the browser stretched the cell unpredictably and the underlying scroll content briefly leaked at the edges. The `left: 0` is removed from the class — `position: sticky`, `left`, and `right` are now set inline on every sticky cell, with `left: 'auto'` / `right: 'auto'` set explicitly on the *opposite* side to neutralize any stale class rule. 2. The gradient shadow span fades to transparent on its outer edge, so without something opaque underneath the shadow region the user could see fragments of the underlying scroll content drifting through the shadow as columns went past. Adding a sharp `box-shadow` of the cell's own background color (`--mantine-color-body`) on the inner edge of the last sticky-left and first sticky-right cells extends the opaque background by `--lvt-shadow-width` outside the cell border. The gradient now fades into solid cell color rather than into bleeding-through scroll content — same visual result as mantine-datatable.
…ension The earlier `box-shadow` extension fix tried to push opaque cell color 4px outside the cell border, but the offset shadow visually overlapped the table's outer borders during scroll, and the header sticky cells ended up looking translucent because their background no longer matched the surrounding row. This commit replaces the workaround: - Sticky cells declare `--lvt-sticky-bg` (= `var(--mantine-color-body)`, with a dark-scheme override). The class uses `background: var(--lvt-sticky-bg)` so the cell paints an explicit, opaque color — no `inherit` (which bleeds through because `<tr>` has no background of its own) and no cross-cutting box-shadow. - The shadow gradient now ends at `var(--lvt-sticky-bg)` instead of `transparent`. Both stops are fully opaque, so the gradient transitions into solid cell color and the underlying scroll content cannot peek through the shadow strip while it scrolls past. - The earlier box-shadow rules on `[data-sticky-shadow]` cells are removed entirely — the gradient itself is now self-occluding.
…tatable Following user feedback that the previous `<span>`-based shadow caused header transparency, vanishing outer borders, and content peek-through during scroll, the implementation is rewritten to match `mantine-datatable` exactly: - Drop the dedicated `stickyColumnShadow` Styles API selector (and the associated `<span>` element rendered inside the cell). The shadow is now an `::after` pseudo-element keyed off the existing `data-sticky-shadow` attribute on the sticky cell itself. - Use a stacked two-stop linear gradient at low alpha (`rgba(0,0,0,0.05)` light / `rgba(0,0,0,0.25)` dark, mirroring mantine-datatable's `--mantine-datatable-shadow-background-*` values). The lower alpha keeps the shadow visible without bleeding the underlying content through. - Sticky cells declare `overflow: visible` directly in the class so the pseudo-element is never clipped, and keep an opaque background via `--lvt-sticky-bg` (theme-aware: `--mantine-color-body` light, `--mantine-color-dark-7` dark) — `<tr>` has no background so any `inherit`-based fallback would let scroll content through. - Width default bumped from 4px to 8px to match mantine-datatable's visual weight. - Updated the Jest test to assert `data-sticky-shadow` on the cell instead of the removed `<span>` (47/47 still passing). - Updated the Styles API docs accordingly: removed `stickyColumnShadow` selector, added `data-sticky-shadow` modifier for both `cell` and `headerCell`.
Root cause of the user-visible 'header transparent + scroll content
peek-through' was a CSS specificity battle:
- Existing rule: `[data-mantine-color-scheme='dark'] .headerCell {
background: transparent }` — specificity (0, 1, 1).
- My sticky rule: `.stickyHeaderColumn { background: var(--lvt-sticky-bg) }`
— specificity (0, 1, 0).
The .headerCell rule won the cascade in dark mode and reset the sticky
header cell's background to `transparent`. With a transparent
background the underlying scroll content was visible through the cell —
exactly the artifact the user reported (column titles overlapping like
"NSize", header looking transparent, etc.).
Fix: add an explicit `background: var(--lvt-sticky-bg)` to the
existing dark-mode override (which already targets the sticky classes).
The override now matches the .headerCell rule's specificity (0, 1, 1)
AND comes later in the stylesheet, so the cascade picks the sticky
rule. Verified live: the sticky header cell's computed
`backgroundColor` is now `rgb(36, 36, 36)` (was `rgba(0, 0, 0, 0)`)
and the visual artifacts are gone.
Adds an opt-in built-in scroll wrapper that solves the long-standing
"outer borders disappear during horizontal scroll" issue when using
sticky columns + `withTableBorder`. Inspired by user feedback after
trying multiple workarounds at the cell level.
API:
- `scrollProps?: { minWidth?, maxHeight? }` — when set, ListViewTable
renders its own native-scroll viewport around the `<Table>` instead
of relying on `Mantine.Table.ScrollContainer`. The OUTER `<Box>`
becomes non-scrolling and carries the border + border-radius, so they
stay fixed regardless of scroll position.
- `borderRadius?: MantineRadius` — radius of the outer container when
`scrollProps` is set (default `'sm'`).
- New `scrollViewport` Styles API selector for the inner viewport div.
Implementation notes:
- The scroll wrapper is rendered as a real `<div>` always, but uses
`display: contents` when `scrollProps` is not set so it disappears
from the layout entirely — preserves the previous (no-internal-scroll)
behavior with zero side effects for consumers who keep using
`Table.ScrollContainer`.
- When `scrollProps` is set the renderer forces `withTableBorder=false`
on Mantine's `<Table>` so the only border is the one we draw on the
outer `<Box>` (no double border).
- CSS uses `var(--mantine-color-default-border)` (light) and
`var(--mantine-color-dark-4)` (dark) for the border color so it
matches Mantine's table border styling automatically.
- The `useStickyShadow` hook auto-detects the closest scrollable
ancestor, so it picks up the new internal viewport without any code
change.
Demo + docs: the Pinned Columns demo now uses
`scrollProps={{ minWidth: 1300 }}` + `borderRadius="md"` instead of
wrapping in `Table.ScrollContainer`. The docs section gains a
"Recommended setup with scrollProps" subsection explaining when to
use which wrapper. Visual verification (Chrome): all four outer
borders stay continuous during horizontal scroll, rounded corners
preserved, no content peek-through inside sticky cells.
Replace `Table.ScrollContainer` wrappers with the built-in
`scrollProps={{ minWidth }}` + `borderRadius` API in both demos so the
outer border and rounded corners stay fixed during horizontal scroll.
Update the Horizontal Scrolling section in docs.mdx to recommend
`scrollProps` while noting that `Table.ScrollContainer` is still
supported (with the trade-off that the border moves with the scroll
content).
Move the table border + border-radius onto the outer `<Box>` wrapper unconditionally, instead of only when `scrollProps` is set. Mantine's native `withTableBorder` on the inner `<table>` is now always disabled so the wrapper owns the border in both scroll and non-scroll modes — keeping the visual identical, but making `borderRadius` effective on every configuration with `withTableBorder=true`. Add a `borderRadius` size control to the Usage configurator so the new prop is discoverable from the first demo without requiring the user to also enable `scrollProps`.
overflow: hidden on the wrapper made it a sticky containing block, which trapped the thead inside .root and prevented sticky positioning relative to the page viewport. Use clip-path: inset(0 round X) to clip the rounded corners visually without establishing a scroll containing block.
Previously clip-path used the default border-box reference, which clipped slightly into the top-left/top-right corners of the wrapper border due to antialiasing at the rounded edge. Switching to padding-box keeps the clip strictly inside the border, so the corner border line stays intact.
box-shadow:inset (previous attempt) was painted underneath the cell backgrounds and ended up invisible. Restoring the real CSS border on .root and moving the rounded-corner clip-path onto the inner viewport keeps the border line untouched while still clipping the table content to the rounded shape. Sticky header and horizontal-scroll-with-sticky- columns both continue to work.
- New `borderWidth` prop lets consumers control the outer wrapper border thickness when `withTableBorder` is set; exposed in the Usage configurator alongside the existing `borderRadius` control. - New `useStickyHeaderShadow` hook drives `--lvt-header-shadow-opacity` on the table, fading in a soft drop shadow under the thead while it is stuck at `stickyHeaderOffset` — same UX pattern as the existing sticky-column gradient shadow, and zero React re-renders during scroll.
- Wire up `--lvt-shadow-color` (already documented but not actually read) into the sticky-column gradient: consumers can now override the pinned-column shadow tint via `vars`. - Document the existing `--lvt-header-shadow-opacity` driver in the Styles API and point users to `classNames.header` / `styles.header` for full shadow customization (color/blur/spread). - Audit ListViewTable.styles-api.ts to match the implementation: add missing modifiers (`data-with-table-border`, `data-with-scroll`, `data-dragging` / `data-resizing` on root; `data-focused` on row and headerCell; `data-sticky-shadow` on headerCell), drop the stale `data-sticky` aliases, refresh selector descriptions, and correct the default `--lvt-shadow-width` (8px). - README: mention the new pinned-column / sticky-header shadows and the outer `borderWidth` prop.
- Move `Horizontal Scrolling` and `Scroll Area Integration` next to `Pinned Columns` so all scroll-related material is contiguous; the Pinned Columns section no longer has its own `Recommended setup` sub-block (the same guidance now lives in Horizontal Scrolling). - Demote `Keyboard Navigation` from H2 to H3 under `Row Selection` — per the existing copy, it is automatically enabled by `selectionMode` and belongs with the selection material. - Mention the new sticky-header drop shadow in the Sticky Header section and the new `borderWidth` prop / shadow customization variables in the Pinned Columns section.
- ListViewTable.tsx: replace stale 'shadow span' wording with '::after pseudo' (the shadow rendering was rewritten back to a pseudo-element in 0b06401 but the inline comment in renderHeaderCell still referred to the old span-based approach). - use-sticky-shadow.ts: rewrite the JSDoc RTL paragraph — RTL styling is not actually shipped, so don't imply [dir='rtl'] CSS rules exist. Replaced with a note that the hook reports the magnitude only and RTL consumers should override the shadow CSS themselves. No behavior change.
…iners Detection used to compare `thead.top` (in viewport coords) directly to `stickyHeaderOffset`, which only matches when the table is sticky to the page itself. Inside Mantine's `ScrollArea` (or any container with `scrollProps.maxHeight`), the thead is sticky relative to the inner viewport, so the threshold becomes `scrollerTop + offset`. Add the scroller's top to the comparison so the shadow fades in correctly in both modes.
…ehind it Inside a fixed-height container (Mantine `ScrollArea`, `scrollProps.maxHeight`, etc.) the thead's natural position already coincides with its sticky position — so the previous position-based detection treated the header as 'stuck' from the start, painting the shadow even when no content was scrolled past. Switch to a per-mode signal: - Page-scrolled: keep the position check (thead.top vs offset). - Container-scrolled: use `scroller.scrollTop > 0` so the shadow only appears once the user has scrolled content underneath the thead.
The .root:focus-visible outline kept the entire table highlighted even after the user moved focus into a specific row, which produced two overlapping outlines (table + row). Limit the wrapper outline to the case where no row has keyboard focus yet — i.e., right after tabbing in — so as soon as arrow keys engage the row outline takes over.
The demo previously rendered a 3-column table with no interactive features, so half of the Styles API selectors (`resizeHandle`, `dragHandle`, `stickyColumn` / `stickyHeaderColumn`, `scrollViewport`, `selectedRow`, `focusedRow`) were never present in the DOM and consumers couldn't actually try styling them. Switch the demo to a richer setup that exercises the full surface: column reordering + resizing, multi-row selection (so keyboard nav shows `focusedRow`), a sticky-left column with the header drop shadow on `stickyHeader`, and `scrollProps` so the inner `scrollViewport` is in the tree.
Four 220px-wide columns made the inner viewport tight and visually crowded. Two columns (sticky-left Name + Modified) are enough to render every selector listed in the panel — sticky column, drag handle, resize handle, scroll viewport, header shadow, selected/focused row — without horizontal cramping. Also drop the horizontal `scrollProps.minWidth` since the table now fits, and keep `maxHeight: 280` so the vertical `scrollViewport` and the sticky-header drop shadow still engage.
Mantine's sticky thead resolves to z-index 3 (or 4 with `withTableBorder`), but our sticky-left/right body cells use z-index 10 — so when the user scrolled inside a fixed-height container, the sticky-left first body cell would peek above the pinned thead and 'duplicate' the first row next to the column. Match Mantine's selector specificity (`.header[data-sticky]`) and bump the thead to z-index 12 so the header always wins on vertical scroll. The rule only applies when the thead is in sticky mode, so it has no effect on non-sticky tables.
- borderRadius: route through Mantine's `getRadius()` so numeric and
custom values (e.g. `12`, `'1rem'`) resolve to valid CSS instead of
generating an undefined `var(--mantine-radius-${X})`. Same fix
applied to the inner `clip-path`.
- borderColor: regression — Mantine's native `<table>` border is
always disabled now, so the public `borderColor` prop no longer
reached any visible border. Resolve it through Mantine's
`getThemeColor()` in the `varsResolver` and expose
`--lvt-border-color`; the wrapper border now reads that var with a
fallback to `--mantine-color-default-border` (light) or
`--mantine-color-dark-4` (dark).
- stickyHeaderOffset: the page-scrolled detection used to call
`parseFloat(offset)`, which dropped units like `'2rem'`. Read
`getComputedStyle(thead).top` instead — that value is always in
pixels regardless of how the user wrote the offset.
- JSDoc: `column.sticky` referenced a non-existent
`stickyColumnShadow` selector → point users to the actual
`stickyColumn` / `stickyHeaderColumn` pseudo-element approach.
- JSDoc: `withTableBorder` no longer 'falls back to Mantine's native
`<table>` border' — refresh the description to say the wrapper
always owns the border.
- CSS comment: `.scrollViewport` no longer uses `display: contents`;
rewrite the surrounding comment to describe the actual conditional
inline styles applied by the renderer.
- Tests: add `use-sticky-header-shadow.test.tsx` covering the
enabled/disabled and container-scrolled (`scrollTop > 0`) branches.
When a sticky column was active alongside `enableColumnResizing`, the resize handle visually responded but the columns snapped back to even distribution. Chromium ignores explicit `width` on table cells with `position: sticky` once `table-layout: fixed` kicks in — the auto-distributed track width wins, so the inline `style.width` set by `useColumnResize` had no effect. Pin both `min-width` and `max-width` to the resize width while resize is active. The hook already clamps drag deltas against the column's `minWidth` / `maxWidth` constraints before writing `columnWidths[key]`, so locking min/max to the same value is safe and forces the layout to honor the resized track regardless of cell positioning.
The previous `isResizeActive ? 'fixed' : ...` heuristic ran into a Chromium quirk: under `table-layout: fixed` the renderer re-distributes track widths evenly when any cell is `position: sticky`, ignoring the inline `width` (and even `min-width` / `max-width`) we set from `useColumnResize`. With a sticky-left column + `enableColumnResizing` the drag handle visually responded but the columns snapped right back to the even split. `auto` layout honors the inline cell widths; the existing `getTableStyle` already locks the table's outer width to a fixed pixel value, so we don't lose the 'no surprise reflow' guarantee `fixed` was meant to provide. Also drop the speculative `min-width`/`max-width` pin from the previous attempt — it was ineffective (Chromium ignores those on sticky cells in fixed mode too) and is no longer needed under `auto`.
The previous attempt switched table-layout from `fixed` to `auto` during resize when sticky columns were present. That worked for the narrow case but introduced two regressions: - Auto layout proportionally redistributes when the sum of inline cell widths is less than the table width, so explicit widths set by the resize hook didn't always take effect. - Auto layout uses the column's content min-width as a floor, so shrinking a column below its content width was silently clamped. Switch back to `table-layout: fixed` for *all* resize-active cases, and inject a `<colgroup>` populated from `columnWidths` while resize is active. `<col>` widths are honored by Chromium under fixed layout even on sticky cells (which inline `<th>` widths are not), so the resize handle now responds correctly with both pinned and non-pinned columns and content cannot push columns wider than the user dragged them. The colgroup is only rendered when `isResizeActive` is true, so the non-resize render path is completely unaffected. Verified with the React handler in a manual integration: drag -100 on the sticky-left column in the Styles API demo updates Name 285 → 185 and Modified 258 → 358 exactly.
`useColumnResize` only snapshotted column widths when `isResizeActive` flipped from false to true, then reused that snapshot indefinitely as the `leftStartWidth` / `rightStartWidth` baseline for every subsequent drag. That made the next drag use stale state whenever the rendered widths no longer matched the recorded `columnWidths` map — most visibly when the user switched `resizeMode` from `standard` to `finder` mid-session, since each mode produces a different distribution. The finder-mode drag would then start from the standard-mode end-state (not from what the user saw), shrinking 'untouched' columns in the process. Snapshot fresh from the DOM at the start of every drag so the math always matches the visible layout.
The colgroup-driven width override was needed only because Chromium ignores inline `<th>` widths on `position: sticky` cells under `table-layout: fixed`. Rendering it unconditionally during resize slightly altered layout for *non-sticky* resize cases — most visibly in Finder mode, where the table width and column redistribution depend on the interplay between cell widths, `min-width: 100%` on `.table`, and the inline width set by `getTableStyle`. Gate the colgroup behind `hasStickyColumns` so the regular resize path is untouched (matches the pre-shadow-feature behavior exactly) while pinned tables still get the only fix that makes column resize actually work for them.
This reverts the recent attempts to make column resize work alongside sticky columns: - 08db9dd render resize colgroup only when sticky columns are present - 0318f6c re-snapshot column widths on every drag, not just the first - 331028f use <colgroup> for resize widths to fix sticky-column resize - fe5ef1d switch resize-active table layout from fixed to auto - a50d8e2 column resize on sticky cells under table-layout: fixed Each fix solved the sticky case but quietly changed the column-width distribution in the regular Standard / Finder modes — drags ended up shrinking neighbours that should have stayed put. Reverting restores the exact pre-fix resize behavior on non-sticky tables. Sticky columns + column resize remains a known limitation (Chromium ignores inline `<th>` widths on `position: sticky` cells under `table-layout: fixed`); to reflect that, drop `enableColumnResizing` from the Styles API demo and call it out in the demo's source comment. The dedicated resize demos (Resize Mode, Auto-Fit Columns, Configurator) still cover the `resizeHandle` selector.
When neither `scrollProps` nor `withTableBorder` is set, the `scrollViewport` wrapper had no inline style and rendered as a default block `<div>`. That introduced an extra layout box between `.root` and `<table>`, subtly altering how the table's `width: 100%` and `min-width: 100%` resolved against its containing block. The user-visible side-effect: in resize Finder/Standard modes, dragging a handle could change neighbouring column widths instead of keeping them put. Restore `display: contents` for the no-scroll / no-border case so the wrapper is invisible to layout and the table relates directly to `.root` — identical to the rendering that existed before `scrollProps` was introduced. The wrapper still becomes a real block when it actually has work to do (`overflow: auto` for scroll, `clip-path` for rounded-corner clipping).
The Mantine `<Table>` style was built as
`{ ...getTableStyle(), minWidth: scrollProps?.minWidth }`. When
`scrollProps` is not provided, `scrollProps?.minWidth` evaluates to
`undefined`, and the explicit `minWidth: undefined` *overwrites* the
`min-width` that `getTableStyle` had just set for resize mode.
Without that inline `min-width`, the CSS class `.table { min-width:
100% }` won the cascade and forced the table back up to the parent
width on every drag — so the browser proportionally redistributed
the slack across all columns. In Finder mode that looked like
"untouched" columns moving when the user only resized one.
Spread `scrollProps.minWidth` only when it's actually set so the
resize hook's `min-width` survives.
Now that the underlying `min-width` clobber is fixed (`f54a449`), the sticky-only `<colgroup>` workaround no longer collides with Finder/Standard mode column distribution. Re-introduce it (gated on `isResizeActive && hasStickyColumns`) so the resize handle on a pinned column actually moves the track again — Chromium ignores inline `<th>` widths on `position: sticky` cells under `table-layout: fixed`, but it always honors `<col>` widths. Non-sticky resize keeps the original `<th>` width path unchanged (no colgroup is rendered) so Finder/Standard column distribution remains identical to the pre-fix behavior verified earlier. Re-enable `enableColumnResizing` on the Styles API demo so the `resizeHandle` selector is exercised again.
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.
Closes #27.
Originally scoped to pinned-column visual feedback, this PR has grown into a broader pass on the table's outer wrapper, sticky behaviors, and the Styles API surface. All changes are additive — no breaking changes — so this targets a minor (
4.1.0) release.Highlights
Pinned columns
column.stickyextended frombooleantoboolean | 'left' | 'right';truekeeps meaning'left'(full backward compat).useStickyShadowviarequestAnimationFrame-throttled scroll events — no React re-renders during scroll.--lvt-shadow-color(now actually wired up — was documented but unused before) and--lvt-shadow-width.Sticky header shadow
<thead>while it is stuck atstickyHeaderOffset, fading back out when sticky releases. Same UX pattern as the column shadow.useStickyHeaderShadowhook drives--lvt-header-shadow-opacity; full shadow shape/color is customizable viaclassNames.header/styles.header.Outer wrapper border + scroll
scrollProps?: { minWidth?, maxHeight? }renders a non-scrolling outer wrapper with native horizontal/vertical scroll inside, sowithTableBorderandborderRadiusstay fixed at the edges during scroll — the recommended setup with sticky columns.borderRadius?: MantineRadius(default'sm') and newborderWidth?: number | stringprops on the wrapper..rootwrapper (not on Mantine's<table>) so it's stable regardless of scroll. Internal viewport usesclip-pathto clip the rounded corners —overflow: hiddenwas avoided because it would create a scroll containing block and breakposition: stickyon the thead.Documentation & Styles API
ListViewTable.styles-api.tsagainst the implementation — added missing modifiers (data-with-table-border,data-with-scroll,data-dragging/data-resizingon root;data-focusedon row/headerCell), updated selector descriptions, corrected the default--lvt-shadow-width(8px), removed stale entries.borderRadiusandborderWidthcontrols.Test plan
borderRadius+borderWidth(0–8) render without artifacts on all 4 cornersscrollPropskeeps the border fixed during horizontal scrollTable.ScrollContainerstill works (legacy path)yarn test(47/47)yarn build🤖 Generated with Claude Code