Skip to content

feat(Table): implement row pinning#6115

Merged
benjamincanac merged 13 commits intonuxt:v4from
0xA1337:v4
Mar 17, 2026
Merged

feat(Table): implement row pinning#6115
benjamincanac merged 13 commits intonuxt:v4from
0xA1337:v4

Conversation

@0xA1337
Copy link
Contributor

@0xA1337 0xA1337 commented Feb 27, 2026

🔗 Linked issue

❓ Type of change

  • 📖 Documentation (updates to the documentation or readme)
  • 🐞 Bug fix (a non-breaking change that fixes an issue)
  • 👌 Enhancement (improving an existing functionality)
  • ✨ New feature (a non-breaking change that adds functionality)
  • 🧹 Chore (updates to the build process or auxiliary tools and libraries)
  • ⚠️ Breaking change (fix or feature that would cause existing functionality to change)

📚 Description

The UTable component currently ignores TanStack Table's row pinning state when rendering. While the pinning state is correctly managed internally (via v-model:row-pinning and the TanStack APIs), pinned rows are not rendered in their pinned positions -- they remain in their natural order.

This is because the template uses tableApi.getRowModel().rows which returns all rows in a flat list without any pinning awareness. TanStack Table expects consumers to render three separate row groups: getTopRows(), getCenterRows(), and getBottomRows().

Changes:

  • Render top-pinned, center, and bottom-pinned rows separately in the <tbody> template
  • Update the virtualizer to only count center rows (pinned rows render outside the virtual window)
  • Add data-pinned attribute on <tr> elements ("top", "bottom", or omitted) for styling purposes
  • Add snapshot test for row pinning
  • Add documentation example with a "favorite" star button using row.pin('top')

📝 Checklist

  • I have linked an issue or discussion.
  • I have updated the documentation accordingly.

@github-actions github-actions bot added the v4 #4488 label Feb 27, 2026
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 27, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds end-to-end row-pinning support: a new Vue 3 example component (TableRowPinningExample.vue) implementing pin/unpin actions and initial top-pinned rows; updates runtime Table.vue to compute and render topRows, centerRows, and bottomRows and to use centerRows for virtualization keys and counts when pinned sections exist; updates a playground page to manage RowPinningState and bind v-model:row-pinning; inserts a documentation section describing row pinning (duplicated across docs); and adds table tests covering row pinning with and without virtualization.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(Table): implement row pinning' clearly and concisely summarizes the main feature being added: row pinning support for the Table component.
Description check ✅ Passed The description is directly related to the changeset, explaining the problem (row pinning state ignored), solution (rendering three row groups separately), and listing specific changes made.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Tip

You can validate your CodeRabbit configuration file in your editor.

If your editor has YAML language server, you can enable auto-completion and validation by adding # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json at the top of your CodeRabbit configuration file.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/runtime/components/Table.vue (1)

628-645: ⚠️ Potential issue | 🟠 Major

Bottom-pinned rows are misplaced in virtualized mode.

At Line 644, bottomRows are rendered without virtualization offset. Since center rows are translated, bottom rows appear right after the rendered window instead of after the full center range.

💡 Proposed fix
-          <ReuseRowTemplate v-for="row in bottomRows" :key="row.id" :row="row" />
+          <ReuseRowTemplate
+            v-for="row in bottomRows"
+            :key="row.id"
+            :row="row"
+            :style="virtualizer
+              ? { transform: `translateY(${virtualizer.getTotalSize() - renderedSize}px)` }
+              : undefined"
+          />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/runtime/components/Table.vue` around lines 628 - 645, Bottom-pinned rows
are rendered without the virtualization offset so they appear immediately after
the rendered window; fix by applying the same translateY offset equal to the
virtualized center content height when virtualizer is present. In the template
branch where virtualizer is truthy, render the bottomRows with ReuseRowTemplate
but add a style prop that sets transform:
`translateY(${virtualizer.getTotalSize()}px)` (or the virtualizer API that
returns total center height) so bottomRows are positioned after the full
virtualized center range; ensure you reference virtualizer, bottomRows, and
ReuseRowTemplate when making the change.
🧹 Nitpick comments (1)
test/components/Table.spec.ts (1)

193-194: Add a virtualized row-pinning case to this matrix.

This PR also changes the virtualized render path; adding one virtualize + rowPinning scenario here would protect against regressions.

🧪 Suggested test entry
-    ['with row pinning', { props: { ...props, rowPinning: { top: ['2'], bottom: ['3'] } } }]
+    ['with row pinning', { props: { ...props, rowPinning: { top: ['2'], bottom: ['3'] } } }],
+    ['with row pinning and virtualization', { props: { ...props, virtualize: true, rowPinning: { top: ['2'], bottom: ['3'] } } }]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/components/Table.spec.ts` around lines 193 - 194, Add a new test matrix
entry that combines virtualized rendering with row pinning to guard the changed
virtualize path: update the matrix that currently contains entries like ['with
body-bottom slot', { props, slots: { 'body-bottom': () => 'Body bottom slot' }
}] and ['with row pinning', { props: { ...props, rowPinning: { top: ['2'],
bottom: ['3'] } } }] by adding an entry such as ['with virtualize + row
pinning', { props: { ...props, virtualize: true, rowPinning: { top: ['2'],
bottom: ['3'] } } }] so the test suite exercises the virtualize code path
together with rowPinning (ensure you reference the same props variable used in
the matrix).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/app/components/content/examples/table/TableRowPinningExample.vue`:
- Around line 115-120: The UTable component usage is missing a data-slot
attribute; add a data-slot to the template element that renders <UTable> (for
example data-slot="table" or an appropriate name for this slot) so the line with
<UTable v-model:row-pinning="rowPinning" :data="data" :columns="columns"
class="flex-1" /> becomes <UTable data-slot="table"
v-model:row-pinning="rowPinning" :data="data" :columns="columns" class="flex-1"
/> (reference symbols: UTable, rowPinning, data, columns).

---

Outside diff comments:
In `@src/runtime/components/Table.vue`:
- Around line 628-645: Bottom-pinned rows are rendered without the
virtualization offset so they appear immediately after the rendered window; fix
by applying the same translateY offset equal to the virtualized center content
height when virtualizer is present. In the template branch where virtualizer is
truthy, render the bottomRows with ReuseRowTemplate but add a style prop that
sets transform: `translateY(${virtualizer.getTotalSize()}px)` (or the
virtualizer API that returns total center height) so bottomRows are positioned
after the full virtualized center range; ensure you reference virtualizer,
bottomRows, and ReuseRowTemplate when making the change.

---

Nitpick comments:
In `@test/components/Table.spec.ts`:
- Around line 193-194: Add a new test matrix entry that combines virtualized
rendering with row pinning to guard the changed virtualize path: update the
matrix that currently contains entries like ['with body-bottom slot', { props,
slots: { 'body-bottom': () => 'Body bottom slot' } }] and ['with row pinning', {
props: { ...props, rowPinning: { top: ['2'], bottom: ['3'] } } }] by adding an
entry such as ['with virtualize + row pinning', { props: { ...props, virtualize:
true, rowPinning: { top: ['2'], bottom: ['3'] } } }] so the test suite exercises
the virtualize code path together with rowPinning (ensure you reference the same
props variable used in the matrix).

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2438d45 and ecedca5.

⛔ Files ignored due to path filters (2)
  • test/components/__snapshots__/Table-vue.spec.ts.snap is excluded by !**/*.snap
  • test/components/__snapshots__/Table.spec.ts.snap is excluded by !**/*.snap
📒 Files selected for processing (4)
  • docs/app/components/content/examples/table/TableRowPinningExample.vue
  • docs/content/docs/2.components/table.md
  • src/runtime/components/Table.vue
  • test/components/Table.spec.ts

@pkg-pr-new
Copy link

pkg-pr-new bot commented Feb 27, 2026

npm i https://pkg.pr.new/@nuxt/ui@6115

commit: 4d54a32

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/content/docs/2.components/table.md`:
- Line 519: Update the docs text to fix wording: change the phrase "can be
binded with `v-model`" to "can be bound with `v-model`" in the sentence
describing the `row-pinning` prop; locate the sentence mentioning the
`row-pinning` prop and `v-model` (around the current line) and replace "binded"
with the correct past participle "bound".

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: cbad7e4d-da18-49b9-b64d-e1c33e8cdb4c

📥 Commits

Reviewing files that changed from the base of the PR and between 1817aa8 and 5422c5c.

📒 Files selected for processing (1)
  • docs/content/docs/2.components/table.md

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/runtime/components/Table.vue (1)

628-630: Minor: Inconsistent null handling between key and row prop.

Line 628 uses optional chaining (?.id) while line 630 uses non-null assertion (!). Since the virtualizer count is correctly set to centerRows.value.length, both accesses should always be valid. Consider using non-null assertion consistently for the key as well:

-            <template v-for="(virtualRow, index) in virtualizer.getVirtualItems()" :key="centerRows[virtualRow.index]?.id">
+            <template v-for="(virtualRow, index) in virtualizer.getVirtualItems()" :key="centerRows[virtualRow.index]!.id">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/runtime/components/Table.vue` around lines 628 - 630, The key uses
optional chaining while the row prop uses a non-null assertion, causing
inconsistent null handling; update the key expression to use a non-null
assertion as well (i.e., replace centerRows[virtualRow.index]?.id with
centerRows[virtualRow.index]!.id) so both accesses are consistent for the
ReuseRowTemplate rendering driven by virtualizer.getVirtualItems() and
centerRows.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/content/docs/2.components/table.md`:
- Line 317: Update the heading "### With row pinning :badge{label=\"Soon\"
class=\"align-text-top\"}" to remove or replace the "Soon" badge since row
pinning is implemented; either remove the :badge snippet entirely or change it
to a version label such as :badge{label="4.1+" class="align-text-top"} so the
heading reflects availability (locate the "With row pinning" heading in the docs
content and edit the badge text accordingly).

---

Nitpick comments:
In `@src/runtime/components/Table.vue`:
- Around line 628-630: The key uses optional chaining while the row prop uses a
non-null assertion, causing inconsistent null handling; update the key
expression to use a non-null assertion as well (i.e., replace
centerRows[virtualRow.index]?.id with centerRows[virtualRow.index]!.id) so both
accesses are consistent for the ReuseRowTemplate rendering driven by
virtualizer.getVirtualItems() and centerRows.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a9dfceac-ae4e-4d0b-b8b0-0e330a920a40

📥 Commits

Reviewing files that changed from the base of the PR and between cb33c22 and 4888c25.

⛔ Files ignored due to path filters (2)
  • test/components/__snapshots__/Table-vue.spec.ts.snap is excluded by !**/*.snap
  • test/components/__snapshots__/Table.spec.ts.snap is excluded by !**/*.snap
📒 Files selected for processing (5)
  • docs/app/components/content/examples/table/TableRowPinningExample.vue
  • docs/content/docs/2.components/table.md
  • playgrounds/nuxt/app/pages/components/table.vue
  • src/runtime/components/Table.vue
  • test/components/Table.spec.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • test/components/Table.spec.ts
  • docs/app/components/content/examples/table/TableRowPinningExample.vue

Copy link
Member

@benjamincanac benjamincanac left a comment

Choose a reason for hiding this comment

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

@0xA1337 I've made a few improvements but otherwise it looks good thanks! 😊

I did try to implement sticky on rows when pinned but it added lots of complexity to calc the positions so I gave up for now.

@benjamincanac benjamincanac merged commit fbd60d9 into nuxt:v4 Mar 17, 2026
18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

v4 #4488

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants