Skip to content

feat(lab): DialogHeroHeader - #5105

Open
jiunshinn wants to merge 5 commits into
meta:mainfrom
jiunshinn:feat/4182-dialog-hero-header
Open

jiunshinn wants to merge 5 commits into
meta:mainfrom
jiunshinn:feat/4182-dialog-hero-header

Conversation

@jiunshinn

@jiunshinn jiunshinn commented Aug 16, 2026 •

Copy link
Copy Markdown
Contributor

Closes #4182

User need

Add a media-led header for onboarding, announcements, and featured Dialog content. It composes through Layout.header, with full-bleed media above the title and a close button over the media's trailing corner. It lands in the canary-only @astryxdesign/lab package.

Problem and solution fit

  • Implement the requested required title/media, optional custom Heading, hidden title, start content, title truncation, and media-aware close button.
  • Let Dialog apply the visible title's default focus after showModal(), with eligible explicit descendant requests taking priority. Hidden and inline titles do not request focus; closing restores the invoker.
  • Put the title id and focus request on the rendered heading itself, including a caller-provided Heading, so the post-open focus target keeps its heading role (Dialog spec AR3). The custom heading is not cloned; attributes the caller already set (id, tabIndex) are left alone.
  • Follow the rendered heading node, not the title prop. A re-render with a new title element changes nothing, so focus stays on the heading. A heading that the caller's component renders later, or replaces with another element, receives the handshake when it appears, and Dialog then checks its default name again.
  • Keep adjacent startContent out of the dialog's accessible name, and preserve the close button's DOM identity when media mode changes.
  • Complete the component docs, Storybook states, and packaged CLI showcase.
  • Read the Dialog context through a new internal Core entry, @astryxdesign/core/internal/dialog-context, registered in the export generator next to internal/tokenizer-presentation.
  • Add the new lab routes to the reviewed ShadCN route lock.

Current specification

The anatomy and media-mode choice follow the issue's reference API and media-theme follow-up. Focus timing, eligibility, restoration, and inline behavior follow the current Dialog contract (FR1–FR6, AR3). Bleed follows the current container-padding contract.

The Core entry follows spec:AST-017 FR21 and architecture:react-component-runtime INV13. A module without a public contract is exported only through Core's ./internal/* subpaths: undocumented, outside the compatibility promise, versioned in lockstep with Core, and allowed by lint only in Core and Lab. The lab DialogHeroHeader API itself still needs maintainer acceptance.

Public delta and caller burden

DialogHeroHeader exports DialogHeroHeaderProps and DialogHeroHeaderMediaMode from lab. Required props are title: string | ReactElement and media: ReactNode. Optional props are mediaMode, isTitleHidden, startContent, maxLines, onOpenChange, and hasDivider, plus BaseProps/ref.

Callers choose the header, supply the media, and specify its light/dark luminance when needed. As with the current DialogHeader, passing onOpenChange enables the close button; omitting it hides the button. The callback receives false. maxLines follows Heading naming and only applies to string titles. Existing DialogHeader callers require no changes.

// Compact header
<DialogHeader title="Welcome" onOpenChange={setIsOpen} />

// Media-led header in the same Layout.header slot
<DialogHeroHeader
  title="Welcome"
  media={<img src={heroImage} alt="" width="100%" />}
  mediaMode="dark"
  onOpenChange={setIsOpen}
/>

Core's public API is unchanged. The internal entry re-exports useDialogContext only. The internal context value gains an optional syncDefaultName(), which Dialog provides so a composed header can ask it to check its default name again after the title element changes.

Dialog's initial focus now skips a data-autofocus request that is hidden, inert, or cannot take focus, and tries the next request. A header's default title request comes after every explicit request.

Design and alternatives

A distinct header reflects the different anatomy. MediaTheme controls close-button contrast; no per-icon color override or new theme token is added. The component retains lab placement, and actions stay in LayoutFooter.

Evidence

MediaBleedGeometry (Storybook) measures the media against the dialog surface in real Chromium: inline-start, inline-end, and block-start edges at theme-default, padding 0/2/8, a public --astryx-dialog-padding override, and RTL. The title must stay inset by the cancelled padding, so the fixtures cannot pass trivially.

CustomHeadingFocusAfterRerender (Storybook) opens a modal with a custom h1 title and re-renders the header three times. Focus and aria-labelledby must stay on the heading. Against the previous implementation, Chromium reported focus left the custom heading after a re-render: <body>.

Both stories are registered in story-play-guard.js, so CI runs them as required checks.

Browser checks cover a real modal, opening/closing focus, light/dark media, narrow screens, RTL, custom padding, hidden/custom headings, and default rendering. The showcase is available through astryx template DialogHeroHeaderShowcase --package @astryxdesign/lab in a consumer with lab installed.

Scope

  • One capability is added.
  • Supporting fixes address its focus, naming, export, documentation, and showcase paths.
  • Consumer docs and focused regression tests ship with the feature.
  • Public text and artifacts contain no internal operational context.

Testing

Rebased onto main at 8f6f000.

  • DialogHeroHeader, Dialog, and the internal entry: 155 passed. The four new handshake tests (re-render without attribute churn, late heading, self-replacing heading, Dialog's late-title check) fail against the previous implementation and pass now.
  • story-play-guard.js against a fresh Storybook build: both DialogHeroHeader stories pass. CustomHeadingFocusAfterRerender failed against the previous implementation. The two PowerSearch near-full-token-row fixtures fail locally; this PR does not touch PowerSearch.
  • Full ui Vitest project: 16,078 passed, 1 failed (packages/core/src/postinstall.test.mjs, outside this PR's files).
  • Full node Vitest project: the official package list test now passes after pnpm bundle:cli-official-packages. The remaining local failures are macOS-environment tests outside this PR's files (BSD cp, bash 3.2, a case-insensitive filesystem, the global CLI cache); Linux CI is the confirmation.
  • Docsite generate + test: passed. The ShadCN route-lock check passes without UPDATE_SHADCN_ROUTE_LOCK.
  • sync-exports --check, core and lab typechecks, lab:readiness:check, changeset, knowledge, and SYNC checks: passed.

@vercel

vercel Bot commented Aug 16, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
astryx Ready Ready Preview Oct 27, 2026 11:02pm UTC

Request Review

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Aug 16, 2026
@github-actions github-actions Bot added community Authored by a community contributor (not on the eng/design team) needs:code-review High-risk change (new package/component/API) — needs human code review before merge labels Aug 16, 2026
@jiunshinn
jiunshinn marked this pull request as ready for review August 16, 2026 11:48
@github-actions

github-actions Bot commented Aug 16, 2026 •

Copy link
Copy Markdown
Contributor

PR Analysis Report

📚 Storybook Preview

View Storybook for this PR

🧪 Sandbox Preview

View Sandbox for this PR

New Components

DialogHeroHeader (@astryxdesign/lab) · View in Storybook
Metric Value
Entry File (ESM) 314B
Lines of Code 227
Source Files 2
Complexity High (27)
Exports DialogHeroHeader
Props Count N/A
Has Tests Yes
Has Stories Yes

Component Size

Component Import before Import after Delta CSS before CSS after Delta
core/AlertDialog 57.0KB 57.1KB +96B 12.4KB 12.4KB 0B
core/CommandPalette 51.0KB 51.1KB +101B 11.8KB 11.8KB 0B
core/CommandPaletteInput 32.5KB 32.5KB 0B 8.1KB 8.1KB 0B
core/CommandPaletteItem 8.4KB 8.4KB 0B 3.5KB 3.5KB 0B
core/Dialog 16.2KB 16.3KB +115B 7.1KB 7.1KB 0B
core/DialogHeader 49.0KB 49.0KB 0B 9.8KB 9.8KB 0B
core/PowerSearch 158.2KB 158.2KB 0B 19.9KB 19.9KB 0B
core/Tokenizer 68.8KB 68.8KB 0B 16.5KB 16.5KB 0B
lab/CommentThread 216.0KB 216.1KB +97B 2.4KB 2.4KB 0B
lab/Comments 220.6KB 220.7KB +89B 2.5KB 2.5KB 0B
lab/DialogHeroHeader N/A 61.1KB N/A N/A 372B N/A
lab/MobilePowerSearch 158.2KB 158.3KB +98B 1.4KB 1.4KB 0B
lab/MobileTokenizer 91.2KB 91.3KB +99B 775B 775B 0B
richtext/RichTextEditorToolbar N/A 212.6KB N/A N/A 454B N/A

Sizes are gzip. Import counts the JavaScript the import loads eagerly; CSS counts the package stylesheet rules referenced by the modules it can load from its own package, lazily loaded ones included.

Bundle Size Summary

Gzip figures measure each package's entry file, not the code an import pulls in.

Package Entry (ESM) Entry gzip before Entry gzip after Delta
@astryxdesign/core 4.9KB 1.2KB 1.2KB 0B
@astryxdesign/lab 4.7KB 1.9KB 2.0KB +42B
Package stylesheet Gzip before Gzip after Delta
@astryxdesign/core 34.6KB 34.6KB 0B
@astryxdesign/lab 11.1KB 11.2KB +86B

Accessibility Audit

Status: No accessibility violations detected.

Visual Regression

55 of 502 shot(s) changed.

A change here is a question, not a failure: check whether the after is the
picture you intended. Record that review on the PR. Baseline maintenance is an
explicit dispatch of CI; this report never rewrites the baseline or adds a release gate.

component story theme mode pixels
Selector Indicator Space Bottom Sheet Narrow End RTL neutral light 753,933
Selector Indicator Space Bottom Sheet Wide Start neutral light 740,072
RichTextEditor Markdown parity: side by side probe dark 558,840
RichTextEditor Markdown parity: side by side probe light 540,756
Chat Full AI Chat probe light 230,823
Chat Full AI Chat probe dark 229,805
AppShell With Mobile Nav probe light 217,010
AppShell With Mobile Nav probe dark 216,960
Collapsible Dividers probe dark 66,445
Collapsible Dividers probe light 66,445
AppShell Playground neutral dark 65,198
AppShell Playground neutral light 63,788
Chat Default neutral dark 36,304
Chat Default neutral light 35,767
Chat Empty State probe light 23,531
Chat Empty State probe dark 22,622
ChatAutoScroll Scroll Behavior Comparison neutral dark 15,378
ChatAutoScroll Scroll Behavior Comparison neutral light 14,107
Chat With Attachments probe light 13,406
Chat With Attachments probe dark 11,232

and 35 more.


Generated by PR Enrichment workflow | Storybook | Sandbox | View full report

@astracat-bot astracat-bot 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.

Thank you for building the media-led Dialog header and for wiring its focus, naming, and showcase paths.

Approval is waiting on a maintainer decision. This PR adds a new public DialogHeroHeader API and design in the lab package and a new stable @astryxdesign/core/Dialog/DialogContext subpath, and the description notes the core subpath still needs maintainer acceptance. Please have a maintainer provide or identify the governing current spec for both surfaces — the header's required and optional props, full-bleed and overlay anatomy, and the exported context, hook, and value shape — so the accepted contract is discoverable for future reviewers and consumers.

Two independent fixes will still be needed once that is settled:

  • For a custom heading element, initial focus currently lands on a generic wrapper around the heading rather than on the heading itself. Please put the focus and labelling handshake on the actual heading and cover the post-open focused element in a regression test.
  • As a new full-bleed participant, please add a focused regression that verifies the media's computed edge compensation across representative Dialog padding and in RTL.

The Dialog post-open focus ordering, naming, and inline behaviour otherwise follow the current Dialog contract on inspection, and the docsite, CLI, and export projections are consistent with the new surfaces.

[Automated review]

@jiunshinn
jiunshinn force-pushed the feat/4182-dialog-hero-header branch from 5b28b9a to 827a1c6 Compare September 30, 2026 13:29
@jiunshinn

Copy link
Copy Markdown
Contributor Author

Thanks for the review. I rebased onto main and fixed both independent items.

Custom heading focus (42659a4). The title id and focus request now go on the rendered heading element, never on a wrapper. So the post-open target keeps its heading role and name (Dialog spec AR3). cloneElement is banned by @astryx/no-react-introspection, so the header uses a layout effect instead, the same way Tooltip wires aria-describedby onto its child. It runs before Dialog resolves the default name and initial focus in the same commit. It only adds attributes the heading does not already have, and cleanup removes only values it set. A caller-owned id or tabIndex stays in place; the prop docs say to use aria-labelledby in that case. New tests cover:

  • post-open document.activeElement is the custom h1, and aria-labelledby points at it;
  • the slot has no id or tabindex;
  • a caller-owned id and tabIndex are kept;
  • inline and hidden custom headings make no focus request;
  • a replacement heading gets the handshake.

All five fail on the previous implementation.

Full-bleed regression (bb06064). Lab/DialogHeroHeader › MediaBleedGeometry measures the media area against the dialog surface in Chromium. The fixtures are theme-default, padding 0/2/8, a public --astryx-dialog-padding: 20px override, and RTL at 8 and with the override. Edges are measured logically, so RTL checks inline-start on the right. The title must sit exactly at the cancelled inset, the 0/2/8 insets must be strictly ordered, and RTL must match LTR, so no fixture passes trivially. The story is registered in story-play-guard.js, so CI runs it as a required check. As a red check, I injected CSS that drops the inline-end or block-start cancel, uses a physical margin in RTL, or over-cancels. The guard failed in each case.

Rebase. #6340 now imports nested core subpaths into the playground, so I dropped this PR's generator and test changes there. Dialog/DialogContext moved into FILE_MODULE_SUBPATH_EXPORTS next to Markdown/remark. The earlier docsite-test and registry-contract failures came from the unrefreshed ShadCN route lock. The lock now adds the two new lab routes, the same step as #6640.

On the governing spec: the header's props and anatomy follow @cixzhang's reference API on #4182 and the media-mode follow-up. The @astryxdesign/core/Dialog/DialogContext subpath is still the open maintainer decision. I'm happy to adjust whichever surface the accepted spec settles on.

@astracat-bot astracat-bot 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.

Thank you for building the media-led Dialog header and for wiring its focus, naming, and showcase paths.

Approval is waiting on a maintainer decision. This PR adds a new public DialogHeroHeader API and design in the lab package and a new stable @astryxdesign/core/Dialog/DialogContext subpath, and the description notes the core subpath still needs maintainer acceptance. Please have a maintainer provide or identify the governing current spec for both surfaces — the header's required and optional props, full-bleed and overlay anatomy, accepted custom-title element contract, and the exported context, hook, and value shape — so the accepted contract is discoverable for future reviewers and consumers.

The two fixes from the earlier review are addressed in this revision: a custom heading now receives the focus and labelling handshake on the heading element itself, with a regression asserting the post-open focused element, and the new media bleed geometry story covers representative Dialog padding, a theme override, and RTL as required CI. The Dialog post-open focus ordering, naming, and inline behaviour otherwise follow the current Dialog contract on inspection, and the docsite, CLI, and export projections are consistent with the new surfaces.

[Automated review]

github-actions Bot added a commit that referenced this pull request Sep 30, 2026
@jiunshinn
jiunshinn force-pushed the feat/4182-dialog-hero-header branch from 827a1c6 to 5e8895d Compare October 3, 2026 23:27
@jiunshinn

Copy link
Copy Markdown
Contributor Author

Rebased onto main at 73a6506d60. The conflict resolution retains both PowerSearch touch/RTL guards and the DialogHeroHeader media-bleed guard. The Lab integration keeps main's component/template discovery; the new showcase uses the required same-stem .doc.mjs descriptor.

Validation: 144 focused Dialog/DialogHeroHeader/export/integration tests, Lab docs typecheck, strict lint/repository checks, full package build, and Storybook build pass. The DialogHeroHeader media-bleed story passes across its padding/theme/RTL fixtures. The full story guard still reports two failures in the unchanged PowerSearch near-full-token-row fixtures; I have not described that aggregate as green.

@cixzhang Please re-review the updated head and provide or identify the governing current contract for the header's public props/anatomy/custom-heading requirements and the Dialog/DialogContext subpath (DialogContext, useDialogContext, and DialogContextValue). The earlier focus-target and full-bleed review findings remain covered. This update does not treat the unresolved API/design decision as approved.

The formal review-request API rejects my account's permissions in this repository, so this comment is the re-review request; existing reviewer assignments are retained.

@astracat-bot astracat-bot 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.

Approval is waiting on a maintainer to provide or identify the governing spec for the new lab DialogHeroHeader API and design (props, full-bleed media with the overlaid close button, the accepted custom-title element contract, and default title focus) and for the new stable @astryxdesign/core/Dialog/DialogContext subpath and its exports. Independently of that decision, these need fixing:

  • With a custom Heading title, the first parent re-render after the modal opens moves focus from the heading to the page body in Chromium, because the layout effect keyed on title removes the tabindex it added before re-adding it; key it on the heading node and add a browser test that re-renders after opening.
  • A custom heading that renders after the header mounts, or changes element, never receives the title id, so the dialog loses its accessible name.
  • .changeset/integration-component-import-hints.md announces a CLI fix, but the CLI change is a file header and the new test already passes on the base; test-only changes need no Changeset.

The branch also needs a rebase onto main.

[Automated review]

…, and Lab reads the Dialog context through an internal entry
@jiunshinn
jiunshinn force-pushed the feat/4182-dialog-hero-header branch from 5e8895d to 3da9bc5 Compare October 27, 2026 22:46
@jiunshinn

Copy link
Copy Markdown
Contributor Author

Rebased onto main at 8f6f0008fe and addressed the 10/3 review. The branch is now four commits: the core change, the lab component, the CLI official package index entry, and the handshake fix.

The core export is now an internal entry. This PR no longer adds the public @astryxdesign/core/Dialog/DialogContext subpath. spec:AST-017 FR21 keeps a module without a public contract internal and exports it only through Core's ./internal/* subpaths. Lab's header now imports useDialogContext from @astryxdesign/core/internal/dialog-context. The entry has the same shape as internal/tokenizer-presentation from #8735: named re-exports only, and lint allows it only in Core and Lab. The public custom-header guidance is removed from Dialog.doc.mjs, so Core's public API is unchanged.

Focus loss on re-render. The handshake now follows the rendered heading node, not the title prop. A re-render with a new title element finds the same heading and changes nothing. The new CustomHeadingFocusAfterRerender story opens a modal with a custom h1, re-renders the header three times, and asserts that focus and aria-labelledby stay on the heading. It is registered in story-play-guard.js. Against the previous implementation in Chromium it failed with focus left the custom heading after a re-render: <body>, and it passes now. A jsdom test also asserts that the heading gets no attribute mutation across a re-render.

Late or replaced heading. A MutationObserver on the slot applies the handshake when the caller's component renders its heading later or replaces it with another element. Dialog looks for its title once, at mount. So a late title could get the id while the dialog still had no aria-labelledby. The internal Dialog context now has an optional syncDefaultName(). The header calls it after its heading changes, and Dialog runs its existing check again. New tests cover a heading that renders after mount, a heading that replaces itself (h2 to h3), and the Dialog-level check. All four new tests fail on the previous implementation.

Changeset. I removed .changeset/integration-component-import-hints.md, together with the CLI header and the test from that commit. They are not related to #4182, and the test already passed on main. Core now has two changesets: a [fix] for the Dialog focus order (component:Dialog FR2: eligible requests only, header title last), and an internal [chore] for the new entry.

Local results are in the PR description. In short: the focused suites, the docsite generate and tests, and the core and lab typechecks pass. In story-play-guard.js, both DialogHeroHeader stories pass. The two PowerSearch near-full-token-row fixtures still fail locally, as in my 10/3 run; this PR does not touch PowerSearch. A few macOS-only tool tests outside this PR's files also fail locally; Linux CI is the confirmation.

@cixzhang With the core subpath gone, the one open decision is the lab DialogHeroHeader API: its props, the full-bleed media with the overlaid close button, the custom-heading contract, and the default title focus. These follow your reference API and the media-mode follow-up on #4182. I'm happy to adjust to whatever you decide. The formal review-request API rejects my account's permissions here, so this comment is the re-review request.

@astracat-bot astracat-bot 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.

Approval is waiting on an engineering owner to provide or identify the governing spec for the Lab DialogHeroHeader API and design: its props, the always-on full-bleed media, the close button overlaid on the media, the custom-title element contract, and the hero title as the dialog's default focus target. An engineering owner records that decision by commenting /approve-spec followed by the full head SHA on this pull request; the author may do so if they are an engineering owner. Separately, the change is 2,146 authored lines, over the 2,000-line scope limit, so please split it, for example by landing the Core portion (the Dialog focus fall-through and title ordering, the internal/dialog-context entry and syncDefaultName, with their tests and Changesets) first, which brings this pull request under the limit. The re-render focus, late-heading and Changeset issues from the previous review are fixed.

[Automated review]

This branch was successfully deployed

1 active deployment
Preview — f97e24a0 Deployed Oct 27, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Meta Open Source bot. community Authored by a community contributor (not on the eng/design team) needs:code-review High-risk change (new package/component/API) — needs human code review before merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(lab): DialogHeroHeader — hero-style Dialog header (port from XDSModalHeroHeader)

1 participant