diff --git a/app/soapbox/hooks/__tests__/useDimensions.test.ts b/app/soapbox/hooks/__tests__/useDimensions.test.ts new file mode 100644 index 000000000..ec86d691a --- /dev/null +++ b/app/soapbox/hooks/__tests__/useDimensions.test.ts @@ -0,0 +1,76 @@ +import { renderHook, act } from '@testing-library/react-hooks'; + +import { useDimensions } from '../useDimensions'; + +let listener: ((rect: any) => void) | undefined = undefined; + +(window as any).ResizeObserver = class ResizeObserver { + constructor(ls) { + listener = ls; + } + + observe() {} + disconnect() {} +}; + +describe('useDimensions()', () => { + it('defaults to 0', () => { + const { result } = renderHook(() => useDimensions()); + + act(() => { + const div = document.createElement('div'); + (result.current[0] as any)(div); + }); + + expect(result.current[1]).toMatchObject({ + width: 0, + height: 0, + }); + }); + + it('measures the dimensions of a DOM element', () => { + const { result } = renderHook(() => useDimensions()); + + act(() => { + const div = document.createElement('div'); + (result.current[0] as any)(div); + }); + + act(() => { + listener!([ + { + contentRect: { + width: 200, + height: 200, + }, + }, + ]); + }); + + expect(result.current[1]).toMatchObject({ + width: 200, + height: 200, + }); + }); + + it('disconnects on unmount', () => { + const disconnect = jest.fn(); + (window as any).ResizeObserver = class ResizeObserver { + observe() {} + disconnect() { + disconnect(); + } + }; + + const { result, unmount } = renderHook(() => useDimensions()); + + act(() => { + const div = document.createElement('div'); + (result.current[0] as any)(div); + }); + + expect(disconnect).toHaveBeenCalledTimes(0); + unmount(); + expect(disconnect).toHaveBeenCalledTimes(1); + }); +}); diff --git a/app/soapbox/hooks/index.ts b/app/soapbox/hooks/index.ts index c25cb082a..2cd767c2d 100644 --- a/app/soapbox/hooks/index.ts +++ b/app/soapbox/hooks/index.ts @@ -1,6 +1,7 @@ export { useAccount } from './useAccount'; export { useAppDispatch } from './useAppDispatch'; export { useAppSelector } from './useAppSelector'; +export { useDimensions } from './useDimensions'; export { useFeatures } from './useFeatures'; export { useOnScreen } from './useOnScreen'; export { useOwnAccount } from './useOwnAccount'; diff --git a/app/soapbox/hooks/useDimensions.ts b/app/soapbox/hooks/useDimensions.ts new file mode 100644 index 000000000..8ba699925 --- /dev/null +++ b/app/soapbox/hooks/useDimensions.ts @@ -0,0 +1,38 @@ +import { Ref, useEffect, useMemo, useState } from 'react'; + +type UseDimensionsRect = { width: number, height: number }; +type UseDimensionsResult = [Ref, any] + +const defaultState: UseDimensionsRect = { + width: 0, + height: 0, +}; + +const useDimensions = (): UseDimensionsResult => { + const [element, ref] = useState(null); + const [rect, setRect] = useState(defaultState); + + const observer = useMemo( + () => + new (window as any).ResizeObserver((entries: any) => { + if (entries[0]) { + const { width, height } = entries[0].contentRect; + setRect({ width, height }); + } + }), + [], + ); + + useEffect((): any => { + if (!element) return null; + observer.observe(element); + + return () => { + observer.disconnect(); + }; + }, [element]); + + return [ref, rect]; +}; + +export { useDimensions }; diff --git a/package.json b/package.json index 9c438abc7..31b4b956a 100644 --- a/package.json +++ b/package.json @@ -202,6 +202,7 @@ }, "devDependencies": { "@testing-library/jest-dom": "^5.16.4", + "@testing-library/react-hooks": "^8.0.1", "@testing-library/user-event": "^14.0.3", "@typescript-eslint/eslint-plugin": "^5.15.0", "@typescript-eslint/parser": "^5.15.0", diff --git a/yarn.lock b/yarn.lock index f13fa58a4..b88349ff0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2329,6 +2329,14 @@ lodash "^4.17.15" redent "^3.0.0" +"@testing-library/react-hooks@^8.0.1": + version "8.0.1" + resolved "https://registry.yarnpkg.com/@testing-library/react-hooks/-/react-hooks-8.0.1.tgz#0924bbd5b55e0c0c0502d1754657ada66947ca12" + integrity sha512-Aqhl2IVmLt8IovEVarNDFuJDVWVvhnr9/GCU6UUnrYXwgDFF9h2L2o2P9KBni1AST5sT6riAyoukFLyjQUgD/g== + dependencies: + "@babel/runtime" "^7.12.5" + react-error-boundary "^3.1.0" + "@testing-library/react@^12.1.4": version "12.1.4" resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-12.1.4.tgz#09674b117e550af713db3f4ec4c0942aa8bbf2c0" @@ -9616,6 +9624,13 @@ react-dom@^17.0.2: object-assign "^4.1.1" scheduler "^0.20.2" +react-error-boundary@^3.1.0: + version "3.1.4" + resolved "https://registry.yarnpkg.com/react-error-boundary/-/react-error-boundary-3.1.4.tgz#255db92b23197108757a888b01e5b729919abde0" + integrity sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA== + dependencies: + "@babel/runtime" "^7.12.5" + react-event-listener@^0.6.0: version "0.6.6" resolved "https://registry.yarnpkg.com/react-event-listener/-/react-event-listener-0.6.6.tgz#758f7b991cad9086dd39fd29fad72127e1d8962a"