UNPKG

6.6 kBMarkdownView Raw
1# `@shopify/react-shortcuts`
2
3[![Build Status](https://travis-ci.org/Shopify/quilt.svg?branch=master)](https://travis-ci.org/Shopify/quilt)
4[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE.md) [![npm version](https://badge.fury.io/js/%40shopify%2Freact-shortcuts.svg)](https://badge.fury.io/js/%40shopify%2Freact-shortcuts) ![bundle size badge](https://img.shields.io/bundlephobia/minzip/@shopify/react-shortcuts.svg)
5
6Declarative and performant library for matching shortcut combinations in React applications.
7
8## Installation
9
10```bash
11$ yarn add @shopify/react-shortcuts
12```
13
14## Usage
15
16The library exposes three main parts, `<ShortcutProvider />`, `<Shortcut />` and `ShortcutManager`.
17
18### ShortcutProvider
19
20Wrapping your application in a `<ShortcutProvider />` allows you to use `<Shortcut />` components anywhere in your application, internally sharing a single `ShortcutManager` instance to minimize listeners and collisions.
21
22```ts
23// App.ts
24
25import * as React from 'react';
26import {ShortcutProvider} from '@shopify/react-shortcuts';
27
28export default function App() {
29 <ShortcutProvider>{/* the rest of your app */}</ShortcutProvider>;
30}
31```
32
33### Shortcut
34
35Shortcut is used to register a new keyboard shortcut to `ShortcutManager`. It can be triggered globally or only when a node is focused.
36
37**Note: a `<Shortcut />` must have a `<ShortcutProvider />` somewhere above it in the tree.**
38
39#### API
40
41```ts
42export interface Props {
43 /*
44 keys that, when pressed sequentially, will trigger `onMatch`
45 */
46 ordered: Key[];
47 /*
48 modifier keys that need to be kept pressed along with `keys` to trigger `onMatch`
49 */
50 held?: HeldKey;
51 /*
52 a callback that will trigger when the key combination is pressed
53 */
54 onMatch(matched: {ordered: Key[]; held?: ModifierKey[]}): void;
55 /*
56 a node that, when supplied, will be used to only fire `onMatch` when it is focused
57 */
58 node?: HTMLElement | null;
59 /*
60 a boolean that lets you temporarily disable the shortcut
61 */
62 ignoreInput?: boolean;
63 /*
64 a boolean that lets you opt out of swallowing the key event and let it propagate
65 */
66 allowDefault?: boolean;
67}
68```
69
70#### Basic example
71
72```ts
73// MyComponent.tsx
74
75import * as React from 'react';
76import {Shortcut} from '@shopify/react-shortcuts';
77
78export default function MyComponent() {
79 return (
80 <div>
81 {/* some app markup here */}
82 <Shortcut ordered={['f', 'o', 'o']} onMatch={() => console.log('foo')} />
83 </div>
84 );
85}
86```
87
88#### With modifier keys
89
90```ts
91// MyComponent.tsx
92
93import * as React from 'react';
94import {Shortcut} from '@shopify/react-shortcuts';
95
96export default function MyComponent() {
97 return (
98 <div>
99 {/* some app markup here */}
100 <Shortcut
101 held={['Control', 'Shift']}
102 ordered={['B']}
103 onMatch={() => console.log('bar!')}
104 />
105 </div>
106 );
107}
108```
109
110You may also want to provide alternate groupings of `held` modifier keys. For example, “undo/redo” key combos are slightly different on Windows vs Mac OS. The below example will register `onMatch` if either `Control + z` or `Meta + z` is pressed simultaneously.
111
112```ts
113// MyComponent.tsx
114
115import * as React from 'react';
116import {Shortcut} from '@shopify/react-shortcuts';
117
118export default function MyComponent() {
119 return (
120 <div>
121 {/* some app markup here */}
122 <Shortcut
123 held={[['Control'], ['Meta']]}
124 ordered={['z']}
125 onMatch={() => console.log('undo!')}
126 />
127 </div>
128 );
129}
130```
131
132**Some Gotchas**
133
1341. `Meta` refers to the “Command” key on Mac keyboards.
1352. `Fn` and `FnLock` keys are not supported because they don't produce events, as mentioned in the [spec](https://w3c.github.io/uievents-key/#key-Fn)
136
137#### On a focused node
138
139Provide a node in the form of a `ref`. `<Shortcut />` will automatically subscribe to the `ShortcutManager` once the node is available.
140
141```ts
142// MyComponent.tsx
143
144import * as React from 'react';
145import {Shortcut} from '@shopify/react-shortcuts';
146
147class MyComponent extends React.Component {
148 state = {};
149
150 render() {
151 const {fooNode} = this.state;
152 return (
153 <div>
154 <button ref={node => this.setState({fooNode: node})} />
155 <Shortcut
156 node={fooNode}
157 ordered={['f', 'o', 'o']}
158 onMatch={() => console.log('foo')}
159 />
160 </div>
161 );
162 }
163}
164```
165
166### useShortcut
167
168`<Shortcut />` invokes a hook, `useShortcut()`, under the hood. This hook is also available for use from this package. It will also register a new keyboard shortcut to the `ShortcutManager` and the API is very similar.
169
170#### API
171
172```ts
173function useShortcut(
174 // All inputs are the same as the above definitions for the props to <Shortcut />
175 ordered: Key[],
176 onMatch: (matched: {ordered: Key[]; held?: HeldKey}) => void,
177 options: {
178 held?: HeldKey;
179 node?: HTMLElement | null;
180 ignoreInput?: boolean;
181 allowDefault?: boolean;
182 } = {},
183);
184```
185
186#### Basic example
187
188The below example illustrates the same basic functionality as the `<Shortcut />` example above. However, it uses the `useShortcut()` hook and the component has been removed.
189
190```ts
191// MyComponent.tsx
192
193import * as React from 'react';
194import {useShortcut} from '@shopify/react-shortcuts';
195
196export default function MyComponent() {
197 useShortcut(['f', 'o', 'o'], () => console.log('foo'));
198
199 return <div>{/* some app markup here */}</div>;
200}
201```
202
203### ShortcutManager
204
205`ShortcutManager` is created by `ShortcutProvider` and handles the logic for calling the appropriate shortcut callbacks and avoiding conflicts. You should never need to use it directly in application code, though you may want access to it in tests.
206
207#### Example jest test
208
209Given a component implementing a `<Shortcut />`
210
211```ts
212// MyComponent.tsx
213
214export default function MyComponent() {
215 return (
216 <div>
217 {/* some app markup here */}
218 <Shortcut ordered={['f', 'o', 'o']} onMatch={() => console.log('foo')} />
219 </div>
220 );
221}
222```
223
224you might want to add a `ShortcutManager` to it's context in an enzyme test to prevent errors
225
226```ts
227// MyComponent.test.tsx
228
229import * as React from 'react';
230import {mount} from 'enzyme';
231import {ShortcutManager, Shortcut} from '@shopify/react-shortcuts';
232
233import MyComponent from './MyComponent';
234
235describe('my-component', () => {
236 it('renders a shortcut for f,o,o', () => {
237 const component = mount(<MyComponent />, {
238 context: {shortcutManager: new ShortcutManager()},
239 });
240
241 const shortcut = component.find(Shortcut);
242
243 expect(shortcut.prop('ordered')).toEqual(['f', 'o', 'o']);
244 });
245});
246```