Unverified Commit 6e281b25 authored by Andrey Azov's avatar Andrey Azov Committed by GitHub
Browse files

Create our own useResizeObserver hook (#213)

And remove dependency on the `use-resize-observer` library
parent 3dcc6359
Pipeline #47180 passed with stages
in 3 minutes and 53 seconds
......@@ -18525,7 +18525,8 @@
"resize-observer-polyfill": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
"integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg=="
"integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==",
"dev": true
},
"resolve": {
"version": "1.12.0",
......@@ -21994,14 +21995,6 @@
"integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==",
"dev": true
},
"use-resize-observer": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/use-resize-observer/-/use-resize-observer-3.1.0.tgz",
"integrity": "sha512-g9eIqzZUtxW6/S0+fe79ZuDeYxk7c9R3iArv3YOkkgY7IfJHJxIXglVhmxKRovprcEqfZ+6yq30MbQ5WjdhXQw==",
"requires": {
"resize-observer-polyfill": "^1.5.0"
}
},
"util": {
"version": "0.11.1",
"resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz",
......
......@@ -73,7 +73,6 @@
"redux-thunk": "2.3.0",
"rxjs": "6.5.3",
"typesafe-actions": "5.1.0",
"use-resize-observer": "3.1.0",
"what-input": "5.2.6"
},
"devDependencies": {
......
import React from 'react';
import React, { useRef } from 'react';
import { connect } from 'react-redux';
import useResizeObserver from 'use-resize-observer';
import useResizeObserver from 'src/shared/hooks/useResizeObserver';
import * as constants from './chromosomeNavigatorConstants';
......@@ -39,7 +40,8 @@ export type ChromosomeNavigatorProps = WrapperProps & {
};
export const ChromosomeNavigatorWrapper = (props: WrapperProps) => {
const [containerRef, containerWidth] = useResizeObserver();
const containerRef = useRef<HTMLDivElement>(null);
const { width: containerWidth } = useResizeObserver({ ref: containerRef });
return (
<div
......
import { createAction } from 'typesafe-actions';
import { ActionCreator, Action } from 'redux';
import { ThunkAction } from 'redux-thunk';
import { BreakpointWidth } from './globalConfig';
import { getBreakpointWidth } from './globalSelectors';
import { getBreakpoint } from './globalHelper';
export const updateBreakpointWidth = createAction(
import { RootState } from 'src/store';
export const setBreakpointWidth = createAction(
'browser/update-breakpoint-width'
)<BreakpointWidth>();
export const updateBreakpointWidth: ActionCreator<ThunkAction<
void,
any,
null,
Action<string>
>> = (width: number) => async (dispatch, getState: () => RootState) => {
const state = getState();
const currentBreakpointWidth = getBreakpointWidth(state);
const newBreakpointWidth = getBreakpoint(width);
if (newBreakpointWidth !== currentBreakpointWidth) {
dispatch(setBreakpointWidth(newBreakpointWidth));
}
};
......@@ -8,7 +8,7 @@ export default function globalReducer(
action: ActionType<typeof global>
) {
switch (action.type) {
case getType(global.updateBreakpointWidth):
case getType(global.setBreakpointWidth):
return { ...state, breakpointWidth: action.payload };
default:
return state;
......
......@@ -5,6 +5,9 @@ import { Root } from './Root';
import Header from '../header/Header';
import Content from '../content/Content';
import privacyBannerService from '../shared/components/privacy-banner/privacy-banner-service';
import windowService from 'src/services/window-service';
import MockResizeObserver from 'tests/mocks/mockResizeObserver';
jest.mock('../header/Header', () => () => 'Header');
jest.mock('../content/Content', () => () => 'Content');
......@@ -24,6 +27,9 @@ describe('<Root />', () => {
const getRenderedRoot = (props: any) => <Root {...props} />;
beforeEach(() => {
jest
.spyOn(windowService, 'getResizeObserver')
.mockImplementation((): any => MockResizeObserver);
wrapper = mount(getRenderedRoot(defaultProps));
});
......
import React, { FunctionComponent, useEffect, useState } from 'react';
import React, { useEffect, useState, useRef } from 'react';
import { connect } from 'react-redux';
import useResizeObserver from 'use-resize-observer';
import useResizeObserver from 'src/shared/hooks/useResizeObserver';
import { BreakpointWidth } from '../global/globalConfig';
import { updateBreakpointWidth } from '../global/globalActions';
import Header from '../header/Header';
import Content from '../content/Content';
......@@ -9,34 +13,20 @@ import privacyBannerService from '../shared/components/privacy-banner/privacy-ba
import ErrorBoundary from 'src/shared/components/error-boundary/ErrorBoundary';
import { GeneralErrorScreen } from 'src/shared/components/error-screen';
import { updateBreakpointWidth } from '../global/globalActions';
import { getBreakpointWidth } from '../global/globalSelectors';
import { RootState } from '../store';
import { BreakpointWidth } from '../global/globalConfig';
import { getBreakpoint } from '../global/globalHelper';
import styles from './Root.scss';
type StateProps = {
breakpointWidth: BreakpointWidth;
};
type DispatchProps = {
type Props = {
updateBreakpointWidth: (breakpointWidth: BreakpointWidth) => void;
};
type OwnProps = {};
type RootProps = StateProps & DispatchProps & OwnProps;
export const Root: FunctionComponent<RootProps> = (props: RootProps) => {
const [ref, width] = useResizeObserver();
const currentBreakpoint: BreakpointWidth = getBreakpoint(width);
export const Root = (props: Props) => {
const elementRef = useRef<HTMLDivElement>(null);
const { width } = useResizeObserver<HTMLDivElement>({ ref: elementRef });
const [showPrivacyBanner, setShowPrivacyBanner] = useState(false);
useEffect(() => {
props.updateBreakpointWidth(currentBreakpoint);
}, [props.updateBreakpointWidth, currentBreakpoint]);
props.updateBreakpointWidth(width);
}, [width]);
useEffect(() => {
setShowPrivacyBanner(privacyBannerService.shouldShowBanner());
......@@ -48,7 +38,7 @@ export const Root: FunctionComponent<RootProps> = (props: RootProps) => {
};
return (
<div ref={ref as React.RefObject<HTMLDivElement>} className={styles.root}>
<div ref={elementRef} className={styles.root}>
<ErrorBoundary fallbackComponent={GeneralErrorScreen}>
<Header />
<Content />
......@@ -58,13 +48,6 @@ export const Root: FunctionComponent<RootProps> = (props: RootProps) => {
);
};
const mapStateToProps = (state: RootState): StateProps => ({
breakpointWidth: getBreakpointWidth(state)
});
const mapDispatchToProps: DispatchProps = { updateBreakpointWidth };
const mapDispatchToProps = { updateBreakpointWidth };
export default connect(
mapStateToProps,
mapDispatchToProps
)(Root);
export default connect(null, mapDispatchToProps)(Root);
......@@ -19,6 +19,7 @@ const mockSessionStorage: any = {
const mockWindowService: WindowServiceInterface = {
getLocalStorage: () => mockLocalStorage,
getSessionStorage: () => mockSessionStorage,
getResizeObserver: jest.fn(),
getWindow: jest.fn(),
getFileReader: jest.fn(),
getLocation: jest.fn()
......
......@@ -9,6 +9,7 @@ export interface WindowServiceInterface {
getSessionStorage: () => Storage;
getLocation: () => Location;
getFileReader: () => FileReader;
getResizeObserver: () => typeof ResizeObserver;
}
class WindowService implements WindowServiceInterface {
......@@ -32,6 +33,10 @@ class WindowService implements WindowServiceInterface {
return new FileReader();
}
public getResizeObserver() {
return ResizeObserver;
}
// return viewport dimensions in the ClientRect format
public getDimensions() {
const width = window.innerWidth;
......
import React, { ReactElement, useState, useEffect, useRef } from 'react';
import useResizeObserver from 'use-resize-observer';
import useResizeObserver from 'src/shared/hooks/useResizeObserver';
import { getSpeciesItemWidths } from './speciesTabsWrapperHelpers';
import styles from './SingleLineSpeciesWrapper.scss';
......@@ -64,8 +64,9 @@ const getItemsContainerWidth = (
const SingleLineWrapper = (props: Props) => {
const { speciesTabs } = props;
const linkRef = useRef<HTMLDivElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const [hoveredItemIndex, setHoveredItemIndex] = useState<number | null>(null);
const [containerRef, containerWidth] = useResizeObserver();
const { width: containerWidth } = useResizeObserver({ ref: containerRef });
const linkWidth = getLinkWidth(linkRef);
const itemsContainerWidth = getItemsContainerWidth(containerWidth, linkWidth);
const speciesTabsProps = React.Children.map(speciesTabs, (tab) => tab.props);
......
// modified from https://github.com/ZeeCoder/use-resize-observer/blob/master/src/index.js
import { useEffect, useState, useRef, useMemo, RefObject } from 'react';
import windowService from 'src/services/window-service';
type Params<T> = {
ref?: RefObject<T> | null;
};
export default function<T extends HTMLElement>(params: Params<T> = {}) {
const defaultRef = useRef(null);
const ref = params.ref || defaultRef;
const [size, setSize] = useState({ width: 0, height: 0 });
const sizeRef = useRef({
width: 0,
height: 0
});
useEffect(() => {
if (ref && !ref.current) {
return;
}
const element = ref.current;
if (!element) {
return;
}
const ResizeObserver = windowService.getResizeObserver();
const resizeObserver = new ResizeObserver((entries) => {
if (!Array.isArray(entries)) {
return;
}
// We are only observing one element
const entry = entries[0];
if (!entry) {
return;
}
// `Math.round` is in line with how CSS resolves sub-pixel values
const newWidth = Math.round(entry.contentRect.width);
const newHeight = Math.round(entry.contentRect.height);
if (
sizeRef.current.width !== newWidth ||
sizeRef.current.height !== newHeight
) {
sizeRef.current.width = newWidth;
sizeRef.current.height = newHeight;
setSize({ width: newWidth, height: newHeight });
}
});
resizeObserver.observe(element);
return () => resizeObserver.unobserve(element);
}, [ref]);
return { ref, ...size };
}
......@@ -21,7 +21,7 @@
<body>
<div id="ens-app"></div>
<script src="https://polyfill.io/v3/polyfill.min.js?features=Object.assign%2CPromise%2Cfetch%2CIntersectionObserver%2CIntersectionObserverEntry"></script>
<script src="https://polyfill.io/v3/polyfill.min.js?features=Object.assign%2CPromise%2Cfetch%2CIntersectionObserver%2CIntersectionObserverEntry%2CResizeObserver"></script>
</body>
</html>
class MockResizeObserver {
public observe() {
return jest.fn();
}
public unobserve() {
return jest.fn();
}
}
export default MockResizeObserver;
/**
* This is a temporary definitions file, which will become unnecessary when Typescript adds type defitions
* for ResizeObserver (when this feature gets implemented in all major browsers)
* See this issue for discussion: https://github.com/Microsoft/TypeScript/issues/28502
*/
//=====BELOW: copy-pasted from https://gist.github.com/strothj/708afcf4f01dd04de8f49c92e88093c3 ========
/**
* The **ResizeObserver** interface reports changes to the dimensions of an
* [Element](https://developer.mozilla.org/en-US/docs/Web/API/Element)'s content
* or border box, or the bounding box of an
* [SVGElement](https://developer.mozilla.org/en-US/docs/Web/API/SVGElement).
*
* > **Note**: The content box is the box in which content can be placed,
* > meaning the border box minus the padding and border width. The border box
* > encompasses the content, padding, and border. See
* > [The box model](https://developer.mozilla.org/en-US/docs/Learn/CSS/Building_blocks/The_box_model)
* > for further explanation.
*
* `ResizeObserver` avoids infinite callback loops and cyclic dependencies that
* are often created when resizing via a callback function. It does this by only
* processing elements deeper in the DOM in subsequent frames. Implementations
* should, if they follow the specification, invoke resize events before paint
* and after layout.
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver
*/
class ResizeObserver {
/**
* The **ResizeObserver** constructor creates a new `ResizeObserver` object,
* which can be used to report changes to the content or border box of an
* `Element` or the bounding box of an `SVGElement`.
*
* @example
* var ResizeObserver = new ResizeObserver(callback)
*
* @param callback
* The function called whenever an observed resize occurs. The function is
* called with two parameters:
* * **entries**
* An array of
* [ResizeObserverEntry](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserverEntry)
* objects that can be used to access the new dimensions of the element
* after each change.
* * **observer**
* A reference to the `ResizeObserver` itself, so it will definitely be
* accessible from inside the callback, should you need it. This could be
* used for example to automatically unobserve the observer when a certain
* condition is reached, but you can omit it if you don't need it.
*
* The callback will generally follow a pattern along the lines of:
* ```js
* function(entries, observer) {
* for (let entry of entries) {
* // Do something to each entry
* // and possibly something to the observer itself
* }
* }
* ```
*
* The following snippet is taken from the
* [resize-observer-text.html](https://mdn.github.io/dom-examples/resize-observer/resize-observer-text.html)
* ([see source](https://github.com/mdn/dom-examples/blob/master/resize-observer/resize-observer-text.html))
* example:
* @example
* const resizeObserver = new ResizeObserver(entries => {
* for (let entry of entries) {
* if(entry.contentBoxSize) {
* h1Elem.style.fontSize = Math.max(1.5, entry.contentBoxSize.inlineSize/200) + 'rem';
* pElem.style.fontSize = Math.max(1, entry.contentBoxSize.inlineSize/600) + 'rem';
* } else {
* h1Elem.style.fontSize = Math.max(1.5, entry.contentRect.width/200) + 'rem';
* pElem.style.fontSize = Math.max(1, entry.contentRect.width/600) + 'rem';
* }
* }
* });
*
* resizeObserver.observe(divElem);
*/
constructor(callback: ResizeObserverCallback);
/**
* The **disconnect()** method of the
* [ResizeObserver](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver)
* interface unobserves all observed
* [Element](https://developer.mozilla.org/en-US/docs/Web/API/Element) or
* [SVGElement](https://developer.mozilla.org/en-US/docs/Web/API/SVGElement)
* targets.
*/
disconnect: () => void;
/**
* The `observe()` method of the
* [ResizeObserver](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver)
* interface starts observing the specified
* [Element](https://developer.mozilla.org/en-US/docs/Web/API/Element) or
* [SVGElement](https://developer.mozilla.org/en-US/docs/Web/API/SVGElement).
*
* @example
* resizeObserver.observe(target, options);
*
* @param target
* A reference to an
* [Element](https://developer.mozilla.org/en-US/docs/Web/API/Element) or
* [SVGElement](https://developer.mozilla.org/en-US/docs/Web/API/SVGElement)
* to be observed.
*
* @param options
* An options object allowing you to set options for the observation.
* Currently this only has one possible option that can be set.
*/
observe: (target: Element, options?: ResizeObserverObserveOptions) => void;
/**
* The **unobserve()** method of the
* [ResizeObserver](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver)
* interface ends the observing of a specified
* [Element](https://developer.mozilla.org/en-US/docs/Web/API/Element) or
* [SVGElement](https://developer.mozilla.org/en-US/docs/Web/API/SVGElement).
*/
unobserve: (target: Element) => void;
}
interface ResizeObserverObserveOptions {
/**
* Sets which box model the observer will observe changes to. Possible values
* are `content-box` (the default), and `border-box`.
*
* @default "content-box"
*/
box?: 'content-box' | 'border-box';
}
/**
* The function called whenever an observed resize occurs. The function is
* called with two parameters:
*
* @param entries
* An array of
* [ResizeObserverEntry](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserverEntry)
* objects that can be used to access the new dimensions of the element after
* each change.
*
* @param observer
* A reference to the `ResizeObserver` itself, so it will definitely be
* accessible from inside the callback, should you need it. This could be used
* for example to automatically unobserve the observer when a certain condition
* is reached, but you can omit it if you don't need it.
*
* The callback will generally follow a pattern along the lines of:
* @example
* function(entries, observer) {
* for (let entry of entries) {
* // Do something to each entry
* // and possibly something to the observer itself
* }
* }
*
* @example
* const resizeObserver = new ResizeObserver(entries => {
* for (let entry of entries) {
* if(entry.contentBoxSize) {
* h1Elem.style.fontSize = Math.max(1.5, entry.contentBoxSize.inlineSize/200) + 'rem';
* pElem.style.fontSize = Math.max(1, entry.contentBoxSize.inlineSize/600) + 'rem';
* } else {
* h1Elem.style.fontSize = Math.max(1.5, entry.contentRect.width/200) + 'rem';
* pElem.style.fontSize = Math.max(1, entry.contentRect.width/600) + 'rem';
* }
* }
* });
*
* resizeObserver.observe(divElem);
*/
type ResizeObserverCallback = (
entries: ResizeObserverEntry[],
observer: ResizeObserver
) => void;
/**
* The **ResizeObserverEntry** interface represents the object passed to the
* [ResizeObserver()](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver/ResizeObserver)
* constructor's callback function, which allows you to access the new
* dimensions of the
* [Element](https://developer.mozilla.org/en-US/docs/Web/API/Element) or
* [SVGElement](https://developer.mozilla.org/en-US/docs/Web/API/SVGElement)
* being observed.
*/
interface ResizeObserverEntry {
/**
* An object containing the new border box size of the observed element when
* the callback is run.
*/
readonly borderBoxSize: ResizeObserverEntryBoxSize;
/**
* An object containing the new content box size of the observed element when
* the callback is run.
*/
readonly contentBoxSize: ResizeObserverEntryBoxSize;
/**
* A [DOMRectReadOnly](https://developer.mozilla.org/en-US/docs/Web/API/DOMRectReadOnly)
* object containing the new size of the observed element when the callback is
* run. Note that this is better supported than the above two properties, but
* it is left over from an earlier implementation of the Resize Observer API,
* is still included in the spec for web compat reasons, and may be deprecated
* in future versions.
*/
// node_modules/typescript/lib/lib.dom.d.ts
readonly contentRect: DOMRectReadOnly;
/**
* A reference to the
* [Element](https://developer.mozilla.org/en-US/docs/Web/API/Element) or
* [SVGElement](https://developer.mozilla.org/en-US/docs/Web/API/SVGElement)
* being observed.
*/
readonly target: Element;
}
/**
* The **borderBoxSize** read-only property of the
* [ResizeObserverEntry](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserverEntry)
* interface returns an object containing the new border box size of the
* observed element when the callback is run.
*/
interface ResizeObserverEntryBoxSize {
/**
* The length of the observed element's border box in the block dimension. For
* boxes with a horizontal
* [writing-mode](https://developer.mozilla.org/en-US/docs/Web/CSS/writing-mode),
* this is the vertical dimension, or height; if the writing-mode is vertical,
* this is the horizontal dimension, or width.
*/
blockSize: number;
/**
* The length of the observed element's border box in the inline dimension.
* For boxes with a horizontal
* [writing-mode](https://developer.mozilla.org/en-US/docs/Web/CSS/writing-mode),
* this is the horizontal dimension, or width; if the writing-mode is
* vertical, this is the vertical dimension, or height.
*/
inlineSize: number;
}
interface Window {
ResizeObserver: ResizeObserver;
}
declare const ResizeObserver: ResizeObserver;
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment