Skip to content

[WIP/RFC] feat(portal): PortalContainerContext for federated portal isolation#4694

Draft
JChan106 wants to merge 2 commits into
box:masterfrom
JChan106:preview-portal-container
Draft

[WIP/RFC] feat(portal): PortalContainerContext for federated portal isolation#4694
JChan106 wants to merge 2 commits into
box:masterfrom
JChan106:preview-portal-container

Conversation

@JChan106

@JChan106 JChan106 commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Status: WIP / RFC — for discussion, not yet ready to merge. Part of an exploration into isolating a federated consumer's bundled box-ui-elements CSS from the host document. We may not land this exact approach; opening to discuss the pattern.

Problem

preview-client is a Module Federation remote embedded in a host document (EUA). It scopes its bundled box-ui-elements CSS under a root element (.preview-overlay) so those global styles can't leak into / override the host (e.g. sign-client) — see PREVIEW-1152. But BUIE (and the blueprint-web components BUIE renders) portal Tooltip/Flyout/Modal/DropdownMenu to document.body, outside that root, so scoped rules can't reach them and preview's own portaled UI renders unstyled / invisible.

Change

Introduce PortalContainerContext (default null = current behavior). A consumer wraps its subtree in one provider pointing at its own root; portals resolve their target as explicit prop > context container > document.body.

Wired: Portal / Tooltip / Flyout consume the context (Tooltip & Flyout set both react-tether renderElementTo and Tether bodyElement); ContentSidebar passes the container to its internal TooltipProvider; AddTaskMenuV2 (DropdownMenu.Content) and TaskModalV2 (Modal.Content) pass it per call site.

Open questions / known gaps (why WIP)

  • No unified blueprint portal provider (DSYS-440). blueprint's Select/Modal/DropdownMenu/DatePicker take container as a prop only (no context fallback), so every call site must be wired individually. A unified provider in blueprint is the real durable fix.
  • External packages out of reach. @box/activity-feed renders its own blueprint portals; not fixable here.
  • One case (AddTask dropdown) still resolved container to null at runtime during testing — root cause unconfirmed (provider ancestry vs. MF module identity). Needs follow-up.

Backward compatibility

No behavior change when no provider is present. Existing Portal/Tooltip/Flyout tests pass (snapshots updated for bodyElement); ContentSidebar/AddTaskMenuV2/TaskModalV2 suites pass (88 tests).

Ref: PREVIEW-1156

…IEW-1156)

Federated consumers that scope their CSS under a root element need portaled
UI (Tooltip, Flyout, Portal/Modal) to render inside that root rather than
document.body, so scoped styles reach it. Today the portal target is resolved
per-instance with no global override.

Add PortalContainerContext. Portal, Tooltip, and Flyout consume it to resolve
their portal target as: explicit prop > context container > document.body.
Behavior is unchanged when no provider is present (backward compatible).
A consumer wraps its subtree in one provider pointing at its own root.

For Tooltip and Flyout, the context element is set as BOTH react-tether's
renderElementTo (initial mount) AND Tether's bodyElement (positioning
re-parent target) — otherwise Tether's positioning engine moves the element
back to document.body on every reposition.
@JChan106 JChan106 requested a review from a team as a code owner July 14, 2026 03:43
@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Adds PortalContainerContext and applies it to Portal, Flyout, and Tooltip. Components resolve portal containers from explicit props or context, falling back to document.body; Portal tests cover context selection and explicit prop precedence.

Changes

Portal container integration

Layer / File(s) Summary
Portal container context contract
src/common/PortalContainerContext.tsx
Defines a nullable HTMLElement context with a null default and display name.
Portal container resolution
src/components/portal/Portal.tsx, src/components/portal/__tests__/Portal.test.tsx
Resolves containers from explicit props, context, or document.body, excludes container from DOM props, and tests context behavior and precedence.
Flyout and Tooltip container wiring
src/components/flyout/Flyout.js, src/components/tooltip/Tooltip.tsx
Uses the context container for tether mounting and positioning when no explicit container is supplied.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Provider as PortalContainerContext.Provider
  participant Portal
  participant Flyout
  participant Tooltip
  participant Container as DOM container
  Provider->>Portal: provide default container
  Provider->>Flyout: provide default container
  Provider->>Tooltip: provide default container
  Portal->>Container: mount portal content
  Flyout->>Container: configure tether mounting and positioning
  Tooltip->>Container: configure tether mounting and positioning
Loading

Suggested reviewers: tjuanitas

Poem

A rabbit hops through portals bright,
Guiding each tooltip to its site.
Flyouts tether, modals gleam,
Context carries every dream.
document.body waits below.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the main change: adding PortalContainerContext for portal isolation.
Description check ✅ Passed The description is detailed and covers the problem, proposed change, open questions, and backward compatibility.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot 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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/components/portal/Portal.tsx (1)

12-49: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Handle context and prop updates to prevent stale container bindings.

Because this.container is assigned and mutated only in the constructor, it will not update if props.container or this.context change later. This is particularly problematic for context values driven by React refs, which are typically null on the first render and populate on the second. If Portal mounts before the ref populates, it will permanently attach to document.body and never move to the intended context container.

Add a componentDidUpdate lifecycle method to detect changes to the resolved container and reparent this.layer.

🐛 Proposed fix

Add the componentDidUpdate method to dynamically update the container:

     componentWillUnmount() {
         if (this.container && this.layer) {
             this.container.removeChild(this.layer);
         }
         this.layer = null;
     }
+
+    componentDidUpdate() {
+        const nextContainer = this.props.container ?? this.context ?? document.body;
+        if (this.container !== nextContainer) {
+            if (this.container && this.layer) {
+                this.container.removeChild(this.layer);
+            }
+            this.container = nextContainer;
+            if (this.container && this.layer) {
+                this.container.appendChild(this.layer);
+            }
+        }
+    }
 
     layer: HTMLDivElement | null | undefined;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/portal/Portal.tsx` around lines 12 - 49, Update Portal’s
container binding by adding componentDidUpdate to recompute the resolved
container from the current props.container, context, and document.body. When the
resolved container differs from this.container, remove this.layer from the old
container, append it to the new container when available, and update
this.container, preserving the existing layer and rendering behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/components/portal/Portal.tsx`:
- Around line 12-49: Update Portal’s container binding by adding
componentDidUpdate to recompute the resolved container from the current
props.container, context, and document.body. When the resolved container differs
from this.container, remove this.layer from the old container, append it to the
new container when available, and update this.container, preserving the existing
layer and rendering behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c51245a5-70c0-4bd4-9752-1ff029829012

📥 Commits

Reviewing files that changed from the base of the PR and between 8530e4f and fc76ad1.

⛔ Files ignored due to path filters (1)
  • src/components/tooltip/__tests__/__snapshots__/Tooltip.test.tsx.snap is excluded by !**/*.snap
📒 Files selected for processing (5)
  • src/common/PortalContainerContext.tsx
  • src/components/flyout/Flyout.js
  • src/components/portal/Portal.tsx
  • src/components/portal/__tests__/Portal.test.tsx
  • src/components/tooltip/Tooltip.tsx

@JChan106 JChan106 marked this pull request as draft July 14, 2026 16:56
…ls (PREVIEW-1156)

Wire the blueprint-web portals rendered by ContentSidebar to the
PortalContainerContext container so federated consumers (preview-client) can
keep them inside a scoped root instead of document.body:

- ContentSidebar: pass the context container to its <TooltipProvider>, so
  sidebar nav/tab tooltips portal into the scope (its own TooltipProvider
  otherwise reset the container to document.body).
- AddTaskMenuV2: pass the container to <DropdownMenu.Content>.
- TaskModalV2: pass the container to <Modal.Content>.

Blueprint has no unified portal-container provider yet (see DSYS-440); until
then each portal is wired per call site. box-ui-elements' own Flyout/tether
dropdowns (VersionsItemActions, AnnotationActivityMenu) are already covered by
the Flyout change; TaskFormV2's DatePicker already portals into its form.
@JChan106 JChan106 changed the title feat(portal): add PortalContainerContext for portal redirection [WIP/RFC] feat(portal): PortalContainerContext for federated portal isolation Jul 14, 2026
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