diff --git a/.changeset/slow-jokes-matter.md b/.changeset/slow-jokes-matter.md new file mode 100644 index 0000000000..d2f5b3d6eb --- /dev/null +++ b/.changeset/slow-jokes-matter.md @@ -0,0 +1,6 @@ +--- +"@twilio-paste/keyboard-key": patch +"@twilio-paste/core": patch +--- + +[KeyboardKey] fixed issue with shift key being captured as capitals on subsequesnt key presses diff --git a/cypress/integration/sitemap-vrt/constants.ts b/cypress/integration/sitemap-vrt/constants.ts index ca86529876..e9754f6749 100644 --- a/cypress/integration/sitemap-vrt/constants.ts +++ b/cypress/integration/sitemap-vrt/constants.ts @@ -132,6 +132,9 @@ export const SITEMAP = [ "/components/input/", "/components/input/api", "/components/input/changelog", + "components/keyboard-key", + "components/keyboard-key/api", + "components/keyboard-key/changelog", "/components/label/", "/components/label/api", "/components/label/changelog", diff --git a/packages/paste-core/components/keyboard-key/__tests__/hooks.spec.tsx b/packages/paste-core/components/keyboard-key/__tests__/hooks.spec.tsx index e717daebf3..be5ede3a3c 100644 --- a/packages/paste-core/components/keyboard-key/__tests__/hooks.spec.tsx +++ b/packages/paste-core/components/keyboard-key/__tests__/hooks.spec.tsx @@ -40,7 +40,7 @@ describe("Hooks", () => { }); await waitFor(() => { - expect(result.current?.activeKeys).toEqual(["Control"]); + expect(result.current?.activeKeys).toEqual(["control"]); }); await act(async () => { @@ -49,11 +49,11 @@ describe("Hooks", () => { }); await waitFor(() => { - expect(result.current?.activeKeys).toEqual(["Control", "d", "v"]); + expect(result.current?.activeKeys).toEqual(["control", "d", "v"]); }); await act(async () => { - fireEvent.keyUp(window, { key: "Control" }); + fireEvent.keyUp(window, { key: "control" }); fireEvent.keyUp(window, { key: "d" }); }); @@ -73,7 +73,7 @@ describe("Hooks", () => { }); await waitFor(() => { - expect(result.current?.activeKeys).toEqual(["Control"]); + expect(result.current?.activeKeys).toEqual(["control"]); }); await act(async () => { @@ -81,7 +81,7 @@ describe("Hooks", () => { }); await waitFor(() => { - expect(result.current?.activeKeys).toEqual(expect.arrayContaining(["Control", "b"])); + expect(result.current?.activeKeys).toEqual(expect.arrayContaining(["control", "b"])); expect(onCombinationPress).toHaveBeenCalled(); }); }); @@ -97,7 +97,7 @@ describe("Hooks", () => { }); await waitFor(() => { - expect(result.current?.activeKeys).toEqual(["Control"]); + expect(result.current?.activeKeys).toEqual(["control"]); }); await act(async () => { @@ -105,7 +105,7 @@ describe("Hooks", () => { }); await waitFor(() => { - expect(result.current?.activeKeys).toEqual(["Control", "d"]); + expect(result.current?.activeKeys).toEqual(["control", "d"]); expect(onCombinationPress).not.toHaveBeenCalled(); }); }); @@ -121,7 +121,7 @@ describe("Hooks", () => { }); await waitFor(() => { - expect(result.current?.activeKeys).toEqual(["Control"]); + expect(result.current?.activeKeys).toEqual(["control"]); }); await act(async () => { @@ -130,7 +130,7 @@ describe("Hooks", () => { }); await waitFor(() => { - expect(result.current?.activeKeys).toEqual(expect.arrayContaining(["Control", "v", "b"])); + expect(result.current?.activeKeys).toEqual(expect.arrayContaining(["control", "v", "b"])); expect(onCombinationPress).not.toHaveBeenCalled(); }); }); @@ -148,7 +148,7 @@ describe("Hooks", () => { }); await waitFor(() => { - expect(result.current?.activeKeys).toEqual(["Control"]); + expect(result.current?.activeKeys).toEqual(["control"]); }); await act(async () => { @@ -156,7 +156,7 @@ describe("Hooks", () => { }); await waitFor(() => { - expect(result.current?.activeKeys).toEqual(expect.arrayContaining(["Control", "b"])); + expect(result.current?.activeKeys).toEqual(expect.arrayContaining(["control", "b"])); expect(onCombinationPress).not.toHaveBeenCalled(); }); }); @@ -180,7 +180,7 @@ describe("Hooks", () => { }); await waitFor(() => { - expect(result.current?.activeKeys).toEqual(["Control"]); + expect(result.current?.activeKeys).toEqual(["control"]); }); await act(async () => { @@ -189,7 +189,7 @@ describe("Hooks", () => { }); await waitFor(() => { - expect(result.current?.activeKeys).toEqual(["Control", "d", "v"]); + expect(result.current?.activeKeys).toEqual(["control", "d", "v"]); }); await act(async () => { @@ -222,7 +222,7 @@ describe("Hooks", () => { }); await waitFor(() => { - expect(result.current?.activeKeys).toEqual(["Control"]); + expect(result.current?.activeKeys).toEqual(["control"]); }); await act(async () => { @@ -230,7 +230,7 @@ describe("Hooks", () => { }); await waitFor(() => { - expect(result.current?.activeKeys).toEqual(expect.arrayContaining(["Control", "b"])); + expect(result.current?.activeKeys).toEqual(expect.arrayContaining(["control", "b"])); expect(onCombinationPress1).toHaveBeenCalled(); expect(onCombinationPress2).not.toHaveBeenCalled(); }); @@ -241,7 +241,7 @@ describe("Hooks", () => { }); await waitFor(() => { - expect(result.current?.activeKeys).toEqual(expect.arrayContaining(["Control", "b", "c"])); + expect(result.current?.activeKeys).toEqual(expect.arrayContaining(["control", "b", "c"])); expect(onCombinationPress1).not.toHaveBeenCalled(); expect(onCombinationPress2).not.toHaveBeenCalled(); }); @@ -251,7 +251,7 @@ describe("Hooks", () => { }); await waitFor(() => { - expect(result.current?.activeKeys).toEqual(expect.arrayContaining(["Control", "c"])); + expect(result.current?.activeKeys).toEqual(expect.arrayContaining(["control", "c"])); expect(onCombinationPress1).not.toHaveBeenCalled(); expect(onCombinationPress2).toHaveBeenCalled(); }); @@ -277,7 +277,7 @@ describe("Hooks", () => { }); await waitFor(() => { - expect(result.current?.activeKeys).toEqual(["Control"]); + expect(result.current?.activeKeys).toEqual(["control"]); }); await act(async () => { @@ -285,7 +285,7 @@ describe("Hooks", () => { }); await waitFor(() => { - expect(result.current?.activeKeys).toEqual(expect.arrayContaining(["Control", "b"])); + expect(result.current?.activeKeys).toEqual(expect.arrayContaining(["control", "b"])); expect(onCombinationPress1).toHaveBeenCalled(); expect(onCombinationPress2).not.toHaveBeenCalled(); }); @@ -296,7 +296,7 @@ describe("Hooks", () => { }); await waitFor(() => { - expect(result.current?.activeKeys).toEqual(expect.arrayContaining(["Control", "b", "c"])); + expect(result.current?.activeKeys).toEqual(expect.arrayContaining(["control", "b", "c"])); expect(onCombinationPress1).not.toHaveBeenCalled(); expect(onCombinationPress2).not.toHaveBeenCalled(); }); @@ -306,7 +306,7 @@ describe("Hooks", () => { }); await waitFor(() => { - expect(result.current?.activeKeys).toEqual(expect.arrayContaining(["Control", "c"])); + expect(result.current?.activeKeys).toEqual(expect.arrayContaining(["control", "c"])); expect(onCombinationPress1).not.toHaveBeenCalled(); expect(onCombinationPress2).not.toHaveBeenCalled(); }); diff --git a/packages/paste-core/components/keyboard-key/package.json b/packages/paste-core/components/keyboard-key/package.json index 50e3664666..cc14d941eb 100644 --- a/packages/paste-core/components/keyboard-key/package.json +++ b/packages/paste-core/components/keyboard-key/package.json @@ -3,7 +3,7 @@ "version": "0.0.0", "category": "typography", "status": "production", - "description": "A keyboard key distinguishes a keyboard command or shortcut from other text.", + "description": "A Keyboard Key distinguishes a keyboard command or shortcut from other text.", "author": "Twilio Inc.", "license": "MIT", "main:dev": "src/index.tsx", diff --git a/packages/paste-core/components/keyboard-key/src/hooks.ts b/packages/paste-core/components/keyboard-key/src/hooks.ts index 48e8049c47..64427f4705 100644 --- a/packages/paste-core/components/keyboard-key/src/hooks.ts +++ b/packages/paste-core/components/keyboard-key/src/hooks.ts @@ -27,7 +27,7 @@ const useKeyEvents = (): { activeKeys: string[] } => { const handleKeyDown = (e: KeyboardEvent): void => { if (!e.repeat) { setActiveKeys((prev) => { - return Array.from(new Set([...prev, e.key])); + return Array.from(new Set([...prev, e.key.toLowerCase()])); }); } }; @@ -41,7 +41,7 @@ const useKeyEvents = (): { activeKeys: string[] } => { if (e.key === "Meta") { setActiveKeys([]); } else { - setActiveKeys((prev) => [...prev].filter((k) => k !== e.key)); + setActiveKeys((prev) => [...prev].filter((k) => k.toLowerCase() !== e.key.toLowerCase())); } }; @@ -59,7 +59,8 @@ const useKeyEvents = (): { activeKeys: string[] } => { }; const stringArrayMatches = (arr1: string[], arr2: string[]): boolean => - JSON.stringify(arr1.sort((a, b) => a.localeCompare(b))) === JSON.stringify(arr2.sort((a, b) => a.localeCompare(b))); + JSON.stringify(arr1.sort((a, b) => a.localeCompare(b)).map((s) => s.toLowerCase())) === + JSON.stringify(arr2.sort((a, b) => a.localeCompare(b))); export const useKeyCombination = ({ keys, diff --git a/packages/paste-website/package.json b/packages/paste-website/package.json index 77db2fa787..3839473171 100644 --- a/packages/paste-website/package.json +++ b/packages/paste-website/package.json @@ -84,6 +84,7 @@ "@twilio-paste/inline-control-group": "^13.0.2", "@twilio-paste/input": "^9.1.3", "@twilio-paste/input-box": "^10.1.1", + "@twilio-paste/keyboard-key": "^0.0.0", "@twilio-paste/label": "^13.1.1", "@twilio-paste/lexical-library": "^4.2.0", "@twilio-paste/list": "^8.2.1", diff --git a/packages/paste-website/src/component-examples/KeyboardKeyExamples.ts b/packages/paste-website/src/component-examples/KeyboardKeyExamples.ts new file mode 100644 index 0000000000..301b51f5b4 --- /dev/null +++ b/packages/paste-website/src/component-examples/KeyboardKeyExamples.ts @@ -0,0 +1,182 @@ +export const basicExample = ` + Control + B + +`.trim(); + +export const defaultExample = `<> + Press{" "} + Control + S + {" "}to save. +`.trim(); + +export const disabledExample = `const DisabledExample = () => { + const menu = useMenuState(); + return ( + + Open the Menu for the disabled Keyboard Key in context: + + Edit + + + + + Cut + + Cmd + X + + + + + + Paste + + Cmd + V + + + + + + Save + + Cmd + S + + + + + + ); +}; + +render( + +)`.trim(); + +export const pressedExample = `const PressedExample = () => { + const keyCombinationState = useKeyCombination({ + keys: ["Shift", "s"], + onCombinationPress: ()=> {}, + enablePressStyles: true, + }); + + return ( + <> + Press the “Shift” or “S” key to reveal the pressed states below: + + Shift + S + + + ) +} + +render( + +)`.trim(); + +export const tooltipExample = ` + + + + + + +`.trim(); + +export const useKeyCombinationExample = `const HookExample = () => { + const [combinationTriggeredText, setCombinationTriggeredText] = React.useState(""); + useKeyCombination({ + keys: ["Shift", "q"], + onCombinationPress: () => { + setCombinationTriggeredText("Shift + Q pressed"); + }, + enablePressStyles: true, + }); + + return ( + <> + Press the ShiftQ key to reveal the pressed states below: + Combination triggered: {combinationTriggeredText} + + ) +} + +render( + +)`.trim(); + +export const useKeyCombinationsExample = `const HookExample = () => { + const [combinationTriggeredText, setCombinationTriggeredText] = React.useState(""); + + useKeyCombinations({ + combinations: [ + { + keys: ["Control", "b"], + onCombinationPress: () => { + setCombinationTriggeredText("Control + B pressed"); + }, + disabled: false, + }, + { + keys: ["Control", "k"], + onCombinationPress: () => { + setCombinationTriggeredText("Control + K pressed"); + }, + disabled: false, + }, + { + keys: ["Control", "y"], + onCombinationPress: () => { + setCombinationTriggeredText("Control + Y pressed"); + }, + disabled: false, + }, + ], + }); + + return ( + <> + Use the following combinations to test. You can also set the disabled state in the code block below. + + + + Control + B + + + Control + K + + + Control + Y + + + + Combination triggered: {combinationTriggeredText} + + ); +}; + +render( + +)`.trim(); diff --git a/packages/paste-website/src/components/site-wrapper/site-header/SiteHeaderSearch.tsx b/packages/paste-website/src/components/site-wrapper/site-header/SiteHeaderSearch.tsx index f7307dfe3b..4c1df8801e 100644 --- a/packages/paste-website/src/components/site-wrapper/site-header/SiteHeaderSearch.tsx +++ b/packages/paste-website/src/components/site-wrapper/site-header/SiteHeaderSearch.tsx @@ -1,18 +1,18 @@ import { Box } from "@twilio-paste/box"; import { Button } from "@twilio-paste/button"; import { SearchIcon } from "@twilio-paste/icons/esm/SearchIcon"; -import { InlineCode } from "@twilio-paste/inline-code"; -import { ScreenReaderOnly } from "@twilio-paste/screen-reader-only"; +import { KeyboardKey, KeyboardKeyGroup, useKeyCombination } from "@twilio-paste/keyboard-key"; import { Text } from "@twilio-paste/text"; import { useWindowSize } from "@twilio-paste/utils"; import * as React from "react"; -import { useHotkeys } from "react-hotkeys-hook"; import { SiteSearch } from "../../site-search"; const SiteHeaderSearch: React.FC = () => { const [isOpen, setIsOpen] = React.useState(false); const { breakpointIndex } = useWindowSize(); + const isMacOS = navigator.platform.toUpperCase().includes("MAC"); + const platformTriggerKey = isMacOS ? "Meta" : "Control"; const onOpen = (): void => { setIsOpen(true); @@ -22,7 +22,12 @@ const SiteHeaderSearch: React.FC = () => { setIsOpen(false); }; - useHotkeys("mod+k", onOpen); + const keyCombinationState = useKeyCombination({ + keys: [platformTriggerKey, "k"], + onCombinationPress: onOpen, + enablePressStyles: true, + disabled: isOpen, + }); return ( <> @@ -54,6 +59,7 @@ const SiteHeaderSearch: React.FC = () => { _active={{ boxShadow: "shadowBorderPrimaryStronger", }} + aria-keyshortcuts={`${isMacOS ? "Command" : "Control"} + K`} > @@ -64,11 +70,12 @@ const SiteHeaderSearch: React.FC = () => { {breakpointIndex === 0 ? null : ( <> - diff --git a/packages/paste-website/src/pages/components/keyboard-key/api.mdx b/packages/paste-website/src/pages/components/keyboard-key/api.mdx new file mode 100644 index 0000000000..83cee5b104 --- /dev/null +++ b/packages/paste-website/src/pages/components/keyboard-key/api.mdx @@ -0,0 +1,62 @@ +import Changelog from '@twilio-paste/keyboard-key/CHANGELOG.md'; // I don't know why this is needed but if you remove it the page fails to render +import packageJson from '@twilio-paste/keyboard-key/package.json'; + +import {SidebarCategoryRoutes} from '../../../constants'; +import ComponentPageLayout from '../../../layouts/ComponentPageLayout'; +import {getFeature, getNavigationData, getComponentApi} from '../../../utils/api'; + +export const meta = { + title: 'Keyboard Key - Components', + package: '@twilio-paste/keyboard-key', + description: packageJson.description, + slug: '/components/keyboard-key/api', +}; + +export default ComponentPageLayout; + +export const getStaticProps = async () => { + const navigationData = await getNavigationData(); + const feature = await getFeature('Keyboard Key'); + const {componentApi, componentApiTocData} = getComponentApi('@twilio-paste/keyboard-key'); + return { + props: { + data: { + ...packageJson, + ...feature, + }, + componentApi, + mdxHeadings: [...mdxHeadings, ...componentApiTocData], + navigationData, + pageHeaderData: { + categoryRoute: SidebarCategoryRoutes.COMPONENTS, + githubUrl: 'https://github.com/twilio-labs/paste/tree/main/packages/paste-core/components/keyboard-key', + storybookUrl: '/?path=/story/components-keyboardkey' + }, + }, + }; +}; + +## Installation + +```bash +yarn add @twilio-paste/keyboard-key - or - yarn add @twilio-paste/core +``` + +## Usage + +```jsx +import { KeyboardKeyGroup, KeyboardKey } from '@twilio-paste/core/keyboard-key'; + +const KeyboardKeyExample = () => { + return ( + + Control + B + + ); +}; +``` + +## Props + + diff --git a/packages/paste-website/src/pages/components/keyboard-key/changelog.mdx b/packages/paste-website/src/pages/components/keyboard-key/changelog.mdx new file mode 100644 index 0000000000..2184d43817 --- /dev/null +++ b/packages/paste-website/src/pages/components/keyboard-key/changelog.mdx @@ -0,0 +1,36 @@ +import {SidebarCategoryRoutes} from '../../../constants'; +import Changelog from '@twilio-paste/keyboard-key/CHANGELOG.md'; +import packageJson from '@twilio-paste/keyboard-key/package.json'; +import ComponentPageLayout from '../../../layouts/ComponentPageLayout'; +import {getFeature, getNavigationData} from '../../../utils/api'; + +export const meta = { + title: 'Keyboard Key - Components', + package: '@twilio-paste/keyboard-key', + description: packageJson.description, + slug: '/components/keyboard-key/changelog', +}; + +export default ComponentPageLayout; + +export const getStaticProps = async () => { + const navigationData = await getNavigationData(); + const feature = await getFeature('Keyboard Key'); + return { + props: { + data: { + ...packageJson, + ...feature, + }, + navigationData, + mdxHeadings, + pageHeaderData: { + categoryRoute: SidebarCategoryRoutes.COMPONENTS, + githubUrl: 'https://github.com/twilio-labs/paste/tree/main/packages/paste-core/components/keyboard-key', + storybookUrl: '/?path=/story/components-keyboardkey' + }, + }, + }; +}; + + diff --git a/packages/paste-website/src/pages/components/keyboard-key/index.mdx b/packages/paste-website/src/pages/components/keyboard-key/index.mdx new file mode 100644 index 0000000000..a225938902 --- /dev/null +++ b/packages/paste-website/src/pages/components/keyboard-key/index.mdx @@ -0,0 +1,396 @@ +import { KeyboardKey, KeyboardKeyGroup, useKeyCombination, useKeyCombinations } from "@twilio-paste/keyboard-key"; +import { Callout, CalloutHeading, CalloutText } from "@twilio-paste/callout"; +import { Anchor } from "@twilio-paste/anchor"; +import { Box } from "@twilio-paste/box"; +import { Button } from "@twilio-paste/button"; +import { useMenuState, Menu, MenuButton, MenuItem } from "@twilio-paste/menu"; +import { Paragraph } from "@twilio-paste/paragraph"; +import { Table, THead, Tr, Th, TBody, Td } from "@twilio-paste/table"; +import { Stack } from "@twilio-paste/stack"; +import { Tooltip } from "@twilio-paste/tooltip"; +import { ChevronDownIcon } from "@twilio-paste/icons/esm/ChevronDownIcon"; +import { SearchIcon } from "@twilio-paste/icons/esm/SearchIcon"; +import packageJson from "@twilio-paste/keyboard-key/package.json"; + +import { SidebarCategoryRoutes } from "../../../constants"; +import { Blockquote } from "../../../components/Blockquote"; +import ComponentPageLayout from "../../../layouts/ComponentPageLayout"; +import { getFeature, getNavigationData } from "../../../utils/api"; +import { Do, DoDont, Dont } from "../../../components/DoDont"; +import { + basicExample, + disabledExample, + pressedExample, + defaultExample, + tooltipExample, + useKeyCombinationsExample, + useKeyCombinationExample, +} from "../../../component-examples/KeyboardKeyExamples"; +import { Text } from "@twilio-paste/text"; + +export const meta = { + title: "Keyboard Key - Components", + package: "@twilio-paste/keyboard-key", + description: packageJson.description, + slug: "/components/keyboard-key/", +}; + +export default ComponentPageLayout; + +export const getStaticProps = async () => { + const navigationData = await getNavigationData(); + const feature = await getFeature("Keyboard Key"); + return { + props: { + data: { + ...packageJson, + ...feature, + }, + navigationData, + mdxHeadings, + pageHeaderData: { + categoryRoute: SidebarCategoryRoutes.COMPONENTS, + githubUrl: "https://github.com/twilio-labs/paste/tree/main/packages/paste-core/components/keyboard-key", + storybookUrl: "/?path=/story/components-keyboardkey", + }, + }, + }; +}; + + + {basicExample} + + +## Guidelines + +### About Keyboard Key and Keyboard Key Group + +A Keyboard Key distinguishes a keyboard command or shortcut from other text. + +Keyboard shortcuts are used for extremely frequent platform-level actions (like activating search), or in canvas or productivity tools like Studio or Flex. In general, **avoid implementing keyboard shortcuts**, especially with single keys, because they can override shortcuts that are already set by operating systems, browsers, assistive technologies, or user preferences. + +
+ Not every task on your application needs a shortcut, so observe your users interacting with your application to + determine the most common tasks and prioritize keyboard shortcuts for these. +
+ +Keyboards can also vary across operating systems and global regions, so make sure your key commands work for all users. + +Reference [this list of existing common keyboard shortcuts](#common-keyboard-shortcuts) before creating a new one. + + + + Are you considering implementing a new keyboard shortcut? + + Reach out to us in a{" "} + + Github Discussion + {" "} + so we can keep keyboard shortcuts standardized across our platforms by connecting you with other teams, like Flex + and Studio, who have already built them into features. + + + + +### Accessibility + +To make sure users easily understand keyboard commands, use text instead of symbols to write out modifier keys like Command, Control, and Option. Example: Use “Command” instead of “⌘”. Use the abbreviation, e.g., “Cmd”, only when space is limited. + +To expose the presence of shortcuts to assistive technologies, use the [`aria-keyshortcuts`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-keyshortcuts) attribute on the element that gets activated by the shortcut. + +## Examples + +### Default Keyboard Key + +Use Keyboard Key for a single key command. Use Keyboard Key Group for key combination commands or shortcuts. When showing a key combination, do not put a “+” between Keyboard Keys or in Keyboard Key Group. + + + {defaultExample} + + +### Disabled Keyboard Key + +Disabled Keyboard Keys should be used when the element a shortcut activates is disabled, like a disabled Menu item or Button. + + + {disabledExample} + + +### Pressed Keyboard Key + +Use pressed Keyboard Keys to give visual feedback when a key is pressed. + +This is especially helpful when onboarding users to keyboard shortcuts. However, use the pressed state thoughtfully, only when it enhances the user experience. It can be distracting in cases where a user is using the keyboard for other interactions, like on a page with form fields. + +The `KeyboardKeyGroup` accepts state returned from the [`useKeyCombination`](#usekeycombination) hook that allows pressed styling to be enabled. You must also specify `keyEvent` on the component to detect the correct key pressed. + +A mapping of key events can be found [here](https://www.freecodecamp.org/news/javascript-keycode-list-keypress-event-key-codes/). + + + {pressedExample} + + +## Keyboard Key within Tooltip + +Add Keyboard Key(s) to a [Tooltip](/components/tooltip) to show a shortcut associated with an interactive component. Use the `actionHeader` and `keyCombinationsActions` to display a heading and the associated keyboard shortcut (or shortcuts, if displaying for multiple operating systems/keyboards). + + + {tooltipExample} + + +## Keyboard combination hooks + +### useKeyCombination + +The `useKeyCombination` hook provides a way to configure the combination keys, callback, and additional state that can be used in `KeyboardKeyGroup` to enable pressed styling. + + + {useKeyCombinationExample} + + +### useKeyCombinations + +The `useKeyCombinations` is similar to [`useKeyCombination`](#useKeyCombination) but allows you to configure multiple combinations and callback mappings. Use this hook when you have many combinations on the same page. This will not include the ability to configure pressed styling and is designed to provide functionality when in Menu items or Tooltips. + + + {useKeyCombinationsExample} + + +## Composition Notes + +Keyboard Key is mainly a presentational component, and can't detect operating systems. When a user needs to press different keys on different operating systems (e.g., “Command” on Mac and “Control” on Windows), make sure to either list both shortcuts or programmatically swap the shortcut displayed based on the operating system. + +When writing out keys: + +- Use title case. Example: “Caps Lock”, not “Caps lock”. +- For modifier keys like Control, Command, and Option, spell out the key instead of using abbreviations or symbols. Example: “Control”, not “Ctrl” or “^”. Use the abbreviation only when space is limited. +- Use “Enter” instead of “Return”. + +## Common keyboard shortcuts + +Common platform-level shortcuts that are used across Twilio include: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ActionWindows/LinuxMac
Open search + + Control + K + + + + Command + K + +
Bold text + + Control + B + + + + Command + B + +
Italicize text + + Control + I + + + + Command + I + +
Underline text + + Control + U + + + + Command + U + +
+
+ +From [W3C's guide to developing a keyboard interface](https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/#keyassignmentconventionsforcommonfunctions): +The following keyboard commands should be used in any context where the actions are appropriate. + +**Use these commands only for the actions specified.** Do not use them for any other command: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ActionWindows/LinuxMac
Copy to clipboard + + Control + C + + + + Command + C + +
Paste from clipboard + + Control + V + + + + Command + V + +
Cut to clipboard + + Control + X + + + + Command + X + +
Undo last action + + Control + Z + + + + Command + Z + +
Redo action + + Control + Y + + + + Command + Shift + Y + +
+
+ +### Keyboard commands to avoid + +These keyboard commands should be avoided since they're used by operating systems or assistive technologies: + +- Any modifier keys (a keyboard key that changes the function of other keys when pressed together) + any of Tab, Enter, Space, or Escape. +- Alt + a function key. +- Caps Lock + any other combination of keys. +- Insert + any combination of other keys. +- Scroll Lock + any combination of other keys. +- + Control + Option + + any combination of other keys. + +Read more about other conflicts with browsers and international keyboards in [W3C's guide](https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/#keyboardshortcuts). + +## When to use Keyboard Key + + + + + + + + + + diff --git a/yarn.lock b/yarn.lock index fb8b9ca970..9a7fc3df91 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16116,6 +16116,7 @@ __metadata: "@twilio-paste/inline-control-group": ^13.0.2 "@twilio-paste/input": ^9.1.3 "@twilio-paste/input-box": ^10.1.1 + "@twilio-paste/keyboard-key": ^0.0.0 "@twilio-paste/label": ^13.1.1 "@twilio-paste/lexical-library": ^4.2.0 "@twilio-paste/list": ^8.2.1