You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
feat: make the index table taller with a scrolling page layout (#1122) - #1153
Closes#1122. Supersedes #1121 and #1120 (Kaspars' experiment), which can be closed once this merges.
Stacked on #1139 (#998). The base is fran/998-in-flow-header. Merge #1139 first, then this PR, and release them together, so consumers migrate once.
On index pages the page now scrolls instead of the table box, so the rows get far more of the window. At 1440 × 800 on meta-disco Files, rows get 608px (76%) once scrolled, up from 301px (38%).
Everything that moves is CSS, mostly scroll-driven animations. The only JavaScript is three measurements written to CSS variables, the virtualizer following the page, and an extra copy of the column head in the markup.
What changed
Page scroll: the index page scrolls; the table box only scrolls sideways.
What pins: the header; the filter sidebar; the entity row when it has tabs, and always below md, where it holds the Filter button (a title alone scrolls away from md up); and the column head, 16px below whatever is pinned above it.
What scrolls away: alerts, the toolbar, pagination and the footer.
Pinned column head: a sticky element can't pin to the window from inside a sideways scroll box, so an aria-hidden copy of the head pins instead.
It follows the box's sideways scroll through a scroll-driven animation (scroll-timeline, timeline-scope, translateX(calc(-100% + 100cqw))).
Its column widths come from the real table (--table-columns, through a ResizeObserver), so auto columns line up.
Its controls (sort labels, select-all) stay clickable, but a TableHeadContext gives them tabIndex -1, so keyboard and screen-reader users only meet the real head.
A pinned band masks the 16px gap and draws the table's rounded top edge.
Sidebar and footer: the sticky filter sidebar shrinks as the footer scrolls in (the footer's view timeline drives its max-height), so its top never slides under the header.
Virtualization:
useVirtualization({ pageScroll }) virtualizes against the window, offset by the table's distance from the top of the page.
It defaults to false, so consumers' own tables (brc-analytics, ncpi-dataset-catalog, hca-atlas-tracker, meta-disco-browser) keep virtualizing in their box.
Table and TableCreator take pageScroll (default false); the index table view turns it on. Without it, Table renders as before.
The observers restart if the table remounts after "No Results".
Scroll padding and measurements:
On index pages, scroll-padding-top clears the pinned row and head, so focus and hash links stop below them.
The tabs row and head heights are measured (--entity-layout-height, --table-head-height) because they wrap. useLayoutHeight gains an optional key.
Table surfaces: the table draws its own grey background and borders, and the paper stays white, so an overscroll bounce doesn't flash grey. The Graph view is unchanged.
Loading: the overlay is isolated inside the paper, so it stays under the pinned row, and its icon pins to the middle of the window on long tables.
Fallback: without scroll-driven animations (Firefox before 159, Safari before 26), one feature query, SUPPORTS_SCROLL_DRIVEN_ANIMATIONS, switches everything back to today's layout. The page fits the window, the table scrolls in its box with its own sticky head, and the virtualizer uses the box. The footer still pushes the sidebar up, as today.
Why
The rows only had 38% of the window: the header, notices, toolbar, column head, pagination and a fixed footer all stayed on screen (cc-design#337). Kaspars recommended a page that scrolls, with the header, tabs, filter sidebar and column head pinned. This builds that CSS-first, on #998's in-flow header and measured variables.
Tabs are detected in CSS (:has(.MuiTabs-root)), not from config.
Fallback scope: where scroll-driven animations aren't supported, the whole index layout falls back together.
Current Firefox (158) is in that set until 159 ships; Firefox 159, Chrome 116+ and Safari 26 get the new layout (the query also requires timeline-scope).
In those browsers the footer still pushes the sidebar up, as today (agreed).
Height fallbacks: the pinned row and head heights fall back to 57px until measured. The head's height is a page-wide variable, one index table per page (agreed).
Scroll padding adds the head's height only where a table is rendered, so the Graph view and "No Results" don't get it.
Set up a consumer with this build and the #998 migration, then open an index page (e.g. meta-disco or anvil-cmg /files) in Chrome at 1440 × 800. Close Next's dev "Security Insight" dialog first, because it locks scroll.
The issue's definition of done:
Dave has seen the demo and approved it. Use the captures below: rows go from 301px (37.6%) to 608px (76.0%) at 1440 × 800, scrolled. Live:
Scroll /files. The alert, key and toolbar scroll away; the header, tabs row and sidebar pin; the column head pins 16px below the tabs, with a rounded top edge and a 1px line under it.
Turn on every column under Edit Columns, scroll down, then wheel sideways over the table. The pinned head stays aligned, and clicking a pinned heading sorts.
Scroll to the end of the page. The sidebar shrinks above the footer, and its top stays under the header.
Narrow the window to about 1000px. The tabs row with Filter pins, and the drawer opens from it.
At 390 (device mode), the tabs row pins and the cards scroll under it.
Tab through the head and rows. There are no double stops, and focus isn't hidden under the pinned bars.
In Firefox 158 or earlier, you get today's layout: the page fits the window and the table scrolls in its box.
Every site on findable-ui upgraded and checked. All nine consumers were checked on this build (reports and screenshots in ~/Downloads/findable-1122/<repo>/):
Consumer
Result
brc-analytics
Pass. One migration item: Priority Pathogens fallback.
hprc-data-explorer
Pass, including grouping.
anvil-portal (Citations)
Pass.
ncpi-dataset-catalog
Pass, including the research table.
data-browser (anvil-cmg, hca-dcp)
Pass. lungmap is blocked by its backend.
meta-disco-browser
Pass. Donors and BioSamples are blocked by a backend mismatch.
ucsc-brain-explorer
Pass. One optional item.
hca-atlas-tracker
Pass. Grouping workaround in #1614, LinkCell in #1613.
data-portal
Not applicable.
Each consumer's migration actions are saved in its own notes and collected in ~/Downloads/findable-1122/MIGRATION-NOTES.md.
the index page scrolls instead of the table box (cc-design#337). the header,
the tabs row (when there are tabs), the filter sidebar and the column head
stay pinned; the title, alerts, toolbar, pagination and footer scroll away.
- the column head pins 16px below the tabs row, or the header without tabs.
a band masks that gap and draws the table's rounded top edge.
- the table box still scrolls sideways. a copy of the column head
(aria-hidden, out of the tab order) pins instead of the head inside the
box, and follows its sideways scroll through a scroll-driven animation.
- rows are virtualized against the window, offset by the table's distance
from the top of the page.
- the tabs row height is measured into --entity-layout-height.
- index pages extend scroll-padding-top past the pinned tabs and head.
- the table draws its own background and borders; the paper stays white.
- where scroll-driven animations aren't supported (firefox before 159,
safari before 26), the page fits the window and the table scrolls in its
own box, as before, including virtualization against the box.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ns (#1122)
content-sized columns (e.g. width auto) resolved to different widths in the
head-only copy, shifting every later column. also target the copy by its
aria-hidden attribute rather than :first-child, which emotion flags for ssr.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…inned head copy (#1122)
a content-sized column can change width as virtualized rows render while the
table keeps its size, so each head cell is observed as well as the table.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ge scroll is asked for (#1122)
consumers (brc-analytics, ncpi-dataset-catalog, hca-atlas-tracker and
meta-disco-browser) call usevirtualization for their own tables, which scroll
in a box. the index table opts in with pagescroll: true.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ll padding (#1122)
a wrapping column title makes the head taller than the one-line fallback, which
left focused rows partly under it.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…stays below the header (#1122)
the footer's view timeline drives the sidebar's max-height, by as much of the
footer as is in view. the feature query moves to a shared styles constant;
without scroll-driven animations the footer pushes the sidebar up, as before.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ts (#1122)
- the column head's offset derives from the index scroll padding, so the
tabs-or-below-md rule lives in one place; named fallbacks.
- the shared gridtable head is back to top: 0; only the pinned copy reads
--table-head-top.
- the band and the head copy each sit under one @supports and breakpoint
block; the copy uses the pinned mixin.
- custom property names become constants; the feature query is renamed
supports_scroll_driven_animations, since the sidebar uses it too.
- the column widths are written to the head copy only, and unchanged
values aren't rewritten; the virtualizer modes share their options.
- tests: one css-support mock helper, one aftereach, imported constants.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: cc-claude-tools <cc-claude-tools@clevercanary.com>
- the table's observers (page offset, column widths, head height) restart
when the table is rendered again after "no results"; uselayoutheight takes
an optional key for this.
- the paper isolates its loading overlay, so it stays under the pinned
entity layout row.
- the band and the head's share of the scroll padding apply only where
there's a table (not the graph view or no results).
- the entity row's share of the scroll padding applies only where the row
pins (with scroll-driven animations).
- tests restore their css and scrolly overrides.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: cc-claude-tools <cc-claude-tools@clevercanary.com>
…t-in page scroll (#1122)
- a tablehead context gives the pinned copy's controls (sort labels and the
select-all checkbox) tabindex -1: still clickable, out of the tab order.
replaces the unreleased sorttabindex prop; header renderers can read it.
- table and tablecreator take pagescroll (default false); the index table
view turns it on. without it, the table renders as before: its own scroll
box, no head copy.
- on the index page, the loading icon pins to the middle of the window
rather than the middle of a table-tall overlay.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: cc-claude-tools <cc-claude-tools@clevercanary.com>
…uery (#1122)
chrome 115 has animation-timeline but not timeline-scope (116+), which the
head copy and the sidebar need to read timelines from outside their subtree.
those browsers now get the full fallback.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: cc-claude-tools <cc-claude-tools@clevercanary.com>
The reason will be displayed to describe this comment to others. Learn more.
🔵 Needs a closer look
The coupled scrolling, virtualization, and focus behavior needs final human browser validation and demo sign-off, and the loading-indicator issue remains unresolved.
No open issues 🎉
1 previously missed issue
In code that hasn't changed since last review
Loading icon scrolls away because grid row lacks sticky height
src/components/Index/indexView.styles.ts:164
LoadingPaper is a grid (loading.styles.ts), so align-content: start packs its single icon row down to the icon's intrinsic height plus margin. The sticky icon is constrained to that grid area, leaving no vertical space in which to stick; on a long table it scrolls out of view instead of remaining visible during loading. Use a column flex layout here, or otherwise give the icon a containing block spanning the overlay.
…column (#1122)
the overlay's paper is a grid, so with align-content start the icon's row
shrank to the icon and left it no room to stick; a column flexbox gives it
the whole overlay.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: cc-claude-tools <cc-claude-tools@clevercanary.com>
Copilot's previously missed finding (src/components/Index/indexView.styles.ts:164, the loading icon can't stick inside LoadingPaper's grid): fixed in c6f1d85. On the index paper the overlay is now a column flexbox, so the sticky icon's range is the whole overlay. Checked in Playwright on anvil-cmg /files (3,021px paper) with an overlay shaped like Loading: the icon stays at 386px in a 900px window with the page scrolled to the top, the middle and near the end.
The reason will be displayed to describe this comment to others. Learn more.
🔵 Needs a closer look
The unresolved heading-focus behavior and coordinated cross-browser scrolling changes require final human interaction validation.
No open issues 🎉
1 previously missed issue
In code that hasn't changed since last review
Avoid hiding pinned header while focused heading is offscreen
src/components/Table/table.styles.ts:76
This hides the pinned head for the entire time a real heading has keyboard focus, even after that heading scrolls off screen. For example, tab to a sort label and then scroll down with PageDown or the mouse wheel without moving focus: the real head cannot stick outside its horizontal scroll container, and the copy remains hidden, so the pinned column headings disappear. Limit this suppression to when the focused real head actually overlaps the copy, or mirror its focus indication onto the copy instead.
…in line (#1122)
a view timeline on the table's scroll wrapper, inset to the pin line, shows
the copy during exit-crossing only. at rest the table's own head is in view,
focus ring included, so the focus rule that hid the copy (and could leave the
page with no pinned head while scrolling) is gone.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: cc-claude-tools <cc-claude-tools@clevercanary.com>
Copilot's previously missed finding (review 5636652773, "Avoid hiding pinned header while focused heading is offscreen", src/components/Table/table.styles.ts:76): fixed in e30b09d.
The :has(:focus-visible) rule that hid the pinned copy is gone, so the page is never left without a pinned head while focus stays in the table's own head.
The copy now shows only once the table's top has passed the pin line, using a view timeline on the table's scroll wrapper (--table-scroll-view, inset to the pin line). At rest the table's own head is in view, so its focus ring shows there.
Tabbing into the head while scrolled down still scrolls the page up to the real head. That, and a focus indicator for the clipped pinned copy, are tracked in #1154.
The reason will be displayed to describe this comment to others. Learn more.
🔵 Needs a closer look
The coupled browser-dependent layout and virtualization changes need final human visual and accessibility validation, and the required design approval remains unchecked.
No open issues 🎉
Review effort: Balanced
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #1122. Supersedes #1121 and #1120 (Kaspars' experiment), which can be closed once this merges.
Stacked on #1139 (#998). The base is
fran/998-in-flow-header. Merge #1139 first, then this PR, and release them together, so consumers migrate once.On index pages the page now scrolls instead of the table box, so the rows get far more of the window. At 1440 × 800 on meta-disco Files, rows get 608px (76%) once scrolled, up from 301px (38%).
Everything that moves is CSS, mostly scroll-driven animations. The only JavaScript is three measurements written to CSS variables, the virtualizer following the page, and an extra copy of the column head in the markup.
What changed
aria-hiddencopy of the head pins instead.scroll-timeline,timeline-scope,translateX(calc(-100% + 100cqw))).--table-columns, through a ResizeObserver), soautocolumns line up.TableHeadContextgives themtabIndex -1, so keyboard and screen-reader users only meet the real head.max-height), so its top never slides under the header.useVirtualization({ pageScroll })virtualizes against the window, offset by the table's distance from the top of the page.false, so consumers' own tables (brc-analytics, ncpi-dataset-catalog, hca-atlas-tracker, meta-disco-browser) keep virtualizing in their box.TableandTableCreatortakepageScroll(defaultfalse); the index table view turns it on. Without it,Tablerenders as before.scroll-padding-topclears the pinned row and head, so focus and hash links stop below them.--entity-layout-height,--table-head-height) because they wrap.useLayoutHeightgains an optional key.SUPPORTS_SCROLL_DRIVEN_ANIMATIONS, switches everything back to today's layout. The page fits the window, the table scrolls in its box with its own sticky head, and the virtualizer uses the box. The footer still pushes the sidebar up, as today.Why
The rows only had 38% of the window: the header, notices, toolbar, column head, pagination and a fixed footer all stayed on screen (cc-design#337). Kaspars recommended a page that scrolls, with the header, tabs, filter sidebar and column head pinned. This builds that CSS-first, on #998's in-flow header and measured variables.
Assumptions I made
:has(.MuiTabs-root)), not from config.timeline-scope).@supports notfor old browsers;How to verify
Set up a consumer with this build and the #998 migration, then open an index page (e.g. meta-disco or anvil-cmg /files) in Chrome at 1440 × 800. Close Next's dev "Security Insight" dialog first, because it locks scroll.
The issue's definition of done:
Dave has seen the demo and approved it. Use the captures below: rows go from 301px (37.6%) to 608px (76.0%) at 1440 × 800, scrolled. Live:
Merged and released in findable-ui. Merge feat!: move the app header into flow and replace the measured layout offsets with css (#998) #1139 first, then this; release together.
Every site on findable-ui upgraded and checked. All nine consumers were checked on this build (reports and screenshots in
~/Downloads/findable-1122/<repo>/):Each consumer's migration actions are saved in its own notes and collected in
~/Downloads/findable-1122/MIGRATION-NOTES.md.Scroll the whole index page and keep the tabs and table head sticky #1120 and feat: scroll the whole index page and keep the tabs and table head sticky (#1120) #1121 closed or folded in. This PR supersedes them; close them on merge.
Locally:
check-format,lint,test-compileand 948 tests pass. New tests cover:useLayoutHeight's key.Screenshots
findable-ui-1122-files-after-scroll-390x844.webm
findable-ui-1122-files-after-scroll-1440x800.webm
findable-ui-1122-files-after-wide-sideways-1440x800.webm
findable-ui-1122-files-before-scroll-1440x800.webm
🤖 Generated with Claude Code
🐦 Built with cc-claude-tools