This repository has been archived by the owner on Dec 13, 2020. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 35
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Revert "Revert "Refactor shortcuts""
- Loading branch information
1 parent
896ad0d
commit f429f9c
Showing
32 changed files
with
1,225 additions
and
585 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
Oops, something went wrong.