Skip to content

[Platform]: Add sticky profile header with widget navigator - #1006

Open
carcruz wants to merge 2 commits into
refactor/ui-package-mui-facadefrom
feat/sticky-profile-header
Open

[Platform]: Add sticky profile header with widget navigator#1006
carcruz wants to merge 2 commits into
refactor/ui-package-mui-facadefrom
feat/sticky-profile-header

Conversation

@carcruz

@carcruz carcruz commented Jul 27, 2026

Copy link
Copy Markdown
Member

Description

GitHub PR-style sticky bar for target/disease/drug/variant/study/credible-set profile pages: shows the entity title/icon/external references plus a dropdown (with filter) listing every widget on the page, so long profiles (target has 18 widgets) get a quick jump menu instead of just scrolling.

  • StickyProfileHeader (packages/ui): fixed-position bar, reveals once the Profile/Associated tabs row scrolls out of view (not always-on), full viewport width, no shadow. Reuses each widget's hasData()/shortName the same way the existing Summary cards do, so disabled/empty widgets are greyed out consistently. Active widget label crossfades in on change.
  • useActiveSection: IntersectionObserver-based scrollspy reporting which widget sits in the upper-middle of the viewport. Section bodies are React.lazy/Suspense-loaded, so a MutationObserver rescans for their DOM nodes as they mount.
  • URL sync: active widget id syncs to the URL hash while scrolling (raw history.replaceState — fires often, doesn't need a real navigation), while explicit jumps (dropdown pick, Summary card click) use navigate() with replace+preventScrollReset, since those are one-off user actions where going through the router is correct. useScrollToHashOnMount replays a deep-linked hash on load once the lazy-loaded section exists.
  • Icon/externalLinks lifted out of each entity's Header.tsx into an exported buildHeaderMeta(), so the sticky bar and the full-size header render from one source of truth instead of duplicating the xref-building logic.
  • Bug fix: BaselineExpression's tab body had a key={crypto.randomUUID()} that force-remounted (losing tab/chart state) on every render — unrelated pre-existing bug, surfaced while testing the scroll-driven URL sync.

Issue:
Deploy preview:

Type of change

  • New feature (non-breaking change which adds functionality)
  • Bug fix (non-breaking change which fixes an issue)

How Has This Been Tested?

  • yarn turbo run build --filter=platform after each change, no new type errors
  • Manual verification in-browser across all 6 profile page types: sticky reveal timing, full-width layout, dropdown filter/greying, active-section tracking while scrolling, hash deep-linking (copy URL mid-scroll into a fresh tab), BaselineExpression no longer resetting on scroll

Checklist:

  • I have commented my code, particularly in hard-to-understand areas
  • My changes generate no new warnings
  • I have made corresponding changes to the documentation

carcruz added 2 commits July 27, 2026 22:06
…sion's tab body

The Fragment wrapping BaselineExpression's Summary/GTEx tabs got a fresh
crypto.randomUUID() key on every render, discarding all local state
(selected tab, chart view) on any re-render, not just scroll-driven ones.
… tracking, and URL deep-linking

GitHub PR-style sticky bar for target/disease/drug/variant/study/credible-set
profile pages: shows the entity title + icon + external references, and a
dropdown (with filter) listing every widget on the page so long profiles
(target has 18 widgets) get a quick jump menu instead of just scrolling.

- StickyProfileHeader (packages/ui): fixed-position bar, reveals once the
  Profile/Associated tabs row scrolls out of view (not always-on), full
  viewport width. Reuses definitions' hasData()/shortName the same way the
  existing Summary cards do, so disabled/empty widgets are greyed out
  consistently.
- useActiveSection: IntersectionObserver-based scrollspy that reports which
  widget is centered in the upper-middle of the viewport. Section bodies are
  React.lazy/Suspense-loaded, so a MutationObserver rescans for their DOM
  nodes as they mount.
- URL sync: the active widget's id syncs to the URL hash while scrolling
  (raw history.replaceState - fires often, doesn't need a real navigation),
  and explicit jumps (dropdown pick, Summary card click) use react-router's
  navigate() with replace+preventScrollReset, since those are one-off,
  user-initiated actions where going through the router is correct per its
  docs. useScrollToHashOnMount replays a deep-linked hash on load once the
  lazy-loaded section exists.
- Icon/externalLinks are lifted out of each entity's Header.tsx into an
  exported buildHeaderMeta() so the sticky bar and the full-size header
  render from one source of truth instead of duplicating the xref-building
  logic.

Verified with yarn turbo run build --filter=platform after each step; no new
type errors.
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.

1 participant