Skip to content

Commit a4404ea

Browse files
krisantrobusnkrantzkodiakhq[bot]
authored
feat(keyboard-key): create package (#4136)
* feat(keyboard-key): component init * chore(tools): plopfile syntax fix * chore(tools): plopfile syntax fix * chore(tools): plopfile syntax fix * feat(keyboard-key): styled w/ hook unit tests * feat(keyboard-key): added variants and styles * chore(plop): update tsx dependency * chore(keyboard-key): internal exports in core * chore(keyboard-key): typedocs & build * feat(keyboard-key): change to infline-flex style * chore(keyboard-key): added stroy * chore(keyboard-key): linting * fix(keyboard-key): command logic * feat(design-tokens): added new box shadows to support keyboard-keys * chore(ci-cd): added chagesets * fix(keyboard-key): boxShadow stylings * fix(keyboard-key): remove null component wrapper * fix(keyboard-key): aria-hidden * chore(keyboard-key): refactor * chore(keyboard-key): code cleanup * chore(keyboard-key): code cleanup * chore(keyboard-key): typedocs * chore(keyboard-key): fix tests * fix(keyboard-key): aria and diableBrowserShortcuts * fix(keyboard-key): props fix * chore(keyboard-key): playgorund storybook * chore(keyboard-key): formatting fix * chore(keyboard-key): stories update * chore(keyboard-key): formatting fix * chore(keyboard-key): typo * Update .changeset/sweet-mugs-admire.md Co-authored-by: Nora Krantz <[email protected]> * Update .changeset/shaggy-sheep-confess.md Co-authored-by: Nora Krantz <[email protected]> * chore(keyboard-key): address PR comments * fix(keyboard-key): no exported member * fix(keyboard-key): typedocs * fix(keyboard-key): typedocs * chore(keyboard-key): update prop name to eventKey * chore(keyboard-key): typedoc changes --------- Co-authored-by: Nora Krantz <[email protected]> Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
1 parent 65d6054 commit a4404ea

File tree

31 files changed

+4331
-6
lines changed

31 files changed

+4331
-6
lines changed

.changeset/confused-whale.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@twilio-paste/codemods": minor
3+
---
4+
5+
[KeyboardKey] added a new component to display visual indicators for keyboard shortcuts available to users

.changeset/shaggy-sheep-confess.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@twilio-paste/core": minor
3+
"@twilio-paste/design-tokens": minor
4+
---
5+
6+
[Design Tokens] added new design tokens shadowBorderBottomWeak and shadowBorderBottomInverseWeaker to support new feature, KeybaordKey.

.changeset/sweet-mugs-admire.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@twilio-paste/keyboard-key": major
3+
"@twilio-paste/core": minor
4+
---
5+
6+
[KeyboardKey] added a new component to display visual indicators for keyboard shortcuts available to users

.codesandbox/ci.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
"/packages/paste-core/components/inline-control-group",
5757
"/packages/paste-core/components/input",
5858
"/packages/paste-core/components/input-box",
59+
"/packages/paste-core/components/keyboard-key",
5960
"/packages/paste-core/components/label",
6061
"/packages/paste-libraries/lexical",
6162
"/packages/paste-core/components/list",

packages/paste-codemods/tools/.cache/mappings.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,10 @@
147147
"Prefix": "@twilio-paste/core/input-box",
148148
"Suffix": "@twilio-paste/core/input-box",
149149
"getInputChevronIconColor": "@twilio-paste/core/input-box",
150+
"KeyboardKey": "@twilio-paste/core/keyboard-key",
151+
"KeyboardKeyGroup": "@twilio-paste/core/keyboard-key",
152+
"useKeyCombination": "@twilio-paste/core/keyboard-key",
153+
"useKeyCombinations": "@twilio-paste/core/keyboard-key",
150154
"Label": "@twilio-paste/core/label",
151155
"RequiredDot": "@twilio-paste/core/label",
152156
"List": "@twilio-paste/core/list",

packages/paste-core/components/keyboard-key/CHANGELOG.md

Whitespace-only changes.
Lines changed: 315 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,315 @@
1+
import { act, fireEvent, render, renderHook, screen, waitFor } from "@testing-library/react";
2+
import * as React from "react";
3+
4+
import { useKeyCombination, useKeyCombinations } from "../src";
5+
import { Default } from "../stories/index.stories";
6+
7+
describe("Hooks", () => {
8+
it("should handle pressed styling", async () => {
9+
const { getAllByText } = render(<Default />);
10+
11+
const controlKey = getAllByText("Control")[0];
12+
const bKey = getAllByText("B")[0];
13+
expect(controlKey).toBeDefined();
14+
expect(controlKey).toHaveStyleRule("background-color", "rgb(249, 249, 250)");
15+
expect(bKey).toHaveStyleRule("background-color", "rgb(249, 249, 250)");
16+
17+
await act(async () => {
18+
fireEvent.keyDown(controlKey, { key: "Control" });
19+
});
20+
21+
expect(controlKey).toHaveStyleRule("background-color", "rgb(225, 227, 234)");
22+
expect(bKey).toHaveStyleRule("background-color", "rgb(249, 249, 250)");
23+
24+
await act(async () => {
25+
fireEvent.keyUp(controlKey, { key: "Control" });
26+
});
27+
28+
expect(controlKey).toHaveStyleRule("background-color", "rgb(249, 249, 250)");
29+
expect(bKey).toHaveStyleRule("background-color", "rgb(249, 249, 250)");
30+
});
31+
32+
describe("useKeyCombination", () => {
33+
it("should update activeKeys on keydown and keyup", async () => {
34+
const { result } = renderHook(() => useKeyCombination({ keys: ["Control", "b"], onCombinationPress: jest.fn() }));
35+
36+
expect(result.current?.activeKeys).toEqual([]);
37+
38+
await act(async () => {
39+
fireEvent.keyDown(window, { key: "Control" });
40+
});
41+
42+
await waitFor(() => {
43+
expect(result.current?.activeKeys).toEqual(["Control"]);
44+
});
45+
46+
await act(async () => {
47+
fireEvent.keyDown(window, { key: "d" });
48+
fireEvent.keyDown(window, { key: "v" });
49+
});
50+
51+
await waitFor(() => {
52+
expect(result.current?.activeKeys).toEqual(["Control", "d", "v"]);
53+
});
54+
55+
await act(async () => {
56+
fireEvent.keyUp(window, { key: "Control" });
57+
fireEvent.keyUp(window, { key: "d" });
58+
});
59+
60+
await waitFor(() => {
61+
expect(result.current?.activeKeys).toEqual(["v"]);
62+
});
63+
});
64+
65+
it("should call onCombinationPress when keys match", async () => {
66+
const onCombinationPress = jest.fn();
67+
const { result } = renderHook(() => useKeyCombination({ keys: ["Control", "b"], onCombinationPress }), {});
68+
69+
expect(result.current?.activeKeys).toEqual([]);
70+
71+
await act(async () => {
72+
fireEvent.keyDown(window, { key: "Control" });
73+
});
74+
75+
await waitFor(() => {
76+
expect(result.current?.activeKeys).toEqual(["Control"]);
77+
});
78+
79+
await act(async () => {
80+
fireEvent.keyDown(window, { key: "b" });
81+
});
82+
83+
await waitFor(() => {
84+
expect(result.current?.activeKeys).toEqual(expect.arrayContaining(["Control", "b"]));
85+
expect(onCombinationPress).toHaveBeenCalled();
86+
});
87+
});
88+
89+
it("should not call onCombinationPress when keys do not match", async () => {
90+
const onCombinationPress = jest.fn();
91+
const { result } = renderHook(() => useKeyCombination({ keys: ["Control", "b"], onCombinationPress }), {});
92+
93+
expect(result.current?.activeKeys).toEqual([]);
94+
95+
await act(async () => {
96+
fireEvent.keyDown(window, { key: "Control" });
97+
});
98+
99+
await waitFor(() => {
100+
expect(result.current?.activeKeys).toEqual(["Control"]);
101+
});
102+
103+
await act(async () => {
104+
fireEvent.keyDown(window, { key: "d" });
105+
});
106+
107+
await waitFor(() => {
108+
expect(result.current?.activeKeys).toEqual(["Control", "d"]);
109+
expect(onCombinationPress).not.toHaveBeenCalled();
110+
});
111+
});
112+
113+
it("should not call onCombinationPress when keys are present but more are pressed", async () => {
114+
const onCombinationPress = jest.fn();
115+
const { result } = renderHook(() => useKeyCombination({ keys: ["Control", "b"], onCombinationPress }), {});
116+
117+
expect(result.current?.activeKeys).toEqual([]);
118+
119+
await act(async () => {
120+
fireEvent.keyDown(window, { key: "Control" });
121+
});
122+
123+
await waitFor(() => {
124+
expect(result.current?.activeKeys).toEqual(["Control"]);
125+
});
126+
127+
await act(async () => {
128+
fireEvent.keyDown(window, { key: "v" });
129+
fireEvent.keyDown(window, { key: "b" });
130+
});
131+
132+
await waitFor(() => {
133+
expect(result.current?.activeKeys).toEqual(expect.arrayContaining(["Control", "v", "b"]));
134+
expect(onCombinationPress).not.toHaveBeenCalled();
135+
});
136+
});
137+
138+
it("should not call onCombinationPress when disabled", async () => {
139+
const onCombinationPress = jest.fn();
140+
const { result } = renderHook(() =>
141+
useKeyCombination({ keys: ["Control", "b"], onCombinationPress, disabled: true }),
142+
);
143+
144+
expect(result.current?.activeKeys).toEqual([]);
145+
146+
await act(async () => {
147+
fireEvent.keyDown(window, { key: "Control" });
148+
});
149+
150+
await waitFor(() => {
151+
expect(result.current?.activeKeys).toEqual(["Control"]);
152+
});
153+
154+
await act(async () => {
155+
fireEvent.keyDown(window, { key: "b" });
156+
});
157+
158+
await waitFor(() => {
159+
expect(result.current?.activeKeys).toEqual(expect.arrayContaining(["Control", "b"]));
160+
expect(onCombinationPress).not.toHaveBeenCalled();
161+
});
162+
});
163+
});
164+
165+
describe("useKeyCombinations", () => {
166+
it("should update activeKeys on keydown and keyup", async () => {
167+
const { result } = renderHook(() =>
168+
useKeyCombinations({
169+
combinations: [
170+
{ keys: ["Control", "b"], onCombinationPress: jest.fn() },
171+
{ keys: ["Control", "c"], onCombinationPress: jest.fn() },
172+
],
173+
}),
174+
);
175+
176+
expect(result.current?.activeKeys).toEqual([]);
177+
178+
await act(async () => {
179+
fireEvent.keyDown(window, { key: "Control" });
180+
});
181+
182+
await waitFor(() => {
183+
expect(result.current?.activeKeys).toEqual(["Control"]);
184+
});
185+
186+
await act(async () => {
187+
fireEvent.keyDown(window, { key: "d" });
188+
fireEvent.keyDown(window, { key: "v" });
189+
});
190+
191+
await waitFor(() => {
192+
expect(result.current?.activeKeys).toEqual(["Control", "d", "v"]);
193+
});
194+
195+
await act(async () => {
196+
fireEvent.keyUp(window, { key: "Control" });
197+
fireEvent.keyUp(window, { key: "d" });
198+
});
199+
200+
await waitFor(() => {
201+
expect(result.current?.activeKeys).toEqual(["v"]);
202+
});
203+
});
204+
205+
it("should call onCombinationPress when keys match", async () => {
206+
const onCombinationPress1 = jest.fn();
207+
const onCombinationPress2 = jest.fn();
208+
209+
const { result } = renderHook(() =>
210+
useKeyCombinations({
211+
combinations: [
212+
{ keys: ["Control", "b"], onCombinationPress: onCombinationPress1 },
213+
{ keys: ["Control", "c"], onCombinationPress: onCombinationPress2 },
214+
],
215+
}),
216+
);
217+
218+
expect(result.current?.activeKeys).toEqual([]);
219+
220+
await act(async () => {
221+
fireEvent.keyDown(window, { key: "Control" });
222+
});
223+
224+
await waitFor(() => {
225+
expect(result.current?.activeKeys).toEqual(["Control"]);
226+
});
227+
228+
await act(async () => {
229+
fireEvent.keyDown(window, { key: "b" });
230+
});
231+
232+
await waitFor(() => {
233+
expect(result.current?.activeKeys).toEqual(expect.arrayContaining(["Control", "b"]));
234+
expect(onCombinationPress1).toHaveBeenCalled();
235+
expect(onCombinationPress2).not.toHaveBeenCalled();
236+
});
237+
238+
await act(async () => {
239+
onCombinationPress1.mockClear();
240+
fireEvent.keyDown(window, { key: "c" });
241+
});
242+
243+
await waitFor(() => {
244+
expect(result.current?.activeKeys).toEqual(expect.arrayContaining(["Control", "b", "c"]));
245+
expect(onCombinationPress1).not.toHaveBeenCalled();
246+
expect(onCombinationPress2).not.toHaveBeenCalled();
247+
});
248+
249+
await act(async () => {
250+
fireEvent.keyUp(window, { key: "b" });
251+
});
252+
253+
await waitFor(() => {
254+
expect(result.current?.activeKeys).toEqual(expect.arrayContaining(["Control", "c"]));
255+
expect(onCombinationPress1).not.toHaveBeenCalled();
256+
expect(onCombinationPress2).toHaveBeenCalled();
257+
});
258+
});
259+
260+
it("should not call onCOmbinationPress when disabled", async () => {
261+
const onCombinationPress1 = jest.fn();
262+
const onCombinationPress2 = jest.fn();
263+
264+
const { result } = renderHook(() =>
265+
useKeyCombinations({
266+
combinations: [
267+
{ keys: ["Control", "b"], onCombinationPress: onCombinationPress1 },
268+
{ keys: ["Control", "c"], onCombinationPress: onCombinationPress2, disabled: true },
269+
],
270+
}),
271+
);
272+
273+
expect(result.current?.activeKeys).toEqual([]);
274+
275+
await act(async () => {
276+
fireEvent.keyDown(window, { key: "Control" });
277+
});
278+
279+
await waitFor(() => {
280+
expect(result.current?.activeKeys).toEqual(["Control"]);
281+
});
282+
283+
await act(async () => {
284+
fireEvent.keyDown(window, { key: "b" });
285+
});
286+
287+
await waitFor(() => {
288+
expect(result.current?.activeKeys).toEqual(expect.arrayContaining(["Control", "b"]));
289+
expect(onCombinationPress1).toHaveBeenCalled();
290+
expect(onCombinationPress2).not.toHaveBeenCalled();
291+
});
292+
293+
await act(async () => {
294+
onCombinationPress1.mockClear();
295+
fireEvent.keyDown(window, { key: "c" });
296+
});
297+
298+
await waitFor(() => {
299+
expect(result.current?.activeKeys).toEqual(expect.arrayContaining(["Control", "b", "c"]));
300+
expect(onCombinationPress1).not.toHaveBeenCalled();
301+
expect(onCombinationPress2).not.toHaveBeenCalled();
302+
});
303+
304+
await act(async () => {
305+
fireEvent.keyUp(window, { key: "b" });
306+
});
307+
308+
await waitFor(() => {
309+
expect(result.current?.activeKeys).toEqual(expect.arrayContaining(["Control", "c"]));
310+
expect(onCombinationPress1).not.toHaveBeenCalled();
311+
expect(onCombinationPress2).not.toHaveBeenCalled();
312+
});
313+
});
314+
});
315+
});

0 commit comments

Comments
 (0)