Skip to content

Commit

Permalink
feat: eliminate resize-observer-polyfill dependency (#12)
Browse files Browse the repository at this point in the history
  • Loading branch information
igordanchenko authored Jan 10, 2022
1 parent 8fab24d commit c9e0069
Show file tree
Hide file tree
Showing 6 changed files with 112 additions and 36 deletions.
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,15 @@ default. Keep in mind that responsive parameters may contribute to cumulative la
load. If CLS becomes an issue in your case, you may want to consider using hard-coded values for `columns`, `spacing`
, `padding`, etc., instead of the default responsive values.

### ResizeObserver

React Photo Album relies on [ResizeObserver](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver) to respond
to container size changes (i.e., when the browser window gets resized, device orientation changes or page layout causes
a shift in container dimensions). React Photo Album no longer bundles ResizeObserver polyfill as part of the library
since, as of January 2022, over 91% of browsers support it natively. If your use case requires you to support some older
browsers, you can accomplish this by either installing a global ResizeObserver polyfill or by supplying a ponyfill via
the `resizeObserverProvider` parameter.

## Credits

- Thanks to Sandra G (aka [neptunian](https://github.com/neptunian)) for authoring the
Expand Down
13 changes: 0 additions & 13 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 0 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,6 @@
"engines": {
"node": ">=12"
},
"dependencies": {
"resize-observer-polyfill": "^1.5.1"
},
"devDependencies": {
"@commitlint/cli": "^16.0.1",
"@commitlint/config-conventional": "^16.0.0",
Expand Down
45 changes: 26 additions & 19 deletions src/PhotoAlbum.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import * as React from "react";
import ResizeObserver from "resize-observer-polyfill";

import Layout from "./Layout";
import RowsLayout from "./components/layouts/RowsLayout";
Expand Down Expand Up @@ -47,6 +46,7 @@ const PhotoAlbum = <T extends Photo>(props: PhotoAlbumProps<T>): JSX.Element =>
renderContainer,
renderRowContainer,
renderColumnContainer,
resizeObserverProvider,
instrumentation,
} = props;

Expand All @@ -65,32 +65,39 @@ const PhotoAlbum = <T extends Photo>(props: PhotoAlbumProps<T>): JSX.Element =>
}
}, []);

const setContainerRef = React.useCallback(
(node) => {
if (observerRef.current && containerRef.current) {
observerRef.current.unobserve(containerRef.current);
}
const setContainerRef = React.useCallback((node) => {
if (observerRef.current && containerRef.current) {
observerRef.current.unobserve(containerRef.current);
}

containerRef.current = node;
containerRef.current = node;

if (node) {
if (!observerRef.current) {
observerRef.current = new ResizeObserver(() => updateDimensions());
}
observerRef.current.observe(node);
}
},
[updateDimensions]
);
if (node && observerRef.current) {
observerRef.current.observe(node);
}
}, []);

useLayoutEffect(() => {
updateDimensions();

const observerCallback = () => updateDimensions();
if (typeof ResizeObserver !== "undefined") {
observerRef.current = new ResizeObserver(observerCallback);
} else if (resizeObserverProvider) {
observerRef.current = resizeObserverProvider(observerCallback);
}

if (observerRef.current && containerRef.current) {
observerRef.current.observe(containerRef.current);
}

return () => {
observerRef.current?.disconnect();
observerRef.current = null;
if (observerRef.current) {
observerRef.current.disconnect();
observerRef.current = null;
}
};
}, [updateDimensions]);
}, [updateDimensions, resizeObserverProvider]);

const layoutOptions = resolveLayoutOptions({
containerWidth: containerWidth || defaultContainerWidth,
Expand Down
6 changes: 6 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ export type PhotoAlbumProps<T extends Photo = Photo> = {
renderRowContainer?: RenderRowContainer;
/** custom column container rendering function */
renderColumnContainer?: RenderColumnContainer;
/** ResizeObserver factory to be used when global ResizeObserver is unavailable */
resizeObserverProvider?: ResizeObserverProvider;
/** internal instrumentation - use on your own risk */
instrumentation?: Instrumentation;
};
Expand Down Expand Up @@ -158,6 +160,10 @@ export type ColumnContainerProps = {

export type RenderColumnContainer = (props: PropsWithChildren<ColumnContainerProps>) => JSX.Element;

export type ResizeObserverProvider = (
callback: (entries: ResizeObserverEntry[], observer: ResizeObserver) => void
) => ResizeObserver;

/** internal instrumentation for research and performance testing purposes, subject to change without notice */
export type Instrumentation = {
fullGraphSearch?: boolean;
Expand Down
72 changes: 71 additions & 1 deletion test/PhotoAlbum.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as React from "react";
import renderer from "react-test-renderer";
import { render, screen } from "@testing-library/react";
import { act, render, screen } from "@testing-library/react";

import { PhotoAlbum } from "../src";
import Layout from "../src/Layout";
Expand Down Expand Up @@ -240,6 +240,76 @@ describe("PhotoAlbum", () => {
expect(onClick.mock.calls[0][1]).toBe(testPhotos[0]);
});

it("supports global ResizeObserver", () => {
const resizeObserverRef = global.ResizeObserver;
try {
const observeMock = jest.fn();
const unobserveMock = jest.fn();
const disconnectMock = jest.fn();

global.ResizeObserver = jest.fn().mockImplementation(() => ({
observe: observeMock,
unobserve: unobserveMock,
disconnect: disconnectMock,
}));

render(<PhotoAlbum layout={"rows"} photos={photos} />).unmount();

expect(observeMock).toHaveBeenCalledTimes(1);
expect(unobserveMock).toHaveBeenCalledTimes(0);
expect(disconnectMock).toHaveBeenCalledTimes(1);
} finally {
global.ResizeObserver = resizeObserverRef;
}
});

it("supports custom ResizeObserver and observes container ref changes", () => {
const resizeObserverMock = {
observe: jest.fn(),
unobserve: jest.fn(),
disconnect: jest.fn(),
};

const resizeObserverProvider = jest.fn((_) => resizeObserverMock);

const { rerender, unmount } = render(
<PhotoAlbum
layout={"rows"}
photos={photos}
renderContainer={React.forwardRef((props, ref) => (
<div ref={ref} key={1}>
{props.children}
</div>
))}
resizeObserverProvider={resizeObserverProvider}
/>
);

expect(resizeObserverMock.observe.mock.calls.length).toBe(1);

act(() => {
resizeObserverProvider.mock.calls[0][0]();
});

rerender(
<PhotoAlbum
layout={"rows"}
photos={photos}
renderContainer={React.forwardRef((props, ref) => (
<div ref={ref} key={2}>
{props.children}
</div>
))}
resizeObserverProvider={resizeObserverProvider}
/>
);

unmount();

expect(resizeObserverMock.observe.mock.calls.length).toBe(2);
expect(resizeObserverMock.disconnect.mock.calls.length).toBe(1);
});

it("supports instrumentation", () => {
const instrumentation = {
onStartLayoutComputation: jest.fn(),
Expand Down

0 comments on commit c9e0069

Please sign in to comment.