Skip to content

Commit

Permalink
Fix propagation issue where child handlers handling short key sequenc…
Browse files Browse the repository at this point in the history
…es would prevent parent long key sequences from firing

AND

Only bother rebinding hotkeys if mappings have changed (on update etc.)
  • Loading branch information
chrisui committed Apr 13, 2015
1 parent 9e43dfa commit 98b65bc
Show file tree
Hide file tree
Showing 2 changed files with 74 additions and 34 deletions.
34 changes: 21 additions & 13 deletions lib/HotKeyMapMixin.js
Original file line number Diff line number Diff line change
@@ -1,43 +1,51 @@
import React from 'react';
import assign from 'lodash/object/assign';
import isEqual from 'lodash/lang/isEqual';

export default function HotKeyMapMixin(hotKeyMap = {}) {

return {

contextTypes: {
hotKeyMap: React.PropTypes.object
},

childContextTypes: {
hotKeyMap: React.PropTypes.object
},

getChildContext() {
return {
hotKeyMap: this.__hotKeyMap__
};
},

componentWillMount() {
this.updateMap();
},

updateMap() {
this.__hotKeyMap__ = this.buildMap();
const newMap = this.buildMap();

if (!isEqual(newMap, this.__hotKeyMap__)) {
this.__hotKeyMap__ = newMap;
return true;
}

return false;
},

buildMap() {
const parentMap = this.context.hotKeyMap || {};
const thisMap = this.props.keyMap || {};

return assign({}, parentMap, hotKeyMap, thisMap);
},

getMap() {
return this.__hotKeyMap__;
}

};
};

};
74 changes: 53 additions & 21 deletions lib/HotKeys.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,21 @@ import isArray from 'lodash/lang/isArray';
import forEach from 'lodash/collection/forEach';

function getSequencesFromMap(hotKeyMap, hotKeyName) {
const sequences = hotKeyMap[hotKeyName];
const sequences = hotKeyMap[hotKeyName];

// If no sequence is found with this name we assume
// the user is passing a hard-coded sequence as a key
if (!sequences) {
return [hotKeyName];
}

if (isArray(sequences)) {
return sequences;
}
// If no sequence is found with this name we assume
// the user is passing a hard-coded sequence as a key
if (!sequences) {
return [hotKeyName];
}

return [sequences];
if (isArray(sequences)) {
return sequences;
}

return [sequences];
}

const HotKeys = React.createClass({

mixins: [HotKeyMapMixin()],
Expand All @@ -33,27 +33,48 @@ const HotKeys = React.createClass({
handlers: React.PropTypes.object
},

contextTypes: {
hotKeyParent: React.PropTypes.any
},

childContextTypes: {
hotKeyParent: React.PropTypes.any
},

getChildContext() {
return {
hotKeyParent: this
};
},

componentDidMount() {
// Not optimal - imagine hundreds of this component. We need a top level
// delegation point for mousetrap
this.__mousetrap__ = new Mousetrap(
React.findDOMNode(this.refs.focusTrap)
);

this.updateHotKeys();
this.updateHotKeys(true);
},

componentWillReceiveProps() {
componentDidUpdate() {
this.updateHotKeys();
},

componentWillUnmount() {
if (this.context.hotKeyParent) {
this.context.hotKeyParent.childHandledSequence(null);
}

this.__mousetrap__.reset();
},

updateHotKeys() {
updateHotKeys(force = false) {
// Ensure map is up-to-date to begin with
this.updateMap();
// We will only bother continuing if the map was actually updated
if (!this.updateMap() && !force) {
return;
}

const {handlers = {}} = this.props;
const hotKeyMap = this.getMap();
Expand All @@ -67,12 +88,14 @@ const HotKeys = React.createClass({
// Could be optimized as every handler will get called across every bound
// component - imagine making a node a focus point and then having hundreds!
forEach(handlerSequences, (sequence) => {
sequenceHandlers[sequence] = (event) => {
if (this.__isFocused__) {
// Stopping propagation breaks long key sequences if a portion is handled
// up the tree. We need the central manager/delegator!
event.stopPropagation();
return handler(event);
sequenceHandlers[sequence] = (event, sequence) => {
// Check we are actually in focus and that a child hasn't already handled this sequence
if (this.__isFocused__ && sequence !== this.__lastChildSequence__) {
if (this.context.hotKeyParent) {
this.context.hotKeyParent.childHandledSequence(sequence);
}

return handler(event, sequence);
}
}
});
Expand All @@ -84,6 +107,15 @@ const HotKeys = React.createClass({
mousetrap.bind(sequence, handler));
},

childHandledSequence(sequence = null) {
this.__lastChildSequence__ = sequence;

// Traverse up any hot key parents so everyone is aware a child has handled a certain sequence
if (this.context.hotKeyParent) {
this.context.hotKeyParent.childHandledSequence(sequence);
}
},

onFocus() {
this.__isFocused__ = true;

Expand Down

0 comments on commit 98b65bc

Please sign in to comment.