├── .babelrc
├── .eslintignore
├── .eslintrc.js
├── .gitignore
├── .npmignore
├── .size-limit
├── .travis.yml
├── CHANGELOG.md
├── LICENSE
├── README.md
├── package.json
├── prettier.config.js
├── rollup.config.js
├── src
├── index.js
├── index.test.js
└── supports.js
├── test
├── createDOM.js
├── mocha.opts
└── setup.js
└── yarn.lock
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | ["@babel/preset-env", { "modules": false }],
4 | "@babel/preset-react"
5 | ],
6 | "plugins": [
7 | ['@babel/plugin-proposal-class-properties', { loose: true }],
8 | ['@babel/plugin-proposal-object-rest-spread', { loose: true }],
9 | ["babel-plugin-transform-react-remove-prop-types", { "mode": "wrap" }],
10 | "babel-plugin-transform-dev-warning"
11 | ],
12 | "env": {
13 | "test": {
14 | "plugins": [
15 | "@babel/transform-modules-commonjs"
16 | ]
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | dist/
3 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | // So parent files don't get applied
3 | root: true,
4 | globals: {
5 | preval: false,
6 | },
7 | env: {
8 | es6: true,
9 | browser: true,
10 | node: true,
11 | mocha: true,
12 | },
13 | extends: ['airbnb'],
14 | parser: 'babel-eslint',
15 | parserOptions: {
16 | ecmaVersion: 7,
17 | sourceType: 'module',
18 | },
19 | plugins: ['babel', 'jsx-a11y', 'mocha', 'prettier'],
20 | rules: {
21 | 'linebreak-style': 'off', // Don't play nicely with Windows.
22 | 'arrow-body-style': 'off', // Not our taste?
23 | 'arrow-parens': 'off', // Incompatible with prettier
24 | 'object-curly-newline': 'off', // Incompatible with prettier
25 | 'function-paren-newline': 'off', // Incompatible with prettier
26 | indent: 'off', // Incompatible with prettier
27 | 'space-before-function-paren': 'off', // Incompatible with prettier
28 | 'no-confusing-arrow': 'off', // Incompatible with prettier
29 | 'no-mixed-operators': 'off', // Incompatible with prettier
30 | 'consistent-this': ['error', 'self'],
31 | 'max-len': [
32 | 'error',
33 | 100,
34 | 2,
35 | {
36 | ignoreUrls: true,
37 | },
38 | ], // airbnb is allowing some edge cases
39 | 'no-console': 'error', // airbnb is using warn
40 | 'no-alert': 'error', // airbnb is using warn
41 | 'no-param-reassign': 'off', // Not our taste?
42 | 'no-prototype-builtins': 'off', // airbnb use error
43 | 'object-curly-spacing': 'off', // use babel plugin rule
44 | 'no-restricted-properties': 'off', // To remove once react-docgen support ** operator.
45 | 'prefer-destructuring': 'off', // To remove once react-docgen support ** operator.
46 |
47 | 'babel/object-curly-spacing': ['error', 'always'],
48 |
49 | 'react/jsx-indent': 'off', // Incompatible with prettier
50 | 'react/jsx-closing-bracket-location': 'off', // Incompatible with prettier
51 | 'react/jsx-wrap-multilines': 'off', // Incompatible with prettier
52 | 'react/jsx-indent-props': 'off', // Incompatible with prettier
53 | 'react/jsx-handler-names': [
54 | 'error',
55 | {
56 | // airbnb is disabling this rule
57 | eventHandlerPrefix: 'handle',
58 | eventHandlerPropPrefix: 'on',
59 | },
60 | ],
61 | 'react/require-default-props': 'off', // airbnb use error
62 | 'react/forbid-prop-types': 'off', // airbnb use error
63 | 'react/jsx-filename-extension': ['error', { extensions: ['.js'] }], // airbnb is using .jsx
64 | 'react/no-danger': 'error', // airbnb is using warn
65 | 'react/no-direct-mutation-state': 'error', // airbnb is disabling this rule
66 | 'react/no-find-dom-node': 'off', // I don't know
67 | 'react/no-unused-prop-types': 'off', // Is still buggy
68 | 'react/sort-prop-types': 'error', // airbnb do nothing here.
69 | 'react/default-props-match-prop-types': 'off', // Buggy
70 | 'react/jsx-curly-brace-presence': 'off', // Buggy
71 | 'react/destructuring-assignment': 'off', // Fuck no
72 |
73 | 'mocha/handle-done-callback': 'error',
74 | 'mocha/no-exclusive-tests': 'error',
75 | 'mocha/no-global-tests': 'error',
76 | 'mocha/no-pending-tests': 'error',
77 | 'mocha/no-skipped-tests': 'error',
78 |
79 | 'prettier/prettier': ['error'],
80 | },
81 | };
82 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Editor
2 | *.sublime-project
3 | *.sublime-workspace
4 |
5 | # Node
6 | node_modules
7 | *.log
8 |
9 | # OSX
10 | .DS_Store
11 |
12 | # Project
13 | dist
14 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | /*
2 | !/src/*.js
3 | !/dist/*.js
4 | *.spec.js
5 |
--------------------------------------------------------------------------------
/.size-limit:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | name: 'The entry point',
4 | webpack: true,
5 | path: 'dist/react-event-listener.cjs.js',
6 | limit: '1.7 KB'
7 | }
8 | ]
9 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - stable
4 | sudo: false
5 | cache:
6 | directories:
7 | - node_modules
8 | script:
9 | - npm run test
10 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | All notable changes are described on the [Releases](https://github.com/oliviertassinari/react-event-listener/releases) page.
2 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of
4 | this software and associated documentation files (the "Software"), to deal in
5 | the Software without restriction, including without limitation the rights to
6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
7 | the Software, and to permit persons to whom the Software is furnished to do so,
8 | subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
19 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-event-listener
2 |
3 | > A React component for binding events on the global scope.
4 |
5 | [](https://www.npmjs.com/package/react-event-listener)
6 | [](https://www.npmjs.com/package/react-event-listener)
7 | [](https://travis-ci.org/oliviertassinari/react-event-listener)
8 |
9 | [](https://david-dm.org/oliviertassinari/react-event-listener)
10 | [](https://david-dm.org/oliviertassinari/react-event-listener#info=devDependencies&view=list)
11 |
12 | ## Installation
13 |
14 | ```sh
15 | npm install react-event-listener
16 | ```
17 |
18 | ## The problem solved
19 |
20 | This module provides a **declarative way** to bind events to a global `EventTarget`.
21 | It's using the React lifecycle to bind and unbind at the right time.
22 |
23 | ## Usage
24 |
25 | ```js
26 | import React, {Component} from 'react';
27 | import EventListener, {withOptions} from 'react-event-listener';
28 |
29 | class MyComponent extends Component {
30 | handleResize = () => {
31 | console.log('resize');
32 | };
33 |
34 | handleScroll = () => {
35 | console.log('scroll');
36 | };
37 |
38 | handleMouseMove = () => {
39 | console.log('mousemove');
40 | };
41 |
42 | render() {
43 | return (
44 |
45 |
50 |
51 |
52 | );
53 | }
54 | }
55 | ```
56 |
57 | ### Note on server-side rendering
58 |
59 | When doing server side rendering, `document` and `window` aren't available.
60 | You can use a string as a `target`, or check that they exist before rendering
61 | the component.
62 |
63 | ### Note on performance
64 |
65 | You should avoid passing inline functions for listeners, because this creates a new `Function` instance on every
66 | render, defeating `EventListener`'s `shouldComponentUpdate`, and triggering an update cycle where it removes its old
67 | listeners and adds its new listeners (so that it can stay up-to-date with the props you passed in).
68 |
69 | ### Note on testing
70 |
71 | In [this](https://github.com/facebook/react/issues/5043) issue from React, `TestUtils.Simulate.` methods won't bubble up to `window` or `document`. As a result, you must use [`document.dispatchEvent`](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/dispatchEvent) or simulate event using [native DOM api](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/click).
72 |
73 | See our [test cases](https://github.com/oliviertassinari/react-event-listener/blob/master/src/index.spec.js) for more information.
74 |
75 | ## License
76 |
77 | MIT
78 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-event-listener",
3 | "version": "0.6.6",
4 | "description": "A React component that allow to bind events on the global scope",
5 | "main": "dist/react-event-listener.cjs.js",
6 | "scripts": {
7 | "build": "rimraf dist && rollup -c && rimraf dist/react-event-listener.esm.js",
8 | "lint": "eslint . && echo \"eslint: no lint errors\"",
9 | "size": "yarn build && size-limit",
10 | "test:unit": "NODE_ENV=test mocha",
11 | "test:watch": "NODE_ENV=test mocha -w",
12 | "test": "yarn lint && yarn test:unit && yarn size",
13 | "prettier": "find . -name \"*.js\" | grep -v -f .eslintignore | xargs prettier --write",
14 | "version": "yarn build && pkgfiles"
15 | },
16 | "repository": {
17 | "type": "git",
18 | "url": "https://github.com/oliviertassinari/react-event-listener.git"
19 | },
20 | "homepage": "https://github.com/oliviertassinari/react-event-listener",
21 | "keywords": [
22 | "react",
23 | "event",
24 | "listener",
25 | "binding"
26 | ],
27 | "author": "olivier.tassinari@gmail.com",
28 | "license": "MIT",
29 | "bugs": {
30 | "url": "https://github.com/oliviertassinari/react-event-listener/issues"
31 | },
32 | "devDependencies": {
33 | "@babel/core": "^7.0.0",
34 | "@babel/plugin-proposal-class-properties": "^7.0.0",
35 | "@babel/plugin-proposal-object-rest-spread": "^7.0.0",
36 | "@babel/plugin-transform-modules-commonjs": "^7.0.0",
37 | "@babel/plugin-transform-runtime": "^7.2.0",
38 | "@babel/preset-env": "^7.0.0",
39 | "@babel/preset-react": "^7.0.0",
40 | "@babel/register": "^7.0.0",
41 | "babel-eslint": "^9.0.0",
42 | "babel-plugin-transform-dev-warning": "^0.1.0",
43 | "babel-plugin-transform-react-remove-prop-types": "^0.4.9",
44 | "chai": "^4.1.2",
45 | "enzyme": "^3.1.0",
46 | "enzyme-adapter-react-16": "^1.0.1",
47 | "eslint": "^5.0.0",
48 | "eslint-config-airbnb": "^17.0.0",
49 | "eslint-plugin-babel": "^5.0.0",
50 | "eslint-plugin-import": "^2.7.0",
51 | "eslint-plugin-jsx-a11y": "^6.0.0",
52 | "eslint-plugin-mocha": "^5.0.0",
53 | "eslint-plugin-prettier": "^2.3.1",
54 | "eslint-plugin-react": "^7.4.0",
55 | "jsdom": "^12.0.0",
56 | "mocha": "^5.0.0",
57 | "pkgfiles": "^2.3.2",
58 | "prettier": "^1.7.4",
59 | "react": "^16.0.0",
60 | "react-dom": "^16.0.0",
61 | "react-test-renderer": "^16.0.0",
62 | "rimraf": "^2.6.2",
63 | "rollup": "^0.66.2",
64 | "rollup-plugin-babel": "^4.0.0-beta.4",
65 | "rollup-plugin-commonjs": "^9.1.3",
66 | "rollup-plugin-node-resolve": "^3.3.0",
67 | "rollup-plugin-replace": "^2.0.0",
68 | "sinon": "^6.1.2",
69 | "size-limit": "^0.20.0"
70 | },
71 | "dependencies": {
72 | "@babel/runtime": "^7.2.0",
73 | "prop-types": "^15.6.0",
74 | "warning": "^4.0.1"
75 | },
76 | "peerDependencies": {
77 | "react": "^16.3.0"
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/prettier.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | printWidth: 100,
3 | singleQuote: true,
4 | trailingComma: 'all',
5 | bracketSpacing: true,
6 | jsxBracketSameLine: false,
7 | parser: 'babylon',
8 | semi: true,
9 | };
10 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import nodeResolve from 'rollup-plugin-node-resolve';
2 | import babel from 'rollup-plugin-babel';
3 | import commonjs from 'rollup-plugin-commonjs';
4 | import replace from 'rollup-plugin-replace';
5 | import pkg from './package.json';
6 |
7 | const input = './src/index.js';
8 | const external = id => !id.startsWith('.') && !id.startsWith('/');
9 |
10 | const globals = {
11 | react: 'React',
12 | 'prop-types': 'PropTypes',
13 | };
14 |
15 | const getBabelOptions = ({ useESModules }) => ({
16 | exclude: '**/node_modules/**',
17 | runtimeHelpers: true,
18 | plugins: [['@babel/transform-runtime', { useESModules }]],
19 | });
20 |
21 | export default [
22 | {
23 | input,
24 | output: {
25 | file: 'dist/react-event-listener.umd.js',
26 | format: 'umd',
27 | exports: 'named',
28 | name: 'ReactEventListener',
29 | globals,
30 | },
31 | external: Object.keys(globals),
32 | plugins: [
33 | nodeResolve(),
34 | babel(getBabelOptions({ useESModules: true })),
35 | commonjs({ include: '**/node_modules/**' }),
36 | replace({ 'process.env.NODE_ENV': JSON.stringify('development') }),
37 | ],
38 | },
39 | {
40 | input,
41 | output: { file: pkg.main, format: 'cjs', exports: 'named' },
42 | external,
43 | plugins: [babel(getBabelOptions({ useESModules: false }))],
44 | },
45 | {
46 | input,
47 | output: { file: 'dist/react-event-listener.esm.js', format: 'es', exports: 'named' },
48 | external,
49 | plugins: [babel(getBabelOptions({ useESModules: true }))],
50 | },
51 | ];
52 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import warning from 'warning';
4 | import { passiveOption } from './supports';
5 |
6 | const defaultEventOptions = {
7 | capture: false,
8 | passive: false,
9 | };
10 |
11 | function mergeDefaultEventOptions(options) {
12 | return { ...defaultEventOptions, ...options };
13 | }
14 |
15 | function getEventListenerArgs(eventName, callback, options) {
16 | const args = [eventName, callback];
17 | args.push(passiveOption ? options : options.capture);
18 | return args;
19 | }
20 |
21 | function on(target, eventName, callback, options) {
22 | // eslint-disable-next-line prefer-spread
23 | target.addEventListener.apply(target, getEventListenerArgs(eventName, callback, options));
24 | }
25 |
26 | function off(target, eventName, callback, options) {
27 | // eslint-disable-next-line prefer-spread
28 | target.removeEventListener.apply(target, getEventListenerArgs(eventName, callback, options));
29 | }
30 |
31 | function forEachListener(props, iteratee) {
32 | const {
33 | children, // eslint-disable-line no-unused-vars
34 | target, // eslint-disable-line no-unused-vars
35 | ...eventProps
36 | } = props;
37 |
38 | Object.keys(eventProps).forEach(name => {
39 | if (name.substring(0, 2) !== 'on') {
40 | return;
41 | }
42 |
43 | const prop = eventProps[name];
44 | const type = typeof prop;
45 | const isObject = type === 'object';
46 | const isFunction = type === 'function';
47 |
48 | if (!isObject && !isFunction) {
49 | return;
50 | }
51 |
52 | const capture = name.substr(-7).toLowerCase() === 'capture';
53 | let eventName = name.substring(2).toLowerCase();
54 | eventName = capture ? eventName.substring(0, eventName.length - 7) : eventName;
55 |
56 | if (isObject) {
57 | iteratee(eventName, prop.handler, prop.options);
58 | } else {
59 | iteratee(eventName, prop, mergeDefaultEventOptions({ capture }));
60 | }
61 | });
62 | }
63 |
64 | export function withOptions(handler, options) {
65 | warning(options, 'react-event-listener: should be specified options in withOptions.');
66 |
67 | return {
68 | handler,
69 | options: mergeDefaultEventOptions(options),
70 | };
71 | }
72 |
73 | class EventListener extends React.PureComponent {
74 | componentDidMount() {
75 | this.applyListeners(on);
76 | }
77 |
78 | componentDidUpdate(prevProps) {
79 | this.applyListeners(off, prevProps);
80 | this.applyListeners(on);
81 | }
82 |
83 | componentWillUnmount() {
84 | this.applyListeners(off);
85 | }
86 |
87 | applyListeners(onOrOff, props = this.props) {
88 | const { target } = props;
89 |
90 | if (target) {
91 | let element = target;
92 |
93 | if (typeof target === 'string') {
94 | element = window[target];
95 | }
96 |
97 | forEachListener(props, onOrOff.bind(null, element));
98 | }
99 | }
100 |
101 | render() {
102 | return this.props.children || null;
103 | }
104 | }
105 |
106 | EventListener.propTypes = {
107 | /**
108 | * You can provide a single child too.
109 | */
110 | children: PropTypes.node,
111 | /**
112 | * The DOM target to listen to.
113 | */
114 | target: PropTypes.oneOfType([PropTypes.object, PropTypes.string]).isRequired,
115 | };
116 |
117 | export default EventListener;
118 |
--------------------------------------------------------------------------------
/src/index.test.js:
--------------------------------------------------------------------------------
1 | /* eslint-env mocha */
2 |
3 | import React from 'react';
4 | import PropTypes from 'prop-types';
5 | import { shallow } from 'enzyme';
6 | import { assert } from 'chai';
7 | import { spy } from 'sinon';
8 | import { render, unmountComponentAtNode } from 'react-dom';
9 | import { Simulate } from 'react-dom/test-utils';
10 | import EventListener, { withOptions } from './index';
11 |
12 | describe('EventListener', () => {
13 | describe('prop: children', () => {
14 | it('should work without', () => {
15 | const wrapper = shallow();
16 |
17 | assert.strictEqual(wrapper.children().length, 0, 'Should work without children');
18 | });
19 |
20 | it('should render it', () => {
21 | const wrapper = shallow(
22 |
23 | Foo
24 | ,
25 | );
26 |
27 | assert.strictEqual(wrapper.children().length, 1, 'Should render his children.');
28 | });
29 | });
30 |
31 | let node;
32 | beforeEach(() => {
33 | // Pattern from "react-router": http://git.io/vWpfs
34 | node = document.createElement('div');
35 | document.body.appendChild(node);
36 | });
37 |
38 | afterEach(() => {
39 | unmountComponentAtNode(node);
40 | node.parentNode.removeChild(node);
41 | });
42 |
43 | describe('prop: target', () => {
44 | it('should work with a string', () => {
45 | const handleClick = spy();
46 |
47 | render(, node);
48 | document.body.click();
49 | assert.strictEqual(handleClick.callCount, 1);
50 | });
51 |
52 | it('should work with a node', () => {
53 | const handleClick = spy();
54 |
55 | render(, node);
56 | document.body.click();
57 | assert.strictEqual(handleClick.callCount, 1);
58 | });
59 | });
60 |
61 | [
62 | {
63 | contextName: 'using Simulate.click(extraNode)',
64 | name: 'should not invoke event listener to document',
65 | invokeFn(extraNode) {
66 | Simulate.click(extraNode);
67 | },
68 | expectFn(handle) {
69 | assert.strictEqual(handle.callCount, 0);
70 | },
71 | },
72 | {
73 | contextName: 'using extraNode.click()',
74 | name: 'should invoke event listener to document',
75 | invokeFn(extraNode) {
76 | extraNode.click();
77 | },
78 | expectFn(handle) {
79 | assert.strictEqual(handle.callCount, 1);
80 | },
81 | },
82 | ].forEach(({ contextName, name, invokeFn, expectFn }) => {
83 | describe(contextName, () => {
84 | it(name, () => {
85 | class TextComponent extends React.Component {
86 | static propTypes = {
87 | onClick: PropTypes.func,
88 | };
89 |
90 | handleClick = () => {
91 | this.props.onClick();
92 | };
93 |
94 | render() {
95 | return ;
96 | }
97 | }
98 |
99 | const handleClick = spy();
100 |
101 | render(, node);
102 |
103 | assert.strictEqual(handleClick.callCount, 0);
104 |
105 | const extraNode = document.createElement('button');
106 | document.body.appendChild(extraNode);
107 |
108 | invokeFn(extraNode);
109 | expectFn(handleClick);
110 |
111 | extraNode.parentNode.removeChild(extraNode);
112 | });
113 | });
114 | });
115 |
116 | describe('when props change', () => {
117 | it('removes old listeners', () => {
118 | const handleClick = spy();
119 |
120 | render(, node);
121 | render(, node);
122 |
123 | document.body.click();
124 | assert.strictEqual(handleClick.callCount, 0);
125 | });
126 |
127 | it('adds new listeners', () => {
128 | const handleClick = spy();
129 |
130 | render(, node);
131 |
132 | document.body.click();
133 | assert.strictEqual(handleClick.callCount, 0);
134 |
135 | render(, node);
136 |
137 | document.body.click();
138 | assert.strictEqual(handleClick.callCount, 1);
139 | });
140 |
141 | describe('lifecycle', () => {
142 | let extraNode;
143 |
144 | beforeEach(() => {
145 | extraNode = document.createElement('button');
146 | document.body.appendChild(extraNode);
147 | });
148 |
149 | afterEach(() => {
150 | extraNode.parentNode.removeChild(extraNode);
151 | });
152 |
153 | it('removes listeners from old node', () => {
154 | const handleClick = spy();
155 |
156 | render(, node);
157 | render(, node);
158 |
159 | document.body.click();
160 | assert.strictEqual(handleClick.callCount, 0);
161 | });
162 |
163 | it('adds listeners to new node', () => {
164 | const handleClick = spy();
165 |
166 | render(, node);
167 | render(, node);
168 | document.body.click();
169 | assert.strictEqual(handleClick.callCount, 1);
170 | });
171 | });
172 |
173 | it("doesn't update if props are shallow equal", () => {
174 | const handleClick = () => {};
175 | const inst = render(, node);
176 | const componentWillUpdate = inst.componentWillUpdate;
177 | let updated = false;
178 | inst.componentWillUpdate = (...args) => {
179 | updated = true;
180 | componentWillUpdate.bind(inst)(...args);
181 | };
182 | render(, node);
183 | assert.strictEqual(updated, false);
184 | });
185 | });
186 |
187 | describe('when using capture phase', () => {
188 | it('attaches listeners with capture', () => {
189 | let button;
190 | const calls = [];
191 |
192 | render(
193 |
194 | calls.push('outer')} />
195 |
,
203 | node,
204 | );
205 |
206 | assert.strictEqual(calls.length, 0);
207 | button.click();
208 | assert.deepEqual(calls, ['outer', 'inner'], 'Should be called in the right order.');
209 | });
210 | });
211 |
212 | describe('when using withOptions helper', () => {
213 | it('should return handler function & event options of merging default values', () => {
214 | const obj = withOptions(() => 'test', {});
215 | assert.strictEqual(obj.handler(), 'test');
216 | assert.deepEqual(obj.options, { capture: false, passive: false });
217 | });
218 |
219 | it('should work with using withOptions helper', () => {
220 | const handleClick = spy();
221 |
222 | render(, node);
223 | document.body.click();
224 | assert.strictEqual(handleClick.callCount, 1);
225 | });
226 |
227 | it('attaches listeners with capture (withOptions)', () => {
228 | let button = null;
229 | const calls = [];
230 |
231 | render(
232 |
233 | calls.push('outer'), { capture: true })}
236 | />
237 |
,
245 | node,
246 | );
247 |
248 | assert.strictEqual(calls.length, 0);
249 | button.click();
250 | assert.deepEqual(calls, ['outer', 'inner'], 'Should be called in the right order.');
251 | });
252 | });
253 | });
254 |
--------------------------------------------------------------------------------
/src/supports.js:
--------------------------------------------------------------------------------
1 | function defineProperty(object, property, attr) {
2 | return Object.defineProperty(object, property, attr);
3 | }
4 |
5 | // Passive options
6 | // Inspired by https://github.com/Modernizr/Modernizr/blob/master/feature-detects/dom/passiveeventlisteners.js
7 | export const passiveOption = (() => {
8 | let cache = null;
9 |
10 | return (() => {
11 | if (cache !== null) {
12 | return cache;
13 | }
14 |
15 | let supportsPassiveOption = false;
16 |
17 | try {
18 | window.addEventListener(
19 | 'test',
20 | null,
21 | defineProperty({}, 'passive', {
22 | get() {
23 | supportsPassiveOption = true;
24 | },
25 | }),
26 | );
27 | } catch (err) {
28 | //
29 | }
30 |
31 | cache = supportsPassiveOption;
32 |
33 | return supportsPassiveOption;
34 | })();
35 | })();
36 |
37 | export default {};
38 |
--------------------------------------------------------------------------------
/test/createDOM.js:
--------------------------------------------------------------------------------
1 | const { JSDOM } = require('jsdom');
2 | const Node = require('jsdom/lib/jsdom/living/node-document-position');
3 |
4 | // We can use jsdom-global at some point if maintaining these lists is a burden.
5 | const whitelist = ['HTMLElement', 'Performance'];
6 | const blacklist = ['sessionStorage', 'localStorage'];
7 |
8 | function createDOM() {
9 | const dom = new JSDOM('', { pretendToBeVisual: true });
10 | global.window = dom.window;
11 | global.Node = Node;
12 | global.document = dom.window.document;
13 | global.navigator = {
14 | userAgent: 'node.js',
15 | };
16 |
17 | Object.keys(dom.window)
18 | .filter(key => !blacklist.includes(key))
19 | .concat(whitelist)
20 | .forEach(key => {
21 | if (typeof global[key] === 'undefined') {
22 | global[key] = dom.window[key];
23 | }
24 | });
25 | }
26 |
27 | module.exports = createDOM;
28 |
--------------------------------------------------------------------------------
/test/mocha.opts:
--------------------------------------------------------------------------------
1 | --require @babel/register
2 | --reporter dot
3 | --recursive
4 | test/setup.js
5 | src/{,**/}*.test.js
6 |
--------------------------------------------------------------------------------
/test/setup.js:
--------------------------------------------------------------------------------
1 | import Enzyme from 'enzyme';
2 | import Adapter from 'enzyme-adapter-react-16';
3 | import createDOM from './createDOM';
4 |
5 | Enzyme.configure({ adapter: new Adapter() });
6 | createDOM();
7 |
--------------------------------------------------------------------------------