Skip to content
This repository has been archived by the owner on Dec 13, 2020. It is now read-only.

Commit

Permalink
Revert "Revert "Refactor shortcuts""
Browse files Browse the repository at this point in the history
Issue #1283

This reverts commit 2cff8b5.
  • Loading branch information
pablosichert committed Nov 20, 2017
1 parent 896ad0d commit f429f9c
Show file tree
Hide file tree
Showing 32 changed files with 1,225 additions and 585 deletions.
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,6 @@
"react-redux": "~5.0.6",
"react-router": "^3.2.0",
"react-router-redux": "^4.0.8",
"react-shortcuts": "~2.0.0",
"react-tagsinput": "~3.18.0",
"react-translate-component": "~0.14.0",
"redux": "~3.7.2",
Expand Down
38 changes: 38 additions & 0 deletions src/components/Shortcuts/Shortcut.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Component } from 'react';
import PropTypes from 'prop-types';
import keymap from '../../shortcuts/keymap';

export default class Shortcut extends Component {
static contextTypes = {
shortcuts: PropTypes.shape({
subscribe: PropTypes.func.isRequired,
unsubscribe: PropTypes.func.isRequired
}).isRequired
};

static propTypes = {
name: PropTypes.oneOf(Object.keys(keymap)).isRequired,
handler: PropTypes.func.isRequired
};

componentWillMount() {
const { subscribe } = this.context.shortcuts;
const { name, handler } = this.props;

this.name = name;
this.handler = handler;

subscribe(name, handler);
}

componentWillUnmount() {
const { unsubscribe } = this.context.shortcuts;
const { name, handler } = this;

unsubscribe(name, handler);
}

render() {
return null;
}
}
81 changes: 81 additions & 0 deletions src/components/Shortcuts/Shortcut.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/* eslint-env mocha */
import chai, { expect } from 'chai';
import { spy } from 'sinon';
import sinonChai from 'sinon-chai';
import { Component } from 'react';
import Shortcut from './Shortcut';

chai.use(sinonChai);

describe('Shortcut', () => {
it('should be a React component', () => {
expect(Shortcut).to.be.an.instanceOf(Component.constructor);
});

it('should return null from render()', () => {
const shortcut = new Shortcut();

expect(shortcut.render()).to.equal(null);
});

it('should subscribe when mounting', () => {
const shortcut = new Shortcut();

const subscribe = spy();

const name = 'Foo';
const handler = () => {};

shortcut.props = { name, handler };
shortcut.context = { shortcuts: { subscribe } };

shortcut.componentWillMount();

expect(subscribe).to.have.been.calledWith(name, handler);
});

it('should unsubscribe when unmounting', () => {
const shortcut = new Shortcut();

const unsubscribe = spy();

shortcut.context = { shortcuts: { unsubscribe } };

const name = 'Foo';
const handler = () => {};

shortcut.name = name;
shortcut.handler = handler;

shortcut.componentWillUnmount();

expect(unsubscribe).to.have.been.calledWith(name, handler);
});

it('should unsubscribe the same attributes which were subscribed', () => {
const shortcut = new Shortcut();

const subscribe = spy();
const unsubscribe = spy();

shortcut.context = { shortcuts: { subscribe, unsubscribe } };

const name1 = 'Foo';
const handler1 = () => {};

shortcut.props = { name: name1, handler: handler1 };

shortcut.componentWillMount();

expect(subscribe).to.have.been.calledWith(name1, handler1);

const name2 = 'Bar';
const handler2 = () => {};

shortcut.props = { name: name2, handler: handler2 };

shortcut.componentWillUnmount();

expect(unsubscribe).to.have.been.calledWith(name1, handler1);
});
});
126 changes: 126 additions & 0 deletions src/components/Shortcuts/ShortcutProvider.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { Component } from 'react';
import PropTypes from 'prop-types';

export default class ShortcutProvider extends Component {
static propTypes = {
children: PropTypes.node,
hotkeys: PropTypes.object.isRequired,
keymap: PropTypes.object.isRequired
};

static childContextTypes = {
shortcuts: PropTypes.shape({
subscribe: PropTypes.func.isRequired,
unsubscribe: PropTypes.func.isRequired
})
};

keySequence = [];
fired = {};

getChildContext() {
return {
shortcuts: {
subscribe: this.subscribe,
unsubscribe: this.unsubscribe
}
};
}

componentWillMount() {
document.addEventListener('keydown', this.handleKeyDown);
document.addEventListener('keyup', this.handleKeyUp);
}

componentWillUnmount() {
document.removeEventListener('keydown', this.handleKeyDown);
document.removeEventListener('keyup', this.handleKeyUp);
}

handleKeyDown = event => {
const { key } = event;
const { keySequence, fired } = this;
const { hotkeys } = this.props;

if (fired[key]) {
return;
}

fired[key] = true;

this.keySequence = [...keySequence, key];

const serializedSequence = (this.keySequence
.join('+')
.replace(/\s/, 'space')
.toUpperCase()
);

if (!(serializedSequence in hotkeys)) {
return;
}

const bucket = hotkeys[serializedSequence];
const handler = bucket[bucket.length - 1];

if (typeof handler === 'function') {
return handler(event);
}

// eslint-disable-next-line max-len
console.warn(`Handler defined for key sequence "${serializedSequence}" is not a function.`, handler);
};

handleKeyUp = () => {
this.keySequence = [];
this.fired = {};
};

subscribe = (name, handler) => {
const { hotkeys, keymap } = this.props;

if (!(name in this.props.keymap)) {
console.warn(`There are no hotkeys defined for "${name}".`);

return;
}

const key = keymap[name].toUpperCase();
const bucket = hotkeys[key];

hotkeys[key] = [...bucket, handler];
};

unsubscribe = (name, handler) => {
const { hotkeys, keymap } = this.props;

if (!(name in this.props.keymap)) {
console.warn(`There are no hotkeys defined for "${name}".`);

return;
}

const key = keymap[name].toUpperCase();
const bucket = hotkeys[key];
let found = false;

hotkeys[key] = bucket.filter(_handler => {
if (_handler === handler) {
found = true;

return false;
}

return true;
});

if (!found) {
// eslint-disable-next-line max-len
console.warn(`The handler you are trying to unsubscribe from "${name}" has not been subscribed yet.`, handler);
}
};

render() {
return this.props.children;
}
}
Loading

0 comments on commit f429f9c

Please sign in to comment.