Skip to content

Commit

Permalink
feat(keyboard-key): create package (#4136)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
3 people authored Nov 13, 2024
1 parent 65d6054 commit a4404ea
Show file tree
Hide file tree
Showing 31 changed files with 4,331 additions and 6 deletions.
5 changes: 5 additions & 0 deletions .changeset/confused-whale.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@twilio-paste/codemods": minor
---

[KeyboardKey] added a new component to display visual indicators for keyboard shortcuts available to users
6 changes: 6 additions & 0 deletions .changeset/shaggy-sheep-confess.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@twilio-paste/core": minor
"@twilio-paste/design-tokens": minor
---

[Design Tokens] added new design tokens shadowBorderBottomWeak and shadowBorderBottomInverseWeaker to support new feature, KeybaordKey.
6 changes: 6 additions & 0 deletions .changeset/sweet-mugs-admire.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@twilio-paste/keyboard-key": major
"@twilio-paste/core": minor
---

[KeyboardKey] added a new component to display visual indicators for keyboard shortcuts available to users
1 change: 1 addition & 0 deletions .codesandbox/ci.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
"/packages/paste-core/components/inline-control-group",
"/packages/paste-core/components/input",
"/packages/paste-core/components/input-box",
"/packages/paste-core/components/keyboard-key",
"/packages/paste-core/components/label",
"/packages/paste-libraries/lexical",
"/packages/paste-core/components/list",
Expand Down
4 changes: 4 additions & 0 deletions packages/paste-codemods/tools/.cache/mappings.json
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,10 @@
"Prefix": "@twilio-paste/core/input-box",
"Suffix": "@twilio-paste/core/input-box",
"getInputChevronIconColor": "@twilio-paste/core/input-box",
"KeyboardKey": "@twilio-paste/core/keyboard-key",
"KeyboardKeyGroup": "@twilio-paste/core/keyboard-key",
"useKeyCombination": "@twilio-paste/core/keyboard-key",
"useKeyCombinations": "@twilio-paste/core/keyboard-key",
"Label": "@twilio-paste/core/label",
"RequiredDot": "@twilio-paste/core/label",
"List": "@twilio-paste/core/list",
Expand Down
Empty file.
315 changes: 315 additions & 0 deletions packages/paste-core/components/keyboard-key/__tests__/hooks.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,315 @@
import { act, fireEvent, render, renderHook, screen, waitFor } from "@testing-library/react";
import * as React from "react";

import { useKeyCombination, useKeyCombinations } from "../src";
import { Default } from "../stories/index.stories";

describe("Hooks", () => {
it("should handle pressed styling", async () => {
const { getAllByText } = render(<Default />);

const controlKey = getAllByText("Control")[0];
const bKey = getAllByText("B")[0];
expect(controlKey).toBeDefined();
expect(controlKey).toHaveStyleRule("background-color", "rgb(249, 249, 250)");
expect(bKey).toHaveStyleRule("background-color", "rgb(249, 249, 250)");

await act(async () => {
fireEvent.keyDown(controlKey, { key: "Control" });
});

expect(controlKey).toHaveStyleRule("background-color", "rgb(225, 227, 234)");
expect(bKey).toHaveStyleRule("background-color", "rgb(249, 249, 250)");

await act(async () => {
fireEvent.keyUp(controlKey, { key: "Control" });
});

expect(controlKey).toHaveStyleRule("background-color", "rgb(249, 249, 250)");
expect(bKey).toHaveStyleRule("background-color", "rgb(249, 249, 250)");
});

describe("useKeyCombination", () => {
it("should update activeKeys on keydown and keyup", async () => {
const { result } = renderHook(() => useKeyCombination({ keys: ["Control", "b"], onCombinationPress: jest.fn() }));

expect(result.current?.activeKeys).toEqual([]);

await act(async () => {
fireEvent.keyDown(window, { key: "Control" });
});

await waitFor(() => {
expect(result.current?.activeKeys).toEqual(["Control"]);
});

await act(async () => {
fireEvent.keyDown(window, { key: "d" });
fireEvent.keyDown(window, { key: "v" });
});

await waitFor(() => {
expect(result.current?.activeKeys).toEqual(["Control", "d", "v"]);
});

await act(async () => {
fireEvent.keyUp(window, { key: "Control" });
fireEvent.keyUp(window, { key: "d" });
});

await waitFor(() => {
expect(result.current?.activeKeys).toEqual(["v"]);
});
});

it("should call onCombinationPress when keys match", async () => {
const onCombinationPress = jest.fn();
const { result } = renderHook(() => useKeyCombination({ keys: ["Control", "b"], onCombinationPress }), {});

expect(result.current?.activeKeys).toEqual([]);

await act(async () => {
fireEvent.keyDown(window, { key: "Control" });
});

await waitFor(() => {
expect(result.current?.activeKeys).toEqual(["Control"]);
});

await act(async () => {
fireEvent.keyDown(window, { key: "b" });
});

await waitFor(() => {
expect(result.current?.activeKeys).toEqual(expect.arrayContaining(["Control", "b"]));
expect(onCombinationPress).toHaveBeenCalled();
});
});

it("should not call onCombinationPress when keys do not match", async () => {
const onCombinationPress = jest.fn();
const { result } = renderHook(() => useKeyCombination({ keys: ["Control", "b"], onCombinationPress }), {});

expect(result.current?.activeKeys).toEqual([]);

await act(async () => {
fireEvent.keyDown(window, { key: "Control" });
});

await waitFor(() => {
expect(result.current?.activeKeys).toEqual(["Control"]);
});

await act(async () => {
fireEvent.keyDown(window, { key: "d" });
});

await waitFor(() => {
expect(result.current?.activeKeys).toEqual(["Control", "d"]);
expect(onCombinationPress).not.toHaveBeenCalled();
});
});

it("should not call onCombinationPress when keys are present but more are pressed", async () => {
const onCombinationPress = jest.fn();
const { result } = renderHook(() => useKeyCombination({ keys: ["Control", "b"], onCombinationPress }), {});

expect(result.current?.activeKeys).toEqual([]);

await act(async () => {
fireEvent.keyDown(window, { key: "Control" });
});

await waitFor(() => {
expect(result.current?.activeKeys).toEqual(["Control"]);
});

await act(async () => {
fireEvent.keyDown(window, { key: "v" });
fireEvent.keyDown(window, { key: "b" });
});

await waitFor(() => {
expect(result.current?.activeKeys).toEqual(expect.arrayContaining(["Control", "v", "b"]));
expect(onCombinationPress).not.toHaveBeenCalled();
});
});

it("should not call onCombinationPress when disabled", async () => {
const onCombinationPress = jest.fn();
const { result } = renderHook(() =>
useKeyCombination({ keys: ["Control", "b"], onCombinationPress, disabled: true }),
);

expect(result.current?.activeKeys).toEqual([]);

await act(async () => {
fireEvent.keyDown(window, { key: "Control" });
});

await waitFor(() => {
expect(result.current?.activeKeys).toEqual(["Control"]);
});

await act(async () => {
fireEvent.keyDown(window, { key: "b" });
});

await waitFor(() => {
expect(result.current?.activeKeys).toEqual(expect.arrayContaining(["Control", "b"]));
expect(onCombinationPress).not.toHaveBeenCalled();
});
});
});

describe("useKeyCombinations", () => {
it("should update activeKeys on keydown and keyup", async () => {
const { result } = renderHook(() =>
useKeyCombinations({
combinations: [
{ keys: ["Control", "b"], onCombinationPress: jest.fn() },
{ keys: ["Control", "c"], onCombinationPress: jest.fn() },
],
}),
);

expect(result.current?.activeKeys).toEqual([]);

await act(async () => {
fireEvent.keyDown(window, { key: "Control" });
});

await waitFor(() => {
expect(result.current?.activeKeys).toEqual(["Control"]);
});

await act(async () => {
fireEvent.keyDown(window, { key: "d" });
fireEvent.keyDown(window, { key: "v" });
});

await waitFor(() => {
expect(result.current?.activeKeys).toEqual(["Control", "d", "v"]);
});

await act(async () => {
fireEvent.keyUp(window, { key: "Control" });
fireEvent.keyUp(window, { key: "d" });
});

await waitFor(() => {
expect(result.current?.activeKeys).toEqual(["v"]);
});
});

it("should call onCombinationPress when keys match", async () => {
const onCombinationPress1 = jest.fn();
const onCombinationPress2 = jest.fn();

const { result } = renderHook(() =>
useKeyCombinations({
combinations: [
{ keys: ["Control", "b"], onCombinationPress: onCombinationPress1 },
{ keys: ["Control", "c"], onCombinationPress: onCombinationPress2 },
],
}),
);

expect(result.current?.activeKeys).toEqual([]);

await act(async () => {
fireEvent.keyDown(window, { key: "Control" });
});

await waitFor(() => {
expect(result.current?.activeKeys).toEqual(["Control"]);
});

await act(async () => {
fireEvent.keyDown(window, { key: "b" });
});

await waitFor(() => {
expect(result.current?.activeKeys).toEqual(expect.arrayContaining(["Control", "b"]));
expect(onCombinationPress1).toHaveBeenCalled();
expect(onCombinationPress2).not.toHaveBeenCalled();
});

await act(async () => {
onCombinationPress1.mockClear();
fireEvent.keyDown(window, { key: "c" });
});

await waitFor(() => {
expect(result.current?.activeKeys).toEqual(expect.arrayContaining(["Control", "b", "c"]));
expect(onCombinationPress1).not.toHaveBeenCalled();
expect(onCombinationPress2).not.toHaveBeenCalled();
});

await act(async () => {
fireEvent.keyUp(window, { key: "b" });
});

await waitFor(() => {
expect(result.current?.activeKeys).toEqual(expect.arrayContaining(["Control", "c"]));
expect(onCombinationPress1).not.toHaveBeenCalled();
expect(onCombinationPress2).toHaveBeenCalled();
});
});

it("should not call onCOmbinationPress when disabled", async () => {
const onCombinationPress1 = jest.fn();
const onCombinationPress2 = jest.fn();

const { result } = renderHook(() =>
useKeyCombinations({
combinations: [
{ keys: ["Control", "b"], onCombinationPress: onCombinationPress1 },
{ keys: ["Control", "c"], onCombinationPress: onCombinationPress2, disabled: true },
],
}),
);

expect(result.current?.activeKeys).toEqual([]);

await act(async () => {
fireEvent.keyDown(window, { key: "Control" });
});

await waitFor(() => {
expect(result.current?.activeKeys).toEqual(["Control"]);
});

await act(async () => {
fireEvent.keyDown(window, { key: "b" });
});

await waitFor(() => {
expect(result.current?.activeKeys).toEqual(expect.arrayContaining(["Control", "b"]));
expect(onCombinationPress1).toHaveBeenCalled();
expect(onCombinationPress2).not.toHaveBeenCalled();
});

await act(async () => {
onCombinationPress1.mockClear();
fireEvent.keyDown(window, { key: "c" });
});

await waitFor(() => {
expect(result.current?.activeKeys).toEqual(expect.arrayContaining(["Control", "b", "c"]));
expect(onCombinationPress1).not.toHaveBeenCalled();
expect(onCombinationPress2).not.toHaveBeenCalled();
});

await act(async () => {
fireEvent.keyUp(window, { key: "b" });
});

await waitFor(() => {
expect(result.current?.activeKeys).toEqual(expect.arrayContaining(["Control", "c"]));
expect(onCombinationPress1).not.toHaveBeenCalled();
expect(onCombinationPress2).not.toHaveBeenCalled();
});
});
});
});
Loading

0 comments on commit a4404ea

Please sign in to comment.