diff --git a/__mocks__/@juggle/resize-observer.ts b/__mocks__/@juggle/resize-observer.ts deleted file mode 100644 index c2a2571..0000000 --- a/__mocks__/@juggle/resize-observer.ts +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -/* eslint-env es6 */ -/* eslint-disable header/header */ -import { ResizeObserver, ResizeObserverEntry } from '@juggle/resize-observer'; - -const callbackField = Symbol(); - -const mockObserve = jest.fn(function (this: MockResizeObserver, el: HTMLElement) { - // JSDOM does not support CSS. This mock allows to set element sizes via inline styles - // and they will be passed into the handlers - const { width, height } = el.style; - const size = { inlineSize: parseInt(width) || 0, blockSize: parseInt(height) || 0 }; - const cb = this[callbackField]; - cb([{ borderBoxSize: [size], contentBoxSize: [size], target: el }], this); -}); -const mockUnobserve = jest.fn(); -const mockDisconnect = jest.fn(); - -class MockResizeObserver implements ResizeObserver { - [callbackField]: (...args: any[]) => void; - - constructor(cb: (...args: any[]) => void) { - this[callbackField] = cb; - } - - get observe() { - return mockObserve; - } - - get unobserve() { - return mockUnobserve; - } - - get disconnect() { - return mockDisconnect; - } -} - -export { ResizeObserverEntry }; -export { MockResizeObserver as ResizeObserver }; diff --git a/package-lock.json b/package-lock.json index 9c3c95e..7c277c7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,6 @@ "name": "@cloudscape-design/component-toolkit", "version": "1.0.0-beta", "dependencies": { - "@juggle/resize-observer": "^3.3.1", "tslib": "^2.3.1" }, "devDependencies": { @@ -2728,11 +2727,6 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@juggle/resize-observer": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-3.4.0.tgz", - "integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==" - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -17802,11 +17796,6 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "@juggle/resize-observer": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-3.4.0.tgz", - "integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==" - }, "@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", diff --git a/package.json b/package.json index ea35ec7..56ef413 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,6 @@ "prepare": "husky" }, "dependencies": { - "@juggle/resize-observer": "^3.3.1", "tslib": "^2.3.1" }, "devDependencies": { diff --git a/src/container-queries/__tests__/use-container-query-pure.test.tsx b/src/container-queries/__tests__/use-container-query-pure.test.tsx new file mode 100644 index 0000000..5e1a501 --- /dev/null +++ b/src/container-queries/__tests__/use-container-query-pure.test.tsx @@ -0,0 +1,29 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import React from 'react'; +import { render } from '@testing-library/react'; +import { ContainerQueryEntry } from '../interfaces'; +import useContainerQuery from '../use-container-query'; + +function TestComponent({ mapFn = () => '' }: { mapFn?: (entry: ContainerQueryEntry) => string }) { + const [value, ref] = useContainerQuery(mapFn); + return
; +} + +test('should work in JSDOM environment without any mocks', () => { + // making sure this API does not exist + expect(typeof ResizeObserver).toBe('undefined'); + const mapFn = jest.fn(() => ''); + const component = render(); + expect(mapFn).toHaveBeenCalledWith( + { + target: component.getByTestId('test'), + contentBoxWidth: 0, + contentBoxHeight: 0, + borderBoxWidth: 0, + borderBoxHeight: 0, + }, + null + ); +}); diff --git a/src/container-queries/__tests__/use-container-query.test.tsx b/src/container-queries/__tests__/use-container-query.test.tsx index 07f1747..c3fc1ef 100644 --- a/src/container-queries/__tests__/use-container-query.test.tsx +++ b/src/container-queries/__tests__/use-container-query.test.tsx @@ -4,8 +4,8 @@ import React from 'react'; import { render } from '@testing-library/react'; import useContainerQuery from '../use-container-query'; -import { ResizeObserver } from '@juggle/resize-observer'; import { ContainerQueryEntry } from '../interfaces'; +import '../../internal/container-queries/__tests__/resize-observer-mock'; function TestComponent({ mapFn = () => '' }: { mapFn?: (entry: ContainerQueryEntry) => string }) { const [value, ref] = useContainerQuery(mapFn); diff --git a/src/internal/container-queries/__tests__/resize-observer-mock.ts b/src/internal/container-queries/__tests__/resize-observer-mock.ts new file mode 100644 index 0000000..fd4da19 --- /dev/null +++ b/src/internal/container-queries/__tests__/resize-observer-mock.ts @@ -0,0 +1,66 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +/** + * Mock ResizeObserver for Jest tests + * + * This module provides a mock implementation of the native ResizeObserver API + * for use in Jest test environments where ResizeObserver may not be available. + */ + +const mockObserve = jest.fn(); +const mockUnobserve = jest.fn(); +const mockDisconnect = jest.fn(); + +// Create the ResizeObserver mock constructor +global.ResizeObserver = jest.fn().mockImplementation(() => ({ + observe: mockObserve, + unobserve: mockUnobserve, + disconnect: mockDisconnect, +})); + +// Mock the prototype methods for backward compatibility with tests that access prototype +Object.defineProperty(global.ResizeObserver, 'prototype', { + value: { + observe: mockObserve, + unobserve: mockUnobserve, + disconnect: mockDisconnect, + }, + writable: false, +}); + +/** + * Mock getBoundingClientRect to return dimensions based on CSS styles + * + * In JSDOM test environments, getBoundingClientRect() returns all zeros because + * JSDOM doesn't perform actual layout calculations. This mock extracts width/height + * from computed CSS styles to provide realistic dimensions for ResizeObserver tests. + * + * Only mock in browser environments (not SSR tests where Element is undefined). + */ +if (typeof Element !== 'undefined') { + Element.prototype.getBoundingClientRect = jest.fn(function () { + const style = window.getComputedStyle(this); + const width = parseFloat(style.width) || 0; + const height = parseFloat(style.height) || 0; + + return { + width, + height, + top: 0, + left: 0, + bottom: height, + right: width, + x: 0, + y: 0, + toJSON: () => ({}), + }; + }); +} + +// Export the mock functions for test access if needed +module.exports = { + mockObserve, + mockUnobserve, + mockDisconnect, +}; diff --git a/src/internal/container-queries/__tests__/use-resize-observer.test.tsx b/src/internal/container-queries/__tests__/use-resize-observer.test.tsx index 06feb6e..9c2c38d 100644 --- a/src/internal/container-queries/__tests__/use-resize-observer.test.tsx +++ b/src/internal/container-queries/__tests__/use-resize-observer.test.tsx @@ -4,8 +4,8 @@ import React, { useState, useRef } from 'react'; import { render } from '@testing-library/react'; import { useResizeObserver } from '../use-resize-observer'; -import { ResizeObserver } from '@juggle/resize-observer'; import { ContainerQueryEntry } from '../interfaces'; +import './resize-observer-mock'; function TestComponent({ mapFn = () => '' }: { mapFn?: (entry: ContainerQueryEntry) => string }) { const ref = useRef(null); diff --git a/src/internal/container-queries/use-resize-observer.ts b/src/internal/container-queries/use-resize-observer.ts index 4118a44..9dfcb72 100644 --- a/src/internal/container-queries/use-resize-observer.ts +++ b/src/internal/container-queries/use-resize-observer.ts @@ -2,7 +2,6 @@ // SPDX-License-Identifier: Apache-2.0 import { unstable_batchedUpdates } from 'react-dom'; -import { ResizeObserver, ResizeObserverEntry } from '@juggle/resize-observer'; import { useEffect, useLayoutEffect } from 'react'; import { ContainerQueryEntry, ElementReference } from './interfaces'; import { useStableCallback } from '../stable-callback'; @@ -42,7 +41,8 @@ export function useResizeObserver(elementRef: ElementReference, onObserve: (entr () => { const element = typeof elementRef === 'function' ? elementRef() : elementRef?.current; if (element) { - onObserve(convertResizeObserverEntry(new ResizeObserverEntry(element))); + const rect = element.getBoundingClientRect(); + onObserve(convertElementToEntry(element, rect)); } }, // This effect is only needed for the first render to provide a synchronous update. @@ -52,7 +52,7 @@ export function useResizeObserver(elementRef: ElementReference, onObserve: (entr useEffect(() => { const element = typeof elementRef === 'function' ? elementRef() : elementRef?.current; - if (element) { + if (element && typeof ResizeObserver !== 'undefined') { let connected = true; const observer = new ResizeObserver(entries => { // Prevent observe notifications on already unmounted component. @@ -80,3 +80,19 @@ function convertResizeObserverEntry(entry: ResizeObserverEntry): ContainerQueryE borderBoxHeight: entry.borderBoxSize[0].blockSize, }; } + +function convertElementToEntry(element: Element, rect: DOMRect): ContainerQueryEntry { + const computedStyle = window.getComputedStyle(element); + const paddingLeft = parseFloat(computedStyle.paddingLeft) || 0; + const paddingRight = parseFloat(computedStyle.paddingRight) || 0; + const paddingTop = parseFloat(computedStyle.paddingTop) || 0; + const paddingBottom = parseFloat(computedStyle.paddingBottom) || 0; + + return { + target: element, + contentBoxWidth: rect.width - paddingLeft - paddingRight, + contentBoxHeight: rect.height - paddingTop - paddingBottom, + borderBoxWidth: rect.width, + borderBoxHeight: rect.height, + }; +}