Skip to content

Commit d887c2d

Browse files
committed
fix(v7-q12): HelpModal via createPortal(document.body) escapes transformed ancestors
The help popup was being positioned relative to the Palette tile (or any other transformed ancestor like React Flow canvas) instead of the viewport. Per CSS spec, position:fixed is relative to the nearest *transformed* ancestor, not always the viewport — so React Flow's .react-flow__viewport (with transform: translate/scale) re-rooted the fixed-position frame and the modal landed inside whatever node hosted the ? icon. Fix: render the modal markup via createPortal(modal, document.body) so the DOM tree is reparented to <body>, where no transform ancestor exists. SSR / test envs without document fall back to inline render. Tests (vbgui/tests/HelpModalPortal.test.tsx, 2/2 PASS): - opening a brick ? mounts the modal under document.body, NOT inside the Palette aside or the render container - opening an adapter ? portals out the same way Regression: vbgui vitest 509 -> 511 (+2) PASS.
1 parent 70792af commit d887c2d

2 files changed

Lines changed: 43 additions & 2 deletions

File tree

vbgui/src/components/HelpIcon.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// not gated on touching the component being explained.
44

55
import { useState } from "react";
6+
import { createPortal } from "react-dom";
67
import { TENSOR_DIAGRAMS } from "./diagrams";
78
import { T } from "@/theme";
89

@@ -930,8 +931,13 @@ export interface HelpModalProps {
930931

931932
export function HelpModal({ topic, onClose }: HelpModalProps): JSX.Element {
932933
const entry = HELP_TOPICS[topic];
933-
934-
return (
934+
935+
// V7-Q12: render via createPortal(document.body) so the modal escapes
936+
// any transformed ancestor (React Flow canvas, palette, anything
937+
// with a CSS transform). Without this, position:fixed is positioned
938+
// relative to the nearest transformed ancestor (per CSS spec), which
939+
// pinned the modal inside the palette tile instead of the viewport.
940+
const modal = (
935941
<div
936942
data-testid="help-modal-backdrop"
937943
role="dialog"
@@ -1158,6 +1164,10 @@ export function HelpModal({ topic, onClose }: HelpModalProps): JSX.Element {
11581164
</div>
11591165
</div>
11601166
);
1167+
1168+
// SSR / test envs without a document fall back to inline rendering.
1169+
if (typeof document === "undefined") return modal;
1170+
return createPortal(modal, document.body);
11611171
}
11621172

11631173
function Section({
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { describe, it, expect } from "vitest";
2+
import { render, screen, fireEvent } from "@testing-library/react";
3+
import { Palette } from "@/components/Palette";
4+
5+
describe("V7-Q12 HelpModal renders via createPortal(document.body)", () => {
6+
it("opening a brick ? mounts the modal under document.body, "
7+
+ "NOT inside the Palette aside", () => {
8+
const { container } = render(<Palette />);
9+
10+
fireEvent.click(screen.getByTestId("help-icon-brick_attention"));
11+
12+
const modal = screen.getByTestId("help-modal-brick_attention");
13+
const palette = screen.getByTestId("palette");
14+
15+
// The portal target is document.body — the modal MUST NOT be
16+
// inside the palette (or anywhere in the render container).
17+
expect(palette.contains(modal)).toBe(false);
18+
expect(container.contains(modal)).toBe(false);
19+
20+
// It must be somewhere under document.body though.
21+
expect(document.body.contains(modal)).toBe(true);
22+
});
23+
24+
it("opening an adapter ? also portals out", () => {
25+
const { container } = render(<Palette />);
26+
fireEvent.click(screen.getByTestId("help-icon-adapter_rmsnorm"));
27+
const modal = screen.getByTestId("help-modal-adapter_rmsnorm");
28+
expect(container.contains(modal)).toBe(false);
29+
expect(document.body.contains(modal)).toBe(true);
30+
});
31+
});

0 commit comments

Comments
 (0)