Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions docs/useDocumentVisibility.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# `useDocumentVisibility`

React sensor hook that tracks document visibility state using the [Page Visibility API](https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API).

## Usage

```jsx
import {useDocumentVisibility} from 'react-use';

const Demo = () => {
const defaultState = document.visibilityState === 'visible';
const isVisible = useDocumentVisibility(defaultState);

return (
<div>
Document is {isVisible ? 'visible' : 'hidden'}
</div>
);
};
```

## Reference

```js
const isVisible = useDocumentVisibility(initialState);
```

- `initialState` &mdash; `boolean`, optional initial state before the actual visibility is determined, defaults to `false`.
- `isVisible` &mdash; `boolean`, whether the document is currently visible (tab is in foreground).
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export { default as useCustomCompareEffect } from './useCustomCompareEffect';
export { default as useDebounce } from './useDebounce';
export { default as useDeepCompareEffect } from './useDeepCompareEffect';
export { default as useDefault } from './useDefault';
export { default as useDocumentVisibility } from './useDocumentVisibility';
export { default as useDrop } from './useDrop';
export { default as useDropArea } from './useDropArea';
export { default as useEffectOnce } from './useEffectOnce';
Expand Down
19 changes: 19 additions & 0 deletions src/useDocumentVisibility.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { useEffect, useState } from 'react';

const useDocumentVisibility = (defaultState: boolean = false) => {
const [isVisible, setIsVisible] = useState(defaultState);

useEffect(() => {
const handleVisibilityChange = () => setIsVisible(document.visibilityState === 'visible');

document.addEventListener('visibilitychange', handleVisibilityChange);

return () => {
document.removeEventListener('visibilitychange', handleVisibilityChange);
};
}, []);

return isVisible;
};

export default useDocumentVisibility;
22 changes: 22 additions & 0 deletions stories/useDocumentVisibility.story.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { storiesOf } from '@storybook/react';
import * as React from 'react';
import useDocumentVisibility from '../src/useDocumentVisibility';
import ShowDocs from './util/ShowDocs';

const Demo = () => {
const defaultState = document.visibilityState === 'visible';
const isVisible = useDocumentVisibility(defaultState);

return (
<div>
<p>Switch to another browser tab to see the visibility state change.</p>
<div style={{ fontSize: '24px', fontWeight: 'bold' }}>
Document is {isVisible ? '👁️ Visible' : '🙈 Hidden'}
</div>
</div>
);
};

storiesOf('Sensors/useDocumentVisibility', module)
.add('Docs', () => <ShowDocs md={require('../docs/useDocumentVisibility.md')} />)
.add('Demo', () => <Demo />);
92 changes: 92 additions & 0 deletions tests/useDocumentVisibility.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { renderHook, act } from '@testing-library/react-hooks';
import useDocumentVisibility from '../src/useDocumentVisibility';

describe('useDocumentVisibility', () => {
const originalVisibilityState = document.visibilityState;

afterEach(() => {
Object.defineProperty(document, 'visibilityState', {
configurable: true,
value: originalVisibilityState,
});
});

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

it('should return false initially', () => {
const { result } = renderHook(() => useDocumentVisibility());

expect(result.current).toBe(false);
});

it('should return true initially when initialState is true', () => {
const { result } = renderHook(() => useDocumentVisibility(true));

expect(result.current).toBe(true);
});

it('should return false initially when initialState is false', () => {
const { result } = renderHook(() => useDocumentVisibility(false));

expect(result.current).toBe(false);
});

it('should return true when document becomes visible', () => {
const { result } = renderHook(() => useDocumentVisibility(true));

act(() => {
Object.defineProperty(document, 'visibilityState', {
configurable: true,
value: 'visible',
});
document.dispatchEvent(new Event('visibilitychange'));
});

expect(result.current).toBe(true);
});

it('should return false when document becomes hidden', () => {
const { result } = renderHook(() => useDocumentVisibility());

act(() => {
Object.defineProperty(document, 'visibilityState', {
configurable: true,
value: 'visible',
});
document.dispatchEvent(new Event('visibilitychange'));
});
expect(result.current).toBe(true);

act(() => {
Object.defineProperty(document, 'visibilityState', {
configurable: true,
value: 'hidden',
});
document.dispatchEvent(new Event('visibilitychange'));
});
expect(result.current).toBe(false);
});

it('should add event listener on mount', () => {
const addEventListenerSpy = jest.spyOn(document, 'addEventListener');

renderHook(() => useDocumentVisibility());

expect(addEventListenerSpy).toHaveBeenCalledWith('visibilitychange', expect.any(Function));

addEventListenerSpy.mockRestore();
});

it('should remove event listener on unmount', () => {
const removeEventListenerSpy = jest.spyOn(document, 'removeEventListener');

const { unmount } = renderHook(() => useDocumentVisibility());
unmount();

expect(removeEventListenerSpy).toHaveBeenCalledWith('visibilitychange', expect.any(Function));

removeEventListenerSpy.mockRestore();
});
});