-
-
Notifications
You must be signed in to change notification settings - Fork 3.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
- Loading branch information
Showing
8 changed files
with
334 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 />); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
Oops, something went wrong.