Skip to content

Commit

Permalink
Try: Add component for handling keyboard events (#1944)
Browse files Browse the repository at this point in the history
* Try: Add component for handling keyboard events

* Warn about consistent callback reference for shortcuts

* Remove editable detection from keyboard handling

Already default behavior for Mousetrap

https://craig.is/killing/mice#api.bind.text-fields
  • Loading branch information
aduth authored Jul 19, 2017
1 parent 659c64f commit 614b843
Show file tree
Hide file tree
Showing 8 changed files with 98 additions and 56 deletions.
1 change: 1 addition & 0 deletions components/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export { default as ExternalLink } from './external-link';
export { default as FormToggle } from './form-toggle';
export { default as FormTokenField } from './form-token-field';
export { default as IconButton } from './icon-button';
export { default as KeyboardShortcuts } from './keyboard-shortcuts';
export { default as Notice } from './notice';
export { default as NoticeList } from './notice/list';
export { default as Panel } from './panel';
Expand Down
56 changes: 56 additions & 0 deletions components/keyboard-shortcuts/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
Keyboard Shortcuts
==================

`<KeyboardShortcuts />` is a component which renders no children of its own, but instead handles keyboard sequences during the lifetime of the rendering element.

It uses the [Mousetrap](https://craig.is/killing/mice) library to implement keyboard sequence bindings.

## Example

Render `<KeyboardShortcuts />` with a `shortcuts` prop object:

```jsx
class SelectAllDetection extends Component {
constructor() {
super( ...arguments );

this.setAllSelected = this.setAllSelected.bind( this );

this.state = { isAllSelected: false };
}

setAllSelected() {
this.setState( { isAllSelected: true } );
}

render() {
return (
<div>
<KeyboardShorcuts shortcuts={ {
'mod+a': this.setAllSelected,
} } />
Combination pressed? { isAllSelected ? 'Yes' : 'No' }
</div>
);
}
}
```
__Note:__ The value of each shortcut should be a consistent function reference, not an anonymous function. Otherwise, the callback will not be correctly unbound when the component unmounts.
__Note:__ The callback will not be invoked if the key combination occurs in an editable field.
## Props
The component accepts the following props:
### shortcuts
An object of shortcut bindings, where each key is a keyboard combination, the value of which is the callback to be invoked when the key combination is pressed.
- Type: `Object`
- Required: No
## References
- [Mousetrap documentation](https://craig.is/killing/mice)
32 changes: 32 additions & 0 deletions components/keyboard-shortcuts/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* External dependencies
*/
import Mousetrap from 'mousetrap';
import { forEach } from 'lodash';

/**
* WordPress dependencies
*/
import { Component } from 'element';

class KeyboardShortcuts extends Component {
componentWillMount() {
this.toggleBindings( true );
}

componentWillUnmount() {
this.toggleBindings( false );
}

toggleBindings( isActive ) {
forEach( this.props.shortcuts, ( callback, key ) => {
Mousetrap[ isActive ? 'bind' : 'unbind' ]( key, callback );
} );
}

render() {
return null;
}
}

export default KeyboardShortcuts;
20 changes: 8 additions & 12 deletions editor/modes/visual-editor/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { first, last } from 'lodash';
*/
import { __ } from 'i18n';
import { Component, findDOMNode } from 'element';
import { CHAR_A } from 'utils/keycodes';
import { KeyboardShortcuts } from 'components';

/**
* Internal dependencies
Expand All @@ -19,15 +19,14 @@ import VisualEditorBlockList from './block-list';
import PostTitle from '../../post-title';
import { getBlockUids } from '../../selectors';
import { clearSelectedBlock, multiSelect } from '../../actions';
import { isEditableElement } from '../../utils/dom';

class VisualEditor extends Component {
constructor() {
super( ...arguments );
this.bindContainer = this.bindContainer.bind( this );
this.bindBlocksContainer = this.bindBlocksContainer.bind( this );
this.onClick = this.onClick.bind( this );
this.onKeyDown = this.onKeyDown.bind( this );
this.selectAll = this.selectAll.bind( this );
}

componentDidMount() {
Expand All @@ -52,16 +51,10 @@ class VisualEditor extends Component {
}
}

onKeyDown( event ) {
selectAll( event ) {
const { uids } = this.props;
if (
! isEditableElement( document.activeElement ) &&
( event.ctrlKey || event.metaKey ) &&
event.keyCode === CHAR_A
) {
event.preventDefault();
this.props.multiSelect( first( uids ), last( uids ) );
}
event.preventDefault();
this.props.multiSelect( first( uids ), last( uids ) );
}

render() {
Expand All @@ -77,6 +70,9 @@ class VisualEditor extends Component {
onKeyDown={ this.onKeyDown }
ref={ this.bindContainer }
>
<KeyboardShortcuts shortcuts={ {
'mod+a': this.selectAll,
} } />
<PostTitle />
<VisualEditorBlockList ref={ this.bindBlocksContainer } />
</div>
Expand Down
13 changes: 0 additions & 13 deletions editor/utils/dom.js

This file was deleted.

30 changes: 0 additions & 30 deletions editor/utils/test/dom.js

This file was deleted.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"lodash": "^4.17.4",
"moment": "^2.18.1",
"moment-timezone": "^0.5.13",
"mousetrap": "^1.6.1",
"prop-types": "^15.5.10",
"react": "^15.5.4",
"react-autosize-textarea": "^0.4.2",
Expand Down
1 change: 0 additions & 1 deletion utils/keycodes.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,3 @@ export const UP = 38;
export const RIGHT = 39;
export const DOWN = 40;
export const DELETE = 46;
export const CHAR_A = 'A'.charCodeAt( 0 );

0 comments on commit 614b843

Please sign in to comment.