Skip to content
This repository has been archived by the owner on Jul 8, 2023. It is now read-only.

Commit

Permalink
➕ add with-page-visibility-props
Browse files Browse the repository at this point in the history
  • Loading branch information
deepsweet committed Aug 21, 2017
1 parent 0521180 commit ffd031c
Show file tree
Hide file tree
Showing 7 changed files with 365 additions and 0 deletions.
19 changes: 19 additions & 0 deletions packages/with-page-visibility-props/demo/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import React from 'react';
import { compose } from 'recompose';
import withLog from '@hocs/with-log';

import withPageVisibilityProps from '../src/';

const Demo = () => (
<h1>Switch to another tab, go back here and check console logs.</h1>
);

export default compose(
withPageVisibilityProps({
isVisible: 'visible',
isHidden: 'hidden',
isPrerendering: 'prerender',
isUnloaded: 'unloaded'
}),
withLog()
)(Demo);
33 changes: 33 additions & 0 deletions packages/with-page-visibility-props/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"name": "@hocs/with-page-visibility-props",
"library": "withPageVisibilityProps",
"version": "0.1.0",
"description": "Page Visibility HOC for React",
"keywords": [
"react",
"hoc",
"recompose",
"visibility",
"page-visibility"
],
"main": "lib/index.js",
"module": "es/index.js",
"files": [
"dist/",
"es/",
"lib/"
],
"repository": "deepsweet/hocs",
"author": "Kir Belevich <kir@belevi.ch> (https://github.com/deepsweet)",
"license": {
"type": "MIT",
"url": "https://github.com/deepsweet/hocs/blob/master/license.md"
},
"publishConfig": {
"access": "public"
},
"peerDependencies": {
"react": "^15.6.1",
"recompose": "^0.25.0"
}
}
60 changes: 60 additions & 0 deletions packages/with-page-visibility-props/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# :see_no_evil: with-page-visibility-props

[![npm](https://img.shields.io/npm/v/@hocs/with-page-visibility-props.svg?style=flat-square)](https://www.npmjs.com/package/@hocs/with-page-visibility-props) [![ci](https://img.shields.io/travis/deepsweet/hocs/master.svg?style=flat-square)](https://travis-ci.org/deepsweet/hocs) [![coverage](https://img.shields.io/codecov/c/github/deepsweet/hocs/master.svg?style=flat-square)](https://codecov.io/github/deepsweet/hocs) [![deps](https://david-dm.org/deepsweet/hocs.svg?path=packages/with-page-visibility-props&style=flat-square)](https://david-dm.org/deepsweet/hocs?path=packages/with-page-visibility-props)

Part of a [collection](https://github.com/deepsweet/hocs) of Higher-Order Components for React, especially useful with [Recompose](https://github.com/acdlite/recompose).

Dynamically map page visibility state to boolean props using [Page Visibility API](https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API) ([Can I use?](https://caniuse.com/#feat=pagevisibility)).

> A few examples:
>
> * A site has an image carousel that shouldn't advance to the next slide unless the user is viewing the page.
> * An application showing a dashboard of information doesn't want to poll the server for updates when the page isn't visible.
> * A page wants to detect when it is being prerendered so it can keep accurate count of page views.
> * A site wants to switch off sounds when a device is in standby mode (user pushes power button to turn screen off)

## Install

```
yarn add recompose @hocs/with-page-visibility-props
```

## Usage

```js
withPageVisibilityProps(
pageVisibilityMatchers: {
[propName: string]: string
}
): HigherOrderComponent
```

Where page visibility matcher's value is one of the [`visibilityState`](https://developer.mozilla.org/en-US/docs/Web/API/Document/visibilityState) possible values such as `'visible'`, `'hidden'` and so on.

```js
import React from 'react';
import { compose } from 'recompose';
import withPageVisibilityProps from '@hocs/with-page-visibility-props';
import withLog from '@hocs/with-log';

const Demo = () => (
<h1>Switch to another tab, go back here and check console logs.</h1>
);

export default compose(
withPageVisibilityProps({
isVisible: 'visible',
isHidden: 'hidden',
isPrerendering: 'prerender',
isUnloaded: 'unloaded'
}),
withLog()
)(Demo);
```

:tv: [Check out live demo](https://www.webpackbin.com/bins/-Ks3HfdUxla_iIfoJVm1).

## Notes

* Target Component will be just passed through on unsupported platforms (i.e. `global.document.visibilityState` is `undefined`) like IE9, JSDOM (so Jest as well) or with Server-Side Rendering. This means that there will be no boolean props (i.e. `undefined`) which might be expected, but you can take care of it using Recompose [`defaultProps`](https://github.com/acdlite/recompose/blob/master/docs/API.md#defaultprops) HOC if it's really necessary.
59 changes: 59 additions & 0 deletions packages/with-page-visibility-props/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { Component } from 'react';
import { createEagerFactory, setDisplayName, wrapDisplayName } from 'recompose';

const isPageVisibiitySupported = global.document &&
typeof global.document.visibilityState !== 'undefined';

const mapVisibilityToProps = (propsVisibility = {}) => Object.keys(propsVisibility)
.reduce((result, prop) => ({
...result,
[prop]: propsVisibility[prop] === global.document.visibilityState
}), {});

const withPageVisibilityProps = (propsVisibility) => (Target) => {
if (!isPageVisibiitySupported) {
return Target;
}

const factory = createEagerFactory(Target);

class WithPageVisibilityProps extends Component {
constructor(props, context) {
super(props, context);

this.state = mapVisibilityToProps(propsVisibility);
this.onVibisilityChange = this.onVibisilityChange.bind(this);
}

componentDidMount() {
global.document.addEventListener('visibilitychange', this.onVibisilityChange, false);
}

componentWillUnmount() {
global.document.removeEventListener('visibilitychange', this.onVibisilityChange);
}

onVibisilityChange() {
this.setState(
mapVisibilityToProps(propsVisibility)
);
}

render() {
return factory({
...this.props,
...this.state
});
}
}

if (process.env.NODE_ENV !== 'production') {
return setDisplayName(
wrapDisplayName(Target, 'withPageVisibilityProps')
)(WithPageVisibilityProps);
}

return WithPageVisibilityProps;
};

export default withPageVisibilityProps;
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`withPageVisibilityProps Page Visibility API is not supported should just pass Target component through 1`] = `
<Target
a={1}
b={2}
c={3}
/>
`;

exports[`withPageVisibilityProps Page Visibility API is supported display name should not wrap display name in production env 1`] = `<WithPageVisibilityProps />`;

exports[`withPageVisibilityProps Page Visibility API is supported display name should wrap display name in non-production env 1`] = `
<withPageVisibilityProps(Target)>
<Target />
</withPageVisibilityProps(Target)>
`;

exports[`withPageVisibilityProps Page Visibility API is supported should handle page visibility state change 1`] = `
<Target
isHidden={true}
isVisible={false}
/>
`;

exports[`withPageVisibilityProps Page Visibility API is supported should remove event listener on unmount 1`] = `
Array [
Array [],
Array [],
Array [],
Array [],
]
`;

exports[`withPageVisibilityProps Page Visibility API is supported should set initial state 1`] = `
<Target
isHidden={false}
isVisible={true}
/>
`;
150 changes: 150 additions & 0 deletions packages/with-page-visibility-props/test/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import React from 'react';
import { mount } from 'enzyme';

const Target = () => null;

describe('withPageVisibilityProps', () => {
describe('Page Visibility API is supported', () => {
let withPageVisibilityProps = null;
let origVisibilityState = null;

beforeAll(() => {
origVisibilityState = global.document.visibilityState;
});

beforeEach(() => {
jest.resetModules();

Object.defineProperty(global.document, 'visibilityState', {
get: () => 'visible',
configurable: true
});
withPageVisibilityProps = require('../src/').default;
});

afterAll(() => {
Object.defineProperty(global.document, 'visibilityState', {
get: () => origVisibilityState
});
});

it('should set initial state', () => {
const EnhancedTarget = withPageVisibilityProps({
isVisible: 'visible',
isHidden: 'hidden'
})(Target);
const wrapper = mount(
<EnhancedTarget/>
);

expect(wrapper.find(Target)).toMatchSnapshot();
});

it('should handle page visibility state change', () => {
const EnhancedTarget = withPageVisibilityProps({
isVisible: 'visible',
isHidden: 'hidden'
})(Target);
const wrapper = mount(
<EnhancedTarget/>
);

Object.defineProperty(global.document, 'visibilityState', {
get: () => 'hidden',
configurable: true
});
global.document.dispatchEvent(new CustomEvent('visibilitychange'));

expect(wrapper.find(Target)).toMatchSnapshot();
});

it('should remove event listener on unmount', () => {
const mockVisibility = jest.fn(() => 'hidden');
const EnhancedTarget = withPageVisibilityProps({
isVisible: 'visible',
isHidden: 'hidden'
})(Target);
const wrapper = mount(
<EnhancedTarget/>
);

wrapper.unmount();

Object.defineProperty(global.document, 'visibilityState', {
get: mockVisibility,
configurable: true
});
global.document.dispatchEvent(new CustomEvent('visibilitychange'));

expect(mockVisibility.mock.calls).toMatchSnapshot();
});

describe('display name', () => {
let origNodeEnv = null;

beforeAll(() => {
origNodeEnv = process.env.NODE_ENV;
});

afterAll(() => {
process.env.NODE_ENV = origNodeEnv;
});

it('should wrap display name in non-production env', () => {
process.env.NODE_ENV = 'test';

const EnhancedTarget = withPageVisibilityProps()(Target);
const wrapper = mount(
<EnhancedTarget/>
);

expect(wrapper).toMatchSnapshot();
});

it('should not wrap display name in production env', () => {
process.env.NODE_ENV = 'production';

const EnhancedTarget = withPageVisibilityProps()(Target);
const wrapper = mount(
<EnhancedTarget/>
);

expect(wrapper).toMatchSnapshot();
});
});
});

describe('Page Visibility API is not supported', () => {
let withPageVisibilityProps = null;
let origVisibilityState = null;

beforeAll(() => {
origVisibilityState = global.document.visibilityState;
});

beforeEach(() => {
jest.resetModules();

Object.defineProperty(global.document, 'visibilityState', {
get: () => {},
configurable: true
});
withPageVisibilityProps = require('../src/').default;
});

afterAll(() => {
Object.defineProperty(global.document, 'visibilityState', {
get: () => origVisibilityState
});
});

it('should just pass Target component through', () => {
const EnhancedTarget = withPageVisibilityProps()(Target);
const wrapper = mount(
<EnhancedTarget a={1} b={2} c={3}/>
);

expect(wrapper).toMatchSnapshot();
});
});
});
4 changes: 4 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ Dynamically map [CSS Media Queries](https://developer.mozilla.org/en-US/docs/Web

Dynamically map visibility of a component to boolean props using [Intersection Observer API](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API) ([Can I use?](https://caniuse.com/#feat=intersectionobserver)).

### :see_no_evil: [with-page-visibility-props](packages/with-page-visibility-props)

Dynamically map page visibility state to boolean props using [Page Visibility API](https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API) ([Can I use?](https://caniuse.com/#feat=pagevisibility)).

### :hourglass: [debounce-handler](packages/debounce-handler)

Helps to debounce handlers like `onChange`.
Expand Down

0 comments on commit ffd031c

Please sign in to comment.