-
Notifications
You must be signed in to change notification settings - Fork 94
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: new hook
useIntersectionObserver
- Loading branch information
Showing
7 changed files
with
424 additions
and
0 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
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,50 @@ | ||
import * as React from 'react'; | ||
import { useRef } from 'react'; | ||
import { useIntersectionObserver } from '../..'; | ||
|
||
export const Example: React.FC = () => { | ||
const rootRef = useRef<HTMLDivElement>(null); | ||
const elementRef = useRef<HTMLDivElement>(null); | ||
const intersection = useIntersectionObserver(elementRef, { root: rootRef, threshold: [0, 0.5] }); | ||
|
||
return ( | ||
<div style={{ display: 'flex', flexWrap: 'wrap' }}> | ||
<div style={{ width: '100%', marginBottom: '16px' }}> | ||
Below scrollable container holds the rectangle that turns green when ot visible by 50% or | ||
more. | ||
</div> | ||
|
||
<div | ||
ref={rootRef} | ||
style={{ | ||
width: '40%', | ||
height: '40vh', | ||
overflow: 'auto', | ||
}}> | ||
<div | ||
ref={elementRef} | ||
style={{ | ||
background: (intersection?.intersectionRatio ?? 0) >= 0.5 ? 'green' : 'red', | ||
width: '10vw', | ||
height: '10vw', | ||
margin: '50vh auto', | ||
}} | ||
/> | ||
</div> | ||
<pre style={{ flexGrow: 1, marginLeft: '16px' }}> | ||
{JSON.stringify( | ||
{ | ||
boundingClientRect: intersection?.boundingClientRect, | ||
intersectionRatio: intersection?.intersectionRatio, | ||
intersectionRect: intersection?.intersectionRect, | ||
isIntersecting: intersection?.isIntersecting, | ||
rootBounds: intersection?.rootBounds, | ||
time: intersection?.time, | ||
}, | ||
null, | ||
2 | ||
)} | ||
</pre> | ||
</div> | ||
); | ||
}; |
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,49 @@ | ||
import { Canvas, Meta, Story } from '@storybook/addon-docs/blocks'; | ||
import { Example } from './example.stories'; | ||
|
||
<Meta title="Sensor/useIntersectionObserver" component={Example} /> | ||
|
||
# useIntersectionObserver | ||
|
||
Tracks intersection of a target element with an ancestor element or with a top-level document's viewport. | ||
|
||
- SSR-friendly. | ||
- Effective - uses single `IntersectionObserver` for hooks with same options. | ||
- Allows using React reference as root. | ||
- Does not produce references for you. | ||
|
||
#### Example | ||
|
||
<Canvas> | ||
<Story story={Example} inline /> | ||
</Canvas> | ||
|
||
## Reference | ||
|
||
```ts | ||
export function useIntersectionObserver<T extends Element>( | ||
target: RefObject<T> | T | null, | ||
{ threshold = [0], root, rootMargin = '0px' }: IUseIntersectionObserverOptions = {} | ||
): IntersectionObserverEntry | undefined; | ||
``` | ||
|
||
#### Arguments | ||
|
||
- **target** _`RefObject<T> | T | null`_ - React reference or Element to track. | ||
- **options** - Like `IntersectionObserver` options but `root` can also be react reference. | ||
- **threshold** _`RefObject<Element | Document> | Element | Document | null`_ | ||
_(default: `document`)_ - An `Element` or `Document` object (or it's react reference) which is | ||
an ancestor of the intended target, whose bounding rectangle will be considered the viewport. | ||
Any part of the target not visible in the visible area of the root is not considered visible. | ||
- **rootMargin** _`string`_ _(default: `0px`)_ - A string which specifies a set of offsets to add | ||
to the root's bounding_box when calculating intersections, effectively shrinking or growing the | ||
root for calculation purposes. The syntax is approximately the same as that for the CSS margin | ||
property. | ||
- **threshold** _`number[]`_ _(default: `[0]`)_ - Array of numbers between 0.0 and 1.0, specifying | ||
a ratio of intersection area to total bounding box area for the observed target. A value of 0.0 | ||
means that even a single visible pixel counts as the target being visible. 1.0 means that the | ||
entire target element is visible. | ||
|
||
#### Return | ||
|
||
`IntersectionObserverEntry` as it is returned from `IntersectionObserver` |
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,136 @@ | ||
import { act, renderHook } from '@testing-library/react-hooks/dom'; | ||
import { useIntersectionObserver } from '../..'; | ||
import Mock = jest.Mock; | ||
|
||
describe('useIntersectionObserver', () => { | ||
let IntersectionObserverSpy: Mock<IntersectionObserver>; | ||
const initialRO = global.ResizeObserver; | ||
|
||
beforeAll(() => { | ||
IntersectionObserverSpy = jest.fn(() => ({ | ||
observe: jest.fn(), | ||
unobserve: jest.fn(), | ||
disconnect: jest.fn(), | ||
takeRecords: () => [], | ||
root: document, | ||
rootMargin: '0px', | ||
thresholds: [0], | ||
})); | ||
|
||
global.IntersectionObserver = IntersectionObserverSpy; | ||
jest.useFakeTimers(); | ||
}); | ||
|
||
beforeEach(() => { | ||
IntersectionObserverSpy.mockClear(); | ||
}); | ||
|
||
afterAll(() => { | ||
global.ResizeObserver = initialRO; | ||
jest.useRealTimers(); | ||
}); | ||
|
||
it('should be defined', () => { | ||
expect(useIntersectionObserver).toBeDefined(); | ||
}); | ||
|
||
it('should render', () => { | ||
const { result } = renderHook(() => useIntersectionObserver(null)); | ||
expect(result.error).toBeUndefined(); | ||
}); | ||
|
||
it('should return undefined on first render', () => { | ||
const div1 = document.createElement('div'); | ||
const { result } = renderHook(() => useIntersectionObserver(div1)); | ||
expect(result.current).toBeUndefined(); | ||
}); | ||
|
||
it('should create IntersectionObserver instance only for unique set of options', () => { | ||
expect(IntersectionObserverSpy).toHaveBeenCalledTimes(0); | ||
const div1 = document.createElement('div'); | ||
const div2 = document.createElement('div'); | ||
|
||
renderHook(() => useIntersectionObserver(div1)); | ||
renderHook(() => useIntersectionObserver(div2)); | ||
|
||
expect(IntersectionObserverSpy).toHaveBeenCalledTimes(1); | ||
}); | ||
|
||
it('should return intersection entry', () => { | ||
const div1 = document.createElement('div'); | ||
const div1Ref = { current: div1 }; | ||
const div2 = document.createElement('div'); | ||
|
||
const { result: res1 } = renderHook(() => useIntersectionObserver(div1Ref)); | ||
const { result: res2, unmount } = renderHook(() => | ||
useIntersectionObserver(div2, { threshold: [0, 1] }) | ||
); | ||
|
||
expect(res1.current).toBeUndefined(); | ||
expect(res2.current).toBeUndefined(); | ||
|
||
const entry1 = { target: div1 }; | ||
const entry2 = { target: div2 }; | ||
|
||
act(() => { | ||
IntersectionObserverSpy.mock.calls[0][0]([entry1]); | ||
IntersectionObserverSpy.mock.calls[1][0]([entry2]); | ||
jest.advanceTimersByTime(1); | ||
}); | ||
|
||
expect(res1.current).toBe(entry1); | ||
expect(res2.current).toBe(entry2); | ||
|
||
unmount(); | ||
|
||
const entry3 = { target: div1 }; | ||
act(() => { | ||
IntersectionObserverSpy.mock.calls[0][0]([entry3]); | ||
jest.advanceTimersByTime(1); | ||
}); | ||
|
||
expect(res1.current).toBe(entry3); | ||
}); | ||
|
||
it('two hooks observing same target should use single observer', () => { | ||
const div1 = document.createElement('div'); | ||
const div2 = document.createElement('div'); | ||
|
||
const { result: res1 } = renderHook(() => | ||
useIntersectionObserver(div1, { root: { current: div2 } }) | ||
); | ||
const { result: res2, unmount } = renderHook(() => | ||
useIntersectionObserver(div1, { root: { current: div2 } }) | ||
); | ||
|
||
expect(res1.current).toBeUndefined(); | ||
expect(res2.current).toBeUndefined(); | ||
|
||
const entry1 = { target: div1 }; | ||
|
||
act(() => { | ||
IntersectionObserverSpy.mock.calls[0][0]([entry1]); | ||
jest.advanceTimersByTime(1); | ||
}); | ||
|
||
expect(res1.current).toBe(entry1); | ||
expect(res2.current).toBe(entry1); | ||
}); | ||
|
||
it('should disconnect observer if last hook unmounted', () => { | ||
const div1 = document.createElement('div'); | ||
|
||
const { result, unmount } = renderHook(() => useIntersectionObserver(div1)); | ||
const entry1 = { target: div1 }; | ||
|
||
act(() => { | ||
IntersectionObserverSpy.mock.calls[0][0]([entry1]); | ||
jest.advanceTimersByTime(1); | ||
}); | ||
|
||
expect(result.current).toBe(entry1); | ||
|
||
unmount(); | ||
expect(IntersectionObserverSpy.mock.results[0].value.disconnect).toHaveBeenCalled(); | ||
}); | ||
}); |
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,13 @@ | ||
import { renderHook } from '@testing-library/react-hooks/server'; | ||
import { useIntersectionObserver } from '../..'; | ||
|
||
describe('useIntersectionObserver', () => { | ||
it('should be defined', () => { | ||
expect(useIntersectionObserver).toBeDefined(); | ||
}); | ||
|
||
it('should render', () => { | ||
const { result } = renderHook(() => useIntersectionObserver(null)); | ||
expect(result.error).toBeUndefined(); | ||
}); | ||
}); |
Oops, something went wrong.