Skip to content

feat!: move the app header into flow and replace the measured layout offsets with css (#998) - #1139

Open
frano-m wants to merge 3 commits into
mainfrom
fran/998-in-flow-header
Open

frano-m wants to merge 3 commits into
mainfrom
fran/998-in-flow-header

Conversation

@frano-m

@frano-m frano-m commented Oct 21, 2026

Copy link
Copy Markdown
Contributor

Summary

Closes #998.

Moves the app header into normal flow (position: sticky) and replaces every JS-measured layout offset with CSS. Content now follows the header at its real height in the server-rendered HTML. The ~57px hydration layout shift on main (clevercanary/hca-atlas-tracker#1543) is gone, and wrapping announcements can no longer leave page titles under the header.

This is a breaking change (feat!). It removes the layout-dimensions provider and API. Migration steps are in #998.

What changed

  • Header: position: sticky at every width, announcements still inside. A server-rendered global html { scroll-padding-top: var(--header-height) } lets hash links, focus and scrollIntoView clear it on every page.
  • Measurement: Header and Footer measure themselves with useLayoutHeight, which writes --header-height / --footer-height on <html> from the ResizeObserver callback, before paint and without re-rendering. CSS reads them with fallbacks (57px / 56px).
  • AppLayout: a grid (auto 1fr auto) with min-height: 100vh. The page grows, so the header stays pinned, and height: 100% below main still fills the space between header and footer.
  • No more offsets: Main, ContentLayout, Error, IndexView and the ResearchView drawer no longer push content down by the header height. Sticky elements pin at top: var(--header-height) through a shared pinned() style.
  • Focus: focusable elements inside regions that pin within the scroll padding (the header, the dictionary title and filters) get scroll-margin-top: -100vh. Focusing them, e.g. a menu restoring focus to its trigger, no longer jumps the page.
  • Explore view, permanent sidebar, research drawer: contain: size, so they fill the main row without growing it. The page fits the window from md up; below md the explore view is 100vh - header and the footer follows.
  • Data dictionary:

Breaking changes

  • Removed LayoutDimensionsProvider, useLayoutDimensions and providers/layoutDimensions (dimensions.header/footer.height, headerRef, footerRef).
  • Removed useLayoutSpacing and the LayoutSpacing type.
  • Removed header-offset props from styled components: headerHeight on ContentGrid, NavigationGrid, OutlineGrid and Positioner; offset on ErrorLayout; spacing props on StyledGridEntityView, the research drawer and the data dictionary layouts.
  • Removed MainWithOffset, and renamed the styled export in Main/main.styles from Main to StyledMain.
  • The header is in flow, so any offset a consumer adds from the header height now doubles.

Verification

All nine consumers (anvil-portal, brc-analytics, data-browser, data-portal, hca-atlas-tracker, hprc-data-explorer, meta-disco-browser, ncpi-dataset-catalog, ucsc-brain-explorer) were captured before and after, in five rounds:

  • desktop 1440×900 and mobile 390×844, at top, middle and bottom, plus full-page shots
  • scroll videos on sticky pages, CLS via PerformanceObserver, deep-link landings and focus probes
  • banner cases, using real or mocked announcements and the session-timeout banner

Each consumer migrated in a scratch worktree, and its typecheck and lint pass. Each has a migration.patch ready for its own follow-up PR.

  • No regressions caused by findable-ui on the final build. The remaining differences are consumer migration items.
  • The MAIN hydration shift is gone, and CLS is the same or lower everywhere (e.g. brc home 1.00 → 0.009 after migration).
  • Data dictionary: pixel-identical to published in data-portal at 1440 and 390, deep links land as on published (better on mobile), and the filtered deep link lands at published's position.
  • Header and menu focus while scrolled: no page jumps; published jumps up to 768px.
  • Locally: test-compile, lint, check-format, tsc build and 886 tests pass. New tests cover useLayoutHeight (writes and removes the variable, including a measured 0) and the header's position: sticky.

Follow-ups

🤖 Generated with Claude Code

Copilot AI balanced review requested due to automatic review settings October 21, 2026 03:20

Copilot AI 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.

🔵 Needs a closer look

The breaking layout overhaul has an unresolved rendering issue and needs final human review of browser-validation results.

1 open issue
What changed in this PR

Moves the shared app header into normal flow and replaces React-managed layout offsets with CSS to reduce hydration layout shifts. This breaking change removes the layout-dimensions APIs.

Changes:

  • Adds local height measurement, CSS variables, and sticky positioning.
  • Reworks page and dictionary layouts around CSS grid.
  • Removes obsolete spacing APIs and adds measurement and header tests.
File Description
tests/​useLayoutHeight.test.tsx Tests height measurement and cleanup.
tests/​headerPosition.test.tsx Tests sticky header positioning.
src/​views/​ResearchView/​assistant/​components/​Drawer/​drawer.tsx Removes spacing props.
src/​views/​ResearchView/​assistant/​components/​Drawer/​drawer.styles.ts Adds CSS-based drawer sizing.
src/​styles/​common/​mixins/​pinned.ts Adds sticky and focus styles.
src/​styles/​common/​mixins/​breakpoints.ts Adds the 1024px upward breakpoint.
src/​providers/​layoutDimensions/​types.ts Removes provider types.
src/​providers/​layoutDimensions/​provider.tsx Removes the dimensions provider.
src/​providers/​layoutDimensions/​hook.ts Removes the dimensions hook.
src/​providers/​layoutDimensions/​context.tsx Removes dimensions context.
src/​providers/​layoutDimensions/​constants.ts Removes default dimensions.
src/​hooks/​UseLayoutSpacing/​types.ts Removes spacing types.
src/​hooks/​UseLayoutSpacing/​hook.ts Removes the spacing hook.
src/​hooks/​useHtmlStyle/​types.ts Supports CSS custom properties.
src/​hooks/​useHtmlStyle/​constants.ts Defines height property names.
src/​components/​Layout/​hooks/​UseLayoutHeight/​utils.ts Writes observed heights to CSS.
src/​components/​Layout/​hooks/​UseLayoutHeight/​types.ts Defines measurement results.
src/​components/​Layout/​hooks/​UseLayoutHeight/​hook.ts Adds local height measurement.
src/​components/​Layout/​components/​Sidebar/​sidebar.styles.ts Adds size containment.
src/​components/​Layout/​components/​Sidebar/​components/​SidebarPositioner/​sidebarPositioner.tsx Removes measured height props.
src/​components/​Layout/​components/​Sidebar/​components/​SidebarPositioner/​sidebarPositioner.styles.ts Pins below the header using CSS.
src/​components/​Layout/​components/​Main/​main.tsx Removes the header offset.
src/​components/​Layout/​components/​Main/​main.styles.ts Replaces offset styles with StyledMain.
src/​components/​Layout/​components/​Header/​header.tsx Adds measurement and global scroll padding.
src/​components/​Layout/​components/​Header/​header.styles.ts Adds scroll-padding and focus styles.
src/​components/​Layout/​components/​Header/​components/​Content/​components/​Navigation/​components/​NavigationMenu/​navigationMenu.styles.ts Sizes menus using header height.
src/​components/​Layout/​components/​Header/​common/​constants.ts Sets sticky positioning and height fallback.
src/​components/​Layout/​components/​Footer/​footer.tsx Measures footer height locally.
src/​components/​Layout/​components/​Footer/​footer.styles.ts Uses the shared height constant.
src/​components/​Layout/​components/​Footer/​common/​constants.ts Defines footer height fallback.
src/​components/​Layout/​components/​ContentLayout/​contentLayout.tsx Removes measured offset props.
src/​components/​Layout/​components/​ContentLayout/​contentLayout.styles.ts Uses CSS sticky positioning.
src/​components/​Layout/​components/​ContentLayout/​components/​Main/​main.tsx Updates the styled-main import.
src/​components/​Layout/​components/​AppLayout/​appLayout.styles.ts Introduces the three-row app grid.
src/​components/​Index/​indexView.tsx Removes spacing-hook usage.
src/​components/​Index/​indexView.styles.ts Adds contained, responsive sizing.
src/​components/​Error/​error.tsx Removes measured header spacing.
src/​components/​Error/​error.styles.ts Removes offset styles.
src/​components/​DataDictionary/​hooks/​UseMeasureFilters/​types.ts Removes obsolete measurement types.
src/​components/​DataDictionary/​hooks/​UseMeasureFilters/​hook.ts Removes header-dependent filter measurement.
src/​components/​DataDictionary/​dataDictionary.tsx Uses local measurement and grid layouts.
src/​components/​DataDictionary/​dataDictionary.styles.ts Defines grid areas and scroll padding.
src/​components/​DataDictionary/​components/​Layout/​constants.ts Defines sticky offsets and filter fallback.
src/​components/​DataDictionary/​components/​Layout/​components/​TitleLayout/​types.ts Removes spacing props.
src/​components/​DataDictionary/​components/​Layout/​components/​TitleLayout/​titleLayout.tsx Updates parameter documentation.
src/​components/​DataDictionary/​components/​Layout/​components/​TitleLayout/​titleLayout.styles.ts Pins and truncates desktop titles.
src/​components/​DataDictionary/​components/​Layout/​components/​OutlineLayout/​types.ts Removes spacing props.
src/​components/​DataDictionary/​components/​Layout/​components/​OutlineLayout/​outlineLayout.tsx Updates parameter documentation.
src/​components/​DataDictionary/​components/​Layout/​components/​OutlineLayout/​outlineLayout.styles.ts Uses CSS offsets and footer compensation.
src/​components/​DataDictionary/​components/​Layout/​components/​FiltersLayout/​types.ts Removes spacing props.
src/​components/​DataDictionary/​components/​Layout/​components/​FiltersLayout/​filtersLayout.tsx Updates parameter documentation.
src/​components/​DataDictionary/​components/​Layout/​components/​FiltersLayout/​filtersLayout.styles.ts Adds sticky positioning and collapsible spacing.
src/​components/​DataDictionary/​components/​Layout/​components/​EntitiesLayout/​types.ts Removes the spacing property.
src/​components/​DataDictionary/​components/​Layout/​components/​EntitiesLayout/​entitiesLayout.tsx Stops forwarding spacing props.
src/​components/​DataDictionary/​components/​Layout/​components/​EntitiesLayout/​entitiesLayout.styles.ts Places entities below filters without offsets.

Review effort: Balanced

Copilot AI balanced review requested due to automatic review settings October 21, 2026 03:43

Copilot AI 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.

🔵 Needs a closer look

The breaking layout migration needs final human review of browser verification and consumer compatibility, particularly sizing, sticky positioning, and focus scrolling.

No open issues 🎉

1 resolved since last review

Review effort: Balanced

frano-m and others added 2 commits October 22, 2026 16:38
…offsets with css (#998)

the header was position: fixed, so every page pushed its content down by a
js-measured header height. that height was 0 in the server render and only
corrected on hydration (a ~57px layout shift on main), and wrapping
announcements could leave page titles under the header.

- the app bar is position: sticky at every width, in flow, so content follows
  it at its real height in the server render. announcements stay inside it.
- applayout is a grid (header auto / main 1fr / footer auto) with
  min-height 100vh: the page grows, so the header stays pinned, and
  height: 100% below main still fills the space between header and footer.
- header and footer measure themselves with uselayoutheight, which writes
  --header-height and --footer-height on html from the resizeobserver,
  before paint. css reads them with fallbacks (57px / 56px).
- the header renders a global html scroll-padding-top of the header height,
  so hash links, focus and scrollintoview clear it on every page. focusable
  elements inside pinned regions (header, dictionary title and filters) get
  scroll-margin-top: -100vh so focusing them never jumps the page.
- main, contentlayout, error, indexview and the research drawer no longer
  offset by the header height. sticky elements pin at
  top: var(--header-height) through a shared pinned() style.
- the explore view, permanent sidebar and research drawer use contain: size
  to fill the main row without growing it.
- the data dictionary is a grid of title / outline / filters / entities
  areas with a fixed 74px title row (long titles truncate; #1134 adds
  anchor positioning). entities follow the filters with no measured
  padding, scroll-padding is server-rendered, an empty filters band
  collapses, and the outline keeps its footer compensation via
  var(--footer-height).

breaking changes:
- removed layoutdimensionsprovider, uselayoutdimensions and the
  providers/layoutdimensions module (dimensions.header/footer.height,
  headerref, footerref).
- removed uselayoutspacing and the layoutspacing type.
- removed header-offset props from styled components: headerheight on
  contentgrid, navigationgrid, outlinegrid and positioner; offset on
  errorlayout; spacing props on styledgridentityview, the research drawer
  and the data dictionary layouts.
- removed mainwithoffset, and renamed the styled export in main/main.styles
  from main to styledmain.
- the header is in flow, so any offset a consumer adds from the header
  height now doubles. migration steps are in #998.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ild (#998)

with emotion's default inline server rendering, a <style> element can precede
the filters grid inside the filters layout, so `> :first-child` matched the
style element and the grid lost its top margin until hydration moved the
styles, shifting the content.

the filters grid now carries a stable `data-dictionary-filters` class
(FILTERS_CLASS_NAME, following MARKDOWN_CLASS_NAME), and the filters layout
targets it for its top margin and the empty-band rule. the bottom margin stays
on `> :last-child`, which inline styles never affect. the emotion
unsafe-selector test now covers the filters layout.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 22, 2026 06:42
@frano-m
frano-m force-pushed the fran/998-in-flow-header branch from 899aa72 to f4842e1 Compare October 22, 2026 06:42

Copilot AI 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.

🟡 Changes recommended

The lightbox alignment helper must account for the new root scroll padding to avoid placing tall figures partly behind the header.

1 open issue

Review effort: Balanced

Comment thread src/components/Layout/components/Header/header.styles.ts
… in context (#998)

the header now sets html scroll-padding-top to its height, and scrollintoview
centres within the viewport less that padding. showfigure only subtracted the
figure's own scroll-margin-top when deciding whether a figure is too tall to
centre, so a figure taller than the window less the header was centred and its
top ended up behind the sticky header. it now subtracts the computed
scroll-padding-top of html as well. a test covers a figure that fits the window
but not the window less the header.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 23, 2026 06:56

Copilot AI 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.

🔵 Needs a closer look

The breaking, app-wide layout and focus changes require final human confirmation of cross-browser behavior and consumer migrations.

No open issues 🎉

1 resolved since last review

Review effort: Balanced

@frano-m
frano-m marked this pull request as ready for review October 24, 2026 02:29
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.

Move the app header into flow and replace the measured layout offsets with CSS

3 participants