From d18dec00aaf6d5370e841d3c8105fb1441134e0e Mon Sep 17 00:00:00 2001 From: Ole Martin Handeland Date: Wed, 12 Aug 2026 12:10:21 +0200 Subject: [PATCH 1/3] fix(Suggestion): avoid ResizeObserver feedback loops Floating UI observes suggestion lists while computing their dimensions. Writing width and max-height synchronously from the size middleware can resize that same observed element during observer delivery, causing browsers to report undelivered ResizeObserver notifications. Defer dimension writes to the next animation frame and skip values that are already applied. This preserves list sizing without feeding layout changes back into the active observer cycle. --- .changeset/calm-taxis-resize.md | 5 ++++ packages/web/src/popover/popover.test.ts | 31 ++++++++++++++++++++++++ packages/web/src/popover/popover.ts | 13 +++++++--- 3 files changed, 46 insertions(+), 3 deletions(-) create mode 100644 .changeset/calm-taxis-resize.md diff --git a/.changeset/calm-taxis-resize.md b/.changeset/calm-taxis-resize.md new file mode 100644 index 0000000000..5a3fcd074e --- /dev/null +++ b/.changeset/calm-taxis-resize.md @@ -0,0 +1,5 @@ +--- +'@digdir/designsystemet-web': patch +--- + +**Suggestion:** Prevent ResizeObserver loop errors when positioning the option list. diff --git a/packages/web/src/popover/popover.test.ts b/packages/web/src/popover/popover.test.ts index 166eaa5e7d..f3e866e631 100644 --- a/packages/web/src/popover/popover.test.ts +++ b/packages/web/src/popover/popover.test.ts @@ -73,6 +73,37 @@ describe('popover floating behavior', () => { expect(popover).toHaveAttribute('data-floating', 'bottom-end'); }); + it('defers dimension changes to avoid resizing during ResizeObserver delivery', async () => { + const animationFrames: FrameRequestCallback[] = []; + vi.spyOn(window, 'requestAnimationFrame').mockImplementation((callback) => { + animationFrames.push(callback); + return animationFrames.length; + }); + render(` + +
Content
+ `); + + document.querySelector('button')?.click(); + await tick(); + + const popover = document.getElementById('my-popover') as HTMLElement; + expect(animationFrames.length).toBeGreaterThan(0); + expect(popover.style.width).toBe(''); + expect(popover.style.maxHeight).toBe(''); + + animationFrames.forEach((callback) => { + callback(performance.now()); + }); + + expect(popover.style.width).toBe('120px'); + expect(popover.style.maxHeight).not.toBe(''); + }); + it('does not position when data-placement is "none"', async () => { render(` diff --git a/packages/web/src/popover/popover.ts b/packages/web/src/popover/popover.ts index c957a9b675..b0d323a68a 100644 --- a/packages/web/src/popover/popover.ts +++ b/packages/web/src/popover/popover.ts @@ -105,9 +105,16 @@ function toggle( ? [ size({ apply({ availableHeight }) { - if (overscroll === 'fit') - el.style.width = `${source.offsetWidth}px`; // Use offsetWidth to include padding, matching the width of the source element - el.style.maxHeight = `${Math.max(50, availableHeight - padding * 2)}px`; + const width = `${source.offsetWidth}px`; // Use offsetWidth to include padding, matching the width of the source element + const maxHeight = `${Math.max(50, availableHeight - padding * 2)}px`; + + requestAnimationFrame(() => { + // Avoid changing an observed element during ResizeObserver delivery. + if (overscroll === 'fit' && el.style.width !== width) + el.style.width = width; + if (el.style.maxHeight !== maxHeight) + el.style.maxHeight = maxHeight; + }); }, }), ] From b5075f1bd224325b5b3de26f20cb1de05eee7595 Mon Sep 17 00:00:00 2001 From: Ole Martin Handeland Date: Thu, 13 Aug 2026 10:21:54 +0200 Subject: [PATCH 2/3] Update .changeset/calm-taxis-resize.md Co-authored-by: Tobias Barsnes --- .changeset/calm-taxis-resize.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/calm-taxis-resize.md b/.changeset/calm-taxis-resize.md index 5a3fcd074e..57b19b1a97 100644 --- a/.changeset/calm-taxis-resize.md +++ b/.changeset/calm-taxis-resize.md @@ -2,4 +2,4 @@ '@digdir/designsystemet-web': patch --- -**Suggestion:** Prevent ResizeObserver loop errors when positioning the option list. +**popover:** Prevent ResizeObserver loop errors when positioning the option list. From d74d839129d22a3895f67fa81f89a806b3a86917 Mon Sep 17 00:00:00 2001 From: Ole Martin Handeland Date: Fri, 14 Aug 2026 10:41:39 +0200 Subject: [PATCH 3/3] Applying suggestions from review --- packages/web/src/popover/popover.test.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/web/src/popover/popover.test.ts b/packages/web/src/popover/popover.test.ts index 7ffe9957f2..0f25702138 100644 --- a/packages/web/src/popover/popover.test.ts +++ b/packages/web/src/popover/popover.test.ts @@ -245,6 +245,7 @@ describe('popover overscroll sizing', () => { trigger.click(); await tick(); + await vi.waitFor(() => expect(popover.style.maxHeight).toMatch(/px$/)); const sizeAtOpen = popover.style.maxHeight; expect(sizeAtOpen).toMatch(/px$/); @@ -266,6 +267,7 @@ describe('popover overscroll sizing', () => { trigger.click(); await tick(); + await vi.waitFor(() => expect(popover.style.maxHeight).toMatch(/px$/)); const sizeAtFirstOpen = popover.style.maxHeight; trigger.click(); // Close @@ -274,8 +276,10 @@ describe('popover overscroll sizing', () => { trigger.click(); await tick(); + await vi.waitFor(() => + expect(popover.style.maxHeight).not.toBe(sizeAtFirstOpen), + ); expect(popover.style.maxHeight).toMatch(/px$/); - expect(popover.style.maxHeight).not.toBe(sizeAtFirstOpen); }); });