diff --git a/.babelrc b/.babelrc deleted file mode 100644 index 4f06b0c..0000000 --- a/.babelrc +++ /dev/null @@ -1,6 +0,0 @@ -{ - "presets": [ - "@babel/preset-env", - "@babel/preset-react" - ] -} diff --git a/LICENSE b/LICENSE index 15e8715..7fb1757 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2019 Envato +Copyright (c) 2019-2021 Envato Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 584cb3f..9bf38f6 100644 --- a/README.md +++ b/README.md @@ -13,12 +13,12 @@ --- -> Observe multiple React components with a single ResizeObserver. +> Observe multiple DOM elements with a single ResizeObserver. This package provides you with: -* a context `` with a `ResizeObserver` instance; -* a `useResizeObserver()` hook to observe any element's changes in size. +- a context `` with a `ResizeObserver` instance; +- a `useResizeObserver()` hook to observe any element's size changes. This allows you to know the size of each observed element. @@ -41,14 +41,10 @@ npm install @envato/react-resize-observer-hook ```javascript import { Provider as ResizeObserverProvider } from '@envato/react-resize-observer-hook'; -const App = () => ( - - ... - -); +const App = () => ...; ``` -⚠️ **Caution** — You may need to pass some props to `` to increase browser support. Please refer to the [React Breakpoints API Docs](https://github.com/envato/react-breakpoints/docs/api.md#provider). +⚠️ **Caution** — You may need to pass some props to `` to increase **browser support**. Please refer to the [React Breakpoints API Docs](https://github.com/envato/react-breakpoints/docs/api.md#provider). ## Observe an element @@ -59,7 +55,11 @@ const ObservedDiv = () => { const [ref, observedEntry] = useResizeObserver(); const { width, height } = observedEntry.contentRect; - return
This element is {width}px wide and {height}px high.
+ return ( +
+ This element is {width}px wide and {height}px high. +
+ ); }; ``` @@ -71,13 +71,16 @@ const options = { }; const [ref, observedEntry] = useResizeObserver(options); + +const width = observedEntry.borderBox[0].inlineSize; +const height = observedEntry.borderBox[0].blockSize; ``` See [MDN reference guide](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver) for further information. # Maintainers -* [Marc Dingena](https://github.com/mdingena) (owner) +- [Marc Dingena](https://github.com/mdingena) (owner) # Contributing diff --git a/package.json b/package.json index 14414ce..1381e85 100644 --- a/package.json +++ b/package.json @@ -3,8 +3,9 @@ "version": "1.0.1", "description": "Observe multiple React components with a single ResizeObserver.", "main": "dist/index.js", + "types": "dist/index.d.ts", "scripts": { - "build": "babel src/ -d dist/ --source-maps", + "build": "tsc -b", "prepare": "npm run build" }, "repository": { @@ -12,32 +13,40 @@ "url": "git+https://github.com/envato/react-resize-observer-hook.git" }, "keywords": [ + "resize-observer", "react-hooks", "hooks", "react", - "performance", - "resize-observer" + "performance" ], - "author": "Envato", + "author": "marc.dingena@envato.com", "license": "MIT", "bugs": { "url": "https://github.com/envato/react-resize-observer-hook/issues" }, "homepage": "https://github.com/envato/react-resize-observer-hook#readme", "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" + "react": "16.8 - 17", + "react-dom": "16.8 - 17" }, "devDependencies": { - "@babel/cli": "^7.8.4", - "@babel/core": "^7.8.6", - "@babel/preset-env": "^7.7.7", - "@babel/preset-react": "^7.8.3", + "@types/react": "^17.0.0", + "@typescript-eslint/eslint-plugin": "^4.13.0", + "@typescript-eslint/parser": "^4.13.0", + "babel-eslint": "^10.1.0", + "eslint": "^7.17.0", + "eslint-config-react-app": "^6.0.0", + "eslint-plugin-flowtype": "^5.2.0", + "eslint-plugin-import": "^2.22.1", + "eslint-plugin-jsx-a11y": "^6.4.1", + "eslint-plugin-react": "^7.22.0", + "eslint-plugin-react-hooks": "^4.2.0", "husky": "^4.3.7", "lint-staged": "^10.5.3", "prettier": "^2.2.1", "react": "^17.0.1", - "react-dom": "^17.0.1" + "react-dom": "^17.0.1", + "typescript": "^4.2.2" }, "eslintConfig": { "extends": "react-app" diff --git a/src/Context.ts b/src/Context.ts new file mode 100644 index 0000000..0384071 --- /dev/null +++ b/src/Context.ts @@ -0,0 +1,3 @@ +import { createContext } from 'react'; + +export const Context = createContext(null); diff --git a/src/ExtendedResizeObserverEntry.ts b/src/ExtendedResizeObserverEntry.ts new file mode 100644 index 0000000..48a5949 --- /dev/null +++ b/src/ExtendedResizeObserverEntry.ts @@ -0,0 +1,5 @@ +import { ObservedElement } from './ObservedElement'; + +export interface ExtendedResizeObserverEntry extends ResizeObserverEntry { + target: ObservedElement; +} diff --git a/src/ObservedElement.ts b/src/ObservedElement.ts new file mode 100644 index 0000000..a2011ef --- /dev/null +++ b/src/ObservedElement.ts @@ -0,0 +1,5 @@ +import { ExtendedResizeObserverEntry } from './ExtendedResizeObserverEntry'; + +export interface ObservedElement extends Element { + onResizeObservation?: (resizeObserverEntry: ExtendedResizeObserverEntry) => void; +} diff --git a/src/Provider.tsx b/src/Provider.tsx new file mode 100644 index 0000000..04418c4 --- /dev/null +++ b/src/Provider.tsx @@ -0,0 +1,25 @@ +import { createResizeObserver } from './createResizeObserver'; +import { Context } from './Context'; + +interface ProviderProps { + ponyfill?: typeof ResizeObserver; + children: React.ReactNode; +} + +/** + * See API Docs: {@linkcode https://github.com/envato/react-breakpoints/blob/master/docs/api.md#provider|Provider} + * + * Returns a React context provider with a ResizeObserver instance as its value. + * Uses `window.ResizeObserver` to construct the instance if no `ponyfill` prop is provided. + * @example + * return ( + * + * ... + * + * ); + */ +export const Provider = ({ ponyfill = undefined, children }: ProviderProps) => { + const instance = createResizeObserver(ponyfill || window.ResizeObserver); + + return {children}; +}; diff --git a/src/createResizeObserver.ts b/src/createResizeObserver.ts new file mode 100644 index 0000000..ef71a1e --- /dev/null +++ b/src/createResizeObserver.ts @@ -0,0 +1,22 @@ +import { ExtendedResizeObserverEntry } from './ExtendedResizeObserverEntry'; + +/** + * Creates a new ResizeObserver instance that works with the + * {@linkcode https://github.com/envato/react-breakpoints/blob/master/docs/api.md#useresizeobserver|useResizeObserver} + * hook. + * @example + * const resizeObserverInstance = createResizeObserver(window.ResizeObserver); + * return ( + * + * ... + * + * ); + */ +export const createResizeObserver = (Constructor: typeof ResizeObserver) => { + const handleResizeObserverEntry = (resizeObserverEntry: ExtendedResizeObserverEntry) => { + const { onResizeObservation } = resizeObserverEntry.target; + onResizeObservation && onResizeObservation(resizeObserverEntry); + }; + + return new Constructor((entries: ExtendedResizeObserverEntry[]) => entries.forEach(handleResizeObserverEntry)); +}; diff --git a/src/index.js b/src/index.js deleted file mode 100644 index aeb9f2a..0000000 --- a/src/index.js +++ /dev/null @@ -1,2 +0,0 @@ -export { ResizeObserverContext, createResizeObserver, Provider } from './resizeObserverContext'; -export { useResizeObserver } from './useResizeObserver'; diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..768ae22 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,4 @@ +export { Context } from './Context'; +export { Provider } from './Provider'; +export { createResizeObserver } from './createResizeObserver'; +export { useResizeObserver } from './useResizeObserver'; diff --git a/src/resizeObserverContext.js b/src/resizeObserverContext.js deleted file mode 100644 index a7aa5cf..0000000 --- a/src/resizeObserverContext.js +++ /dev/null @@ -1,32 +0,0 @@ -import React, { createContext } from 'react'; - -const ResizeObserverContext = createContext(null); - -/** - * Bootstraps a new ResizeObserver with a callback function used by the `useResizeObserver` hook. - * @argument {ResizeObserver} [ResizeObserver] - Any ResizeObserver constructor, for example from window.ResizeObserver or a ponyfill. - * @returns {ResizeObserver} Bootstrapped ResizeObserver instance to assign to `ResizeObserverContext.Provider`'s value. - */ -const createResizeObserver = ResizeObserver => { - const handleEntry = entry => { - const { handleResizeObservation } = entry.target; - handleResizeObservation && handleResizeObservation(entry); - }; - - return new ResizeObserver(entries => entries.forEach(handleEntry)); -}; - -/** - * Bootstraps a ResizeObserverContext.Provider with a ResizeObserver instance. - * @argument {Object} props - * @argument {ResizeObserver} [props.ponyfill=undefined] - Optional `ResizeObserver` constructor, for example from a ponyfill. Defaults to `window.ResizeObserver`. CAUTION: https://caniuse.com/#feat=mdn-api_resizeobserver_resizeobserver - * @argument {JSX} props.children - This component's children. - * @returns {JSX} Context.Provider bootstrapped with a ResizeObserver instance. - */ -const Provider = ({ ponyfill = undefined, children }) => { - const instance = createResizeObserver(ponyfill || window.ResizeObserver); - - return {children}; -}; - -export { ResizeObserverContext, createResizeObserver, Provider }; diff --git a/src/useResizeObserver.js b/src/useResizeObserver.js deleted file mode 100644 index cf0f632..0000000 --- a/src/useResizeObserver.js +++ /dev/null @@ -1,39 +0,0 @@ -import { useContext, useCallback, useRef, useState } from 'react'; -import { ResizeObserverContext } from './resizeObserverContext'; - -/** - * Observe an element's size. - * @argument {Object} options - Options object for `ResizeObserver.observe()`. - * @argument {String} options.box - The element's box to observe. - * @returns {Array} Array with: a reference to observed element, a ResizeObserverEntry. - */ -const useResizeObserver = (options = {}) => { - const resizeObserver = useContext(ResizeObserverContext); - - const [observedEntry, setObservedEntry] = useState(null); - - const handleResizeObservation = resizeObserverEntry => setObservedEntry(resizeObserverEntry); - - const ref = useRef(null); - - const setRef = useCallback( - node => { - if (ref.current) { - resizeObserver.unobserve(ref.current); - delete ref.current.handleResizeObservation; - } - - if (node) { - node.handleResizeObservation = handleResizeObservation; - resizeObserver.observe(node, options); - } - - ref.current = node; - }, - [resizeObserver, options] - ); - - return [setRef, observedEntry]; -}; - -export { useResizeObserver }; diff --git a/src/useResizeObserver.ts b/src/useResizeObserver.ts new file mode 100644 index 0000000..327e988 --- /dev/null +++ b/src/useResizeObserver.ts @@ -0,0 +1,51 @@ +import { useContext, useCallback, useRef, useState } from 'react'; +import { Context } from './Context'; +import { ObservedElement } from './ObservedElement'; +import { ExtendedResizeObserverEntry } from './ExtendedResizeObserverEntry'; + +/** + * See API Docs: {@linkcode https://github.com/envato/react-breakpoints/blob/master/docs/api.md#useresizeobserver|useResizeObserver} + * + * Returns a React callback ref to attach to a DOM element. It also returns + * a resize observation entry every time the observed element changes size. + * This causes the component that uses `useResizeObserver` to rerender. + * @example + * const [ref, observedEntry] = useResizeObserver(options); + * const observedWidth = observedEntry.borderBoxSize[0].inlineSize; + * return ( + *
+ * This container is {observedWidth}px wide. + *
+ * ); + */ +export const useResizeObserver = ( + options: ResizeObserverOptions = {} +): [React.RefCallback, ExtendedResizeObserverEntry | null] => { + const resizeObserver = useContext(Context); + + const [observedEntry, setObservedEntry] = useState(null); + + const handleResizeObservation = (resizeObserverEntry: ExtendedResizeObserverEntry) => + setObservedEntry(resizeObserverEntry); + + const ref = useRef(null); + + const setRef = useCallback( + node => { + if (ref.current) { + resizeObserver?.unobserve(ref.current); + delete ref.current.onResizeObservation; + } + + if (node) { + node.onResizeObservation = handleResizeObservation; + resizeObserver?.observe(node, options); + } + + ref.current = node; + }, + [resizeObserver, options] + ); + + return [setRef, observedEntry]; +}; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..52f5d32 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "es5", + "module": "commonjs", + "jsx": "react-jsx", + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "outDir": "./dist", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["./src/**/*"] +}