From f04f2584a8293fc77404e65008ec43465e20dac3 Mon Sep 17 00:00:00 2001 From: Vito Sansevero Date: Tue, 27 Oct 2026 14:54:11 -0700 Subject: [PATCH] fix(model_resolution_picker): refit on the next frame after a resize notification MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - The ResizeObserver callback measured the panel and resized the node synchronously, which forces a layout while the stylesheet may still be applying; WebKit then reports "ResizeObserver loop completed with undelivered notifications" as a page error, and the browser fixture fails any test with a page error (CI WebKit job on PR #108). - The callback now only schedules a single-flight requestAnimationFrame that measures and refits, so no observed box changes during delivery. - Reproduced in the pinned Playwright WebKit image: the picker tests pass 48/48 (16 tests × 3) with this change. --- web/model_resolution_picker.js | 15 ++++++++++++--- web/ui/README.md | 9 +++++---- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/web/model_resolution_picker.js b/web/model_resolution_picker.js index 47ad165..a7ff1fb 100644 --- a/web/model_resolution_picker.js +++ b/web/model_resolution_picker.js @@ -259,11 +259,20 @@ function mountPicker(node) { // The stylesheet, fonts or a wrapped status line can change the content height after a render // (the first fit may even see an unstyled panel): refit whenever a child's box changes. The // children are created once by the view, so observing them at mount covers every render. + // The refit runs on the next frame, never inside the observer delivery: measuring there forces a + // layout while the stylesheet may be applying, and WebKit then reports an undelivered-notifications + // loop error for the observed children. if (typeof ResizeObserver === 'function') { + let refitScheduled = false; const observer = new ResizeObserver(() => { - if (scope.signal.aborted) return; - const measured = measurePanel(); - if (measured && measured !== panelHeight) fitNodeHeight(); + if (scope.signal.aborted || refitScheduled) return; + refitScheduled = true; + requestAnimationFrame(() => { + refitScheduled = false; + if (scope.signal.aborted) return; + const measured = measurePanel(); + if (measured && measured !== panelHeight) fitNodeHeight(); + }); }); for (const child of view.element.children) observer.observe(child); scope.cleanup(() => observer.disconnect()); diff --git a/web/ui/README.md b/web/ui/README.md index 27857b5..d3a25f2 100644 --- a/web/ui/README.md +++ b/web/ui/README.md @@ -128,10 +128,11 @@ render the extension sets the node height to the host's `computeSize()` while keeping the width. The measurement is retried for up to 60 frames when the element is not attached yet (fresh mount, workflow load), skipped while the family table is still loading, and ignored when the panel is hidden by the -host. A `ResizeObserver` on the panel's children refits the node when their -boxes change later, for example when the stylesheet arrives after the first -render, so the panel children must carry no vertical margins (the stylesheet -zeroes them). +host. A `ResizeObserver` on the panel's children schedules a refit on the next +frame when their boxes change later, for example when the stylesheet arrives +after the first render; measuring inside the observer delivery would make WebKit +report an undelivered-notifications loop error. The panel children must carry no +vertical margins (the stylesheet zeroes them). ## Node Dimensions and modal dialogs