From 0784da213b67c42dc95c3e0d7659375bbf59eb1d Mon Sep 17 00:00:00 2001 From: apple <245524539+apples-kksk@users.noreply.github.com> Date: Tue, 12 May 2026 23:31:43 +0800 Subject: [PATCH 1/2] fix(shared): use border box for element resize --- .../dom-events/resize/resizeElement.test.ts | 109 ++++++++++++++++++ .../src/dom-events/resize/resizeElement.ts | 46 +++++++- 2 files changed, 152 insertions(+), 3 deletions(-) create mode 100644 packages/shared/src/dom-events/resize/resizeElement.test.ts diff --git a/packages/shared/src/dom-events/resize/resizeElement.test.ts b/packages/shared/src/dom-events/resize/resizeElement.test.ts new file mode 100644 index 0000000000..09fdaafe99 --- /dev/null +++ b/packages/shared/src/dom-events/resize/resizeElement.test.ts @@ -0,0 +1,109 @@ +import { resizeElement } from './resizeElement' + +const observe = jest.fn() +const unobserve = jest.fn() +let callback: ResizeObserverCallback | undefined + +class ResizeObserverMock { + constructor(cb: ResizeObserverCallback) { + callback = cb + } + + observe = observe + unobserve = unobserve +} + +describe('resizeElement', () => { + const ResizeObserver = global.ResizeObserver + let cleanup: (() => void) | undefined + + beforeEach(() => { + observe.mockClear() + unobserve.mockClear() + cleanup = undefined + + global.ResizeObserver = + ResizeObserverMock as unknown as typeof global.ResizeObserver + }) + + afterEach(() => { + cleanup?.() + jest.resetAllMocks() + global.ResizeObserver = ResizeObserver + }) + + it('observes and reports the border box size', () => { + const target = document.createElement('div') + const handler = jest.fn() + + cleanup = resizeElement(handler, target) + + expect(observe).toHaveBeenCalledWith(target, { box: 'border-box' }) + + callback?.( + [ + { + target, + borderBoxSize: [{ inlineSize: 120, blockSize: 80 }], + contentRect: { width: 100, height: 60 }, + } as ResizeObserverEntry, + ], + {} as ResizeObserver + ) + + expect(handler).toHaveBeenCalledWith({ width: 120, height: 80 }) + }) + + it('maps border box size for vertical writing modes', () => { + const target = document.createElement('div') + const handler = jest.fn() + + target.style.writingMode = 'vertical-rl' + cleanup = resizeElement(handler, target) + + callback?.( + [ + { + target, + borderBoxSize: [{ inlineSize: 120, blockSize: 80 }], + contentRect: { width: 100, height: 60 }, + } as ResizeObserverEntry, + ], + {} as ResizeObserver + ) + + expect(handler).toHaveBeenCalledWith({ width: 80, height: 120 }) + }) + + it('falls back to contentRect when borderBoxSize is unavailable', () => { + const target = document.createElement('div') + const handler = jest.fn() + + cleanup = resizeElement(handler, target) + + callback?.( + [ + { + target, + contentRect: { width: 100, height: 60 }, + } as ResizeObserverEntry, + ], + {} as ResizeObserver + ) + + expect(handler).toHaveBeenCalledWith({ width: 100, height: 60 }) + }) + + it('falls back when border-box observe options are unsupported', () => { + const target = document.createElement('div') + + observe.mockImplementationOnce(() => { + throw new TypeError('box option unsupported') + }) + + cleanup = resizeElement(jest.fn(), target) + + expect(observe).toHaveBeenNthCalledWith(1, target, { box: 'border-box' }) + expect(observe).toHaveBeenNthCalledWith(2, target) + }) +}) diff --git a/packages/shared/src/dom-events/resize/resizeElement.ts b/packages/shared/src/dom-events/resize/resizeElement.ts index a755d63995..6a2c20dcc9 100644 --- a/packages/shared/src/dom-events/resize/resizeElement.ts +++ b/packages/shared/src/dom-events/resize/resizeElement.ts @@ -1,12 +1,43 @@ import type { OnResizeCallback } from '.' let observer: ResizeObserver | undefined +let supportsBorderBox = true const resizeHandlers = new WeakMap>() +const getBorderBoxSize = ( + target: Element, + { borderBoxSize, contentRect }: ResizeObserverEntry +): Pick & + Partial> => { + const boxSize = Array.isArray(borderBoxSize) + ? borderBoxSize[0] + : borderBoxSize + + if (boxSize) { + const isVerticalWritingMode = getComputedStyle(target) + .getPropertyValue('writing-mode') + .startsWith('vertical-') + + return isVerticalWritingMode + ? { + width: boxSize.blockSize, + height: boxSize.inlineSize, + } + : { + width: boxSize.inlineSize, + height: boxSize.blockSize, + } + } + + return contentRect +} + const handleObservation = (entries: ResizeObserverEntry[]) => - entries.forEach(({ target, contentRect }) => { - return resizeHandlers.get(target)?.forEach(handler => handler(contentRect)) + entries.forEach(entry => { + return resizeHandlers + .get(entry.target) + ?.forEach(handler => handler(getBorderBoxSize(entry.target, entry))) }) export function resizeElement(handler: OnResizeCallback, target: HTMLElement) { @@ -40,7 +71,16 @@ export function resizeElement(handler: OnResizeCallback, target: HTMLElement) { elementHandlers.add(handler) if (observer) { - observer.observe(target) + if (supportsBorderBox) { + try { + observer.observe(target, { box: 'border-box' }) + } catch { + supportsBorderBox = false + observer.observe(target) + } + } else { + observer.observe(target) + } } /** From ef91275d96a15ad3a06d39cb4eacc73939da5cc2 Mon Sep 17 00:00:00 2001 From: apple <245524539+apples-kksk@users.noreply.github.com> Date: Wed, 13 May 2026 00:40:16 +0800 Subject: [PATCH 2/2] fix(shared): use border box for element resize --- .changeset/new-melons-cheat.md | 5 +++ .../dom-events/resize/resizeElement.test.ts | 40 +++++++++++++++++-- .../src/dom-events/resize/resizeElement.ts | 23 +++++------ 3 files changed, 52 insertions(+), 16 deletions(-) create mode 100644 .changeset/new-melons-cheat.md diff --git a/.changeset/new-melons-cheat.md b/.changeset/new-melons-cheat.md new file mode 100644 index 0000000000..7209df3c6b --- /dev/null +++ b/.changeset/new-melons-cheat.md @@ -0,0 +1,5 @@ +--- +'@react-spring/shared': patch +--- + +Fix element resize measurements to use border-box dimensions when available. diff --git a/packages/shared/src/dom-events/resize/resizeElement.test.ts b/packages/shared/src/dom-events/resize/resizeElement.test.ts index 09fdaafe99..11fa3c9fd3 100644 --- a/packages/shared/src/dom-events/resize/resizeElement.test.ts +++ b/packages/shared/src/dom-events/resize/resizeElement.test.ts @@ -46,7 +46,7 @@ describe('resizeElement', () => { target, borderBoxSize: [{ inlineSize: 120, blockSize: 80 }], contentRect: { width: 100, height: 60 }, - } as ResizeObserverEntry, + } as unknown as ResizeObserverEntry, ], {} as ResizeObserver ) @@ -67,7 +67,29 @@ describe('resizeElement', () => { target, borderBoxSize: [{ inlineSize: 120, blockSize: 80 }], contentRect: { width: 100, height: 60 }, - } as ResizeObserverEntry, + } as unknown as ResizeObserverEntry, + ], + {} as ResizeObserver + ) + + expect(handler).toHaveBeenCalledWith({ width: 80, height: 120 }) + }) + + it('maps border box size for sideways writing modes', () => { + const target = document.createElement('div') + const handler = jest.fn() + + target.style.writingMode = 'sideways-rl' + + cleanup = resizeElement(handler, target) + + callback?.( + [ + { + target, + borderBoxSize: [{ inlineSize: 120, blockSize: 80 }], + contentRect: { width: 100, height: 60 }, + } as unknown as ResizeObserverEntry, ], {} as ResizeObserver ) @@ -86,7 +108,7 @@ describe('resizeElement', () => { { target, contentRect: { width: 100, height: 60 }, - } as ResizeObserverEntry, + } as unknown as ResizeObserverEntry, ], {} as ResizeObserver ) @@ -106,4 +128,16 @@ describe('resizeElement', () => { expect(observe).toHaveBeenNthCalledWith(1, target, { box: 'border-box' }) expect(observe).toHaveBeenNthCalledWith(2, target) }) + + it('does not swallow non-TypeError observe failures', () => { + const target = document.createElement('div') + const boom = new Error('boom') + + observe.mockImplementationOnce(() => { + throw boom + }) + + expect(() => resizeElement(jest.fn(), target)).toThrow(boom) + expect(observe).toHaveBeenCalledTimes(1) + }) }) diff --git a/packages/shared/src/dom-events/resize/resizeElement.ts b/packages/shared/src/dom-events/resize/resizeElement.ts index 6a2c20dcc9..e3fee9d9a3 100644 --- a/packages/shared/src/dom-events/resize/resizeElement.ts +++ b/packages/shared/src/dom-events/resize/resizeElement.ts @@ -1,7 +1,6 @@ import type { OnResizeCallback } from '.' let observer: ResizeObserver | undefined -let supportsBorderBox = true const resizeHandlers = new WeakMap>() @@ -15,11 +14,12 @@ const getBorderBoxSize = ( : borderBoxSize if (boxSize) { - const isVerticalWritingMode = getComputedStyle(target) - .getPropertyValue('writing-mode') - .startsWith('vertical-') + const writingMode = + getComputedStyle(target).getPropertyValue('writing-mode') + const isOrthogonalWritingMode = + writingMode.startsWith('vertical-') || writingMode.startsWith('sideways-') - return isVerticalWritingMode + return isOrthogonalWritingMode ? { width: boxSize.blockSize, height: boxSize.inlineSize, @@ -71,14 +71,11 @@ export function resizeElement(handler: OnResizeCallback, target: HTMLElement) { elementHandlers.add(handler) if (observer) { - if (supportsBorderBox) { - try { - observer.observe(target, { box: 'border-box' }) - } catch { - supportsBorderBox = false - observer.observe(target) - } - } else { + try { + observer.observe(target, { box: 'border-box' }) + } catch (error) { + if (!(error instanceof TypeError)) throw error + observer.observe(target) } }