From 7856d2202081bb07f83d094c83dd68869878aa22 Mon Sep 17 00:00:00 2001 From: Victor Powell Date: Fri, 4 May 2018 23:20:49 -0700 Subject: [PATCH 1/4] Support React 16 --- .gitignore | 3 + CHANGELOG.md | 4 ++ README.md | 23 +++---- example/index.js | 21 +++--- example/package.json | 4 +- index.js | 149 ++++++++++++++++++++++--------------------- package.json | 53 ++++++++------- syntax.js | 2 +- test.js | 86 ++++++++++++------------- 9 files changed, 180 insertions(+), 165 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a07e7b9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules +example/node_modules +example/yarn.lock diff --git a/CHANGELOG.md b/CHANGELOG.md index 581aaaa..c683a6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 3.0.0 + +Remove mixin in favor of HOC. + ## 2.1.1 * make 'keybindingsOnInputs' opt-in, the default is again that inputs do not trigger keybindings diff --git a/README.md b/README.md index c59758b..cb16007 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![build status](https://secure.travis-ci.org/mapbox/react-keybinding.png)](http://travis-ci.org/mapbox/react-keybinding) -Declarative, lightweight, and robust keybindings mixin for React. +Declarative, lightweight, and robust keybindings HOC for React. * Straightforward `'⌘S'` string syntax for declaring bindings * Automatically binds & unbinds keybindings when components mount and unmount @@ -27,10 +27,10 @@ $ npm install react-keybinding ```js var React = require('react'), + ReactDOM = require('react-dom'), Keybinding = require('../'); -var HelloMessage = React.createClass({ - mixins: [Keybinding], - keybindingsPlatformAgnostic: true, +ReactDOM.render(React.createElement(HelloMessage, { + name: "John", keybindings: { '⌘S': function(e) { console.log('save!'); @@ -39,14 +39,11 @@ var HelloMessage = React.createClass({ '⌘C': 'COPY' }, keybinding: function(event, action) { - // event is the browser event, action is 'COPY' + // event is the browser event + // action is 'COPY' console.log(arguments); }, - render: function() { - return React.createElement("div", null, "Hello"); - } -}); -React.render(React.createElement(HelloMessage, {name: "John"}), document.body); +}), document.body); ``` There's a runnable example in the `./examples` directory: to run it, @@ -63,9 +60,9 @@ react-keybinding in an application. ### API -This module exposes a single mixin called `Keybinding`. +This module exposes a single HOC called `withKeybindings`. -Where you use this mixin on Components, it expects a property called +Where you use this HOC on Components, it expects a prop called `keybindings` of the format: ```js @@ -91,7 +88,7 @@ keybindingsPlatformAgnostic: true, keybindings: { ... } ``` -The mixin provides a method for components called `.getAllKeybindings()`: +The HOC provides a method for components called `.getAllKeybindings()`: this yields an array of all `keybindings` properties on all active components. ## [Syntax](SYNTAX.md) diff --git a/example/index.js b/example/index.js index 3cad449..60e957c 100644 --- a/example/index.js +++ b/example/index.js @@ -1,8 +1,16 @@ var React = require('react'), - Keybinding = require('../'); + ReactDOM = require('react-dom'), + withKeybindings = require('../'), + createReactClass = require('create-react-class'); -var HelloMessage = React.createClass({ - mixins: [Keybinding], +var HelloMessage = withKeybindings(createReactClass({ + render: function() { + return React.createElement("div", null, "Hello"); + } +})); + +ReactDOM.render(React.createElement(HelloMessage, { + name: "John", keybindings: { '⌘S': function(e) { console.log('save!'); @@ -15,9 +23,4 @@ var HelloMessage = React.createClass({ // action is 'COPY' console.log(arguments); }, - render: function() { - return React.createElement("div", null, "Hello"); - } -}); - -React.render(React.createElement(HelloMessage, {name: "John"}), document.body); +}), document.body); diff --git a/example/package.json b/example/package.json index e90aea3..251ca3f 100644 --- a/example/package.json +++ b/example/package.json @@ -9,10 +9,10 @@ "author": "", "license": "ISC", "devDependencies": { - "budo": "^0.1.12", + "budo": "^11.2.0", "watchify": "^2.3.0" }, "dependencies": { - "react": "^0.12.2" + "react": "^16.x" } } diff --git a/index.js b/index.js index bfa5543..f02940a 100644 --- a/index.js +++ b/index.js @@ -1,86 +1,91 @@ var React = require('react'), + PropTypes = require('prop-types'), + createReactClass = require('create-react-class'), parseEvents = require('./src/parse_events.js'), isInput = require('./src/is_input.js'), match = require('./src/match.js'); -/** - * A React mixin that provides keybinding support for components - */ -var Keybinding = { + // This function takes a component... +function withKeybindings(WrappedComponent) { + // ...and returns another component... + return createReactClass({ + /** + * Housekeeping to pass around a single array of all + * currently-active keybinding objects. + */ + childContextTypes: { __keybindings: PropTypes.array }, + contextTypes: { __keybindings: PropTypes.array }, + getChildContext: function() { + return { __keybindings: this.__getKeybindings() }; + }, + __getKeybindings: function() { + this.__keybindings = this.__keybindings || + (this.context && this.context.__keybindings) || []; + return this.__keybindings; + }, - /** - * Housekeeping to pass around a single array of all - * currently-active keybinding objects. - */ - childContextTypes: { - __keybindings: React.PropTypes.array - }, - contextTypes: { - __keybindings: React.PropTypes.array - }, - getChildContext: function() { - return { __keybindings: this.__getKeybindings() }; - }, - __getKeybindings: function() { - this.__keybindings = this.__keybindings || - (this.context && this.context.__keybindings) || []; - return this.__keybindings; - }, + /** + * This is the only method meant to be exposed to the user: it + * returns the global keybinding index for the purposes of documentation + * generation. + */ + getAllKeybindings: function() { + return this.__getKeybindings(); + }, - /** - * This is the only method meant to be exposed to the user: it - * returns the global keybinding index for the purposes of documentation - * generation. - */ - getAllKeybindings: function() { - return this.__getKeybindings(); - }, - - /** - * Internal method: avoids firing keybindings in textareas, - * figures out if they match any of the bindings from this component, - * and then either fires an inline method or the .keybinding() method. - */ - __keybinding: function(event) { - if (isInput(event) && !this.keybindingsOnInputs) return; - for (var i = 0; i < this.matchers.length; i++) { - if (match(this.matchers[i].expectation, event)) { - if (typeof this.matchers[i].action === 'function') { - this.matchers[i].action.apply(this, [event]); - } else { - if (typeof this.keybinding !== 'function') { - throw new Error('non-function keybinding action given but no .keybinding method found on component'); + /** + * Internal method: avoids firing keybindings in textareas, + * figures out if they match any of the bindings from this component, + * and then either fires an inline method or the .keybinding() method. + */ + __keybinding: function(event) { + if (isInput(event) && !this.keybindingsOnInputs) return; + for (var i = 0; i < this.matchers.length; i++) { + if (match(this.matchers[i].expectation, event)) { + if (typeof this.matchers[i].action === 'function') { + this.matchers[i].action.apply(this, [event]); + } else { + if (typeof this.props.keybinding !== 'function') { + throw new Error('non-function keybinding action given but no .keybinding method found on component'); + } + this.props.keybinding.call(this, event, this.matchers[i].action); } - this.keybinding(event, this.matchers[i].action); } } - } - }, + }, - /** - * When the component mounts, bind our event listener and - * add our keybindings to the global index. - */ - componentDidMount: function() { - if (this.keybindings !== undefined) { - this.matchers = parseEvents(this.keybindings, !!this.keybindingsPlatformAgnostic); - this.__boundKeybinding = this.__keybinding.bind(this); - document.addEventListener('keydown', this.__boundKeybinding); - this.__getKeybindings().push(this.keybindings); - } - }, + /** + * When the component mounts, bind our event listener and + * add our keybindings to the global index. + */ + componentDidMount: function() { + if (this.props.keybindings !== undefined) { + this.matchers = parseEvents(this.props.keybindings, !!this.props.keybindingsPlatformAgnostic); + this.__boundKeybinding = this.__keybinding.bind(this); + document.addEventListener('keydown', this.__boundKeybinding); + this.__getKeybindings().push(this.props.keybindings); + } + }, - /** - * When the component unmounts, unbind our event listener and - * remove our keybindings from the global index. - */ - componentWillUnmount: function() { - if (this.keybindings !== undefined && this.__boundKeybinding !== undefined) { - document.removeEventListener('keydown', this.__boundKeybinding); - this.__getKeybindings() - .splice(this.__getKeybindings().indexOf(this.keybindings), 1); + /** + * When the component unmounts, unbind our event listener and + * remove our keybindings from the global index. + */ + componentWillUnmount: function() { + if (this.props.keybindings !== undefined && this.__boundKeybinding !== undefined) { + document.removeEventListener('keydown', this.__boundKeybinding); + this.__getKeybindings() + .splice(this.__getKeybindings().indexOf(this.props.keybindings), 1); + } + }, + render: function() { + const props = Object.assign({}, this.props, { + getAllKeybindings: this.getAllKeybindings.bind(this) + }); + return React.createElement(WrappedComponent, props); } - } -}; + }); + +} -module.exports = Keybinding; +module.exports = withKeybindings; diff --git a/package.json b/package.json index 5fc3eca..0f30d46 100644 --- a/package.json +++ b/package.json @@ -1,33 +1,38 @@ { "peerDependencies": { - "react": ">=0.13.0 <16.0.0" - }, - "name": "@mapbox/react-keybinding", - "license": "MIT", - "author": "Tom MacWright", + "react": ">=0.15.0 <17.0.0" + }, + "name": "@mapbox/react-keybinding", + "license": "MIT", + "author": "Tom MacWright", "repository": { - "url": "git@github.com:mapbox/react-keybinding.git", + "url": "git@github.com:mapbox/react-keybinding.git", "type": "git" - }, - "version": "3.0.0", + }, + "version": "3.0.0", "scripts": { - "test": "browserify test.js | tap-closer | smokestack", + "test": "browserify test.js | tap-closer | smokestack", "test-ff": "node test.js && browserify test.js | tap-closer | smokestack -b firefox" - }, + }, "keywords": [ - "keybinding", - "keybindings", - "react", + "keybinding", + "keybindings", + "react", "react-component" - ], + ], "devDependencies": { - "react": "^0.13.3", - "tap-closer": "^1.0.0", - "tape": "^3.5.0", - "happen": "^0.1.3", - "smokestack": "^3.2.0", - "browserify": "^9.0.3" - }, - "main": "index.js", - "description": "declarative, concise keybindings for react" -} + "browserify": "^9.0.3", + "happen": "^0.1.3", + "react": "^16.3.2", + "smokestack": "^3.2.0", + "tap-closer": "^1.0.0", + "tape": "^3.5.0" + }, + "main": "index.js", + "description": "declarative, concise keybindings for react", + "dependencies": { + "create-react-class": "^15.6.3", + "prop-types": "^15.6.1", + "react-dom": "^16.3.2" + } +} \ No newline at end of file diff --git a/syntax.js b/syntax.js index 7922ccb..43be2e3 100644 --- a/syntax.js +++ b/syntax.js @@ -3,7 +3,7 @@ var codes = require('./src/codes'); /** * This file generates the file SYNTAX.md, which lists - * all of the available key combinations supported by this mixin. + * all of the available key combinations supported by this HOC. */ function pairs(o) { return Object.keys(o).map(function(k) { return [k, o[k]]; }); diff --git a/test.js b/test.js index 96c81f5..1ff7c8a 100644 --- a/test.js +++ b/test.js @@ -1,5 +1,5 @@ var test = require('tape'); - +var createReactClass = require('create-react-class'); var formatCode = require('./src/format_code.js'); var parseCode = require('./src/parse_code.js'); @@ -105,28 +105,28 @@ test('parseEvents platformAgnostic', function(t) { t.end(); }); -var React = require('react/addons'), +var React = require('react'), happen = require('happen'), - TestUtils = React.addons.TestUtils; -var Keybinding = require('./'); + TestUtils = require('react-dom/test-utils'); +var withKeybinding = require('./'); if (process.browser) { test('Keybinding: action', function(t) { - var HelloMessage = React.createClass({ - mixins: [Keybinding], - keybindings: { 'C': 'COPY' }, - keybinding: function(event, action) { - t.equal(action, 'COPY'); - t.equal(typeof event, 'object'); - hello_message.componentWillUnmount(); - t.end(); - }, + var HelloMessage = withKeybinding(createReactClass({ render: function() { return React.createElement('div', null); } - }); + })); var hello_message = TestUtils.renderIntoDocument( - React.createElement(HelloMessage)); + React.createElement(HelloMessage, { + keybindings: { 'C': 'COPY' }, + keybinding: function(event, action) { + t.equal(action, 'COPY'); + t.equal(typeof event, 'object'); + hello_message.componentWillUnmount(); + t.end(); + } + })); happen.once(document, { type: 'keydown', @@ -135,22 +135,22 @@ if (process.browser) { }); test('Keybinding: action with meta', function(t) { - var HelloMessage = React.createClass({ - mixins: [Keybinding], - keybindings: { 'cmd+C': 'COPY' }, - keybinding: function(event, action) { - t.equal(action, 'COPY'); - t.equal(typeof event, 'object'); - t.deepEqual(this.getAllKeybindings(), [{ 'cmd+C': 'COPY' }], 'getAllKeybindings'); - hello_message.componentWillUnmount(); - t.deepEqual(this.getAllKeybindings(), [], 'getAllKeybindings after unmount'); - t.end(); - }, + var HelloMessage = withKeybinding(createReactClass({ render: function() { return React.createElement('div', null); } - }); + })); var hello_message = TestUtils.renderIntoDocument( - React.createElement(HelloMessage)); + React.createElement(HelloMessage, { + keybindings: { 'cmd+C': 'COPY' }, + keybinding: function(event, action) { + t.equal(action, 'COPY'); + t.equal(typeof event, 'object'); + t.deepEqual(this.getAllKeybindings(), [{ 'cmd+C': 'COPY' }], 'getAllKeybindings'); + hello_message.componentWillUnmount(); + t.deepEqual(this.getAllKeybindings(), [], 'getAllKeybindings after unmount'); + t.end(); + } + })); happen.once(document, { type: 'keydown', @@ -160,27 +160,26 @@ if (process.browser) { }); test('Keybinding: ?', function(t) { - var HelloMessage = React.createClass({ - mixins: [Keybinding], - keybindings: { '?': function() { t.pass(); t.end(); } }, + var HelloMessage = withKeybinding(createReactClass({ render: function() { return React.createElement('div', null); } - }); + })); var hello_message = TestUtils.renderIntoDocument( - React.createElement(HelloMessage)); + React.createElement(HelloMessage, { + keybindings: { '?': function() { t.pass(); t.end(); } }, + })); happen.once(document, { type: 'keydown', keyCode: 191, shiftKey: true }); }); test('Keybinding: none by myself', function(t) { - var HelloMessage = React.createClass({ - mixins: [Keybinding], + var HelloMessage = withKeybinding(createReactClass({ componentDidMount: function() { - t.deepEqual(this.getAllKeybindings(), []); + t.deepEqual(this.props.getAllKeybindings(), []); t.end(); }, render: function() { return React.createElement('div', null); } - }); + })); var hello_message = TestUtils.renderIntoDocument( React.createElement(HelloMessage)); }); @@ -188,14 +187,13 @@ if (process.browser) { } else { test('headless', function(t) { - var HelloMessage = React.createClass({ - mixins: [Keybinding], - keybindings: { 'C': 'COPY' }, - keybinding: function(event, action) { - }, + var HelloMessage = withKeybinding(createReactClass({ render: function() { return React.createElement('div', null); } - }); - t.ok(React.renderToString(React.createElement(HelloMessage))); + })); + t.ok(React.renderToString(React.createElement(HelloMessage, { + keybindings: { 'C': 'COPY' }, + keybinding: function(event, action) {} + }))); t.end(); }); From f2c6a3dcfd2cb1ab5b303012e02c58a0492d8152 Mon Sep 17 00:00:00 2001 From: Victor Powell Date: Wed, 9 May 2018 17:03:53 -0700 Subject: [PATCH 2/4] Dont bind. React does this for us. --- index.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/index.js b/index.js index f02940a..176a47a 100644 --- a/index.js +++ b/index.js @@ -61,8 +61,7 @@ function withKeybindings(WrappedComponent) { componentDidMount: function() { if (this.props.keybindings !== undefined) { this.matchers = parseEvents(this.props.keybindings, !!this.props.keybindingsPlatformAgnostic); - this.__boundKeybinding = this.__keybinding.bind(this); - document.addEventListener('keydown', this.__boundKeybinding); + document.addEventListener('keydown', this.__keybinding); this.__getKeybindings().push(this.props.keybindings); } }, @@ -72,15 +71,15 @@ function withKeybindings(WrappedComponent) { * remove our keybindings from the global index. */ componentWillUnmount: function() { - if (this.props.keybindings !== undefined && this.__boundKeybinding !== undefined) { - document.removeEventListener('keydown', this.__boundKeybinding); + if (this.props.keybindings !== undefined && this.__keybinding !== undefined) { + document.removeEventListener('keydown', this.__keybinding); this.__getKeybindings() .splice(this.__getKeybindings().indexOf(this.props.keybindings), 1); } }, render: function() { const props = Object.assign({}, this.props, { - getAllKeybindings: this.getAllKeybindings.bind(this) + getAllKeybindings: this.getAllKeybindings }); return React.createElement(WrappedComponent, props); } From 3118b1acf8846b83a9b4f781c137839d83abede2 Mon Sep 17 00:00:00 2001 From: Victor Powell Date: Sat, 12 May 2018 13:34:43 -0700 Subject: [PATCH 3/4] Make react-dom a devDep --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 0f30d46..8f4a17c 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "browserify": "^9.0.3", "happen": "^0.1.3", "react": "^16.3.2", + "react-dom": "^16.3.2", "smokestack": "^3.2.0", "tap-closer": "^1.0.0", "tape": "^3.5.0" @@ -32,7 +33,6 @@ "description": "declarative, concise keybindings for react", "dependencies": { "create-react-class": "^15.6.3", - "prop-types": "^15.6.1", - "react-dom": "^16.3.2" + "prop-types": "^15.6.1" } } \ No newline at end of file From 5b55300c6728affe86a3a1e852fd198606b4238a Mon Sep 17 00:00:00 2001 From: Oleg Date: Tue, 15 May 2018 13:13:51 -0700 Subject: [PATCH 4/4] Update index.js --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index 176a47a..22ae876 100644 --- a/index.js +++ b/index.js @@ -39,7 +39,7 @@ function withKeybindings(WrappedComponent) { * and then either fires an inline method or the .keybinding() method. */ __keybinding: function(event) { - if (isInput(event) && !this.keybindingsOnInputs) return; + if (isInput(event) && !this.props.keybindingsOnInputs) return; for (var i = 0; i < this.matchers.length; i++) { if (match(this.matchers[i].expectation, event)) { if (typeof this.matchers[i].action === 'function') {