Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add KeybindingHint component #4750

Merged
merged 26 commits into from
Aug 16, 2024
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
9104157
Add `KeybindingHint` component
iansan5653 Jul 18, 2024
66dc77e
Split file and refactor a bit
iansan5653 Jul 18, 2024
04904cd
Split components out into individual files
iansan5653 Jul 18, 2024
cec4e50
Update comments
iansan5653 Jul 18, 2024
8a37a6f
Create `useIsMacOS` hook for SSR support
iansan5653 Jul 18, 2024
b0e1e7a
Add changelog
iansan5653 Jul 18, 2024
46c0135
Format
iansan5653 Jul 18, 2024
9b131f3
Replace space with "space"
iansan5653 Jul 18, 2024
e0345aa
Update exports snapshot
iansan5653 Jul 18, 2024
d7eb853
derp, fix my dumb mistakes
iansan5653 Jul 18, 2024
daea8e6
Try `canUseDOM` instead of `window !== undefined`
iansan5653 Jul 18, 2024
41e1a66
Merge branch 'main' into add-keybinding-hint
iansan5653 Jul 22, 2024
035d5e9
Move to draft status
iansan5653 Jul 25, 2024
cbefb0f
Merge branch 'main' into add-keybinding-hint
iansan5653 Jul 26, 2024
69aa696
Move export to drafts
iansan5653 Aug 6, 2024
253b9b3
Separate out `features` stories and add `onEmphasis` story
iansan5653 Aug 6, 2024
acbd25b
Add examples stories
iansan5653 Aug 6, 2024
0562fa2
Tweak styles
iansan5653 Aug 6, 2024
03b6491
Remove comma between chords
iansan5653 Aug 6, 2024
626d29b
Merge branch 'main' into add-keybinding-hint
iansan5653 Aug 6, 2024
6d0aedf
Update import in docs
iansan5653 Aug 6, 2024
f96742a
Merge branch 'add-keybinding-hint' of https://github.com/primer/react…
iansan5653 Aug 6, 2024
7ca0d77
Form & update tests
iansan5653 Aug 9, 2024
058d9fb
Merge branch 'main' into add-keybinding-hint
iansan5653 Aug 9, 2024
c03d046
Update snapshots, again
iansan5653 Aug 12, 2024
c378c1e
Move stories to Drafts
iansan5653 Aug 13, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/short-boats-cover.md
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
4 changes: 2 additions & 2 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@
"json.schemas": [
{
"fileMatch": ["*.docs.json"],
"url": "./script/components-json/component.schema.json"
"url": "./packages/react/script/components-json/component.schema.json"
},
{
"fileMatch": ["generated/components.json"],
"url": "./script/components-json/output.schema.json"
"url": "./packages/react/script/components-json/output.schema.json"
iansan5653 marked this conversation as resolved.
Show resolved Hide resolved
}
]
}
162 changes: 162 additions & 0 deletions docs/content/KeybindingHint.mdx
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,
}}
/>
2 changes: 2 additions & 0 deletions docs/src/@primer/gatsby-theme-doctocat/nav.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@
url: /Heading
- title: IconButton
url: /IconButton
- title: KeybindingHint
url: /KeybindingHint
Copy link
Member

@siddharthkp siddharthkp Aug 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be under drafts as well in the nav?

(non blocking because these docs are going away soon anyways in favor of https://primer.style/components)

- title: Label
url: /Label
- title: LabelGroup
Expand Down
28 changes: 28 additions & 0 deletions packages/react/src/KeybindingHint/KeybindingHint.docs.json
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": []
}
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: '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',
}
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: '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'},
}
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: 'Components/KeybindingHint',
iansan5653 marked this conversation as resolved.
Show resolved Hide resolved
component: KeybindingHint,
} satisfies Meta<typeof KeybindingHint>

export const Default = {args: {keys: 'Mod+Shift+K'}}
48 changes: 48 additions & 0 deletions packages/react/src/KeybindingHint/KeybindingHint.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import React, {type ReactNode} from 'react'
iansan5653 marked this conversation as resolved.
Show resolved Hide resolved
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'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we setting the displayName property when it already matches the name of the function?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The engine can infer a function name for simple named declarations like function Component() {} and const Component = ()=> {}.

But in this case we are declaring an anonymous function without a name, then sending it through memo and assigning it to a named constant. There's no way for the engine to infer a name for the function so the component would be anonymous without a displayName. This is actually required by the linter.


/**
* 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
Loading
Loading