feat(react-headless-components-preview): add Portal component#36238
Draft
dmytrokirpa wants to merge 2 commits into
Draft
feat(react-headless-components-preview): add Portal component#36238dmytrokirpa wants to merge 2 commits into
dmytrokirpa wants to merge 2 commits into
Conversation
… it in non-modal DialogSurface Adds a headless Portal that renders children into a mount node (defaults to targetDocument.body) while keeping a stable <span hidden> anchor at the original tree location for SSR-safe hydration. Links the mount node to the anchor via setVirtualParent so DOM-walking utilities work across the portal boundary. Non-modal DialogSurface now uses Portal instead of inline createPortal, which fixes a hydration mismatch when the dialog is open during initial render. No dependency on @fluentui/react-portal — the headless package must stay Griffel-free per package constraints. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
📊 Bundle size report
🤖 This report was generated against 4c758e46232f72a041bf8aaed8e1e64ef0db1c3b |
|
Pull request demo site: URL |
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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
Portalcomponent to@fluentui/react-headless-components-previewwith a stable<span hidden>anchor at the original tree location, SSR-safecreatePortalgating, andsetVirtualParentlinking so DOM-walking utilities work across the portal boundary.DialogSurfaceto use the newPortalinstead of inlineReactDOM.createPortal, fixing a hydration mismatch when the dialog renders open on first paint.react-portal's public API shape (simpleReact.FC,mountNodeprop withtargetDocument.bodyfallback) without depending on@fluentui/react-portal— the headless package must stay Griffel-free (no runtime CSS injection, CSP-friendly).Design notes
<span hidden ref={anchorRef}>at the original tree location. On the servermountNodeisundefined, so only the anchor renders; on hydration the anchor still renders identically (portal content lives elsewhere in the DOM, which React's hydration diff ignores). This is the same pattern v9react-portaluses.usePortalcallssetVirtualParent(mountNode, anchor)inuseEffectso utilities likeelementContains, click-outside detection, and focus traversal treat the portalled subtree as logically nested under its React parent. Skipped when the mount node already contains the anchor (cycle guard).react-portaldep: the headless package explicitly avoids Griffel and any Griffel-dependent packages, which rules out reusing v9Portal. The reimplementation drops theme className / dir propagation / focus-visible registration (not needed for headless) and keeps only the SSR anchor + virtual-parent link.API
Test plan
Portal.test.tsx— 11/11 including conformance: renders into default body, renders into custommountNode, hidden anchor at original location, sets virtual parent, skips linking when mount node contains the anchor)renderDialogSurfacerefactor (11/11)type-checkpasseslintpassesgenerate-apiregenerated — newetc/portal.api.mdand updatedetc/dialog.api.md(picked up newmountNode?: HTMLElementfield onDialogSurfaceState)🤖 Generated with Claude Code