Skip to content

fix(table): preserve page and scroll position on data refresh#4156

Open
devbymadde wants to merge 1 commit into
mainfrom
fix/table-preserve-page-on-row-count-change
Open

fix(table): preserve page and scroll position on data refresh#4156
devbymadde wants to merge 1 commit into
mainfrom
fix/table-preserve-page-on-row-count-change

Conversation

@devbymadde

@devbymadde devbymadde commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

What & why

In remote mode, limel-table snapped a paginated list back to page 1 and lost its scroll position whenever the rows were rebuilt — for example when a row was deleted or two objects were merged while the user was on a later page (e.g. page 3 of a list). The root cause is that replaceData() resets Tabulator's internal pagination to the first page and scrolls the row container to top-left, and re-entering the ajax pipeline then emitted changePage/load for page 1 back to the consumer.

This regressed after the remote-paginator refresh was added, and affected any consumer of limel-table in remote mode (the object list in lime-webclient being the visible one).

Changes

All changes are scoped to remote mode.

  • Guard the internal changePage/load emissions in requestData and handleDataSorting while a paginator refresh is in flight, using a depth counter so overlapping refreshes (totalRows and pageSize changing in the same tick) can't clear the guard early. The returned last_page still updates the paginator UI.
  • After replaceData(), restore Tabulator to the controlled page (clamped to the new page count) so getPage() and the paginator UI stay in sync. When the current page no longer exists (the last row on the last page was removed), the clamp is emitted as a genuine changePage.
  • Snapshot and restore the scroll position around the row rebuilds, on both the data-change path and the paginator-refresh path.
  • Skip the paginator-refresh round-trip entirely when the page count is unchanged (the common case, e.g. removing one row of many), which avoids two redundant full row rebuilds per data change.

Testing

  1. Go to an explorer with many pages
  2. Go to a page > 1
  3. Scroll down
  4. Delete one item
  5. You should stay on the same page, scrolled to the same position

Summary by CodeRabbit

  • Bug Fixes
    • Prevented unnecessary page-change and data-load events during table refreshes.
    • Preserved the current page when rows are deleted or data totals change.
    • Correctly clamped pagination to the last available page when needed.
    • Preserved table scroll position during data updates and pagination refreshes.
    • Improved handling of asynchronous data replacement and sorting updates.

Review:

  • Commits are atomic
  • Commits have the correct type for the changes made
  • Commits with breaking changes are marked as such

Browsers tested:

(Check any that applies, it's ok to leave boxes unchecked if testing something didn't seem relevant.)

Windows:

  • Chrome
  • Edge
  • Firefox

Linux:

  • Chrome
  • Firefox

macOS:

  • Chrome
  • Firefox
  • Safari

Mobile:

  • Chrome on Android
  • iOS

@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The table’s remote paginator refresh flow now preserves the controlled page and scroll position, avoids unnecessary data rebuilds, and suppresses refresh-generated events. Unit and e2e tests cover page clamping, restoration, event behavior, and asynchronous updates.

Changes

Remote paginator refresh

Layer / File(s) Summary
Refresh state, pagination, and scroll handling
src/components/table/table.tsx
Tracks paginator refresh depth and page counts, clamps or restores pages, skips unchanged rebuilds, and preserves scroll around asynchronous replaceData() calls.
Remote event suppression and page propagation
src/components/table/table.tsx
Uses Tabulator’s requested page when available and suppresses changePage and load emissions during paginator refreshes.
Paginator behavior tests and async mocks
src/components/table/table.spec.ts, src/components/table/table.e2e.tsx
Adds coverage for refresh event suppression, page restoration and clamping, unchanged page counts, scroll preservation, and settled asynchronous updates.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

Suggested labels: bug

Suggested reviewers: fredrikwallstrom, jgroth

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: preserving pagination page and scroll position during table data refreshes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/table-preserve-page-on-row-count-change

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.

@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown

Documentation has been published to https://lundalogik.github.io/lime-elements/versions/PR-4156/

In remote mode, any refresh that rebuilds the rows — a `totalRows`/
`pageSize` change or a row being removed — round-trips through Tabulator's
`replaceData()`, which resets its internal pagination to the first page
and snaps the row container to top-left. Re-entering the ajax pipeline
then emitted `changePage`/`load` for page 1 back to the consumer, so a
paginated list (e.g. an object list on page 3) jumped back to page 1 and
lost its scroll position whenever a row was deleted or merged on a later
page.

Fixes, all scoped to remote mode:

- Guard the internal `changePage`/`load` emissions in `requestData` and
  `handleDataSorting` while a paginator refresh is in flight, using a
  depth counter so overlapping refreshes (`totalRows` and `pageSize`
  changing in the same tick) can't clear the guard early. The returned
  `last_page` still updates the paginator UI.
- After `replaceData()`, restore Tabulator to the controlled `page`
  (clamped to the new page count) so `getPage()` and the paginator UI
  stay in sync. When the current page no longer exists (last row on the
  last page removed) the clamp is emitted as a genuine `changePage`.
- Snapshot and restore the scroll position around the row rebuilds on
  both the data-change and paginator-refresh paths.
- Skip the paginator-refresh round-trip entirely when the page count is
  unchanged (the common case, e.g. removing one row of many), avoiding
  two redundant full row rebuilds per data change.

Covered by a spec suite for the guard/clamp/restore/skip logic and e2e
tests that reproduce the page reset and scroll loss against real
Tabulator via the `load` event.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@devbymadde devbymadde force-pushed the fix/table-preserve-page-on-row-count-change branch from 9492dfb to 84620ce Compare July 2, 2026 13:07
@devbymadde devbymadde changed the title Fix/table preserve page on row count change fix(table): preserve page and scroll position on data refresh Jul 2, 2026
@FredrikWallstrom

Copy link
Copy Markdown
Contributor

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@FredrikWallstrom FredrikWallstrom self-assigned this Jul 13, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/components/table/table.e2e.tsx`:
- Around line 384-394: Update the zero-event assertions for changePageToFirst
and loadForFirstPage to use toHaveLength(0) directly on each filtered event
collection, preserving the existing filters and expected zero counts.

In `@src/components/table/table.tsx`:
- Around line 751-765: Update the pagination refresh method containing
calculatePageCount and targetPage to return immediately when pageSize is unset
or falsy, before calculating maxPage or invoking replaceData/setPage. Preserve
the existing pagination behavior when pageSize is configured.
- Around line 387-390: Update the row-id change path around preserveScroll and
replaceData in the table component to restore the current controlled page after
the remote data replacement completes. Reuse the existing setPage(targetPage)
logic alongside scroll restoration, ensuring same-page-count replacements on
page 3 restore that page even when refreshRemotePaginator() skips its guard, and
add a regression test for this scenario.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 0961f94f-f48f-430b-89c3-7af8cb7b0dc3

📥 Commits

Reviewing files that changed from the base of the PR and between 9492dfb and 84620ce.

📒 Files selected for processing (3)
  • src/components/table/table.e2e.tsx
  • src/components/table/table.spec.ts
  • src/components/table/table.tsx

Comment on lines +384 to +394
const changePageToFirst = changePageSpy.events.filter(
(event: CustomEvent) => event.detail === 1
);
expect(changePageToFirst.length).toBe(0);

// The URL offset in the consumer is driven by the `load` event, not
// `changePage`, so a `load` for page 1 would also snap the list back.
const loadForFirstPage = loadSpy.events.filter(
(event: CustomEvent) => event.detail?.page === 1
);
expect(loadForFirstPage.length).toBe(0);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the targeted test section with line numbers.
sed -n '350,410p' src/components/table/table.e2e.tsx | cat -n

# See how similar zero-length assertions are written in this file.
rg -n "toBe\\(0\\)|toHaveLength\\(0\\)|length\\).toBe\\(0\\)" src/components/table/table.e2e.tsx

# Sample nearby test style for collection assertions in the repo.
rg -n "toHaveLength\\(|\\.length\\)\\.toBe\\(0\\)" src/components -g '*.e2e.tsx' -g '*.spec.ts' -g '*.test.ts'

Repository: Lundalogik/lime-elements

Length of output: 7163


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '350,410p' src/components/table/table.e2e.tsx | cat -n
rg -n "toBe\\(0\\)|toHaveLength\\(0\\)|length\\)\\.toBe\\(0\\)" src/components/table/table.e2e.tsx
rg -n "toHaveLength\\(|\\.length\\)\\.toBe\\(0\\)" src/components -g '*.e2e.tsx' -g '*.spec.ts' -g '*.test.ts'

Repository: Lundalogik/lime-elements

Length of output: 7163


Use toHaveLength(0) for these zero-event assertions. It reads more clearly than checking .length directly.

🧰 Tools
🪛 GitHub Check: SonarCloud Code Analysis

[warning] 394-394: Prefer a more specific assertion instead of this generic one, e.g. "expect(loadForFirstPage).toHaveLength(0)".

See more on https://sonarcloud.io/project/issues?id=Lundalogik_lime-elements&issues=AZ8ifX5DRFoArOAa2PRU&open=AZ8ifX5DRFoArOAa2PRU&pullRequest=4156


[warning] 387-387: Prefer a more specific assertion instead of this generic one, e.g. "expect(changePageToFirst).toHaveLength(0)".

See more on https://sonarcloud.io/project/issues?id=Lundalogik_lime-elements&issues=AZ8ifX5DRFoArOAa2PRT&open=AZ8ifX5DRFoArOAa2PRT&pullRequest=4156

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/table/table.e2e.tsx` around lines 384 - 394, Update the
zero-event assertions for changePageToFirst and loadForFirstPage to use
toHaveLength(0) directly on each filtered event collection, preserving the
existing filters and expected zero counts.

Source: Linters/SAST tools

Comment on lines +387 to +390
const restoreScroll = this.preserveScroll();
this.tabulator
.replaceData(newData)
.then(restoreScroll, () => {});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== file size ==\n'
wc -l src/components/table/table.tsx

printf '\n== around lines 350-420 ==\n'
sed -n '350,420p' src/components/table/table.tsx

printf '\n== around lines 730-780 ==\n'
sed -n '730,780p' src/components/table/table.tsx

printf '\n== search for page restore / refreshRemotePaginator / replaceData ==\n'
rg -n "refreshRemotePaginator|replaceData\\(|setPage\\(|this\\.page\\b|preserveScroll\\(" src/components/table/table.tsx

Repository: Lundalogik/lime-elements

Length of output: 5077


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== around lines 250-360 ==\n'
sed -n '250,360p' src/components/table/table.tsx

printf '\n== around lines 900-1020 ==\n'
sed -n '900,1020p' src/components/table/table.tsx

printf '\n== search for paginator refresh depth / page change emit / data watch ==\n'
rg -n "paginatorRefreshDepth|changePage\\.emit|paginationInitialPage|paginationCurrentPage|pageLoaded|pageChanged|dataChanged|tableBuilt|renderComplete" src/components/table/table.tsx

Repository: Lundalogik/lime-elements

Length of output: 8250


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
p = Path('src/components/table/table.tsx')
lines = p.read_text().splitlines()
for start,end in [(250,360),(900,1020)]:
    print(f"\n== {start}-{end} ==")
    for i in range(start-1, min(end, len(lines))):
        print(f"{i+1:4d}: {lines[i]}")
PY

Repository: Lundalogik/lime-elements

Length of output: 8881


🌐 Web query:

Tabulator replaceData remote pagination current page reset behavior setPage

💡 Result:

In Tabulator, the replaceData function is primarily designed for loading complete datasets into the table [1][2]. When using remote pagination, replaceData does not function as a standard data refresh for the current page; it often resets the table to the first page or produces unexpected behavior because Tabulator expects to fetch data from the server for each page request [1][2][3]. Because remote pagination relies on the server to provide only the data for the specific page being viewed, there is no local "full dataset" for replaceData to replace [1][2]. Consequently, using replaceData with remote pagination is generally discouraged [4]. If you need to refresh the current page while maintaining the user's position, consider these approaches: 1. Refreshing Data: Use the refreshData function to trigger a reload of the current page from the server [2]. 2. Manual Page Persistence: If you must perform a complex update, you can manually capture the current page index using the getPage function before the update, and then restore it using the setPage function after the data has been loaded [1]. Since replaceData returns a promise, you should execute setPage within the resolution of that promise to ensure the data is loaded first [5]: table.replaceData.then( => { table.setPage(pageNumber); }); Note that with remote pagination, any change to the page index via setPage will inherently trigger an additional server request to fetch that page's data [2].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== data prop declaration and surrounding comments ==\n'
rg -n "Prop\\(|`@Prop`\\(|data:" src/components/table/table.tsx
sed -n '150,250p' src/components/table/table.tsx

printf '\n== search for remote-mode guards around updateData/replaceData ==\n'
rg -n "isRemoteMode\\(|shouldReplace|replaceData\\(newData\\)|updateData\\(patched\\)|updateOrAddData\\(" src/components/table/table.tsx

Repository: Lundalogik/lime-elements

Length of output: 4147


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
p = Path('src/components/table/table.tsx')
lines = p.read_text().splitlines()
for start,end in [(150,250)]:
    print(f"\n== {start}-{end} ==")
    for i in range(start-1, min(end, len(lines))):
        print(f"{i+1:4d}: {lines[i]}")
PY

Repository: Lundalogik/lime-elements

Length of output: 3291


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== around lines 670-700 ==\n'
sed -n '670,700p' src/components/table/table.tsx

printf '\n== around lines 780-840 ==\n'
sed -n '780,840p' src/components/table/table.tsx

printf '\n== around lastPaginatorPageCount initialization/usages ==\n'
rg -n "lastPaginatorPageCount|calculatePageCount\\(" src/components/table/table.tsx

Repository: Lundalogik/lime-elements

Length of output: 3867


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1090,1135p' src/components/table/table.tsx

Repository: Lundalogik/lime-elements

Length of output: 1685


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '720,735p' src/components/table/table.tsx
sed -n '1090,1135p' src/components/table/table.tsx

Repository: Lundalogik/lime-elements

Length of output: 2238


🌐 Web query:

site:tabulator.info replaceData pagination reset page remote pagination

💡 Result:

In Tabulator, the replaceData function is designed to silently replace all data in the table without resetting certain states like the current scroll position, filters, or sorting [1][2]. Because it is intended for silent, background updates, it does not automatically reset pagination to the first page when used [1][2]. If you are using remote pagination and need to reset the table to the first page when performing a data update, you should explicitly call the setPage function immediately after or in conjunction with your data loading method. To reset the page to 1, use the following: table.setPage(1).then(function{ table.replaceData; }); Alternatively, you can chain these actions using the promise returned by the replaceData method if the logic requires the data to load first: table.replaceData.then(function{ table.setPage(1); }); Note that while certain user interactions—such as changing filters, sorting, or toggling group visibility—automatically revert the table to the first page [3], the replaceData method deliberately avoids this behavior to maintain the user's current context [1][2].

Citations:


🌐 Web query:

Tabulator replaceData current page preserved pagination docs

💡 Result:

The Tabulator replaceData function is designed to replace all data in a table while preserving the current scroll position, sorting, and filtering state without triggering the AJAX loading popup [1][2]. However, concerning pagination, there is an important distinction between client-side and remote pagination: 1. Client-Side Pagination: When using client-side pagination, replaceData generally maintains the table state [3]. 2. Remote Pagination: When using remote pagination, the data is managed by the server, not Tabulator itself [4]. In this context, replaceData may not inherently preserve the current page, as the library treats the data update as a new set of information to be handled by the server [4]. If you are using remote pagination and wish to maintain the current page after an update, you can manually persist the page number [4]: - Use the getPage function to retrieve the current page number before performing the update [4]. - Use the setPage function to restore that page number after the update process is complete [4]. Since the replaceData method returns a promise, it is recommended to trigger setPage within that promise to ensure the table has finished processing the new data [2]: table.replaceData.then(function { table.setPage(currentPageNumber); }); If you are using setData instead of replaceData, be aware that setData is intended to reset the table entirely, including returning the pagination to the first page [3].

Citations:


Restore the current page after remote replaceData() updates.
When a row-id change forces replaceData(newData), this path restores scroll but not this.page. In remote pagination, Tabulator can fall back to page 1 while the controlled page prop stays on the old page, and the refreshRemotePaginator() guard skips the page restore when the page count hasn’t changed. Reuse the same setPage(targetPage) logic here and add a regression for a same-page-count replacement on page 3.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/table/table.tsx` around lines 387 - 390, Update the row-id
change path around preserveScroll and replaceData in the table component to
restore the current controlled page after the remote data replacement completes.
Reuse the existing setPage(targetPage) logic alongside scroll restoration,
ensuring same-page-count replacements on page 3 restore that page even when
refreshRemotePaginator() skips its guard, and add a regression test for this
scenario.

Comment on lines +751 to +765
const maxPage = this.calculatePageCount();

if (maxPage === this.lastPaginatorPageCount) {
return;
}

// `replaceData` resolves through `requestData`, which rejects when the
// component is destroyed, and Tabulator's ajax pipeline can reject too.
// Since the watchers call this without awaiting, swallow rejections here
// to avoid an unhandled promise rejection.
const restoreScroll = this.preserveScroll();

const targetPage = Math.min(this.page, Math.max(maxPage, FIRST_PAGE));

this.paginatorRefreshDepth++;
try {
await this.tabulator.replaceData();
if (this.tabulator.getPage?.() !== targetPage) {
await this.tabulator.setPage(targetPage);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Avoid paginating when pageSize is unset.

totalRowsChanged() always invokes this method, but pagination is disabled when pageSize is falsy. calculatePageCount() then returns NaN, so Line 759 can call setPage(NaN) and emit changePage(NaN).

Proposed fix
 private async refreshRemotePaginator() {
-    if (!this.isRemoteMode() || !this.tabulator || !this.initialized) {
+    if (
+        !this.isRemoteMode() ||
+        !this.pageSize ||
+        !this.tabulator ||
+        !this.initialized
+    ) {
         return;
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const maxPage = this.calculatePageCount();
if (maxPage === this.lastPaginatorPageCount) {
return;
}
// `replaceData` resolves through `requestData`, which rejects when the
// component is destroyed, and Tabulator's ajax pipeline can reject too.
// Since the watchers call this without awaiting, swallow rejections here
// to avoid an unhandled promise rejection.
const restoreScroll = this.preserveScroll();
const targetPage = Math.min(this.page, Math.max(maxPage, FIRST_PAGE));
this.paginatorRefreshDepth++;
try {
await this.tabulator.replaceData();
if (this.tabulator.getPage?.() !== targetPage) {
await this.tabulator.setPage(targetPage);
const maxPage = this.calculatePageCount();
if (maxPage === this.lastPaginatorPageCount) {
return;
}
const restoreScroll = this.preserveScroll();
const targetPage = Math.min(this.page, Math.max(maxPage, FIRST_PAGE));
this.paginatorRefreshDepth++;
try {
await this.tabulator.replaceData();
if (this.tabulator.getPage?.() !== targetPage) {
await this.tabulator.setPage(targetPage);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/table/table.tsx` around lines 751 - 765, Update the pagination
refresh method containing calculatePageCount and targetPage to return
immediately when pageSize is unset or falsy, before calculating maxPage or
invoking replaceData/setPage. Preserve the existing pagination behavior when
pageSize is configured.

if (shouldReplace) {
this.pool.releaseAll();
this.tabulator.replaceData(newData);
const restoreScroll = this.preserveScroll();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Tested this branch in the webclient and found a regression: after creating a new record, it used to show up highlighted right away — now it's nowhere to be seen. The highlight itself still works (activeRow just sets a CSS class), but nothing actually scrolls to the row. What made new records visible before was the very snap-to-top this PR removes: the new record sorts to the top of the list, and the scroll reset happened to reveal it. Now the position is kept, so the record sits highlighted somewhere off-screen — or on a different page if you weren't on page 1.

this.tabulator.replaceData(newData);
const restoreScroll = this.preserveScroll();
this.tabulator
.replaceData(newData)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Another thing I ran into while testing: deleting a row can make the table go white for a moment before the scroll position is put back. See video:

Screen.Recording.2026-07-13.at.08.40.47.mov

* all row elements (e.g. `replaceData`), so wrap such operations with this
* and invoke the returned callback once the rebuild has settled.
*/
private preserveScroll(): () => void {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Deleting a row while scrolled all the way down leaves the table a bit above the bottom instead of staying at the bottom. Expected behavior is to stay at the bottom of the table, see video:

Screen.Recording.2026-07-13.at.08.57.19.mov

this.lastPaginatorPageCount = maxPage;
restoreScroll();

if (targetPage !== this.page) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

There's a follow-up problem when the clamp kicks in: currentLoad never gets updated during the refresh (the guard skips it), so it still says page 3. If the user later navigates back to page 3, requestData builds the same load object again, isEqual matches, and no load event is emitted — the consumer never fetches that page's data. Resetting currentLoad when emitting the clamp would avoid that.

if (scrollContainer) {
scrollContainer.scrollTop = scrollTop;
scrollContainer.scrollLeft = scrollLeft;
this.lastPaginatorPageCount = maxPage;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This stores the page count that was computed before the awaits, and that value can be wrong: when totalRows and pageSize change in the same tick, the watchers fire one prop at a time, so the first refresh runs with half-updated props. If that in-between count gets stored here, a later change that happens to land on the same number is skipped, and the paginator keeps showing the old count. Rare in practice, but recomputing calculatePageCount() after the awaits would avoid it.

@FredrikWallstrom FredrikWallstrom removed their assignment Jul 13, 2026
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.

2 participants