Skip to content

Navigate list/detail sidebars through real links that preserve scroll - #1270

Merged
kody-bot merged 2 commits into
mainfrom
cursor/list-detail-prevent-scroll-reset-16ec
Aug 7, 2026
Merged

kody-bot merged 2 commits into
mainfrom
cursor/list-detail-prevent-scroll-reset-16ec

Conversation

@kentcdodds

@kentcdodds kentcdodds commented Aug 7, 2026 •

Copy link
Copy Markdown
Owner

Intent

Selecting a record in the account/admin list/detail views should not scroll the page back to the top. The client router already supports React Router-style scroll preservation via data-prevent-scroll-reset on intercepted anchors (see docs/contributing/architecture/request-lifecycle.md); the list/detail sidebars just never used it — their entries were <button>s calling navigate() programmatically.

Summary

  • AccountManagementListItemButton is now AccountManagementListItemLink: a real <a> carrying data-prevent-scroll-reset, so selecting a record keeps the scroll position and gains link semantics for free (middle/cmd-click opens in a new tab, hover warms the destination loader via intent prefetch). While disabled, the href is dropped so clicks mid-mutation do nothing.
  • Pre-navigation state resets (clearing messages, collapsing delete confirmations, seeding editor state) move to an onNavigate callback gated on the newly exported shouldRouterHandleClick, so modified clicks that open a new tab leave the current pane untouched.
  • All 13 list/detail routes updated: account memories, secrets, values, jobs, packages, package invocation tokens, MCP servers, remote connectors, integrations, email, activity, admin users, and admin platform feedback.
  • The integrations sidebar's OAuth-app group headers become anchors with the attribute too, and the detail pane's connection links pick it up, so every intra-view selection behaves consistently.
  • e2e/admin-rbac.spec.ts selectors updated from getByRole('button', ...) to getByRole('link', ...) for the users list.

Testing

  • npm run validate green locally (typecheck, lint, format, 554 unit test files / 1902 tests, Playwright e2e suite including the updated admin-rbac spec).
  • Manual browser verification: seeded 14 secrets, scrolled /account/secrets to window.scrollY ≈ 439, clicked through several sidebar entries — detail pane swaps while the scroll position stays put (previously it jumped to the top).

secrets_scroll_preserved_demo_trimmed.mp4

System changes

System recap — composes existing primitives (low risk)

Mode: recap · Base: main @ bc4dbfb2 · Head: 08bbbba3

Classification: composes — no primitives added or changed; this PR wires the router's existing data-prevent-scroll-reset handling into the app-ui list/detail views.

Primitives touched

Primitive Group Impact
app-ui surfaces composes — list/detail sidebar entries become anchors using the router's existing scroll-preservation attribute

System map

List/detail sidebar clicks now flow through router-intercepted anchors instead of programmatic navigate() calls, reusing the router's existing scroll restoration.

Legend: green = composes (wiring only) · amber = extended by this PR · red = new primitive · gray = context (unchanged, included only when an edge crosses it).

flowchart LR
	appUi["app-ui<br/>Browser app (Remix 3)"]:::touched
	appUi -->|"anchors with data-prevent-scroll-reset in 13 list/detail routes"| appUi

	classDef touched fill:#1a7f37,color:#fff
	classDef extended fill:#9a6700,color:#fff
	classDef added fill:#cf222e,color:#fff
	classDef untouched fill:#57606a,color:#fff
Loading

Before / after

Aspect Before After
Sidebar entry element <button> + navigate(href) <a href data-prevent-scroll-reset>
Scroll on select jumps to top preserved
New tab / prefetch unavailable native middle/cmd-click, hover intent prefetch
Disabled while mutating disabled attribute href dropped + aria-disabled
Open in Web Open in Cursor 

Summary by CodeRabbit

  • Improvements

    • Updated account and administration list entries to use standard links for navigation.
    • Preserved active styling, query parameters, scroll position, and UI cleanup when opening details.
    • Improved browser behaviors such as opening links in new tabs and copying link destinations.
    • Maintained fast in-app navigation while supporting normal browser link behavior.
  • Tests

    • Updated admin access-control tests to verify user entries through links.

The router already intercepts anchors carrying data-prevent-scroll-reset;
render account/admin list-detail sidebar entries as anchors with that
attribute (AccountManagementListItemButton -> AccountManagementListItemLink)
so selecting a record keeps the page's scroll position and gains real link
semantics (open in new tab, hover intent prefetch). Pre-navigation state
resets move to an onNavigate callback gated on the exported
shouldRouterHandleClick so modified clicks leave the current pane alone.

Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
@coderabbitai

coderabbitai Bot commented Aug 7, 2026 •

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 65077e7f-c290-451c-8ff6-ef205854c51f

📥 Commits

Reviewing files that changed from the base of the PR and between 08bbbba and 3ab6981.

📒 Files selected for processing (1)
  • packages/worker/client/routes/account-package-invocation-tokens.tsx

📝 Walkthrough

Walkthrough

The PR replaces account-management button navigation with router-aware links. It adds shared link behavior for hrefs, disabled states, scroll preservation, and navigation callbacks. The admin RBAC E2E test now locates user entries as links.

Changes

Account navigation

Layer / File(s) Summary
Shared router-aware link contract
packages/worker/client/client-router.tsx, packages/worker/client/routes/account-management-components.tsx
Exports shouldRouterHandleClick and adds AccountManagementListItemLink with href-based navigation, disabled-state handling, scroll preservation, and optional onNavigate callbacks.
Account route list links
packages/worker/client/routes/account-activity.tsx, packages/worker/client/routes/account-email.tsx, packages/worker/client/routes/account-jobs.tsx, packages/worker/client/routes/account-mcp-servers.tsx, packages/worker/client/routes/account-memories.tsx, packages/worker/client/routes/account-package-invocation-tokens.tsx, packages/worker/client/routes/account-packages.tsx, packages/worker/client/routes/account-remote-connectors.tsx, packages/worker/client/routes/account-secrets.tsx, packages/worker/client/routes/account-values.tsx
Replaces button-based detail navigation with links. Existing selection cleanup, message clearing, active states, and route search parameters remain supported.
Integration and admin navigation links
packages/worker/client/routes/account-integrations.tsx, packages/worker/client/routes/admin-platform-feedback.tsx, packages/worker/client/routes/admin-users.tsx, e2e/admin-rbac.spec.ts
Converts integration, feedback, and user entries to links. Integration links preserve search parameters and scroll behavior. The RBAC test clicks username links.

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

Possibly related PRs

Suggested reviewers: vojtaholik

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.76% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: replacing sidebar navigation with real links that preserve scroll position.
Description check ✅ Passed The description includes intent, summary, testing evidence, and system changes with clear details about scope and behavior.
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 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cursor/list-detail-prevent-scroll-reset-16ec

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.

@kody-bot
kody-bot marked this pull request as ready for review August 7, 2026 04:47
@github-actions

github-actions Bot commented Aug 7, 2026 •

Copy link
Copy Markdown
Contributor

🔎 Preview deployed: https://kody-pr-1270.kody-a99.workers.dev

Worker: kody-pr-1270
D1: kody-pr-1270-db
KV: kody-pr-1270-oauth-kv

Mocks:

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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 current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/worker/client/routes/account-package-invocation-tokens.tsx`:
- Around line 1069-1076: Update the AccountManagementListItemLink handlers to
stop calling selectToken(token) during navigation, since it initializes editor
state before the detail route commits. Make onNavigate perform only local
cleanup, and move editor initialization into applyPayload so it runs after the
committed detail route supplies the correct token data; apply the same change to
the corresponding handler around the second referenced link.
🪄 Autofix

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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 45c04f7d-2198-43f1-ac1d-0480a1990830

📥 Commits

Reviewing files that changed from the base of the PR and between bc4dbfb and 08bbbba.

📒 Files selected for processing (16)
  • e2e/admin-rbac.spec.ts
  • packages/worker/client/client-router.tsx
  • packages/worker/client/routes/account-activity.tsx
  • packages/worker/client/routes/account-email.tsx
  • packages/worker/client/routes/account-integrations.tsx
  • packages/worker/client/routes/account-jobs.tsx
  • packages/worker/client/routes/account-management-components.tsx
  • packages/worker/client/routes/account-mcp-servers.tsx
  • packages/worker/client/routes/account-memories.tsx
  • packages/worker/client/routes/account-package-invocation-tokens.tsx
  • packages/worker/client/routes/account-packages.tsx
  • packages/worker/client/routes/account-remote-connectors.tsx
  • packages/worker/client/routes/account-secrets.tsx
  • packages/worker/client/routes/account-values.tsx
  • packages/worker/client/routes/admin-platform-feedback.tsx
  • packages/worker/client/routes/admin-users.tsx

Comment thread packages/worker/client/routes/account-package-invocation-tokens.tsx Outdated
Selecting a token in the sidebar now only clears local confirmation and
message state; applyPayload initializes the editor from the committed
payload. Seeding pre-commit left a window where the URL (which
updateSelectedToken targets) still pointed at the previous token while
the editor held the new token's fields, so a save could cross-write.
Addresses CodeRabbit review on #1270.

Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
@kody-bot
kody-bot merged commit 9a4f3fc into main Aug 7, 2026
10 checks passed
@kody-bot
kody-bot deleted the cursor/list-detail-prevent-scroll-reset-16ec branch August 7, 2026 05:02
kentcdodds pushed a commit that referenced this pull request Aug 7, 2026
…0) (#1273)

* docs(decisions): record the account RecordTable direction

Thirteen account and admin screens split an 810px content column into a
352px list and a 434px record, so `<MetadataGrid columns={3}>` gets 115px
columns and a 36-character UUID wraps into five lines. The cap means the
record is 434px at 1244px wide and also at 2560px.

Records the decision to keep `AccountManagementShell` untouched and stop
splitting the column instead: one `RecordTable` with `mode` selecting where
the record renders (expand / pane / none), rows staying real links so
#1270's scroll-preserving navigation survives.

Supporting material carries the measurements, the four rejected
alternatives, the screen inventory with all 17 `MetadataGrid` call sites,
and four runnable prototypes. The prototypes load the repo's own fonts by
relative path rather than embedding them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0134SBHSA9QGtS4E3uTiaCdf

* Give account and admin records the whole content column (decision 0010)

Seventeen account and admin screens split an 810px content column into a
~370px list and a ~434px record. A `columns={3}` metadata band divided that
434px into 115px columns, so a UUID wrapped over five lines and package names
truncated at fifteen characters — while half the page sat empty.

Both steps of decision 0010:

Content layer. `MetadataGrid` sizes from the room it has
(`repeat(auto-fit, minmax(min(14rem, 100%), 1fr))`) instead of a declared
count, and the `columns` prop is gone. New `IdValue` clips an id to one line
in CSS while keeping the whole value in the DOM for assistive tech and
selection, with a per-field-named copy button (a new `chip` variant of
`CopyTextButton`). New `TimestampValue` owns nowrap, tabular figures, and the
null fallback.

`RecordTable`. One primitive for all seventeen screens: `expand` unfolds the
record under its own row, `pane` puts it below the table, `none` is a table
with no selection. Columns drop by priority through container queries, then
the same `<table>` becomes cards below 620px — these tables live inside a
200px-railed shell, so the viewport says very little about the room they have.
Rows stay real anchors, so `createListDetailRoute`, the `selected` param, and
the scroll-preserving navigation from #1270 are untouched.

`AccountManagementLayout`, its sidebar and list primitives, and the three
`accountManagementTable*Css` exports are deleted.

At 1244px `/account/packages` goes 1498px to 1159px tall, and the metadata band
finally gets three real columns. At 390px it goes 2976px to 2533px, as cards.

Also scopes the Sentry sourcemap inject to build output. It was rewriting the
tracked `packages/worker/public/theme-init.js` in place, which raced the
concurrent `format:check` leg of `npm run validate`.

Where the built API departs from the one 0010 settled — component generics do
not survive `remix/ui`'s JSX, and every screen loads its record separately
from its list — is recorded in the decision index.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Stop a short account page from stretching its own section gaps

Two things the empty `/account/packages` view exposed.

The shell reserves `min-height: 40rem` because the nav rail is absolutely
positioned and contributes no height, so a short page would let the rail
spill over the footer. But `align-content` defaults to `stretch`, which hands
that reserved height to the auto-sized rows: with `align-items: start` pinning
each section to the top of its now-tall track, a page shorter than the floor
grew a gap between every section. On an empty packages list that was 146px of
nothing between the description and the table; it is 44px — one section gap —
now, and the reserved height sits below the last section where it belongs.

This was always true of the shell; the split layout was simply never short
enough to have slack to distribute.

The search field also sliced its placeholder through a letter when it shrank
to share a toolbar row. `text-overflow: ellipsis` fixes it, but has to sit on
the input rather than on `::placeholder`, which does not accept that property.
It covers an overlong value too.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Report a refetch inside the table instead of above it

Typing in a search field that refetches flashed a bare, unpadded
"Loading packages…" line between the page description and the table, shoving
everything below it down and back on every keystroke — and the count slot
vanished at the same time, which resized the search field the reader was
typing into.

`RecordTable` takes a `busy` prop now. It sets `aria-busy` on the region and
dims the count in place; the announcement rides in a visually hidden polite
live region, so the whole thing costs no layout. Measured across a held-open
refetch, the search field and the table both stay at exactly the same size and
position.

The page-level line is kept for the first load, where nothing else is on
screen yet and it reflows nothing. The guard is "has a load ever completed",
not "is there data" — on an empty account those are not the same thing, and
the second one treats every refetch as a first load.

Only the five screens whose status actually flips to `loading` on a refetch
are wired up. The other nine hold their state through a load latch and never
flashed; their line is the only thing marking a first load, so it stays.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Drop the 0010 prototypes

Four self-contained HTML pages, ~7.9k lines, that existed to explore the shape
before it was built. The shape is built; the record keeps the measurements, the
rejected alternatives, and the reasoning. Leaving the pages in the repo commits
us to maintaining a second, diverging copy of the account UI.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Address PR review: two real bugs in RecordTable, and a timestamp that lies

`aria-expanded` was derived from selection alone, but the record row only
renders when a record exists. A selected row whose detail was still loading (or
404'd) reported an expanded region and an `aria-controls` id that was not in
the DOM. Both now follow the row that actually renders.

Worse: `expand` mode dropped the record entirely when the selected row was not
in the current window. A deep link, a filter change, or paging past the
selection left a loaded detail with nothing to unfold under, and it silently
vanished. It falls back to a pane below the table now — one change in the
primitive, so all five expand screens get it.

Two `updated` cells formatted a payload timestamp with
`new Date(value).toLocaleDateString()`. `formatTimestamp` exists because these
arrive as `YYYY-MM-DD HH:MM:SS`, which Safari rejects outright and which every
engine reads as local time rather than UTC. Chrome tolerated it, which is why
the screenshots looked right. Both go through a new `formatTimestampDate` that
shares the normalization.

Also from review:

- The per-cell `Nch` clamps could overflow a card narrower than the clamp.
  They are one exported `recordCellClamp(ch)` now, using `min(Nch, 100%)` —
  the same guard `MetadataGrid` already uses — which also removes fourteen
  near-identical copies.
- `CopyTextButton`'s `ariaLabel` overrode the label text, so a named button
  kept reading "Copy package id" after it had copied. The result rides in a
  `role="status"` region instead.
- `Created` and `Updated` on a package token fell back to "Never", which
  cannot be true of a stored token. They take the default em dash.
- `/account/secrets` showed "No secrets yet" during its first load.
- The ADR quoted the `MetadataGrid` template without the `min()` guard.

Not taken: debouncing the search inputs. Real, but `AccountManagementSearchField`
did not debounce either, so it is pre-existing rather than introduced here, and
changing when the URL updates deserves its own change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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.

3 participants