` as an
+option. This will override the global fallback value.
+
+```jsx
+import React from 'react';
+import { useInView } from 'react-intersection-observer';
+
+const Component = () => {
+ const { ref, inView, entry } = useInView({
+ fallbackInView: true,
+ });
+
+ return (
+
+
{`Header inside viewport ${inView}.`}
+
+ );
+};
+```
+
### Polyfill
You can import the
diff --git a/src/InView.tsx b/src/InView.tsx
index b356a6fa..56df6198 100644
--- a/src/InView.tsx
+++ b/src/InView.tsx
@@ -106,17 +106,29 @@ export class InView extends React.Component<
observeNode() {
if (!this.node || this.props.skip) return;
- const { threshold, root, rootMargin, trackVisibility, delay } = this.props;
-
- this._unobserveCb = observe(this.node, this.handleChange, {
+ const {
threshold,
root,
rootMargin,
- // @ts-ignore
trackVisibility,
- // @ts-ignore
delay,
- });
+ fallbackInView,
+ } = this.props;
+
+ this._unobserveCb = observe(
+ this.node,
+ this.handleChange,
+ {
+ threshold,
+ root,
+ rootMargin,
+ // @ts-ignore
+ trackVisibility,
+ // @ts-ignore
+ delay,
+ },
+ fallbackInView,
+ );
}
unobserve() {
@@ -136,6 +148,7 @@ export class InView extends React.Component<
this.setState({ inView: !!this.props.initialInView, entry: undefined });
}
}
+
this.node = node ? node : null;
this.observeNode();
};
@@ -175,6 +188,7 @@ export class InView extends React.Component<
trackVisibility,
delay,
initialInView,
+ fallbackInView,
...props
} = this.props;
diff --git a/src/__tests__/InView.test.tsx b/src/__tests__/InView.test.tsx
index 947cc767..6a180b84 100644
--- a/src/__tests__/InView.test.tsx
+++ b/src/__tests__/InView.test.tsx
@@ -2,6 +2,7 @@ import React from 'react';
import { screen, fireEvent, render } from '@testing-library/react';
import { intersectionMockInstance, mockAllIsIntersecting } from '../test-utils';
import { InView } from '../InView';
+import { defaultFallbackInView } from '../observe';
it('Should render intersecting', () => {
const callback = jest.fn();
@@ -155,3 +156,66 @@ it('plain children should not catch bubbling onChange event', () => {
fireEvent.change(input, { target: { value: 'changed value' } });
expect(onChange).not.toHaveBeenCalled();
});
+
+it('should render with fallback', () => {
+ const cb = jest.fn();
+ // @ts-ignore
+ window.IntersectionObserver = undefined;
+ render(
+
+ Inner
+ ,
+ );
+ expect(cb).toHaveBeenLastCalledWith(
+ true,
+ expect.objectContaining({ isIntersecting: true }),
+ );
+
+ render(
+
+ Inner
+ ,
+ );
+ expect(cb).toHaveBeenLastCalledWith(
+ false,
+ expect.objectContaining({ isIntersecting: false }),
+ );
+
+ expect(() => {
+ jest.spyOn(console, 'error').mockImplementation(() => {});
+ render(Inner);
+ // @ts-ignore
+ console.error.mockRestore();
+ }).toThrowErrorMatchingInlineSnapshot(
+ `"IntersectionObserver is not a constructor"`,
+ );
+});
+
+it('should render with global fallback', () => {
+ const cb = jest.fn();
+ // @ts-ignore
+ window.IntersectionObserver = undefined;
+ defaultFallbackInView(true);
+ render(Inner);
+ expect(cb).toHaveBeenLastCalledWith(
+ true,
+ expect.objectContaining({ isIntersecting: true }),
+ );
+
+ defaultFallbackInView(false);
+ render(Inner);
+ expect(cb).toHaveBeenLastCalledWith(
+ false,
+ expect.objectContaining({ isIntersecting: false }),
+ );
+
+ defaultFallbackInView(undefined);
+ expect(() => {
+ jest.spyOn(console, 'error').mockImplementation(() => {});
+ render(Inner);
+ // @ts-ignore
+ console.error.mockRestore();
+ }).toThrowErrorMatchingInlineSnapshot(
+ `"IntersectionObserver is not a constructor"`,
+ );
+});
diff --git a/src/__tests__/hooks.test.tsx b/src/__tests__/hooks.test.tsx
index a7de3b25..e952de44 100644
--- a/src/__tests__/hooks.test.tsx
+++ b/src/__tests__/hooks.test.tsx
@@ -6,7 +6,7 @@ import {
mockAllIsIntersecting,
mockIsIntersecting,
} from '../test-utils';
-import { IntersectionOptions } from '../index';
+import { IntersectionOptions, defaultFallbackInView } from '../index';
const HookComponent = ({
options,
@@ -318,3 +318,46 @@ test('should set intersection ratio as the largest threshold smaller than trigge
mockIsIntersecting(wrapper, 0.5);
expect(screen.getByText(/intersectionRatio: 0.5/g)).toBeInTheDocument();
});
+
+test('should handle fallback if unsupported', () => {
+ // @ts-ignore
+ window.IntersectionObserver = undefined;
+ const { rerender } = render(
+ ,
+ );
+ screen.getByText('true');
+
+ rerender();
+ screen.getByText('false');
+
+ expect(() => {
+ jest.spyOn(console, 'error').mockImplementation(() => {});
+ rerender();
+ // @ts-ignore
+ console.error.mockRestore();
+ }).toThrowErrorMatchingInlineSnapshot(
+ `"IntersectionObserver is not a constructor"`,
+ );
+});
+
+test('should handle defaultFallbackInView if unsupported', () => {
+ // @ts-ignore
+ window.IntersectionObserver = undefined;
+ defaultFallbackInView(true);
+ const { rerender } = render();
+ screen.getByText('true');
+
+ defaultFallbackInView(false);
+ rerender();
+ screen.getByText('false');
+
+ defaultFallbackInView(undefined);
+ expect(() => {
+ jest.spyOn(console, 'error').mockImplementation(() => {});
+ rerender();
+ // @ts-ignore
+ console.error.mockRestore();
+ }).toThrowErrorMatchingInlineSnapshot(
+ `"IntersectionObserver is not a constructor"`,
+ );
+});
diff --git a/src/index.tsx b/src/index.tsx
index 43381a62..5ee1f84b 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -2,7 +2,7 @@ import * as React from 'react';
import { InView } from './InView';
export { InView } from './InView';
export { useInView } from './useInView';
-export { observe } from './observe';
+export { observe, defaultFallbackInView } from './observe';
export default InView;
@@ -30,7 +30,10 @@ export interface IntersectionOptions extends IntersectionObserverInit {
triggerOnce?: boolean;
/** Skip assigning the observer to the `ref` */
skip?: boolean;
+ /** Set the initial value of the `inView` boolean. This can be used if you expect the element to be in the viewport to start with, and you want to trigger something when it leaves. */
initialInView?: boolean;
+ /** Fallback to this inView state if the IntersectionObserver is unsupported, and a polyfill wasn't loaded */
+ fallbackInView?: boolean;
/** IntersectionObserver v2 - Track the actual visibility of the element */
trackVisibility?: boolean;
/** IntersectionObserver v2 - Set a minimum delay between notifications */
diff --git a/src/observe.ts b/src/observe.ts
index 471a12ab..e90e794d 100644
--- a/src/observe.ts
+++ b/src/observe.ts
@@ -12,6 +12,18 @@ const observerMap = new Map<
const RootIds: WeakMap = new WeakMap();
let rootId = 0;
+let unsupportedValue: boolean | undefined = undefined;
+
+/**
+ * What should be the default behavior if the IntersectionObserver is unsupported?
+ * Ideally the polyfill has been loaded, you can have the following happen:
+ * - `undefined`: Throw an error
+ * - `true` or `false`: Set the `inView` value to this regardless of intersection state
+ * **/
+export function defaultFallbackInView(inView: boolean | undefined) {
+ unsupportedValue = inView;
+}
+
/**
* Generate a unique ID for the root element
* @param root
@@ -95,14 +107,34 @@ function createObserver(options: IntersectionObserverInit) {
* @param element - DOM Element to observe
* @param callback - Callback function to trigger when intersection status changes
* @param options - Intersection Observer options
+ * @param fallbackInView - Fallback inView value.
* @return Function - Cleanup function that should be triggered to unregister the observer
*/
export function observe(
element: Element,
callback: ObserverInstanceCallback,
options: IntersectionObserverInit = {},
+ fallbackInView = unsupportedValue,
) {
- if (!element) return () => {};
+ if (
+ typeof window.IntersectionObserver === 'undefined' &&
+ fallbackInView !== undefined
+ ) {
+ const bounds = element.getBoundingClientRect();
+ callback(fallbackInView, {
+ isIntersecting: fallbackInView,
+ target: element,
+ intersectionRatio:
+ typeof options.threshold === 'number' ? options.threshold : 0,
+ time: 0,
+ boundingClientRect: bounds,
+ intersectionRect: bounds,
+ rootBounds: bounds,
+ });
+ return () => {
+ // Nothing to cleanup
+ };
+ }
// An observer with the same options can be reused, so lets use this fact
const { id, observer, elements } = createObserver(options);
diff --git a/src/test-utils.ts b/src/test-utils.ts
index df20506e..b82d0a8d 100644
--- a/src/test-utils.ts
+++ b/src/test-utils.ts
@@ -46,7 +46,7 @@ beforeEach(() => {
afterEach(() => {
// @ts-ignore
- global.IntersectionObserver.mockClear();
+ if (global.IntersectionObserver) global.IntersectionObserver.mockClear();
observers.clear();
});
diff --git a/src/useInView.tsx b/src/useInView.tsx
index c3c993b9..f1e63f08 100644
--- a/src/useInView.tsx
+++ b/src/useInView.tsx
@@ -43,6 +43,7 @@ export function useInView({
triggerOnce,
skip,
initialInView,
+ fallbackInView,
}: IntersectionOptions = {}): InViewHookResponse {
const unobserve = React.useRef();
const [state, setState] = React.useState({
@@ -79,6 +80,7 @@ export function useInView({
// @ts-ignore
delay,
},
+ fallbackInView,
);
}
},
@@ -93,6 +95,7 @@ export function useInView({
triggerOnce,
skip,
trackVisibility,
+ fallbackInView,
delay,
],
);