-
Notifications
You must be signed in to change notification settings - Fork 538
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add
KeybindingHint
component (#4750)
* Add `KeybindingHint` component * Split file and refactor a bit * Split components out into individual files * Update comments * Create `useIsMacOS` hook for SSR support * Add changelog * Format * Replace space with "space" * Update exports snapshot * derp, fix my dumb mistakes * Try `canUseDOM` instead of `window !== undefined` * Move to draft status * Move export to drafts * Separate out `features` stories and add `onEmphasis` story * Add examples stories * Tweak styles * Remove comma between chords * Update import in docs * Form & update tests * Update snapshots, again * Move stories to Drafts
- Loading branch information
1 parent
c578afc
commit 414c140
Showing
20 changed files
with
736 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
'@primer/react': minor | ||
--- | ||
|
||
Add `KeybindingHint` component for indicating an available keyboard shortcut |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,162 @@ | ||
--- | ||
title: KeybindingHint | ||
componentId: keybinding_hint | ||
status: Draft | ||
source: https://github.com/primer/react/tree/main/packages/react/src/KeybindingHint | ||
storybook: '/react/storybook?path=/story/components-keybindinghint' | ||
description: Indicates the presence of a keybinding available for an action. | ||
--- | ||
|
||
import data from '../../packages/react/src/KeybindingHint/KeybindingHint.docs.json' | ||
import {ActionList, Button, Text, Box} from '@primer/react' | ||
import {KeybindingHint} from '@primer/react/drafts' | ||
import {TrashIcon} from '@primer/octicons-react' | ||
|
||
Use `KeybindingHint` to make keyboard shortcuts discoverable. Can render visual keybinding hints in condensed (abbreviated) form or expanded form, and provides accessible alternative text for screen reader users. | ||
|
||
<Box sx={{border: '1px solid', borderColor: 'border.default', borderRadius: 2, padding: 6, marginBottom: 3}}> | ||
<ActionList sx={{width: 320}}> | ||
<ActionList.Item> | ||
Move down | ||
<ActionList.TrailingVisual> | ||
<KeybindingHint keys="Mod+ArrowDown" /> | ||
</ActionList.TrailingVisual> | ||
</ActionList.Item> | ||
<ActionList.Item> | ||
Unsubscribe | ||
<ActionList.TrailingVisual> | ||
<KeybindingHint keys="i j" /> | ||
</ActionList.TrailingVisual> | ||
</ActionList.Item> | ||
<ActionList.Item variant="danger"> | ||
<ActionList.LeadingVisual> | ||
<TrashIcon /> | ||
</ActionList.LeadingVisual> | ||
Delete | ||
<ActionList.TrailingVisual> | ||
<KeybindingHint keys="Mod+Shift+Delete" /> | ||
</ActionList.TrailingVisual> | ||
</ActionList.Item> | ||
</ActionList> | ||
</Box> | ||
|
||
```js | ||
import {KeybindingHint} from '@primer/react/drafts' | ||
``` | ||
|
||
## Examples | ||
|
||
### Single keys | ||
|
||
Use the [full names of the keys as returned by `KeyboardEvent.key`](https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values). Key names are case-insensitive. | ||
|
||
```javascript live noinline | ||
render( | ||
<> | ||
<KeybindingHint keys="a" /> <br /> | ||
<KeybindingHint keys="B" /> <br /> | ||
<KeybindingHint keys="ArrowLeft" /> <br /> | ||
<KeybindingHint keys="shift" /> | ||
</>, | ||
) | ||
``` | ||
|
||
#### Special key names | ||
|
||
Because the `+` and space characters are used to build chords and sequences as described below, their names must be spelled out to be used as keys. | ||
|
||
```javascript live noinline | ||
render( | ||
<> | ||
<KeybindingHint keys="Plus" /> <br /> | ||
<KeybindingHint keys="Space" /> | ||
</>, | ||
) | ||
``` | ||
|
||
### Chords | ||
|
||
_Chords_ are multiple keys that are pressed at the same time. Combine keys in a chord with `+`. Keys are automatically sorted into a standardized order so that modifiers come first. | ||
|
||
```javascript live noinline | ||
render( | ||
<> | ||
<KeybindingHint keys="Alt+a" /> <br /> | ||
<KeybindingHint keys="a+Alt" /> <br /> | ||
<KeybindingHint keys="Control+Shift+ArrowUp" /> <br /> | ||
<KeybindingHint keys="Meta+Shift+&" /> | ||
</>, | ||
) | ||
``` | ||
|
||
#### Platform-dependent modifier | ||
|
||
Typical chords use `Command` on MacOS and `Control` on other devices. To automatically render `Command` or `Control` based on the user's operating system, use the special key name `Mod`. | ||
|
||
```javascript live noinline | ||
render(<KeybindingHint keys="Mod+Shift+X" />) | ||
``` | ||
|
||
### Sequences | ||
|
||
_Sequences_ are keys or chords that are pressed one after the other. Combine elements in a sequence with a space. For example, `a b` means "press a, then press b". | ||
|
||
```javascript live noinline | ||
render( | ||
<> | ||
<KeybindingHint keys="a b" /> <br /> | ||
<KeybindingHint keys="Mod+g ArrowLeft" /> | ||
</>, | ||
) | ||
``` | ||
|
||
### Full display format | ||
|
||
The default `condensed` format should be used on UI elements like buttons, menuitems, and inputs. In long-form text (prose), the `full` variant can be used instead to help the text flow better. | ||
|
||
```javascript live noinline | ||
render( | ||
<Text> | ||
Press <KeybindingHint keys="Mod+Enter" format="ful" /> to submit the form. | ||
</Text>, | ||
) | ||
``` | ||
|
||
### `onEmphasis` variant | ||
|
||
When rendering on 'emphasis' colors, use the `onEmphasis` variant. | ||
|
||
```javascript live noinline | ||
const CmdEnterHint = () => <KeybindingHint variant="onEmphasis" keys="Mod+Enter" /> | ||
|
||
render( | ||
<Button variant="primary" trailingVisual={CmdEnterHint}> | ||
Submit | ||
</Button>, | ||
) | ||
``` | ||
|
||
## Props | ||
|
||
<ComponentProps data={data} /> | ||
|
||
## Status | ||
|
||
<ComponentChecklist | ||
items={{ | ||
propsDocumented: true, | ||
noUnnecessaryDeps: true, | ||
adaptsToThemes: true, | ||
adaptsToScreenSizes: true, | ||
fullTestCoverage: true, | ||
usedInProduction: true, | ||
usageExamplesDocumented: true, | ||
hasStorybookStories: true, | ||
designReviewed: false, | ||
a11yReviewed: false, | ||
stableApi: false, | ||
addressedApiFeedback: false, | ||
hasDesignGuidelines: false, | ||
hasFigmaComponent: false, | ||
}} | ||
/> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
28 changes: 28 additions & 0 deletions
28
packages/react/src/KeybindingHint/KeybindingHint.docs.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
{ | ||
"id": "KeybindingHint", | ||
"name": "KeybindingHint", | ||
"status": "draft", | ||
"a11yReviewed": false, | ||
"stories": [], | ||
"importPath": "@primer/react", | ||
"props": [ | ||
{ | ||
"name": "keys", | ||
"type": "string", | ||
"description": "The keys involved in this keybinding." | ||
}, | ||
{ | ||
"name": "format", | ||
"type": "'condensed' | 'full'", | ||
"defaultValue": "'condensed'", | ||
"description": "Control the display format." | ||
}, | ||
{ | ||
"name": "variant", | ||
"type": "'normal' | 'onEmphasis'", | ||
"defaultValue": "'normal'", | ||
"description": "Set to `onEmphasis` for display on 'emphasis' colors." | ||
} | ||
], | ||
"subcomponents": [] | ||
} |
64 changes: 64 additions & 0 deletions
64
packages/react/src/KeybindingHint/KeybindingHint.examples.stories.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
import React from 'react' | ||
import type {Meta, StoryObj} from '@storybook/react' | ||
import {KeybindingHint, type KeybindingHintProps} from '.' | ||
import {Button, ActionList, FormControl, TextInput} from '..' | ||
|
||
export default { | ||
title: 'Drafts/Components/KeybindingHint/Examples', | ||
component: KeybindingHint, | ||
} satisfies Meta<typeof KeybindingHint> | ||
|
||
export const ButtonExample: StoryObj<KeybindingHintProps> = { | ||
render: args => <Button trailingVisual={() => <KeybindingHint {...args} />}>Pull requests</Button>, | ||
args: {keys: 'g p'}, | ||
name: 'Button', | ||
} | ||
|
||
export const PrimaryButton: StoryObj<KeybindingHintProps> = { | ||
render: args => ( | ||
<Button variant="primary" trailingVisual={() => <KeybindingHint {...args} />}> | ||
Submit | ||
</Button> | ||
), | ||
args: {keys: 'Mod+Enter', variant: 'onEmphasis'}, | ||
} | ||
|
||
export const ActionListExample: StoryObj<KeybindingHintProps> = { | ||
render: args => ( | ||
<ActionList sx={{maxWidth: '300px', border: '1px solid', borderColor: 'border.default', borderRadius: 2}}> | ||
<ActionList.Item>Add comment</ActionList.Item> | ||
<ActionList.Item> | ||
Copy text{' '} | ||
<ActionList.TrailingVisual> | ||
<KeybindingHint {...args} /> | ||
</ActionList.TrailingVisual> | ||
</ActionList.Item> | ||
<ActionList.Item>Cancel</ActionList.Item> | ||
</ActionList> | ||
), | ||
args: {keys: 'Mod+c'}, | ||
name: 'ActionList', | ||
} | ||
|
||
export const Prose: StoryObj<KeybindingHintProps> = { | ||
render: args => ( | ||
<p> | ||
Press <KeybindingHint {...args} /> to toggle between write and preview modes. | ||
</p> | ||
), | ||
args: { | ||
keys: 'Mod+Shift+P', | ||
format: 'full', | ||
}, | ||
} | ||
|
||
export const TextInputExample: StoryObj<KeybindingHintProps> = { | ||
render: args => ( | ||
<FormControl> | ||
<FormControl.Label visuallyHidden>Search</FormControl.Label> | ||
<TextInput trailingVisual={() => <KeybindingHint {...args} />} placeholder="Search" /> | ||
</FormControl> | ||
), | ||
args: {keys: '/'}, | ||
name: 'TextInput', | ||
} |
30 changes: 30 additions & 0 deletions
30
packages/react/src/KeybindingHint/KeybindingHint.features.stories.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
import React from 'react' | ||
import type {Meta, StoryObj} from '@storybook/react' | ||
import {KeybindingHint, type KeybindingHintProps} from '.' | ||
import Box from '../Box' | ||
|
||
export default { | ||
title: 'Drafts/Components/KeybindingHint/Features', | ||
component: KeybindingHint, | ||
} satisfies Meta<typeof KeybindingHint> | ||
|
||
const chord = 'Mod+Shift+K' | ||
|
||
export const Condensed = {args: {keys: chord}} | ||
|
||
export const Full = {args: {keys: chord, format: 'full'}} | ||
|
||
const sequence = 'Mod+x y z' | ||
|
||
export const SequenceCondensed = {args: {keys: sequence}} | ||
|
||
export const SequenceFull = {args: {keys: sequence, format: 'full'}} | ||
|
||
export const OnEmphasis: StoryObj<KeybindingHintProps> = { | ||
render: args => ( | ||
<Box sx={{backgroundColor: 'accent.fg', p: 3}}> | ||
<KeybindingHint {...args} /> | ||
</Box> | ||
), | ||
args: {keys: chord, variant: 'onEmphasis'}, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
import type {Meta} from '@storybook/react' | ||
import {KeybindingHint} from './KeybindingHint' | ||
|
||
export default { | ||
title: 'Drafts/Components/KeybindingHint', | ||
component: KeybindingHint, | ||
} satisfies Meta<typeof KeybindingHint> | ||
|
||
export const Default = {args: {keys: 'Mod+Shift+K'}} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
import React, {type ReactNode} from 'react' | ||
import {memo} from 'react' | ||
import Text from '../Text' | ||
import type {KeybindingHintProps} from './props' | ||
import {accessibleSequenceString, Sequence} from './components/Sequence' | ||
|
||
/** `kbd` element with style resets. */ | ||
const Kbd = ({children}: {children: ReactNode}) => ( | ||
<Text | ||
as={'kbd' as 'span'} | ||
sx={{ | ||
color: 'inherit', | ||
fontFamily: 'inherit', | ||
fontSize: 'inherit', | ||
border: 'none', | ||
background: 'none', | ||
boxShadow: 'none', | ||
p: 0, | ||
lineHeight: 'unset', | ||
position: 'relative', | ||
overflow: 'visible', | ||
verticalAlign: 'baseline', | ||
textWrap: 'nowrap', | ||
}} | ||
> | ||
{children} | ||
</Text> | ||
) | ||
|
||
/** Indicates the presence of an available keybinding. */ | ||
// KeybindingHint is a good candidate for memoizing since props will rarely change | ||
export const KeybindingHint = memo((props: KeybindingHintProps) => ( | ||
<Kbd> | ||
<Sequence {...props} /> | ||
</Kbd> | ||
)) | ||
KeybindingHint.displayName = 'KeybindingHint' | ||
|
||
/** | ||
* AVOID: `KeybindingHint` is nearly always sufficient for providing both visible and accessible keyboard hints, and | ||
* will result in a good screen reader experience when used as the target for `aria-describedby` and `aria-labelledby`. | ||
* However, there may be cases where we need a plain string version, such as when building `aria-label` or | ||
* `aria-description`. In that case, this plain string builder can be used instead. | ||
* | ||
* NOTE that this string should _only_ be used when building `aria-label` or `aria-description` props (never rendered | ||
* visibly) and should nearly always also be paired with a visible hint for sighted users. | ||
*/ | ||
export const getAccessibleKeybindingHintString = accessibleSequenceString |
Oops, something went wrong.