Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: streamich/react-use
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v12.3.2
Choose a base ref
...
head repository: streamich/react-use
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: v12.4.0
Choose a head ref
  • 4 commits
  • 11 files changed
  • 3 contributors

Commits on Oct 12, 2019

  1. Copy the full SHA
    8fcf8d4 View commit details
  2. tests: useLogger (#670)

    Kevin Norris authored and wardoost committed Oct 12, 2019
    Copy the full SHA
    87d4613 View commit details
  3. feat: useIntersection (#652)

    React sensor hook that tracks the changes in the intersection of a target element with an ancestor element or with a top-level document's viewport, using the Intersection Observer API
    Kevin Norris authored and wardoost committed Oct 12, 2019
    Copy the full SHA
    d5f359f View commit details
  4. chore(release): 12.4.0 [skip ci]

    # [12.4.0](v12.3.2...v12.4.0) (2019-10-12)
    
    ### Features
    
    * useIntersection ([#652](#652)) ([d5f359f](d5f359f))
    semantic-release-bot committed Oct 12, 2019
    Copy the full SHA
    38dffea View commit details
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
# [12.4.0](https://github.com/streamich/react-use/compare/v12.3.2...v12.4.0) (2019-10-12)


### Features

* useIntersection ([#652](https://github.com/streamich/react-use/issues/652)) ([d5f359f](https://github.com/streamich/react-use/commit/d5f359f))

## [12.3.2](https://github.com/streamich/react-use/compare/v12.3.1...v12.3.2) (2019-10-12)


5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -49,6 +49,7 @@
- [`useGeolocation`](./docs/useGeolocation.md) — tracks geo location state of user's device. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/sensors-usegeolocation--demo)
- [`useHover` and `useHoverDirty`](./docs/useHover.md) — tracks mouse hover state of some element. [![][img-demo]](https://codesandbox.io/s/zpn583rvx)
- [`useIdle`](./docs/useIdle.md) — tracks whether user is being inactive.
- [`useIntersection`](./docs/useIntersection.md) — tracks an HTML element's intersection. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/sensors-useintersection--demo)
- [`useKey`](./docs/useKey.md), [`useKeyPress`](./docs/useKeyPress.md), [`useKeyboardJs`](./docs/useKeyboardJs.md), and [`useKeyPressEvent`](./docs/useKeyPressEvent.md) — track keys. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/sensors-usekeypressevent--demo)
- [`useLocation`](./docs/useLocation.md) and [`useSearchParam`](./docs/useSearchParam.md) — tracks page navigation bar location state.
- [`useMedia`](./docs/useMedia.md) — tracks state of a CSS media query. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/sensors-usemedia--demo)
@@ -104,7 +105,7 @@
- [`useTitle`](./docs/useTitle.md) — sets title of the page.
- [`usePermission`](./docs/usePermission.md) — query permission status for browser APIs.
<br/>
<br/>
<br/>
- [**Lifecycles**](./docs/Lifecycles.md)
- [`useEffectOnce`](./docs/useEffectOnce.md) &mdash; a modified [`useEffect`](https://reactjs.org/docs/hooks-reference.html#useeffect) hook that only runs once.
- [`useEvent`](./docs/useEvent.md) &mdash; subscribe to events.
@@ -135,7 +136,6 @@
- [`useMap`](./docs/useMap.md) &mdash; tracks state of an object. [![][img-demo]](https://codesandbox.io/s/quirky-dewdney-gi161)
- [`useStateValidator`](./docs/useStateValidator.md) &mdash; tracks state of an object. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/state-usestatevalidator--demo)


<br />
<br />
<br />
@@ -160,7 +160,6 @@

[img-demo]: https://img.shields.io/badge/demo-%20%20%20%F0%9F%9A%80-green.svg


<div align="center">
<h1>Contributors</h1>
</div>
36 changes: 36 additions & 0 deletions docs/useIntersection.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# `useIntersection`

React sensor hook that tracks the changes in the intersection of a target element with an ancestor element or with a top-level document's viewport. Uses the [Intersection Observer API](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API) and returns a [IntersectionObserverEntry](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserverEntry).

## Usage

```jsx
import * as React from 'react';
import { useIntersection } from 'react-use';

const Demo = () => {
const intersectionRef = React.useRef(null);
const intersection = useIntersection(intersectionRef, {
root: null,
rootMargin: '0px',
threshold: 1
});

return (
<div ref={intersectionRef}>
{intersection && intersection.intersectionRatio < 1
? 'Obscured'
: 'Fully in view'}
</div>
);
};
```

## Reference

```ts
useIntersection(
ref: RefObject<HTMLElement>,
options: IntersectionObserverInit,
): IntersectionObserverEntry | null;
```
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "react-use",
"version": "12.3.2",
"version": "12.4.0",
"description": "Collection of React Hooks",
"main": "lib/index.js",
"module": "esm/index.js",
@@ -69,6 +69,7 @@
"@semantic-release/changelog": "3.0.4",
"@semantic-release/git": "7.0.16",
"@semantic-release/npm": "5.1.13",
"@shopify/jest-dom-mocks": "^2.8.2",
"@storybook/addon-actions": "5.1.11",
"@storybook/addon-knobs": "5.1.11",
"@storybook/addon-notes": "5.1.11",
53 changes: 53 additions & 0 deletions src/__stories__/useIntersection.story.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { storiesOf } from '@storybook/react';
import * as React from 'react';
import { useIntersection } from '..';
import ShowDocs from './util/ShowDocs';

const Spacer = () => (
<div
style={{
width: '200px',
height: '300px',
backgroundColor: 'whitesmoke',
}}
/>
);

const Demo = () => {
const intersectionRef = React.useRef(null);
const intersection = useIntersection(intersectionRef, {
root: null,
rootMargin: '0px',
threshold: 1,
});

return (
<div
style={{
width: '400px',
height: '400px',
backgroundColor: 'whitesmoke',
overflow: 'scroll',
}}
>
Scroll me
<Spacer />
<div
ref={intersectionRef}
style={{
width: '100px',
height: '100px',
padding: '20px',
backgroundColor: 'palegreen',
}}
>
{intersection && intersection.intersectionRatio < 1 ? 'Obscured' : 'Fully in view'}
</div>
<Spacer />
</div>
);
};

storiesOf('Sensors/useIntersection', module)
.add('Docs', () => <ShowDocs md={require('../../docs/useIntersection.md')} />)
.add('Demo', () => <Demo />);
File renamed without changes.
119 changes: 119 additions & 0 deletions src/__tests__/useIntersection.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import React, { createRef } from 'react';
import ReactDOM from 'react-dom';
import TestUtils from 'react-dom/test-utils';
import TestRenderer from 'react-test-renderer';
import { intersectionObserver } from '@shopify/jest-dom-mocks';
import { renderHook } from '@testing-library/react-hooks';
import { useIntersection } from '..';

beforeEach(() => {
intersectionObserver.mock();
});

afterEach(() => {
intersectionObserver.restore();
});

describe('useIntersection', () => {
const container = document.createElement('div');
let targetRef;

it('should be defined', () => {
expect(useIntersection).toBeDefined();
});

it('should setup an IntersectionObserver targeting the ref element and using the options provided', () => {
TestUtils.act(() => {
targetRef = createRef();
ReactDOM.render(<div ref={targetRef} />, container);
});

expect(intersectionObserver.observers).toHaveLength(0);
const observerOptions = { root: null, threshold: 0.8 };

renderHook(() => useIntersection(targetRef, observerOptions));

expect(intersectionObserver.observers).toHaveLength(1);
expect(intersectionObserver.observers[0].target).toEqual(targetRef.current);
expect(intersectionObserver.observers[0].options).toEqual(observerOptions);
});

it('should return null if a ref without a current value is provided', () => {
targetRef = createRef();

const { result } = renderHook(() => useIntersection(targetRef, { root: null, threshold: 1 }));
expect(result.current).toBe(null);
});

it('should return the first IntersectionObserverEntry when the IntersectionObserver registers an intersection', () => {
TestUtils.act(() => {
targetRef = createRef();
ReactDOM.render(<div ref={targetRef} />, container);
});

const { result } = renderHook(() => useIntersection(targetRef, { root: container, threshold: 0.8 }));

const mockIntersectionObserverEntry = {
boundingClientRect: targetRef.current.getBoundingClientRect(),
intersectionRatio: 0.81,
intersectionRect: container.getBoundingClientRect(),
isIntersecting: true,
rootBounds: container.getBoundingClientRect(),
target: targetRef.current,
time: 300,
};
TestRenderer.act(() => {
intersectionObserver.simulate(mockIntersectionObserverEntry);
});

expect(result.current).toEqual(mockIntersectionObserverEntry);
});

it('should setup a new IntersectionObserver when the ref changes', () => {
let newRef;
TestUtils.act(() => {
targetRef = createRef();
newRef = createRef();
ReactDOM.render(
<div ref={targetRef}>
<span ref={newRef} />
</div>,
container
);
});

const observerOptions = { root: null, threshold: 0.8 };
const { rerender } = renderHook(({ ref, options }) => useIntersection(ref, options), {
initialProps: { ref: targetRef, options: observerOptions },
});

expect(intersectionObserver.observers[0].target).toEqual(targetRef.current);

TestRenderer.act(() => {
rerender({ ref: newRef, options: observerOptions });
});

expect(intersectionObserver.observers[0].target).toEqual(newRef.current);
});

it('should setup a new IntersectionObserver when the options change', () => {
TestUtils.act(() => {
targetRef = createRef();
ReactDOM.render(<div ref={targetRef} />, container);
});

const initialObserverOptions = { root: null, threshold: 0.8 };
const { rerender } = renderHook(({ ref, options }) => useIntersection(ref, options), {
initialProps: { ref: targetRef, options: initialObserverOptions },
});

expect(intersectionObserver.observers[0].options).toEqual(initialObserverOptions);

const newObserverOptions = { root: container, threshold: 1 };
TestRenderer.act(() => {
rerender({ ref: targetRef, options: newObserverOptions });
});

expect(intersectionObserver.observers[0].options).toEqual(newObserverOptions);
});
});
41 changes: 41 additions & 0 deletions src/__tests__/useLogger.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { renderHook } from '@testing-library/react-hooks';
import useLogger from '../useLogger';

const logSpy = jest.spyOn(global.console, 'log').mockImplementation(() => {});

describe('useLogger', () => {
it('should be defined', () => {
expect(useLogger).toBeDefined();
});

it('should log the provided props on mount', () => {
const props = { question: 'What is the meaning?', answer: 42 };
renderHook(() => useLogger('Test', props));

expect(logSpy).toBeCalledTimes(1);
expect(logSpy).toHaveBeenLastCalledWith('Test mounted', props);
});

it('should log when the component has unmounted', () => {
const props = { question: 'What is the meaning?', answer: 42 };
const { unmount } = renderHook(() => useLogger('Test', props));

unmount();

expect(logSpy).toHaveBeenLastCalledWith('Test unmounted');
});

it('should log updates as props change', () => {
const { rerender } = renderHook(
({ componentName, props }: { componentName: string; props: any }) => useLogger(componentName, props),
{
initialProps: { componentName: 'Test', props: { one: 1 } },
}
);

const newProps = { one: 1, two: 2 };
rerender({ componentName: 'Test', props: newProps });

expect(logSpy).toHaveBeenLastCalledWith('Test updated', newProps);
});
});
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -27,6 +27,7 @@ export { default as useHarmonicIntervalFn } from './useHarmonicIntervalFn';
export { default as useHover } from './useHover';
export { default as useHoverDirty } from './useHoverDirty';
export { default as useIdle } from './useIdle';
export { default as useIntersection } from './useIntersection';
export { default as useInterval } from './useInterval';
export { default as useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect';
export { default as useKey } from './useKey';
30 changes: 30 additions & 0 deletions src/useIntersection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { RefObject, useEffect, useState } from 'react';

const useIntersection = (
ref: RefObject<HTMLElement>,
options: IntersectionObserverInit
): IntersectionObserverEntry | null => {
const [intersectionObserverEntry, setIntersectionObserverEntry] = useState<IntersectionObserverEntry | null>(null);

useEffect(() => {
if (ref.current) {
const handler = (entries: IntersectionObserverEntry[]) => {
setIntersectionObserverEntry(entries[0]);
};

const observer = new IntersectionObserver(handler, options);
observer.observe(ref.current);

return () => {
if (ref.current) {
observer.disconnect();
}
};
}
return () => {};
}, [ref, options.threshold, options.root, options.rootMargin]);

return intersectionObserverEntry;
};

export default useIntersection;
Loading