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

Commit

Permalink
Fire latest subscribed handler when key combination is made
Browse files Browse the repository at this point in the history
Issue #1283
  • Loading branch information
pablosichert committed Nov 16, 2017
1 parent 6295286 commit d69a136
Show file tree
Hide file tree
Showing 2 changed files with 191 additions and 2 deletions.
48 changes: 47 additions & 1 deletion src/components/Shortcuts/ShortcutProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,60 @@ export default class ShortcutProvider extends Component {
unsubscribe: PropTypes.func.isRequired
};

hotkeys = {};
keySequence = [];
fired = {};

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

hotkeys = {};
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, hotkeys } = this;

if (fired[key]) {
return;
}

fired[key] = true;

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

const serializedSequence = this.keySequence.join('+').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 "${keySequence}" is not a function.`);
};

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

register = hotkeys => {
this.hotkeys = hotkeys;
Expand Down
145 changes: 144 additions & 1 deletion src/components/Shortcuts/ShortcutProvider.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-env mocha */
import chai, { expect } from 'chai';
import { stub } from 'sinon';
import { spy, stub } from 'sinon';
import sinonChai from 'sinon-chai';
import { Component } from 'react';
import ShortcutProvider from './ShortcutProvider';
Expand All @@ -24,6 +24,49 @@ describe('Shortcuts', () => {
expect(shortcutProvider.render()).to.equal(children);
});

it('should clean up event listeners', () => {
const listeners = [];

global.document = {
addEventListener: (event, handler) => {
listeners.push([event, handler]);
},
removeEventListener: (event, handler) => {
const indices = [];
let i = 0;

for (const listener of listeners) {
if (
listener[0] === event &&
listener[1] === handler
) {
indices.push(i);
}

i++;
}

for (const index of indices.reverse()) {
listeners.splice(index, 1);
}
}
};

try {
const shortcutProvider = new ShortcutProvider;

shortcutProvider.componentWillMount();

shortcutProvider.componentWillUnmount();

expect(listeners).to.have.length(0);
} catch (error) {
throw error;
} finally {
delete global.document;
}
});

it('should expose context', () => {
const shortcutProvider = new ShortcutProvider;

Expand Down Expand Up @@ -169,4 +212,104 @@ describe('Shortcuts', () => {
}
});
});

describe('handleKeyDown', () => {
it('should not fire multiple times for a single key', () => {
const shortcutProvider = new ShortcutProvider;

const key = 'a';

expect(shortcutProvider.fired[key]).to.equal(undefined);

shortcutProvider.handleKeyDown({ key });

expect(shortcutProvider.fired[key]).to.equal(true);
expect(shortcutProvider.keySequence).to.deep.equal(['a']);

shortcutProvider.handleKeyDown({ key });

expect(shortcutProvider.fired[key]).to.equal(true);
expect(shortcutProvider.keySequence).to.deep.equal(['a']);
});

it('should call latest registered handler for that hotkey', () => {
const shortcutProvider = new ShortcutProvider;

const key = 'A';

const handler1 = spy();
const handler2 = spy();
const handler3 = spy();

shortcutProvider.hotkeys = {
[key]: [ handler1, handler2, handler3 ]
};

shortcutProvider.handleKeyDown({ key });

expect(handler1).to.not.have.been.called;
expect(handler1).to.not.have.been.called;
expect(handler3).to.have.been.called;
});

it('should call handler with event as argument', () => {
const shortcutProvider = new ShortcutProvider;

const key = 'A';

const handler = spy();

shortcutProvider.hotkeys = {
[key]: [ handler ]
};

const event = { key };

shortcutProvider.handleKeyDown(event);

expect(handler).to.have.been.calledWith(event);
});

it('should warn when handler is not a function', () => {
const warn = stub(console, 'warn');

try {
const shortcutProvider = new ShortcutProvider;

const key = 'A';

const handler = null;

shortcutProvider.hotkeys = {
[key]: [ handler ]
};

shortcutProvider.handleKeyDown({ key });

expect(warn).to.have.been.called;
} catch (error) {
throw error;
} finally {
warn.restore();
}
});
});

describe('handleKeyUp', () => {
it('should reset key sequence', () => {
const shortcutProvider = new ShortcutProvider;

const key1 = 'a';
const key2 = 'A';
const key3 = 'b';
const key4 = 'c';

shortcutProvider.keySequence = [ key1, key2, key3, key4 ];

shortcutProvider.handleKeyUp();

expect(shortcutProvider.keySequence).to.deep.equal([]);
expect(shortcutProvider.fired).to.deep.equal({});
});
});
});

0 comments on commit d69a136

Please sign in to comment.