Skip to content

Commit

Permalink
feat: New hooks for element size tracking (#413)
Browse files Browse the repository at this point in the history
Co-authored-by: Guilherme Gazzo <guilhermegazzo@gmail.com>
  • Loading branch information
tassoevan and ggazzo authored Nov 19, 2021
1 parent 531803a commit 8ca682c
Show file tree
Hide file tree
Showing 38 changed files with 1,818 additions and 1,577 deletions.
2 changes: 1 addition & 1 deletion packages/css-in-js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
"@rollup/plugin-typescript": "^8.2.1",
"@types/jest": "^27.0.2",
"@types/stylis": "^4.0.1",
"eslint": "^7.32.0",
"eslint": "^8.2.0",
"jest": "^27.3.1",
"lint-all": "workspace:tools/lint-all",
"lint-staged": "^11.2.6",
Expand Down
2 changes: 1 addition & 1 deletion packages/css-supports/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
"@rocket.chat/eslint-config-alt": "workspace:packages/eslint-config-alt",
"@rocket.chat/prettier-config": "workspace:packages/prettier-config",
"@types/jest": "^27.0.2",
"eslint": "^7.32.0",
"eslint": "^8.2.0",
"jest": "^27.3.1",
"lint-all": "workspace:tools/lint-all",
"lint-staged": "^11.2.6",
Expand Down
2 changes: 1 addition & 1 deletion packages/emitter/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
"@rollup/plugin-node-resolve": "^13.0.0",
"@rollup/plugin-typescript": "^8.2.1",
"@types/jest": "^27.0.2",
"eslint": "^7.32.0",
"eslint": "^8.2.0",
"jest": "^27.3.1",
"lint-all": "workspace:tools/lint-all",
"lint-staged": "^11.2.6",
Expand Down
14 changes: 7 additions & 7 deletions packages/eslint-config-alt/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,21 +37,21 @@
"prettier": "^2.3.2"
},
"devDependencies": {
"@babel/eslint-parser": "^7.15.8",
"eslint": "^7.32.0",
"@babel/eslint-parser": "^7.16.3",
"eslint": "^8.2.0",
"lint-all": "workspace:tools/lint-all",
"lint-staged": "^11.2.6",
"prettier": "^2.3.2"
},
"dependencies": {
"@rocket.chat/eslint-config": "^0.4.0",
"@typescript-eslint/eslint-plugin": "^4.33.0",
"@typescript-eslint/parser": "^4.33.0",
"@typescript-eslint/eslint-plugin": "^5.3.1",
"@typescript-eslint/parser": "^5.3.1",
"eslint-config-prettier": "^8.3.0",
"eslint-import-resolver-typescript": "^2.5.0",
"eslint-plugin-import": "^2.25.2",
"eslint-plugin-import": "^2.25.3",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-react": "^7.26.1",
"eslint-plugin-react-hooks": "^4.2.0"
"eslint-plugin-react": "^7.27.0",
"eslint-plugin-react-hooks": "^4.3.0"
}
}
2 changes: 1 addition & 1 deletion packages/fuselage-hooks/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
"@types/react-dom": "^17.0.11",
"@types/resize-observer-browser": "^0.1.6",
"@types/use-subscription": "^1.0.0",
"eslint": "^7.32.0",
"eslint": "^8.2.0",
"jest": "^27.3.1",
"lint-all": "workspace:tools/lint-all",
"lint-staged": "^11.2.6",
Expand Down
2 changes: 2 additions & 0 deletions packages/fuselage-hooks/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
export * from './useAutoFocus';
export * from './useBorderBoxSize';
export * from './useBreakpoints';
export * from './useClipboard';
export * from './useDarkMode';
export * from './useContentBoxSize';
export * from './useDebouncedCallback';
export * from './useDebouncedReducer';
export * from './useDebouncedState';
Expand Down
15 changes: 15 additions & 0 deletions packages/fuselage-hooks/src/useBorderBoxSize.server.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* @jest-environment node
*/

import { renderHook } from '@testing-library/react-hooks/server';
import { useRef } from 'react';

import { useBorderBoxSize } from './useBorderBoxSize';

it('immediately returns zero size', () => {
const { result } = renderHook(() => useBorderBoxSize(useRef()));

expect(result.current.inlineSize).toStrictEqual(0);
expect(result.current.blockSize).toStrictEqual(0);
});
101 changes: 101 additions & 0 deletions packages/fuselage-hooks/src/useBorderBoxSize.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { renderHook, act } from '@testing-library/react-hooks';
import { useRef, RefObject } from 'react';
import { withResizeObserverMock } from 'testing-utils/mocks/withResizeObserverMock';

import { useBorderBoxSize } from './useBorderBoxSize';

withResizeObserverMock();

beforeAll(() => {
jest.useFakeTimers();
});

let element: HTMLElement;

beforeEach(() => {
element = document.createElement('div');
element.style.width = '40px';
element.style.height = '30px';
element.style.padding = '5px';
document.body.append(element);
});

afterEach(() => {
element.remove();
});

const wrapRef = (ref: RefObject<HTMLElement>) => {
Object.assign(ref, { current: element });
return ref;
};

it('immediately returns size', async () => {
const { result } = renderHook(() => useBorderBoxSize(wrapRef(useRef())));

expect(result.current.inlineSize).toStrictEqual(50);
expect(result.current.blockSize).toStrictEqual(40);
});

it('gets the observed element size after resize', async () => {
const { result } = renderHook(() => useBorderBoxSize(wrapRef(useRef())));

// triggers MutationObserver
await act(async () => {
element.style.width = '30px';
element.style.height = '40px';
element.style.padding = '15px';
});

// waits for debounced state mutation
await act(async () => {
jest.advanceTimersByTime(0);
});

expect(result.current.inlineSize).toStrictEqual(60);
expect(result.current.blockSize).toStrictEqual(70);

// triggers MutationObserver
await act(async () => {
element.style.boxSizing = 'border-box';
});

// waits for debounced state mutation
await act(async () => {
jest.advanceTimersByTime(0);
});

expect(result.current.inlineSize).toStrictEqual(30);
expect(result.current.blockSize).toStrictEqual(40);
});

it('debounces the observed element size', async () => {
const halfDelay = 50;
const delay = 2 * halfDelay;

const { result } = renderHook(() =>
useBorderBoxSize(wrapRef(useRef()), { debounceDelay: delay })
);

// triggers MutationObserver
await act(async () => {
element.style.width = '30px';
element.style.height = '40px';
element.style.padding = '15px';
});

// waits for debounced state mutation
await act(async () => {
jest.advanceTimersByTime(halfDelay);
});

expect(result.current.inlineSize).toStrictEqual(50);
expect(result.current.blockSize).toStrictEqual(40);

// wait the callback trigger from ResizeObserver
await act(async () => {
jest.advanceTimersByTime(halfDelay);
});

expect(result.current.inlineSize).toStrictEqual(60);
expect(result.current.blockSize).toStrictEqual(70);
});
67 changes: 67 additions & 0 deletions packages/fuselage-hooks/src/useBorderBoxSize.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { RefObject, useState } from 'react';

import { useDebouncedCallback } from './useDebouncedCallback';
import { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect';

export const useBorderBoxSize = (
ref: RefObject<HTMLElement>,
options: {
debounceDelay?: number;
} = {}
): Readonly<{
inlineSize: number;
blockSize: number;
}> => {
const [size, setSize] = useState(() => ({
inlineSize: ref.current?.offsetWidth ?? 0,
blockSize: ref.current?.offsetHeight ?? 0,
}));

const setSizeWithDebounce = useDebouncedCallback(
setSize,
options.debounceDelay
);

useIsomorphicLayoutEffect(() => {
const element = ref.current;

if (!element) {
return;
}

const observer = new ResizeObserver((entries: ResizeObserverEntry[]) => {
if (entries.length === 0 || entries[0].borderBoxSize.length === 0) {
return;
}

const borderBoxSize = entries[0].borderBoxSize[0];

setSizeWithDebounce((prevSize) => {
if (
prevSize.inlineSize === borderBoxSize.inlineSize &&
prevSize.blockSize === borderBoxSize.blockSize
) {
return prevSize;
}

return {
inlineSize: borderBoxSize.inlineSize,
blockSize: borderBoxSize.blockSize,
};
});
});

observer.observe(element);

setSize({
inlineSize: element.offsetWidth,
blockSize: element.offsetHeight,
});

return () => {
observer.unobserve(element);
};
}, [setSizeWithDebounce]);

return size;
};
15 changes: 15 additions & 0 deletions packages/fuselage-hooks/src/useContentBoxSize.server.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* @jest-environment node
*/

import { renderHook } from '@testing-library/react-hooks/server';
import { useRef } from 'react';

import { useContentBoxSize } from './useContentBoxSize';

it('immediately returns zero size', () => {
const { result } = renderHook(() => useContentBoxSize(useRef()));

expect(result.current.inlineSize).toStrictEqual(0);
expect(result.current.blockSize).toStrictEqual(0);
});
101 changes: 101 additions & 0 deletions packages/fuselage-hooks/src/useContentBoxSize.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { renderHook, act } from '@testing-library/react-hooks';
import { useRef, RefObject } from 'react';
import { withResizeObserverMock } from 'testing-utils/mocks/withResizeObserverMock';

import { useContentBoxSize } from './useContentBoxSize';

withResizeObserverMock();

beforeAll(() => {
jest.useFakeTimers();
});

let element: HTMLElement;

beforeEach(() => {
element = document.createElement('div');
element.style.width = '40px';
element.style.height = '30px';
element.style.padding = '5px';
document.body.append(element);
});

afterEach(() => {
element.remove();
});

const wrapRef = (ref: RefObject<HTMLElement>) => {
Object.assign(ref, { current: element });
return ref;
};

it('immediately returns size', async () => {
const { result } = renderHook(() => useContentBoxSize(wrapRef(useRef())));

expect(result.current.inlineSize).toStrictEqual(40);
expect(result.current.blockSize).toStrictEqual(30);
});

it('gets the observed element size after resize', async () => {
const { result } = renderHook(() => useContentBoxSize(wrapRef(useRef())));

// triggers MutationObserver
await act(async () => {
element.style.width = '30px';
element.style.height = '40px';
element.style.padding = '15px';
});

// waits for debounced state mutation
await act(async () => {
jest.advanceTimersByTime(0);
});

expect(result.current.inlineSize).toStrictEqual(30);
expect(result.current.blockSize).toStrictEqual(40);

// triggers MutationObserver
await act(async () => {
element.style.boxSizing = 'border-box';
});

// waits for debounced state mutation
await act(async () => {
jest.advanceTimersByTime(0);
});

expect(result.current.inlineSize).toStrictEqual(0);
expect(result.current.blockSize).toStrictEqual(10);
});

it('debounces the observed element size', async () => {
const halfDelay = 50;
const delay = 2 * halfDelay;

const { result } = renderHook(() =>
useContentBoxSize(wrapRef(useRef()), { debounceDelay: delay })
);

// triggers MutationObserver
await act(async () => {
element.style.width = '30px';
element.style.height = '40px';
element.style.padding = '15px';
});

// waits for debounced state mutation
await act(async () => {
jest.advanceTimersByTime(halfDelay);
});

expect(result.current.inlineSize).toStrictEqual(40);
expect(result.current.blockSize).toStrictEqual(30);

// wait the callback trigger from ResizeObserver
await act(async () => {
jest.advanceTimersByTime(halfDelay);
});

expect(result.current.inlineSize).toStrictEqual(30);
expect(result.current.blockSize).toStrictEqual(40);
});
Loading

0 comments on commit 8ca682c

Please sign in to comment.