diff --git a/components/index.js b/components/index.js
index 2f2ba501c66cd..e29243f1bff07 100644
--- a/components/index.js
+++ b/components/index.js
@@ -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';
diff --git a/components/keyboard-shortcuts/README.md b/components/keyboard-shortcuts/README.md
new file mode 100644
index 0000000000000..14defab6142d8
--- /dev/null
+++ b/components/keyboard-shortcuts/README.md
@@ -0,0 +1,56 @@
+Keyboard Shortcuts
+==================
+
+`` 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 `` 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 (
+
+
+ Combination pressed? { isAllSelected ? 'Yes' : 'No' }
+
+ );
+ }
+}
+```
+
+__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)
diff --git a/components/keyboard-shortcuts/index.js b/components/keyboard-shortcuts/index.js
new file mode 100644
index 0000000000000..ac75430e0fe90
--- /dev/null
+++ b/components/keyboard-shortcuts/index.js
@@ -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;
diff --git a/editor/modes/visual-editor/index.js b/editor/modes/visual-editor/index.js
index a5c951056796a..8feb5ddf30d6b 100644
--- a/editor/modes/visual-editor/index.js
+++ b/editor/modes/visual-editor/index.js
@@ -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
@@ -19,7 +19,6 @@ 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() {
@@ -27,7 +26,7 @@ class VisualEditor extends Component {
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() {
@@ -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() {
@@ -77,6 +70,9 @@ class VisualEditor extends Component {
onKeyDown={ this.onKeyDown }
ref={ this.bindContainer }
>
+
diff --git a/editor/utils/dom.js b/editor/utils/dom.js
deleted file mode 100644
index ec3feb0f17ba7..0000000000000
--- a/editor/utils/dom.js
+++ /dev/null
@@ -1,13 +0,0 @@
-
-/**
- * Utility function to check whether the domElement provided is editable or not
- * An editable element means we can type in it to edit its content
- * This includes inputs and contenteditables
- *
- * @param {DomElement} domElement DOM Element
- * @return {Boolean} Whether the DOM Element is editable or not
- */
-export function isEditableElement( domElement ) {
- return [ 'textarea', 'input', 'select' ].indexOf( domElement.tagName.toLowerCase() ) !== -1
- || !! domElement.isContentEditable;
-}
diff --git a/editor/utils/test/dom.js b/editor/utils/test/dom.js
deleted file mode 100644
index 37381e3d6d084..0000000000000
--- a/editor/utils/test/dom.js
+++ /dev/null
@@ -1,30 +0,0 @@
-/**
- * Internal dependencies
- */
-import { isEditableElement } from '../dom';
-
-describe( 'isEditableElement', () => {
- it( 'should return false for non editable nodes', () => {
- const div = document.createElement( 'div' );
-
- expect( isEditableElement( div ) ).toBe( false );
- } );
-
- it( 'should return true for inputs', () => {
- const input = document.createElement( 'input' );
-
- expect( isEditableElement( input ) ).toBe( true );
- } );
-
- it( 'should return true for textareas', () => {
- const textarea = document.createElement( 'textarea' );
-
- expect( isEditableElement( textarea ) ).toBe( true );
- } );
-
- it( 'should return true for selects', () => {
- const select = document.createElement( 'select' );
-
- expect( isEditableElement( select ) ).toBe( true );
- } );
-} );
diff --git a/package.json b/package.json
index 50db4f10cd51b..4f2bc0e2e86e0 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/utils/keycodes.js b/utils/keycodes.js
index 3363e4f1ade46..f7f3b6cfe2db8 100644
--- a/utils/keycodes.js
+++ b/utils/keycodes.js
@@ -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 );