fix(browser): clip preview view to its panel column so it can't render out of bounds (#2203)
* fix(browser): clip preview view to its panel column so it can't paint out of bounds The inspector's Browser tab hosts a native WebContentsView positioned from the slot's getBoundingClientRect. That overlay is a window-level layer, so DOM overflow:hidden never clips it. The slot sits inside the inspector's min-w-[280px] reflow guard, so on a narrow panel (small window, or mid collapse/expand) the slot's box spills past its resizable-panel column and the view paints over the terminal/sidebar, "out of the box" (#2202). Two fixes in useBrowserView: - Intersect the measured slot rect with its [data-panel] column before sending bounds, so the view can only ever paint inside that column. - Observe the panel column (not just the slot) with the ResizeObserver. During the flex-grow collapse/expand animation the slot's width stays pinned by min-w, so a slot-only observer never re-fires and the bounds go stale; the column's width changes every frame, so observing it re-measures throughout. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore: format with prettier [skip ci] --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
parent
1983e97bfa
commit
acd811dce3
|
|
@ -89,6 +89,41 @@ describe("useBrowserView", () => {
|
||||||
expect(result.current.viewId).toBe("42:sess-1");
|
expect(result.current.viewId).toBe("42:sess-1");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("clamps the native view to its resizable-panel column when the slot overspills", async () => {
|
||||||
|
const bridge = setupBridge();
|
||||||
|
// The slot is wider than its column (e.g. the `min-w-[280px]` wrapper on a
|
||||||
|
// narrower inspector panel). The native overlay isn't clipped by DOM
|
||||||
|
// overflow, so the reported bounds must be intersected with the column.
|
||||||
|
const column = document.createElement("div");
|
||||||
|
column.setAttribute("data-panel", "");
|
||||||
|
column.getBoundingClientRect = vi.fn(() => ({
|
||||||
|
x: 100,
|
||||||
|
y: 0,
|
||||||
|
width: 150,
|
||||||
|
height: 600,
|
||||||
|
top: 0,
|
||||||
|
right: 250,
|
||||||
|
bottom: 600,
|
||||||
|
left: 100,
|
||||||
|
toJSON: () => ({}),
|
||||||
|
}));
|
||||||
|
const slot = createSlot();
|
||||||
|
column.appendChild(slot);
|
||||||
|
document.body.appendChild(column);
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useBrowserView({ sessionId: "sess-1", active: true, poppedOut: false }));
|
||||||
|
await waitFor(() => expect(bridge.ensure).toHaveBeenCalledWith("sess-1"));
|
||||||
|
act(() => result.current.slotRef(slot));
|
||||||
|
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(bridge.setBounds).toHaveBeenCalledWith({
|
||||||
|
viewId: "42:sess-1",
|
||||||
|
rect: { x: 100, y: 34, width: 150, height: 240 },
|
||||||
|
visible: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it("hides the native view when inactive and on unmount without destroying session state", async () => {
|
it("hides the native view when inactive and on unmount without destroying session state", async () => {
|
||||||
const bridge = setupBridge();
|
const bridge = setupBridge();
|
||||||
const slot = createSlot();
|
const slot = createSlot();
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,26 @@ const EMPTY_NAV_STATE: BrowserNavState = {
|
||||||
|
|
||||||
const HIDDEN_RECT: BrowserRect = { x: 0, y: 0, width: 0, height: 0 };
|
const HIDDEN_RECT: BrowserRect = { x: 0, y: 0, width: 0, height: 0 };
|
||||||
|
|
||||||
|
// The native WebContentsView is a window-level overlay, so DOM `overflow:
|
||||||
|
// hidden` never clips it — it paints wherever the slot's bounding box lands.
|
||||||
|
// Inside the collapsible inspector the slot sits in a `min-w-[280px]` wrapper,
|
||||||
|
// so on a narrow panel (small window, or mid-collapse) the slot's box spills
|
||||||
|
// past its resizable-panel column. Intersect the slot box with that column so
|
||||||
|
// the view can only ever paint inside it, never over the terminal/sidebar.
|
||||||
|
function visibleSlotRect(node: HTMLElement): BrowserRect {
|
||||||
|
const rect = node.getBoundingClientRect();
|
||||||
|
let { left, top, right, bottom } = rect;
|
||||||
|
const column = node.closest<HTMLElement>("[data-panel]");
|
||||||
|
if (column) {
|
||||||
|
const bounds = column.getBoundingClientRect();
|
||||||
|
left = Math.max(left, bounds.left);
|
||||||
|
top = Math.max(top, bounds.top);
|
||||||
|
right = Math.min(right, bounds.right);
|
||||||
|
bottom = Math.min(bottom, bounds.bottom);
|
||||||
|
}
|
||||||
|
return { x: left, y: top, width: Math.max(0, right - left), height: Math.max(0, bottom - top) };
|
||||||
|
}
|
||||||
|
|
||||||
export function useBrowserView({
|
export function useBrowserView({
|
||||||
sessionId,
|
sessionId,
|
||||||
active,
|
active,
|
||||||
|
|
@ -78,15 +98,10 @@ export function useBrowserView({
|
||||||
sendHiddenBounds(id);
|
sendHiddenBounds(id);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const rect = node.getBoundingClientRect();
|
const rect = visibleSlotRect(node);
|
||||||
const payload = {
|
const payload = {
|
||||||
viewId: id,
|
viewId: id,
|
||||||
rect: {
|
rect,
|
||||||
x: rect.x,
|
|
||||||
y: rect.y,
|
|
||||||
width: rect.width,
|
|
||||||
height: rect.height,
|
|
||||||
},
|
|
||||||
visible: rect.width > 0 && rect.height > 0,
|
visible: rect.width > 0 && rect.height > 0,
|
||||||
};
|
};
|
||||||
window.ao?.browser.setBounds(payload);
|
window.ao?.browser.setBounds(payload);
|
||||||
|
|
@ -115,6 +130,13 @@ export function useBrowserView({
|
||||||
if (node) {
|
if (node) {
|
||||||
const observer = new ResizeObserver(scheduleMeasure);
|
const observer = new ResizeObserver(scheduleMeasure);
|
||||||
observer.observe(node);
|
observer.observe(node);
|
||||||
|
// Also track the resizable-panel column: while the inspector
|
||||||
|
// collapse/expand animates, the slot's own width stays pinned by
|
||||||
|
// `min-w-[280px]` (so a slot-only observer never fires), but the
|
||||||
|
// column's width changes every frame. Observing it re-measures
|
||||||
|
// through the whole animation so the view never lags behind.
|
||||||
|
const column = node.closest("[data-panel]");
|
||||||
|
if (column) observer.observe(column);
|
||||||
observerRef.current = observer;
|
observerRef.current = observer;
|
||||||
}
|
}
|
||||||
scheduleMeasure();
|
scheduleMeasure();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue