Skip to content

[table] Empty placeholder scrolls horizontally instead of staying fixed #62

Description

@clddup

Reproduction

https://stackblitz.com/edit/vitejs-vite-4yccznuj?file=src%2FApp.vue

This reproduction uses the official Vite WebContainer template and starts automatically.

Environment

  • antdv-next@1.4.3
  • @v-c/table@^1.1.8
  • vue@^3.5.39
  • Chrome on macOS

Steps to reproduce

  1. Render an empty table with enough columns to require horizontal scrolling.
  2. Set :scroll="{ x: 'max-content' }".
  3. Open the table at the initial scroll position, then scroll horizontally.

Actual behavior

The empty placeholder is centered against the full table width instead of the visible viewport. With many columns it can be completely outside the initial viewport. When it becomes visible, it moves together with the horizontally scrolled table content.

Expected behavior

The empty placeholder should match Ant Design / rc-table behavior: it remains centered in the visible table viewport and does not move when the horizontal scrollbar is scrolled.

Root cause

packages/table/src/Body/ExpandedRow.tsx creates the sticky, viewport-width wrapper only after componentWidth is measured. It currently forwards contentNode to Cell through the default slot.

Cell memoizes its rendered content. The initial slot result is cached while componentWidth === 0, so the new sticky VNode produced after the resize measurement does not reach the DOM.

CellProps already declares a reactive children prop, and computeContent already prioritizes it:

const slotChildren = props.children ?? slots.default?.()

Why the slot and prop are not equivalent here

This follows Vue's documented reactivity model: the setup context and the properties of slots are not reactive. A parent slot update schedules the child to render again, but it does not itself invalidate a computed value that only reads the slots object, as also explained by a Vue maintainer.

Without memoization, rendering contentNode through the default slot or through the children prop produces the same visible child. The difference appears because Cell stores computeContent() in a Vue computed.

With the slot form, ExpandedRow evaluates contentNode in its own render and passes a slot function that closes over that value. Vue updates the slot function when the parent re-renders, but the slots object/function is not a reactive dependency of the child's computed. Calling slots.default() during the initial computation therefore caches the VNode created while componentWidth === 0; replacing the slot on a later parent update does not invalidate that computed value.

With the prop form, computeContent reads props.children. Declared Vue props are reactive inputs, so when the resize measurement makes ExpandedRow produce a new sticky VNode, the changed children prop invalidates the computed value and Cell renders the new wrapper.

Therefore this change is not relying on different rendering semantics between slots and props. It makes the changing child VNode observable to the existing memoization boundary. The prop is already declared by CellProps and already has precedence in computeContent, so this is also the smallest change that preserves the current cell memoization.

Suggested fix

Pass the reactive contentNode through the existing children prop. This is the Vue equivalent of keeping the React child current while preserving the existing memoization:

- <Cell component={cellComponent} prefixCls={prefixCls} colSpan={colSpan}>
-   {contentNode}
- </Cell>
+ <Cell
+   component={cellComponent}
+   prefixCls={prefixCls}
+   colSpan={colSpan}
+   children={contentNode}
+ />

After this change, the measured wrapper is rendered with position: sticky, left: 0, and the visible component width. The placeholder stays fixed while scrolling.

Screenshots

Initial position and two horizontal scroll positions:

Initial position: placeholder is outside the visible viewport Scrolled position: placeholder appears near the right edge Further scrolled position: placeholder moves with table content

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions