Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/new-melons-cheat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@react-spring/shared': patch
---

Fix element resize measurements to use border-box dimensions when available.
143 changes: 143 additions & 0 deletions packages/shared/src/dom-events/resize/resizeElement.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
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 unknown 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 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
)

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 unknown 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)
})

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)
})
})
43 changes: 40 additions & 3 deletions packages/shared/src/dom-events/resize/resizeElement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,40 @@ let observer: ResizeObserver | undefined

const resizeHandlers = new WeakMap<Element, Set<OnResizeCallback>>()

const getBorderBoxSize = (
target: Element,
{ borderBoxSize, contentRect }: ResizeObserverEntry
): Pick<DOMRectReadOnly, 'width' | 'height'> &
Partial<Omit<DOMRectReadOnly, 'width' | 'height'>> => {
const boxSize = Array.isArray(borderBoxSize)
? borderBoxSize[0]
: borderBoxSize

if (boxSize) {
const writingMode =
getComputedStyle(target).getPropertyValue('writing-mode')
const isOrthogonalWritingMode =
writingMode.startsWith('vertical-') || writingMode.startsWith('sideways-')

return isOrthogonalWritingMode
? {
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) {
Expand Down Expand Up @@ -40,7 +71,13 @@ export function resizeElement(handler: OnResizeCallback, target: HTMLElement) {
elementHandlers.add(handler)

if (observer) {
observer.observe(target)
try {
observer.observe(target, { box: 'border-box' })
} catch (error) {
if (!(error instanceof TypeError)) throw error

observer.observe(target)
}
}

/**
Expand Down