Repository: avocode/react-shortcuts
Branch: master
Commit: c02d2bff6a73
Files: 25
Total size: 54.1 KB
Directory structure:
gitextract_gjvnrzkr/
├── .babelrc
├── .eslintrc
├── .gitignore
├── .npmignore
├── .travis.yml
├── LICENSE.md
├── README.md
├── example/
│ ├── app.js
│ ├── index.html
│ ├── keymap.js
│ ├── main.js
│ └── main.less
├── package.json
├── src/
│ ├── component/
│ │ ├── index.js
│ │ └── shortcuts.js
│ ├── helpers.js
│ ├── index.js
│ ├── shortcut-manager.js
│ └── utils.js
├── test/
│ ├── keymap.js
│ ├── mocha.opts
│ ├── shortcut-manager.spec.js
│ ├── shortcuts.spec.js
│ └── utils.js
└── webpack.config.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .babelrc
================================================
{
"presets": ["es2015", "stage-0", "react"],
"plugins": ["add-module-exports"],
"env": {
"production": {
"presets": ["react-optimize"]
}
}
}
================================================
FILE: .eslintrc
================================================
{
"parser": "babel-eslint",
"plugins": [
"import",
"react"
],
"extends": [ "airbnb" ],
"env": {
"browser": true,
"mocha": true,
"node": true
},
"rules": {
"array-bracket-spacing": 0,
"arrow-body-style": 0,
"comma-dangle": [ 2, "always-multiline" ],
"consistent-return": 0,
"default-case": 0,
"dot-notation": 0,
"func-names": 0,
"global-require": 0,
"import/default": 2,
"import/export": 2,
"import/imports-first": 0,
"import/named": 2,
"import/namespace": 2,
"import/no-extraneous-dependencies": [
"warn", {
"devDependencies": true
}
],
"import/no-unresolved": [
"error", {
"commonjs": true,
"amd": true
}
],
"import/prefer-default-export": 1,
"jsx-quotes": 0,
"new-cap": 0,
"max-len": 0,
"no-console": 0,
"no-fallthrough": 1,
"no-global-assign": 0,
"no-irregular-whitespace": [
"error", {
"skipStrings": true,
"skipTemplates": true,
"skipRegExps": true
}
],
"no-lonely-if": 0,
"no-param-reassign": 0,
"no-shadow": 1,
"no-underscore-dangle": 0,
"no-unsafe-negation": 0,
"no-unused-expressions": 0,
"no-unused-vars": [
"warn", {
"vars": "all",
"args": "none"
}
],
"no-use-before-define": [
"warn", {
"functions": false,
"classes": true
}
],
"quote-props": 0,
"react/no-find-dom-node": 1,
"react/prop-types": 1,
"react/no-did-mount-set-state": 1,
"react/no-did-update-set-state": 1,
"react/prefer-stateless-function": 0,
"react/jsx-curly-spacing": 0,
"react/jsx-no-bind": 0,
"react/jsx-filename-extension": 0,
"semi": [ 2, "never" ],
},
"globals": {
"require": false,
"ga": false
},
"settings": {
"import/ignore": [
"node_modules",
"\\.json$"
],
"import/resolver": {
"webpack": {
"config": "webpack-js.config.js"
}
},
"import/parser": "babel-eslint",
}
}
================================================
FILE: .gitignore
================================================
# OS garbage
.DS_Store
Thumbs.db
# built sources
dist/
lib/
# npm stuff
node_modules/
npm-debug.log
coverage/
================================================
FILE: .npmignore
================================================
.babelrc
example
test
*.coffee
*.sh
*.md
*.yml
webpack.config.js
coverage
dist
src
================================================
FILE: .travis.yml
================================================
language: node_js
node_js:
- "12"
================================================
FILE: LICENSE.md
================================================
The MIT License (MIT)
Copyright (c) 2015 Petr Brzek
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
================================================
FILE: README.md
================================================
React Shortcuts
=========
**Manage keyboard shortcuts from one place.**
[][travis]
Intro
------
Managing keyboard shortcuts can sometimes get messy. Or always, if not implemented the right way.
Real problems:
- You can't easily tell which shortcut is bound to which component
- You have to write a lot of boilerplate code (`addEventListeners`, `removeEventListeners`, ...)
- Memory leaks are a real problem if components don’t remove their listeners properly
- Platform specific shortcuts is another headache
- It's more difficult to implement feature like user-defined shortcuts
- You can't easily get allthe application shortcuts and display it (e.g. in settings)
**React shortcuts to the rescue!**
-----------
With `react-shortcuts` you can declaratively manage shortcuts for each one of your React components.
**Important parts of React Shortcuts:**
- Your `keymap` definition
- `ShortcutManager` which handles `keymap`
- `<Shortcut>` component for handling shortcuts
Try online demo
-------
[](https://codesandbox.io/s/l40jjo48nl)
Quick tour
----------
#### 1. `npm install react-shortcuts`
#### 2. **Define application shortcuts**
Create a new JS, Coffee, JSON or CSON file wherever you want (which probably is your project root). And define the shortcuts for your React component.
**Keymap definition**
```json
{
"Namespace": {
"Action": "Shortcut",
"Action_2": ["Shortcut", "Shortcut"],
"Action_3": {
"osx": "Shortcut",
"windows": ["Shortcut", "Shortcut"],
"linux": "Shortcut",
"other": "Shortcut"
}
}
}
```
- `Namespace` should ideally be the component’s `displayName`.
- `Action` describes what will be happening. For example `MODAL_CLOSE`.
- `Keyboard shortcut` can be a string, array of strings or an object which
specifies platform differences (Windows, OSX, Linux, other). The
shortcut may be composed of single keys (`a`, `6`,…), combinations
(`command+shift+k`) or sequences (`up up down down left right left right B A`).
> **Combokeys** is used under the
hood for handling the shortcuts. [Read more][mousetrap] about how you can
specify keys.
##### Example `keymap` definition:
```javascript
export default {
TODO_ITEM: {
MOVE_LEFT: 'left',
MOVE_RIGHT: 'right',
MOVE_UP: ['up', 'w'],
DELETE: {
osx: ['command+backspace', 'k'],
windows: 'delete',
linux: 'delete',
},
},
}
```
Save this file as `keymap.[js|coffee|json|cson]` and require it into your main
file.
```javascript
import keymap from './keymap'
```
#### 3. Rise of the ShortcutsManager
Define your keymap in whichever supported format but in the end it must be an
object. `ShortcutsManager` can’t parse JSON and will certainly not be happy
about the situation.
```javascript
import keymap from './keymap'
import { ShortcutManager } from 'react-shortcuts'
const shortcutManager = new ShortcutManager(keymap)
// Or like this
const shortcutManager = new ShortcutManager()
shortcutManager.setKeymap(keymap)
```
#### 4. Include `shortcutManager` into getChildContext of some parent component. So that `<shortcuts>` can receive it.
```javascript
class App extends React.Component {
getChildContext() {
return { shortcuts: shortcutManager }
}
}
App.childContextTypes = {
shortcuts: PropTypes.object.isRequired
}
```
#### 5. Require the <shortcuts> component
You need to require the component in the file you want to use shortcuts in.
For example `<TodoItem>`.
```javascript
import { Shortcuts } from `react-shortcuts`
class TodoItem extends React.Component {
_handleShortcuts = (action, event) => {
switch (action) {
case 'MOVE_LEFT':
console.log('moving left')
break
case 'MOVE_RIGHT':
console.log('moving right')
break
case 'MOVE_UP':
console.log('moving up')
break
case 'COPY':
console.log('copying stuff')
break
}
}
render() {
return (
<Shortcuts
name='TODO_ITEM'
handler={this._handleShortcuts}
>
<div>Make something amazing today</div>
</Shortcuts>
)
}
}
```
> The `<Shortcuts>` component creates a `<shortcuts>` element in HTML, binds
listeners and adds tabIndex to the element so that it’s focusable.
`_handleShortcuts` is invoked when some of the defined shortcuts fire.
## Custom props for `<Shortcuts>` component
- `handler`: func
- callback function that will fire when a shortcut occurs
- `name`: string
- The name of the namespace specified in keymap file
- `tabIndex`: number
- Default is `-1`
- `className`: string
- `eventType`: string
- Just for gourmets (keyup, keydown, keypress)
- `stopPropagation`: bool
- `preventDefault`: bool
- `targetNodeSelector`: DOM Node Selector like `body` or `.my-class`
- Use this one with caution. It binds listeners to the provided string instead
of the component.
- `global`: bool
- Use this when you have some global app wide shortcuts like `CMD+Q`.
- `isolate`: bool
- Use this when a child component has React's key handler (onKeyUp, onKeyPress, onKeyDown). Otherwise, React Shortcuts stops propagation of that event due to nature of event delegation that React uses internally.
- `alwaysFireHandler`: bool
- Use this when you want events keep firing on the focused input elements.
## Thanks, Atom
This library is inspired by [Atom Keymap].
[Atom Keymap]: https://github.com/atom/atom-keymap/
[travis]: https://travis-ci.org/avocode/react-shortcuts
[mousetrap]: https://craig.is/killing/mice
[keymaps]: https://github.com/atom/atom-keymap/
================================================
FILE: example/app.js
================================================
import React from 'react'
import PropTypes from 'prop-types'
import createClass from 'create-react-class'
import ReactDOMFactories from 'react-dom-factories'
let { Shortcuts } = require('../src')
Shortcuts = React.createFactory(Shortcuts)
const { button, div, h1, p } = ReactDOMFactories
export default createClass({
displayName: 'App',
childContextTypes: {
shortcuts: PropTypes.object.isRequired,
},
getInitialState() {
return { show: true, who: 'Nobody' }
},
getChildContext() {
return { shortcuts: this.props.shortcuts }
},
_handleShortcuts(command) {
switch (command) {
case 'MOVE_LEFT': return this.setState({ who: 'Hemingway - left' })
case 'DELETE': return this.setState({ who: 'Hemingway - delete' })
case 'MOVE_RIGHT': return this.setState({ who: 'Hemingway - right' })
case 'MOVE_UP': return this.setState({ who: 'Hemingway - top' })
}
},
_handleShortcuts2(command) {
switch (command) {
case 'MOVE_LEFT': return this.setState({ who: 'Franz Kafka - left' })
case 'DELETE': return this.setState({ who: 'Franz Kafka - delete' })
case 'MOVE_RIGHT': return this.setState({ who: 'Franz Kafka - right' })
case 'MOVE_UP': return this.setState({ who: 'Franz Kafka - top' })
}
},
_handleRoot(command) {
this.setState({ who: 'Root shortcuts component' })
},
_rebind() {
this.setState({ show: false })
setTimeout(() => {
this.setState({ show: true })
}, 100)
},
render() {
if (!this.state.show) {
return null
}
return (
div({ className: 'root' },
h1({ className: 'who' }, this.state.who),
button({ className: 'rebind', onClick: this._rebind }, 'Rebind listeners'),
Shortcuts({
name: this.constructor.displayName,
handler: this._handleShortcuts,
targetNodeSelector: '#app',
className: 'content',
},
div(null,
h1(null, 'Hemingway'),
p(null, 'Far far away, behind the word mountains, far from the countries Vokalia and Consonantia, there live the blind texts. Separated they live in Bookmarksgrove right at the coast of the Semantics, a large language ocean. A small river named Duden flows by their place and supplies it with the necessary regelialia.')
)
),
Shortcuts({
name: this.constructor.displayName,
handler: this._handleShortcuts2,
stopPropagation: true,
className: 'content',
},
div(null,
h1(null, 'Franz Kafka'),
p(null, 'One morning, when Gregor Samsa woke from troubled dreams, he found himself transformed in his bed into a horrible vermin. He lay on his armour-like back, and if he lifted his head a little he could see his brown belly, slightly domed and divided by arches into stiff sections.')
)
)
)
)
},
})
================================================
FILE: example/index.html
================================================
<!doctype html>
<meta charset="utf-8">
<div id="app"></div>
<script src="/index.js"></script>
================================================
FILE: example/keymap.js
================================================
export default {
App: {
MOVE_LEFT: 'left',
MOVE_RIGHT: 'right',
MOVE_UP: ['up', 'w'],
DELETE: {
osx: ['command+backspace', 'k'],
windows: 'delete',
linux: 'delete',
},
},
}
================================================
FILE: example/main.js
================================================
import React from 'react'
import ReactDOM from 'react-dom'
import './main.less'
import keymap from './keymap'
import App from './app'
import { ShortcutManager } from '../src'
const shortcutManager = new ShortcutManager(keymap)
// Just for testing
window.shortcutManager = shortcutManager
const element = React.createElement(App, { shortcuts: shortcutManager })
ReactDOM.render(element, document.getElementById('app'))
================================================
FILE: example/main.less
================================================
html {
color: #fff;
background: #222;
line-height: 1.5;
}
.root {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
.who {
text-align: center;
}
.content {
width: 400px;
margin: 15px auto;
background: #535394;
padding: 20px;
display: flex;
}
================================================
FILE: package.json
================================================
{
"name": "react-shortcuts",
"description": "React shortcuts",
"version": "2.1.0",
"license": "MIT",
"main": "./lib/",
"maintainers": [
{
"name": "Petr Brzek",
"email": "petr@avocode.com"
}
],
"keywords": [
"react",
"react-component",
"keyboard",
"shortcuts",
"mousetrap"
],
"scripts": {
"prepublish": "babel src/ -d lib/",
"start": "webpack-dev-server --hot --progress --colors",
"test": "mocha"
},
"dependencies": {
"combokeys": "^3.0.1",
"events": "^1.0.2",
"invariant": "^2.1.0",
"just-reduce-object": "^1.0.3",
"platform": "^1.3.0",
"prop-types": "^15.5.8"
},
"peerDependencies": {
"react": "^0.14.8 || ^15 || ^16",
"react-dom": "^0.14.8 || ^15 || ^16"
},
"repository": {
"type": "git",
"url": "git://github.com/avocode/react-shortcuts.git",
"web": "http://github.com/avocode/react-shortcuts"
},
"bugs": {
"url": "http://github.com/avocode/react-shortcuts/issues"
},
"devDependencies": {
"babel-cli": "^6.14.0",
"babel-core": "^6.14.0",
"babel-eslint": "^7.1.1",
"babel-loader": "^6.2.5",
"babel-plugin-add-module-exports": "^0.2.1",
"babel-polyfill": "^6.13.0",
"babel-preset-es2015": "^6.14.0",
"babel-preset-react": "^6.11.1",
"babel-preset-react-optimize": "^1.0.1",
"babel-preset-stage-0": "^6.5.0",
"chai": "^3.5.0",
"chai-enzyme": "^1.0.0-beta.1",
"cheerio": "^0.20.0",
"create-react-class": "^15.6.3",
"css-loader": "^0.15.6",
"enzyme": "^3.0.0",
"enzyme-adapter-react-16": "^1.15.1",
"eslint": "^3.10.2",
"eslint-config-airbnb": "^13.0.0",
"eslint-import-resolver-webpack": "^0.7.0",
"eslint-plugin-import": "^2.2.0",
"eslint-plugin-jsx-a11y": "^2.2.3",
"eslint-plugin-react": "^6.7.1",
"eslint-plugin-standard": "^2.0.1",
"istanbul": "^0.3.18",
"jsdom": "^8.0.4",
"less": "^2.5.1",
"less-loader": "^2.2.0",
"lodash": "^4.15.0",
"mocha": "^2.2.5",
"react": "^16",
"react-dom": "^16",
"react-dom-factories": "^1.0.2",
"simulant": "^0.2.2",
"sinon": "^1.17.5",
"sinon-chai": "^2.8.0",
"style-loader": "^0.12.3",
"webpack": "^1.11.0",
"webpack-dev-server": "^1.10.1"
}
}
================================================
FILE: src/component/index.js
================================================
module.exports = require('./shortcuts')
================================================
FILE: src/component/shortcuts.js
================================================
import React from 'react'
import invariant from 'invariant'
import Combokeys from 'combokeys'
import PropTypes from 'prop-types'
import helpers from '../helpers'
export default class extends React.Component {
static displayName = 'Shortcuts';
static contextTypes = {
shortcuts: PropTypes.object.isRequired,
};
static propTypes = {
children: PropTypes.node,
handler: PropTypes.func,
name: PropTypes.string,
tabIndex: PropTypes.number,
className: PropTypes.string,
eventType: PropTypes.string,
stopPropagation: PropTypes.bool,
preventDefault: PropTypes.bool,
targetNodeSelector: PropTypes.string,
global: PropTypes.bool,
isolate: PropTypes.bool,
alwaysFireHandler: PropTypes.bool,
};
static defaultProps = {
tabIndex: -1,
className: null,
eventType: null,
stopPropagation: true,
preventDefault: false,
targetNodeSelector: null,
global: false,
isolate: false,
alwaysFireHandler: false,
};
componentDidMount() {
this._onUpdate()
if (this.props.name) {
this.context.shortcuts.addUpdateListener(this._onUpdate)
}
}
componentWillUnmount() {
this._unbindShortcuts()
if (this.props.name) {
this.context.shortcuts.removeUpdateListener(this._onUpdate)
}
if (this.props.global) {
const element = this._getElementToBind()
element.removeEventListener(
'shortcuts:global',
this._customGlobalHandler
)
}
}
// NOTE: combokeys must be instance per component
_combokeys = null;
_lastEvent = null;
_bindShortcuts = (shortcutsArr) => {
const element = this._getElementToBind()
element.setAttribute('tabindex', this.props.tabIndex)
this._combokeys = new Combokeys(element, { storeInstancesGlobally: false })
this._decorateCombokeys()
this._combokeys.bind(
shortcutsArr,
this._handleShortcuts,
this.props.eventType
)
if (this.props.global) {
element.addEventListener('shortcuts:global', this._customGlobalHandler)
}
};
_customGlobalHandler = (e) => {
const { character, modifiers, event } = e.detail
let targetNode = null
if (this.props.targetNodeSelector) {
targetNode = document.querySelector(this.props.targetNodeSelector)
}
if (e.target !== this._domNode && e.target !== targetNode) {
this._combokeys.handleKey(character, modifiers, event, true)
}
};
_decorateCombokeys = () => {
const element = this._getElementToBind()
const originalHandleKey = this._combokeys.handleKey.bind(this._combokeys)
// NOTE: stopCallback is a method that is called to see
// if the keyboard event should fire
this._combokeys.stopCallback = (event, domElement, combo) => {
const isInputLikeElement = domElement.tagName === 'INPUT' ||
domElement.tagName === 'SELECT' ||
domElement.tagName === 'TEXTAREA' ||
(domElement.contentEditable && domElement.contentEditable === 'true')
let isReturnString
if (event.key) {
isReturnString = event.key.length === 1
} else {
isReturnString = Boolean(helpers.getCharacter(event))
}
if (
isInputLikeElement && isReturnString && !this.props.alwaysFireHandler
) {
return true
}
return false
}
this._combokeys.handleKey = (
character,
modifiers,
event,
isGlobalHandler
) => {
if (
this._lastEvent &&
event.timeStamp === this._lastEvent.timeStamp &&
event.type === this._lastEvent.type
) {
return
}
this._lastEvent = event
let isolateOwner = false
if (this.props.isolate && !event.__isolateShortcuts) {
event.__isolateShortcuts = true
isolateOwner = true
}
if (!isGlobalHandler) {
element.dispatchEvent(
new CustomEvent('shortcuts:global', {
detail: { character, modifiers, event },
bubbles: true,
cancelable: true,
})
)
}
// NOTE: works normally if it's not an isolated event
if (!event.__isolateShortcuts) {
if (this.props.preventDefault) {
event.preventDefault()
}
if (this.props.stopPropagation && !isGlobalHandler) {
event.stopPropagation()
}
originalHandleKey(character, modifiers, event)
return
}
// NOTE: global shortcuts should work even for an isolated event
if (this.props.global || isolateOwner) {
originalHandleKey(character, modifiers, event)
}
}
};
_getElementToBind = () => {
let element = null
if (this.props.targetNodeSelector) {
element = document.querySelector(this.props.targetNodeSelector)
invariant(
element,
`Node selector '${this.props.targetNodeSelector}' was not found.`
)
} else {
element = this._domNode
}
return element
};
_unbindShortcuts = () => {
if (this._combokeys) {
this._combokeys.detach()
this._combokeys.reset()
}
};
_onUpdate = () => {
const shortcutsArr = this.props.name &&
this.context.shortcuts.getShortcuts(this.props.name)
this._unbindShortcuts()
this._bindShortcuts(shortcutsArr || [])
};
_handleShortcuts = (event, keyName) => {
if (this.props.name) {
const shortcutName = this.context.shortcuts.findShortcutName(
keyName,
this.props.name
)
if (this.props.handler) {
this.props.handler(shortcutName, event)
}
}
};
render() {
return (
<div
ref={(node) => {
this._domNode = node
}}
tabIndex={this.props.tabIndex}
className={this.props.className}
>
{this.props.children}
</div>
)
}
}
================================================
FILE: src/helpers.js
================================================
import platform from 'platform'
const getPlatformName = () => {
let os = platform.os.family || ''
os = os.toLowerCase().replace(/ /g, '')
if (/\bwin/.test(os)) {
os = 'windows'
} else if (/darwin|osx/.test(os)) {
os = 'osx'
} else if (/linux|freebsd|sunos|ubuntu|debian|fedora|redhat|suse/.test(os)) {
os = 'linux'
} else {
os = 'other'
}
return os
}
const getCharacter = (event) => {
if (event.which == null) {
// NOTE: IE
return String.fromCharCode(event.keyCode)
} else if (event.which !== 0 && event.charCode !== 0) {
// NOTE: the rest
return String.fromCharCode(event.which)
}
return null
}
export default { getPlatformName, getCharacter }
================================================
FILE: src/index.js
================================================
module.exports = {
ShortcutManager: require('./shortcut-manager'),
Shortcuts: require('./component/'),
}
================================================
FILE: src/shortcut-manager.js
================================================
import reduce from 'just-reduce-object'
import invariant from 'invariant'
import { EventEmitter } from 'events'
import helpers from './helpers'
import { isPlainObject, findKey, isArray, map, compact, flatten } from './utils'
const warning = (text) => {
if (process && process.env.NODE_ENV !== 'production') {
console.warn(text)
}
}
class ShortcutManager extends EventEmitter {
static CHANGE_EVENT = 'shortcuts:update'
constructor(keymap = {}) {
super()
this._keymap = keymap
}
addUpdateListener(callback) {
invariant(callback,
'addUpdateListener: callback argument is not defined or falsy')
this.on(ShortcutManager.CHANGE_EVENT, callback)
}
removeUpdateListener(callback) {
this.removeListener(ShortcutManager.CHANGE_EVENT, callback)
}
_platformName = helpers.getPlatformName()
_parseShortcutDescriptor = (item) => {
if (isPlainObject(item)) {
return item[this._platformName]
}
return item
}
setKeymap(keymap) {
invariant(keymap,
'setKeymap: keymap argument is not defined or falsy.')
this._keymap = keymap
this.emit(ShortcutManager.CHANGE_EVENT)
}
extendKeymap(keymap) {
invariant(keymap,
'extendKeymap: keymap argument is not defined or falsy.')
this._keymap = Object.assign({}, this._keymap, keymap)
this.emit(ShortcutManager.CHANGE_EVENT)
}
getAllShortcuts() {
return this._keymap
}
getAllShortcutsForPlatform(platformName) {
const _transformShortcuts = (shortcuts) => {
return reduce(shortcuts, (result, keyName, keyValue) => {
if (isPlainObject(keyValue)) {
if (keyValue[platformName]) {
keyValue = keyValue[platformName]
} else {
result[keyName] = _transformShortcuts(keyValue)
return result
}
}
result[keyName] = keyValue
return result
}, {})
}
return _transformShortcuts(this._keymap)
}
getAllShortcutsForCurrentPlatform() {
return this.getAllShortcutsForPlatform(this._platformName)
}
getShortcuts(componentName) {
invariant(componentName,
'getShortcuts: name argument is not defined or falsy.')
const cursor = this._keymap[componentName]
if (!cursor) {
warning(`getShortcuts: There are no shortcuts with name ${componentName}.`)
return
}
const shortcuts = compact(flatten(map(cursor, this._parseShortcutDescriptor)))
return shortcuts
}
_parseShortcutKeyName(obj, keyName) {
const result = findKey(obj, (item) => {
if (isPlainObject(item)) {
item = item[this._platformName]
}
if (isArray(item)) {
const index = item.indexOf(keyName)
if (index >= 0) { item = item[index] }
}
return item === keyName
})
return result
}
findShortcutName(keyName, componentName) {
invariant(keyName,
'findShortcutName: keyName argument is not defined or falsy.')
invariant(componentName,
'findShortcutName: componentName argument is not defined or falsy.')
const cursor = this._keymap[componentName]
const result = this._parseShortcutKeyName(cursor, keyName)
return result
}
}
export default ShortcutManager
================================================
FILE: src/utils.js
================================================
export const isArray = arr => Array.isArray(arr)
export const isPlainObject = (obj) => {
const isObject = typeof obj === 'object' && obj !== null && !isArray(obj)
if (!isObject || (obj.toString && obj.toString() !== '[object Object]')) return false
const proto = Object.getPrototypeOf(obj)
if (proto === null) {
return true
}
const Ctor = Object.prototype.hasOwnProperty.call(proto, 'constructor') && proto.constructor
return typeof Ctor === 'function' && Ctor instanceof Ctor &&
Function.prototype.toString.call(Ctor) === Function.prototype.toString.call(Object)
}
export const findKey = (obj, fn) => {
if (!isPlainObject(obj) && !isArray(obj)) return
const keys = Object.keys(obj)
return keys.find(key => fn(obj[key]))
}
export const compact = arr => arr.filter(Boolean)
const flattenOnce = (arr, recurse = true) => {
return arr.reduce((acc, val) => {
if (isArray(val) && recurse) return acc.concat(flattenOnce(val, false))
acc.push(val)
return acc
}, [])
}
export const flatten = (arr) => {
if (!isArray(arr)) throw new Error('flatten expects an array')
return flattenOnce(arr)
}
export const map = (itr, fn) => {
if (isArray(itr)) return itr.map(fn)
const results = []
const keys = Object.keys(itr)
const len = keys.length
for (let i = 0; i < len; i += 1) {
const key = keys[i]
results.push(fn(itr[key], key))
}
return results
}
================================================
FILE: test/keymap.js
================================================
export default {
'Test': {
MOVE_LEFT: 'left',
MOVE_RIGHT: 'right',
MOVE_UP: ['up', 'w'],
DELETE: {
osx: 'alt+backspace',
windows: 'delete',
linux: 'alt+backspace',
other: 'alt+backspace',
},
},
'Next': {
OPEN: 'alt+o',
ABORT: ['d', 'c'],
CLOSE: {
osx: ['esc', 'enter'],
windows: ['esc', 'enter'],
linux: ['esc', 'enter'],
other: ['esc', 'enter'],
},
},
'TESTING': {
'OPEN': 'enter',
'CLOSE': 'esc',
},
'NON-EXISTING': {},
}
================================================
FILE: test/mocha.opts
================================================
--compilers js:babel-core/register
--recursive
================================================
FILE: test/shortcut-manager.spec.js
================================================
import jsdom from 'jsdom'
import chai from 'chai'
import _ from 'lodash'
import sinonChai from 'sinon-chai'
import sinon from 'sinon'
import keymap from './keymap'
chai.use(sinonChai)
const { expect } = chai
describe('Shortcut manager', () => {
let ShortcutManager = null
before(() => {
global.document = jsdom.jsdom('<html><body></body></html>')
global.window = document.defaultView
global.Image = window.Image
global.navigator = window.navigator
global.CustomEvent = window.CustomEvent
ShortcutManager = require('../src').ShortcutManager
})
it('should return empty object when calling empty constructor', () => {
const manager = new ShortcutManager()
expect(manager.getAllShortcuts()).to.be.empty
})
it('should return all shortcuts', () => {
const manager = new ShortcutManager(keymap)
expect(manager.getAllShortcuts()).to.not.be.empty
expect(manager.getAllShortcuts()).to.be.equal(keymap)
manager.setKeymap({})
expect(manager.getAllShortcuts()).to.be.empty
manager.setKeymap(keymap)
expect(manager.getAllShortcuts()).to.be.equal(keymap)
})
it('should return all shortcuts for the Windows platform', () => {
const manager = new ShortcutManager(keymap)
const keyMapResult = {
'Test': {
MOVE_LEFT: 'left',
MOVE_RIGHT: 'right',
MOVE_UP: ['up', 'w'],
DELETE: 'delete',
},
'Next': {
OPEN: 'alt+o',
ABORT: ['d', 'c'],
CLOSE: ['esc', 'enter'],
},
'TESTING': {
'OPEN': 'enter',
'CLOSE': 'esc',
},
'NON-EXISTING': {},
}
expect(manager.getAllShortcutsForPlatform('windows')).to.eql(keyMapResult)
})
it('should return all shortcuts for the macOs platform', () => {
const manager = new ShortcutManager(keymap)
const keyMapResult = {
'Test': {
MOVE_LEFT: 'left',
MOVE_RIGHT: 'right',
MOVE_UP: ['up', 'w'],
DELETE: 'alt+backspace',
},
'Next': {
OPEN: 'alt+o',
ABORT: ['d', 'c'],
CLOSE: ['esc', 'enter'],
},
'TESTING': {
'OPEN': 'enter',
'CLOSE': 'esc',
},
'NON-EXISTING': {},
}
expect(manager.getAllShortcutsForPlatform('osx')).to.eql(keyMapResult)
})
it('should expose the change event type as a static constant', () =>
expect(ShortcutManager.CHANGE_EVENT).to.exist
)
it('should have static CHANGE_EVENT', () =>
expect(ShortcutManager.CHANGE_EVENT).to.be.equal('shortcuts:update')
)
it('should call onUpdate', () => {
const manager = new ShortcutManager()
const spy = sinon.spy()
manager.addUpdateListener(spy)
manager.setKeymap({})
expect(spy).to.have.beenCalled
})
it('should throw an error when setKeymap is called without arg', () => {
const manager = new ShortcutManager(keymap)
const error = /setKeymap: keymap argument is not defined or falsy./
expect(manager.setKeymap).to.throw(error)
})
it('should extend the keymap', () => {
const manager = new ShortcutManager()
const newKeymap = { 'TESTING-NAMESPACE': {} }
const extendedKeymap = Object.assign({}, keymap, newKeymap)
manager.setKeymap(keymap)
manager.extendKeymap(newKeymap)
expect(manager.getAllShortcuts()).to.eql(extendedKeymap)
})
it('should return array of shortcuts', () => {
const manager = new ShortcutManager(keymap)
let shortcuts = manager.getShortcuts('Test')
expect(shortcuts).to.be.an.array
let shouldContainStrings = _.every(shortcuts, _.isString)
expect(shouldContainStrings).to.be.equal(true)
expect(shortcuts.length).to.be.equal(5)
shortcuts = manager.getShortcuts('Next')
expect(shortcuts).to.be.an.array
shouldContainStrings = _.every(shortcuts, _.isString)
expect(shouldContainStrings).to.be.equal(true)
expect(shortcuts.length).to.be.equal(5)
})
it('should not throw an error when getting not existing key from keymap', () => {
const manager = new ShortcutManager(keymap)
const notExist = () => manager.getShortcuts('NotExist')
expect(notExist).to.not.throw()
})
it('should return correct key label', () => {
const manager = new ShortcutManager()
manager.setKeymap(keymap)
// Test
expect(manager.findShortcutName('alt+backspace', 'Test')).to.be.equal('DELETE')
expect(manager.findShortcutName('w', 'Test')).to.be.equal('MOVE_UP')
expect(manager.findShortcutName('up', 'Test')).to.be.equal('MOVE_UP')
expect(manager.findShortcutName('left', 'Test')).to.be.equal('MOVE_LEFT')
expect(manager.findShortcutName('right', 'Test')).to.be.equal('MOVE_RIGHT')
// Next
expect(manager.findShortcutName('alt+o', 'Next')).to.be.equal('OPEN')
expect(manager.findShortcutName('d', 'Next')).to.be.equal('ABORT')
expect(manager.findShortcutName('c', 'Next')).to.be.equal('ABORT')
expect(manager.findShortcutName('esc', 'Next')).to.be.equal('CLOSE')
expect(manager.findShortcutName('enter', 'Next')).to.be.equal('CLOSE')
})
it('should throw an error', () => {
const manager = new ShortcutManager()
const fn = () => manager.findShortcutName('left')
expect(manager.findShortcutName).to.throw(/findShortcutName: keyName argument is not defined or falsy./)
expect(fn).to.throw(/findShortcutName: componentName argument is not defined or falsy./)
})
})
================================================
FILE: test/shortcuts.spec.js
================================================
import ReactDOMFactories from 'react-dom-factories'
import jsdom from 'jsdom'
import chai from 'chai'
import sinonChai from 'sinon-chai'
import sinon from 'sinon'
import _ from 'lodash'
import enzyme from 'enzyme'
import Adapter from 'enzyme-adapter-react-16'
import keymap from './keymap'
enzyme.configure({ adapter: new Adapter() })
describe('Shortcuts component', () => {
let baseProps = null
let baseContext = null
let simulant = null
let ShortcutManager = null
let Shortcuts = null
let ReactDOM = null
let React = null
chai.use(sinonChai)
const { expect } = chai
beforeEach(() => {
global.document = jsdom.jsdom('<html><body></body></html>')
global.window = document.defaultView
global.Image = window.Image
global.navigator = window.navigator
global.CustomEvent = window.CustomEvent
simulant = require('simulant')
ReactDOM = require('react-dom')
React = require('react')
const chaiEnzyme = require('chai-enzyme')
chai.use(chaiEnzyme())
ShortcutManager = require('../src').ShortcutManager
const shortcutsManager = new ShortcutManager(keymap)
Shortcuts = require('../src/').Shortcuts
baseProps = {
handler: sinon.spy(),
name: 'TESTING',
className: null,
}
baseContext = { shortcuts: shortcutsManager }
})
it('should render component', () => {
const shortcutComponent = React.createElement(Shortcuts, baseProps)
const wrapper = enzyme.mount(shortcutComponent, { context: baseContext })
expect(wrapper.find('div')).to.have.length(1)
})
it('should have a tabIndex of -1 by default', () => {
let shortcutComponent = React.createElement(Shortcuts, baseProps)
let wrapper = enzyme.mount(shortcutComponent, { context: baseContext })
expect(wrapper.props().tabIndex).to.be.equal(-1)
const props = _.assign({}, baseProps, { tabIndex: 42 })
shortcutComponent = React.createElement(Shortcuts, props)
wrapper = enzyme.mount(shortcutComponent, { context: baseContext })
expect(wrapper.props().tabIndex).to.be.equal(props.tabIndex)
let realTabIndex = ReactDOM.findDOMNode(wrapper.instance()).getAttribute('tabindex')
expect(realTabIndex).to.have.equal(String(props.tabIndex))
props.tabIndex = 0
shortcutComponent = React.createElement(Shortcuts, props)
wrapper = enzyme.mount(shortcutComponent, { context: baseContext })
expect(wrapper.props().tabIndex).to.be.equal(props.tabIndex)
realTabIndex = ReactDOM.findDOMNode(wrapper.instance()).getAttribute('tabindex')
expect(realTabIndex).to.have.equal(String(props.tabIndex))
})
it('should not have className by default', () => {
const shortcutComponent = React.createElement(Shortcuts, baseProps)
const wrapper = enzyme.mount(shortcutComponent, { context: baseContext })
expect(wrapper.props().className).to.be.equal(null)
})
it('should have className', () => {
const props = _.assign({}, baseProps, { className: 'testing' })
const shortcutComponent = React.createElement(Shortcuts, props)
const wrapper = enzyme.mount(shortcutComponent, { context: baseContext })
expect(wrapper.props().className).to.be.equal('testing')
expect(wrapper).to.have.className('testing')
})
it('should have isolate prop set to false by default', () => {
const shortcutComponent = React.createElement(Shortcuts, baseProps)
const wrapper = enzyme.mount(shortcutComponent, { context: baseContext })
expect(wrapper.props().isolate).to.be.equal(false)
})
it('should NOT store combokeys instances on Combokeys constructor', () => {
const shortcutComponent = React.createElement(Shortcuts, baseProps)
const wrapper = enzyme.mount(shortcutComponent, { context: baseContext })
expect(wrapper.find('Shortcuts').instance()._combokeys.constructor.instances).to.be.empty
})
it('should have isolate prop', () => {
const props = _.assign({}, baseProps, { isolate: true })
const shortcutComponent = React.createElement(Shortcuts, props)
const wrapper = enzyme.mount(shortcutComponent, { context: baseContext })
expect(wrapper.props().isolate).to.be.equal(true)
})
it('should not have children by default', () => {
const shortcutComponent = React.createElement(Shortcuts, baseProps)
const wrapper = enzyme.mount(shortcutComponent, { context: baseContext })
expect(wrapper.props().children).to.be.equal(undefined)
})
it('should have children', () => {
const props = _.assign({}, baseProps, { children: ReactDOMFactories.div() })
const shortcutComponent = React.createElement(Shortcuts, props)
const wrapper = enzyme.mount(shortcutComponent, { context: baseContext })
expect(wrapper).to.contain(ReactDOMFactories.div())
})
it('should have handler prop', () => {
const shortcutComponent = React.createElement(Shortcuts, baseProps)
const wrapper = enzyme.mount(shortcutComponent, { context: baseContext })
expect(wrapper.props().handler).to.be.function
})
it('should have name prop', () => {
const props = _.assign({}, baseProps,
{ name: 'TESTING' })
const shortcutComponent = React.createElement(Shortcuts, props)
const wrapper = enzyme.mount(shortcutComponent, { context: baseContext })
expect(wrapper.props().name).to.be.equal('TESTING')
})
it('should not have eventType prop by default', () => {
const shortcutComponent = React.createElement(Shortcuts, baseProps)
const wrapper = enzyme.mount(shortcutComponent, { context: baseContext })
expect(wrapper.props().eventType).to.be.equal(null)
})
it('should have eventType prop', () => {
const props = _.assign({}, baseProps, { eventType: 'keyUp' })
const shortcutComponent = React.createElement(Shortcuts, props)
const wrapper = enzyme.mount(shortcutComponent, { context: baseContext })
expect(wrapper.props().eventType).to.be.equal('keyUp')
})
it('should have stopPropagation prop by default', () => {
const shortcutComponent = React.createElement(Shortcuts, baseProps)
const wrapper = enzyme.mount(shortcutComponent, { context: baseContext })
expect(wrapper.props().stopPropagation).to.be.equal(true)
})
it('should have stopPropagation prop set to false', () => {
const props = _.assign({}, baseProps, { stopPropagation: false })
const shortcutComponent = React.createElement(Shortcuts, props)
const wrapper = enzyme.mount(shortcutComponent, { context: baseContext })
expect(wrapper.props().stopPropagation).to.be.equal(false)
})
it('should have preventDefault prop set to false by default', () => {
const shortcutComponent = React.createElement(Shortcuts, baseProps)
const wrapper = enzyme.mount(shortcutComponent, { context: baseContext })
expect(wrapper.props().preventDefault).to.be.equal(false)
})
it('should have preventDefault prop set to true', () => {
const props = _.assign({}, baseProps, { preventDefault: true })
const shortcutComponent = React.createElement(Shortcuts, props)
const wrapper = enzyme.mount(shortcutComponent, { context: baseContext })
expect(wrapper.props().preventDefault).to.be.equal(true)
})
it('should not have targetNodeSelector prop by default', () => {
const shortcutComponent = React.createElement(Shortcuts, baseProps)
const wrapper = enzyme.mount(shortcutComponent, { context: baseContext })
expect(wrapper.props().targetNodeSelector).to.be.equal(null)
})
it('should have targetNode prop', () => {
const props = _.assign({}, baseProps, { targetNodeSelector: 'body' })
const shortcutComponent = React.createElement(Shortcuts, props)
const wrapper = enzyme.mount(shortcutComponent, { context: baseContext })
expect(wrapper.props().targetNodeSelector).to.be.equal('body')
})
it('should have global prop set to false by default', () => {
const shortcutComponent = React.createElement(Shortcuts, baseProps)
const wrapper = enzyme.mount(shortcutComponent, { context: baseContext })
expect(wrapper.props().global).to.be.equal(false)
})
it('should have global prop set to true', () => {
const props = _.assign({}, baseProps, { global: true })
const shortcutComponent = React.createElement(Shortcuts, props)
const wrapper = enzyme.mount(shortcutComponent, { context: baseContext })
expect(wrapper.props().global).to.be.equal(true)
})
it('should fire the handler prop with the correct argument', () => {
const shortcutComponent = React.createElement(Shortcuts, baseProps)
const wrapper = enzyme.mount(shortcutComponent, { context: baseContext })
const node = ReactDOM.findDOMNode(wrapper.instance())
node.focus()
const enter = 13
simulant.fire(node, 'keydown', { keyCode: enter })
expect(wrapper.props().handler).to.have.been.calledWith('OPEN')
const esc = 27
simulant.fire(node, 'keydown', { keyCode: esc })
expect(wrapper.props().handler).to.have.been.calledWith('CLOSE')
})
it('should not fire the handler', () => {
const props = _.assign({}, baseProps, { name: 'NON-EXISTING' })
const shortcutComponent = React.createElement(Shortcuts, props)
const wrapper = enzyme.mount(shortcutComponent, { context: baseContext })
const node = ReactDOM.findDOMNode(wrapper.instance())
node.focus()
const enter = 13
simulant.fire(node, 'keydown', { keyCode: enter })
expect(wrapper.props().handler).to.not.have.been.called
})
it('should not fire twice when global prop is truthy', () => {
const props = _.assign({}, baseProps, { global: true })
const shortcutComponent = React.createElement(Shortcuts, props)
const wrapper = enzyme.mount(shortcutComponent, { context: baseContext })
const node = ReactDOM.findDOMNode(wrapper.instance())
node.focus()
const enter = 13
simulant.fire(node, 'keydown', { keyCode: enter })
expect(wrapper.props().handler).to.have.been.calledOnce
})
it('should not fire when the component has been unmounted', () => {
const handler = sinon.spy()
const shortcutComponent = React.createElement(Shortcuts, { ...baseProps, handler })
const wrapper = enzyme.mount(shortcutComponent, { context: baseContext })
const node = ReactDOM.findDOMNode(wrapper.instance())
node.focus()
wrapper.unmount()
const enter = 13
simulant.fire(node, 'keydown', { keyCode: enter })
expect(handler).to.not.have.been.called
})
it.skip('should update the shortcuts and fire the handler', () => {
const shortcutComponent = React.createElement(Shortcuts, baseProps)
const wrapper = enzyme.mount(shortcutComponent, { context: baseContext })
const node = ReactDOM.findDOMNode(wrapper.instance())
node.focus()
const space = 32
simulant.fire(node, 'keydown', { keyCode: space })
expect(wrapper.props().handler).to.not.have.been.called
const editedKeymap = _.assign({}, keymap, {
'TESTING': {
'SPACE': 'space',
},
}
)
baseContext.shortcuts.setKeymap(editedKeymap)
simulant.fire(node, 'keydown', { keyCode: space })
expect(baseProps.handler).to.have.been.called
// NOTE: rollback the previous keymap
baseContext.shortcuts.setKeymap(keymap)
})
it('should fire the handler from a child input', () => {
const props = _.assign({}, baseProps, {
children: ReactDOMFactories.input({ type: 'text', className: 'input' }),
})
const shortcutComponent = React.createElement(Shortcuts, props)
const wrapper = enzyme.mount(shortcutComponent, { context: baseContext })
const parentNode = ReactDOM.findDOMNode(wrapper.instance())
const node = parentNode.querySelector('.input')
node.focus()
const enter = 13
simulant.fire(node, 'keydown', { keyCode: enter, key: 'Enter' })
expect(wrapper.props().handler).to.have.been.called
})
it('should fire the handler when using targetNodeSelector', () => {
const props = _.assign({}, baseProps, { targetNodeSelector: 'body' })
const shortcutComponent = React.createElement(Shortcuts, props)
const wrapper = enzyme.mount(shortcutComponent, { context: baseContext })
const enter = 13
simulant.fire(document.body, 'keydown', { keyCode: enter, key: 'Enter' })
expect(wrapper.props().handler).to.have.been.called
})
it('should throw and error if targetNodeSelector is not found', () => {
const props = _.assign({}, baseProps, { targetNodeSelector: 'non-existing' })
const shortcutComponent = React.createElement(Shortcuts, props)
try {
enzyme.mount(shortcutComponent, { context: baseContext })
} catch (err) {
expect(err).to.match(/Node selector 'non-existing' {2}was not found/)
}
})
it('should fire the handler from focused input', () => {
const props = _.assign({}, baseProps, {
alwaysFireHandler: true,
children: ReactDOMFactories.input({ type: 'text', className: 'input' }),
})
const shortcutComponent = React.createElement(Shortcuts, props)
const wrapper = enzyme.mount(shortcutComponent, { context: baseContext })
const parentNode = ReactDOM.findDOMNode(wrapper.instance())
const node = parentNode.querySelector('.input')
node.focus()
const enter = 13
simulant.fire(node, 'keydown', { keyCode: enter })
expect(wrapper.props().handler).to.have.been.called
})
describe('Shortcuts component inside Shortcuts component:', () => {
it('should not fire parent handler when child handler is fired', () => {
const props = _.assign({}, baseProps, {
children: React.createElement(Shortcuts, _.assign({}, baseProps, { className: 'test' })),
})
const shortcutComponent = React.createElement(Shortcuts, props)
const wrapper = enzyme.mount(shortcutComponent, { context: baseContext })
const parentNode = ReactDOM.findDOMNode(wrapper.instance())
const node = parentNode.querySelector('.test')
node.focus()
const enter = 13
simulant.fire(node, 'keydown', { keyCode: enter })
expect(baseProps.handler).to.have.been.calledOnce
})
it('should fire parent handler when child handler is fired', () => {
const props = _.assign({}, baseProps, {
children: React.createElement(Shortcuts, _.assign({}, baseProps, { className: 'test', stopPropagation: false })),
})
const shortcutComponent = React.createElement(Shortcuts, props)
const wrapper = enzyme.mount(shortcutComponent, { context: baseContext })
const parentNode = ReactDOM.findDOMNode(wrapper.instance())
const node = parentNode.querySelector('.test')
node.focus()
const enter = 13
simulant.fire(node, 'keydown', { keyCode: enter })
expect(baseProps.handler).to.have.been.calledTwice
})
it('should fire parent handler when parent handler has global prop', () => {
const props = _.assign({}, baseProps, {
children: React.createElement(Shortcuts, _.assign({}, baseProps, { className: 'test' })),
global: true,
})
const shortcutComponent = React.createElement(Shortcuts, props)
const wrapper = enzyme.mount(shortcutComponent, { context: baseContext })
const parentNode = ReactDOM.findDOMNode(wrapper.instance())
const node = parentNode.querySelector('.test')
node.focus()
const enter = 13
simulant.fire(node, 'keydown', { keyCode: enter })
expect(baseProps.handler).to.have.been.calledTwice
})
it('should fire parent handler but not the child handler', () => {
const props = _.assign({}, baseProps, {
children: React.createElement(Shortcuts, _.assign({}, baseProps, { name: 'NON-EXISTING', className: 'test' })),
global: true,
})
const shortcutComponent = React.createElement(Shortcuts, props)
const wrapper = enzyme.mount(shortcutComponent, { context: baseContext })
const parentNode = ReactDOM.findDOMNode(wrapper.instance())
const node = parentNode.querySelector('.test')
node.focus()
const enter = 13
simulant.fire(node, 'keydown', { keyCode: enter })
expect(baseProps.handler).to.have.been.calledOnce
})
it('should fire for all global components', () => {
const props = _.assign({}, baseProps, {
children: React.createElement(Shortcuts, _.assign({}, baseProps, {
global: true,
children: React.createElement(Shortcuts, _.assign({}, baseProps, { name: 'NON-EXISTING', className: 'test' })),
})),
global: true,
})
const shortcutComponent = React.createElement(Shortcuts, props)
const wrapper = enzyme.mount(shortcutComponent, { context: baseContext })
const parentNode = ReactDOM.findDOMNode(wrapper.instance())
const node = parentNode.querySelector('.test')
node.focus()
const enter = 13
simulant.fire(node, 'keydown', { keyCode: enter })
expect(baseProps.handler).to.have.been.calledTwice
})
it('should not fire parent handler when a child has isolate prop set to true', () => {
const childHandlerSpy = sinon.spy()
const props = _.assign({}, baseProps, {
children: React.createElement(Shortcuts, _.assign({}, baseProps, {
className: 'test',
isolate: true,
handler: childHandlerSpy,
})),
})
const shortcutComponent = React.createElement(Shortcuts, props)
const wrapper = enzyme.mount(shortcutComponent, { context: baseContext })
const parentNode = ReactDOM.findDOMNode(wrapper.instance())
const node = parentNode.querySelector('.test')
node.focus()
const enter = 13
simulant.fire(node, 'keydown', { keyCode: enter })
expect(childHandlerSpy).to.have.been.called
expect(baseProps.handler).to.not.have.been.called
})
it('should fire parent handler when is global and a child has isolate prop set to true', () => {
const props = _.assign({}, baseProps, {
global: true,
children: React.createElement(Shortcuts, _.assign({}, baseProps, { className: 'test', isolate: true })),
})
const shortcutComponent = React.createElement(Shortcuts, props)
const wrapper = enzyme.mount(shortcutComponent, { context: baseContext })
const parentNode = ReactDOM.findDOMNode(wrapper.instance())
const node = parentNode.querySelector('.test')
node.focus()
const enter = 13
simulant.fire(node, 'keydown', { keyCode: enter })
expect(baseProps.handler).to.have.been.called
})
})
})
================================================
FILE: test/utils.js
================================================
import chai from 'chai'
import _ from 'lodash'
import { isArray, isPlainObject, findKey, compact, flatten, map } from '../src/utils'
describe('utils', () => {
const { expect } = chai
let primitives
beforeEach(() => {
function fn() { this.a = 1 }
primitives = [
['array'],
{ object: true },
Object.create(null),
'string',
null,
undefined,
NaN,
new Map([[ 1, 'one' ], [ 2, 'two' ]]),
new fn(),
true,
42,
]
})
describe('isArray', () => {
it('should be true for arrays', () => {
primitives.forEach((val, idx) => {
if (idx === 0) {
expect(isArray(val)).to.be.true
expect(_.isArray(val)).to.be.true
} else {
expect(isArray(val)).to.be.false
expect(_.isArray(val)).to.be.false
}
})
})
})
describe('isPlainObject', () => {
it('should be true for plain objects', () => {
primitives.forEach((val, idx) => {
if (idx === 1 || idx === 2) {
expect(isPlainObject(val)).to.be.true
expect(_.isPlainObject(val)).to.be.true
} else {
expect(isPlainObject(val)).to.be.false
expect(_.isPlainObject(val)).to.be.false
}
})
})
})
describe('findKey', () => {
it('should return the matching key', () => {
const obj = {
simple: 1,
obj: {
val: 4,
},
}
const checkOne = val => val === 1
const checkTwo = val => typeof val === 'object'
expect(findKey(obj, checkOne)).to.deep.equal(_.findKey(obj, checkOne))
expect(findKey(obj, checkTwo)).to.deep.equal(_.findKey(obj, checkTwo))
})
})
describe('compact', () => {
it('removes falsy values', () => {
const values = [
true,
false,
10,
0,
null,
undefined,
NaN,
'',
'false, null, 0, "", undefined, and NaN are falsy',
]
expect(compact(values)).to.deep.equal(_.compact(values))
})
})
describe('flatten', () => {
it('flattens an array 1 level', () => {
const value = [1, [2, [3, [4]], 5, [[[6], 7], 8], 9]]
expect(flatten(value)).to.deep.equal(_.flatten(value))
})
})
describe('map', () => {
it('should map an array', () => {
const values = [1, 2, 3, 4]
const mapFn = val => val * 10
expect(map(values, mapFn)).to.deep.equal(_.map(values, mapFn))
expect(map(values, mapFn)).to.deep.equal([10, 20, 30, 40])
// ensure that values array is not mutated
expect(values).to.deep.equal([1, 2, 3, 4])
})
it('should map an object', () => {
const obj = {
one: 1,
two: 2,
three: 3,
}
const mapFn = (val, key) => `${key} - ${val * 10}`
expect(map(obj, mapFn)).to.deep.equal(_.map(obj, mapFn))
expect(map(obj, mapFn)).to.deep.equal([
'one - 10',
'two - 20',
'three - 30',
])
// ensure the object was not mutated
expect(obj).to.deep.equal({
one: 1,
two: 2,
three: 3,
})
})
})
})
================================================
FILE: webpack.config.js
================================================
const webpack = require('webpack')
module.exports = {
entry: [
'webpack-dev-server/client?http://localhost:8080',
'webpack/hot/dev-server',
`${__dirname}/example/main.js`,
],
devtool: 'inline-source-map',
debug: true,
output: {
path: `${__dirname}/dist`,
filename: 'index.js',
},
resolve: {
extensions: ['', '.js'],
},
resolveLoader: {
modulesDirectories: ['node_modules'],
},
plugins: [
new webpack.HotModuleReplacementPlugin(),
],
module: {
loaders: [
{
test: /\.less$/,
loader: 'style-loader!css-loader!less-loader',
},
{
test: /\.js$/,
exclude: /(node_modules|bower_components)/,
loader: 'babel',
},
],
noParse: /\.min\.js/,
},
}
gitextract_gjvnrzkr/ ├── .babelrc ├── .eslintrc ├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE.md ├── README.md ├── example/ │ ├── app.js │ ├── index.html │ ├── keymap.js │ ├── main.js │ └── main.less ├── package.json ├── src/ │ ├── component/ │ │ ├── index.js │ │ └── shortcuts.js │ ├── helpers.js │ ├── index.js │ ├── shortcut-manager.js │ └── utils.js ├── test/ │ ├── keymap.js │ ├── mocha.opts │ ├── shortcut-manager.spec.js │ ├── shortcuts.spec.js │ └── utils.js └── webpack.config.js
SYMBOL INDEX (23 symbols across 4 files)
FILE: example/app.js
method getInitialState (line 18) | getInitialState() {
method getChildContext (line 22) | getChildContext() {
method _handleShortcuts (line 26) | _handleShortcuts(command) {
method _handleShortcuts2 (line 35) | _handleShortcuts2(command) {
method _handleRoot (line 44) | _handleRoot(command) {
method _rebind (line 48) | _rebind() {
method render (line 56) | render() {
FILE: src/component/shortcuts.js
method componentDidMount (line 42) | componentDidMount() {
method componentWillUnmount (line 50) | componentWillUnmount() {
method render (line 220) | render() {
FILE: src/shortcut-manager.js
class ShortcutManager (line 14) | class ShortcutManager extends EventEmitter {
method constructor (line 17) | constructor(keymap = {}) {
method addUpdateListener (line 22) | addUpdateListener(callback) {
method removeUpdateListener (line 28) | removeUpdateListener(callback) {
method setKeymap (line 41) | setKeymap(keymap) {
method extendKeymap (line 48) | extendKeymap(keymap) {
method getAllShortcuts (line 55) | getAllShortcuts() {
method getAllShortcutsForPlatform (line 59) | getAllShortcutsForPlatform(platformName) {
method getAllShortcutsForCurrentPlatform (line 79) | getAllShortcutsForCurrentPlatform() {
method getShortcuts (line 83) | getShortcuts(componentName) {
method _parseShortcutKeyName (line 98) | _parseShortcutKeyName(obj, keyName) {
method findShortcutName (line 113) | findShortcutName(keyName, componentName) {
FILE: test/utils.js
function fn (line 10) | function fn() { this.a = 1 }
Condensed preview — 25 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (59K chars).
[
{
"path": ".babelrc",
"chars": 163,
"preview": "{\n \"presets\": [\"es2015\", \"stage-0\", \"react\"],\n \"plugins\": [\"add-module-exports\"],\n \"env\": {\n \"production\": {\n "
},
{
"path": ".eslintrc",
"chars": 2080,
"preview": "{\n \"parser\": \"babel-eslint\",\n \"plugins\": [\n \"import\",\n \"react\"\n ],\n \"extends\": [ \"airbnb\" ],\n \"env\": {\n \"b"
},
{
"path": ".gitignore",
"chars": 112,
"preview": "# OS garbage\n.DS_Store\nThumbs.db\n\n# built sources\ndist/\nlib/\n\n# npm stuff\nnode_modules/\nnpm-debug.log\ncoverage/\n"
},
{
"path": ".npmignore",
"chars": 83,
"preview": ".babelrc\nexample\ntest\n*.coffee\n*.sh\n*.md\n*.yml\nwebpack.config.js\ncoverage\ndist\nsrc\n"
},
{
"path": ".travis.yml",
"chars": 36,
"preview": "language: node_js\nnode_js:\n - \"12\"\n"
},
{
"path": "LICENSE.md",
"chars": 1077,
"preview": "The MIT License (MIT)\n\nCopyright (c) 2015 Petr Brzek\n\nPermission is hereby granted, free of charge, to any person obtain"
},
{
"path": "README.md",
"chars": 5735,
"preview": "React Shortcuts\n=========\n\n**Manage keyboard shortcuts from one place.**\n\n[\n"
},
{
"path": "src/component/shortcuts.js",
"chars": 5858,
"preview": "import React from 'react'\nimport invariant from 'invariant'\nimport Combokeys from 'combokeys'\nimport PropTypes from 'pro"
},
{
"path": "src/helpers.js",
"chars": 704,
"preview": "import platform from 'platform'\n\nconst getPlatformName = () => {\n let os = platform.os.family || ''\n os = os.toLowerCa"
},
{
"path": "src/index.js",
"chars": 109,
"preview": "module.exports = {\n ShortcutManager: require('./shortcut-manager'),\n Shortcuts: require('./component/'),\n}\n"
},
{
"path": "src/shortcut-manager.js",
"chars": 3233,
"preview": "import reduce from 'just-reduce-object'\nimport invariant from 'invariant'\nimport { EventEmitter } from 'events'\nimport h"
},
{
"path": "src/utils.js",
"chars": 1412,
"preview": "export const isArray = arr => Array.isArray(arr)\n\nexport const isPlainObject = (obj) => {\n const isObject = typeof obj "
},
{
"path": "test/keymap.js",
"chars": 530,
"preview": "export default {\n 'Test': {\n MOVE_LEFT: 'left',\n MOVE_RIGHT: 'right',\n MOVE_UP: ['up', 'w'],\n DELETE: {\n "
},
{
"path": "test/mocha.opts",
"chars": 47,
"preview": "--compilers js:babel-core/register\n--recursive\n"
},
{
"path": "test/shortcut-manager.spec.js",
"chars": 5400,
"preview": "import jsdom from 'jsdom'\nimport chai from 'chai'\nimport _ from 'lodash'\nimport sinonChai from 'sinon-chai'\nimport sinon"
},
{
"path": "test/shortcuts.spec.js",
"chars": 18589,
"preview": "import ReactDOMFactories from 'react-dom-factories'\nimport jsdom from 'jsdom'\nimport chai from 'chai'\nimport sinonChai f"
},
{
"path": "test/utils.js",
"chars": 3144,
"preview": "import chai from 'chai'\nimport _ from 'lodash'\nimport { isArray, isPlainObject, findKey, compact, flatten, map } from '."
},
{
"path": "webpack.config.js",
"chars": 771,
"preview": "const webpack = require('webpack')\n\nmodule.exports = {\n entry: [\n 'webpack-dev-server/client?http://localhost:8080',"
}
]
About this extraction
This page contains the full source code of the avocode/react-shortcuts GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 25 files (54.1 KB), approximately 14.5k tokens, and a symbol index with 23 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.