Skip to content

refactor(ScrollAnchor): pixel-accurate anchor and rename module#757

Merged
lcottercertinia merged 4 commits into
certinia:mainfrom
lukecotter:feat-native-scroll-anchor-feel
May 9, 2026
Merged

refactor(ScrollAnchor): pixel-accurate anchor and rename module#757
lcottercertinia merged 4 commits into
certinia:mainfrom
lukecotter:feat-native-scroll-anchor-feel

Conversation

@lukecotter
Copy link
Copy Markdown
Contributor

@lukecotter lukecotter commented May 9, 2026

⚠️ Depends on #756 — that PR must merge first.
This branch is stacked on feat-row-focus-improvements (the head of #756). Targeting main for visibility, but >please merge #756 first so the diff here resolves to only this PR's changes.
✅ It has merged

📝 PR Overview

Improvement on top of #756. Three changes layered:

  1. Switches the row-focus behaviour from row-centering (which moved the anchor row to the visual middle on every re-render and produced a small visible jump) to pixel-accurate anchoring — the same row stays at the same Y pixel in the holder across sort / filter / tree-toggle / data update.
  2. Renames MiddleRowFocusScrollAnchor to reflect the broader role.
  3. Makes the module data-agnostic so it can drop into any Tabulator table.

🛠️ Changes made

Pixel-accurate anchoring

  • Capture the anchor row's Y offset inside the holder at snapshot time (offsetTop - scrollTop) and restore scrollTop synchronously in renderComplete so the row sits at exactly the same pixel — eliminates the post-render flash.
  • When the anchor row is outside the post-render virtual DOM window, refill the vDom via Tabulator's internal _virtualRenderFill (same hook scrollToRow uses) before reading offsetTop.
  • Replace async scrollToRow + setTimeout + scrollIntoView chain with a single synchronous scrollTop write.
  • Use offsetTop instead of paired getBoundingClientRect calls to avoid forcing extra layout reads on the hot path.

Rename

  • MiddleRowFocusScrollAnchor (module name, file, table option middleRowFocusscrollAnchor, tests, and call sites in AggregatedTable, BottomUpTable, TableShared, TimeOrderTable).

Data-agnostic fallback (when the captured anchor row is no longer in the post-render display set)

  • Removes the apex-log-parser LogEvent import and the timestamp-keyed binary search (_findClosestActive / _findClosestActiveSibling, ~100 lines).
  • Replaces it in _resolveAnchorRow with two legs using only public Tabulator APIs:
    • Collapse case → walk up getTreeParent() to the nearest displayed ancestor. Matches the previous "anchor lands on the parent" feel for the call-tree views.
    • Filter case → capture the anchor row's index in getDisplayRows() pre-render; on restore, use the row at that (clamped) index. Replaces "nearest by timestamp" (only meaningful in time-ordered tables) with "same position in the list" (natural in any sort order).
    • If neither leg yields a row, return null and the restore is a no-op.

Tests added for offset capture, synchronous pixel-accurate restore, both fallback legs, and the exhausted case.

🧩 Type of change (check all applicable)

  • 🐛 Bug fix - something not working as expected
  • ✨ New feature – adds new functionality
  • ♻️ Refactor - internal changes with no user impact
  • ⚡ Performance Improvement
  • 📝 Documentation - README or documentation site changes
  • 🔧 Chore - dev tooling, CI, config
  • 💥 Breaking change

📷 Screenshots / gifs / video [optional]

n/a

🔗 Related Issues

related #756

✅ Tests added?

  • 👍 yes
  • 🙅 no, not needed
  • 🙋 no, I need help

📚 Docs updated?

  • 🔖 README.md
  • 🔖 CHANGELOG.md
  • 📖 help site
  • 🙅 not needed

Anything else we need to know? [optional]

Until #756 lands, the diff against `main` here will include #756's commit too. After #756 merges, the diff will collapse to this PR's three refactor commits.

lukecotter added 3 commits May 9, 2026 10:26
Rename MiddleRowFocus → ScrollAnchor (file, class, module name, table option)
to better reflect its role.

Replace the async re-center (scrollToRow().then + setTimeout(scrollIntoView))
with a synchronous, pixel-accurate restore in renderComplete: capture the
anchor row's offsetTop relative to holder.scrollTop, then on restore set
scrollTop = rowEl.offsetTop - capturedOffset in the same JS turn so the
browser paints the corrected position directly — no visible flash.

Use Tabulator's private _virtualRenderFill to pull the anchor row into the
vDom window when it has been scrolled out, but skip the refill when the row
element is already connected. Fall back to nearest-active-by-timestamp when
the original anchor row has been filtered or collapsed away.
- Capture anchor offset via row.offsetTop - holder.scrollTop instead of
  paired getBoundingClientRect calls to avoid forced layout reads on every
  render-start.

- Skip _virtualRenderFill when the anchor row's element is already
  connected to the DOM — only refill when the row was scrolled out of the
  vDom window.

- Thread displayRows through _findClosestActive so the recursive binary
  search reuses one getDisplayRows() call instead of refetching at every
  tree level.

- Extract _isRowActive helper to dedupe the displayRows.indexOf(_getSelf())
  pattern across four call sites.
…allback

Replaces the timestamp-keyed binary search (`_findClosestActive` /
 `_findClosestActiveSibling`) used when the captured anchor row is no
 longer in the post-render display set. The previous logic depended on
 `originalData.timestamp` from the call-tree row shape, coupling the
 module to apex-log-parser and making it unusable on other Tabulator
 tables.

The replacement in `_resolveAnchorRow` uses only public Tabulator APIs:

- Collapse case: walk up `getTreeParent()` to the nearest displayed
  ancestor. Matches the previous "anchor lands on the parent" feel for
  the call-tree views via the same tree relationship.
- Filter case: capture the anchor row's index in `getDisplayRows()`
  pre-render; on restore, use the row at that index in the post-render
  display set, clamped. Replaces "nearest by timestamp" (only meaningful
  in time-ordered tables) with "same position in the list" (natural in
  any sort order).
- If neither leg yields a row, return null and the restore is a no-op.
lcottercertinia
lcottercertinia previously approved these changes May 9, 2026
@lukecotter lukecotter dismissed lcottercertinia’s stale review May 9, 2026 15:36

The merge-base changed after approval.

lcottercertinia
lcottercertinia previously approved these changes May 9, 2026
@lukecotter lukecotter dismissed lcottercertinia’s stale review May 9, 2026 15:40

The merge-base changed after approval.

@lcottercertinia lcottercertinia merged commit 44426db into certinia:main May 9, 2026
5 checks passed
@lukecotter lukecotter deleted the feat-native-scroll-anchor-feel branch May 9, 2026 15:41
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