Skip to content

Commit 08cb723

Browse files
committed
fix(app): autoResize collapses height:100% layouts; expose autoResize in useApp
setupSizeChangedNotifications measured height by transiently forcing html.style.height = "max-content". During that reflow, descendants with height:100% resolve to auto (parent has no definite size), so apps that set html,body{height:100%} with a viewport-filling child reported a spinner-sized height — and never recovered once the iframe shrank. Measure body.scrollHeight + body margins instead. This still grows on overflow (#525) and shrinks for default-styled bodies (#57), without overriding author styles. Apps that explicitly set body{height:100%} no longer shrink below the host's initial size, which is the semantically correct behavior for a viewport-filling layout. Also expose `autoResize` in UseAppOptions so React apps can opt out without dropping the hook. Addresses #143, #189, #502.
1 parent 01d826a commit 08cb723

2 files changed

Lines changed: 34 additions & 19 deletions

File tree

src/app.ts

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1391,22 +1391,30 @@ export class App extends ProtocolWithEvents<
13911391
scheduled = true;
13921392
requestAnimationFrame(() => {
13931393
scheduled = false;
1394-
const html = document.documentElement;
1394+
const body = document.body;
13951395

1396-
// Measure actual content height by temporarily overriding html sizing.
1397-
// Height uses max-content because fit-content would clamp to the viewport
1398-
// height when content is taller than the iframe, causing internal scrolling.
1396+
// Measure content height via body.scrollHeight + vertical body margins.
13991397
//
1400-
// Width uses window.innerWidth instead of measuring via fit-content.
1401-
// Setting html.style.width to fit-content forces a synchronous reflow at
1402-
// 0px width for responsive apps (whose content derives width from the
1403-
// container rather than having intrinsic width). This causes the browser
1404-
// to clamp scrollLeft on any horizontal scroll containers to 0, permanently
1405-
// destroying their scroll positions.
1406-
const originalHeight = html.style.height;
1407-
html.style.height = "max-content";
1408-
const height = Math.ceil(html.getBoundingClientRect().height);
1409-
html.style.height = originalHeight;
1398+
// scrollHeight captures overflow (so the iframe grows when content is
1399+
// taller than the viewport — see #525) and equals the content height for
1400+
// a default-styled body (so the iframe shrinks to fit — see #57).
1401+
//
1402+
// We previously forced html.style.height = "max-content" to measure
1403+
// intrinsic height, but that makes height:100% descendants resolve to
1404+
// auto during measurement. Apps that set html,body{height:100%} with a
1405+
// viewport-filling child reported a collapsed (spinner-sized) height
1406+
// and then never recovered — see #143. Reading scrollHeight without a
1407+
// style override leaves those layouts intact.
1408+
//
1409+
// Tradeoff: an app that sets body{height:100%} won't shrink below the
1410+
// current viewport height. That's the semantically correct behavior for
1411+
// a viewport-filling layout; apps that need explicit control should
1412+
// pass {autoResize: false} and call sendSizeChanged() manually.
1413+
const bodyStyle = getComputedStyle(body);
1414+
const bodyMarginY =
1415+
(parseFloat(bodyStyle.marginTop) || 0) +
1416+
(parseFloat(bodyStyle.marginBottom) || 0);
1417+
const height = Math.ceil(body.scrollHeight + bodyMarginY);
14101418

14111419
const width = Math.ceil(window.innerWidth);
14121420

src/react/useApp.tsx

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,6 @@ export * from "../app";
77
/**
88
* Options for configuring the {@link useApp `useApp`} hook.
99
*
10-
* Note: This interface does NOT expose {@link App `App`} options like `autoResize`.
11-
* The hook creates the `App` with default options (`autoResize: true`). If you
12-
* need custom `App` options, create the `App` manually instead of using this hook.
13-
*
1410
* @see {@link useApp `useApp`} for the hook that uses these options
1511
* @see {@link useAutoResize `useAutoResize`} for manual auto-resize control with custom `App` options
1612
*/
@@ -21,6 +17,16 @@ export interface UseAppOptions {
2117
* Declares what features this app supports.
2218
*/
2319
capabilities: McpUiAppCapabilities;
20+
/**
21+
* Automatically report size changes to the host.
22+
*
23+
* Disable this for apps that manage their own height (canvases, editors,
24+
* diagrams) or that use viewport-relative sizing like `100vh`, and call
25+
* {@link App.sendSizeChanged `app.sendSizeChanged`} manually instead.
26+
*
27+
* @default true
28+
*/
29+
autoResize?: boolean;
2430
/**
2531
* Called after {@link App `App`} is created but before connection.
2632
*
@@ -120,6 +126,7 @@ export interface AppState {
120126
export function useApp({
121127
appInfo,
122128
capabilities,
129+
autoResize = true,
123130
onAppCreated,
124131
}: UseAppOptions): AppState {
125132
const [app, setApp] = useState<App | null>(null);
@@ -135,7 +142,7 @@ export function useApp({
135142
window.parent,
136143
window.parent,
137144
);
138-
const app = new App(appInfo, capabilities);
145+
const app = new App(appInfo, capabilities, { autoResize });
139146

140147
// Register handlers BEFORE connecting
141148
onAppCreated?.(app);

0 commit comments

Comments
 (0)