Skip to content

Commit dabeb6d

Browse files
v6: Add OS-aware MODIFIER constants to KeycapHint (#4302)
## Summary `KeycapHint` callers now use semantic names (`MODIFIER.Meta`, etc.) and the component renders the right glyph or text per OS. `MODIFIER.Meta` becomes `⌘` on macOS and `Ctrl` on other platforms; `Ctrl`/`Alt`/`Shift` become Mac glyphs (`⌃`/`⌥`/`⇧`) on macOS and plain text elsewhere. `Enter` always renders as `⏎`. ## Test plan - [ ] Open Storybook `Primitives/KeycapHint` on macOS: chips render as ⌘/⏎. - [ ] In Chrome devtools, override the user agent to a Windows or Linux UA and reload Storybook: same stories render as Ctrl/⏎. - [ ] Confirm the run-shortcut chip and command-palette chip in the top bar follow the same OS detection.
1 parent ce7a88f commit dabeb6d

7 files changed

Lines changed: 142 additions & 25 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@graphiql/react': minor
3+
---
4+
5+
`KeycapHint` now takes semantic modifier names via the new `MODIFIER` constant. `MODIFIER.Meta` renders as `` on macOS and `Ctrl` elsewhere; `Ctrl`/`Alt`/`Shift` render as Mac glyphs (``/``/``) on macOS and as plain text on other platforms. `Enter` renders as `` on every platform.

packages/graphiql-react/src/components/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ export { Spinner } from './spinner';
1818
export { Tabs, Tab } from './tabs';
1919
export { Tooltip } from './tooltip';
2020
export { Root as VisuallyHidden } from '@radix-ui/react-visually-hidden';
21-
export { KeycapHint } from './keycap-hint';
22-
export type { KeycapHintProps } from './keycap-hint';
21+
export { KeycapHint, MODIFIER } from './keycap-hint';
22+
export type { KeycapHintProps, ModifierKey } from './keycap-hint';
2323
export { PanelHeader } from './panel-header';
2424
export type { PanelHeaderProps } from './panel-header';
2525
export { SegmentedControl } from './segmented-control';
Lines changed: 46 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,53 @@
11
import type { FC } from 'react';
2+
import { isMacOs } from '../../constants';
23
import './index.css';
34

5+
/**
6+
* Semantic modifier keys for `KeycapHint`.
7+
*
8+
* - `Meta` is the OS-appropriate shortcut modifier: ⌘ on macOS, Ctrl elsewhere.
9+
* - `Ctrl`/`Alt`/`Shift` render as Mac glyphs (⌃/⌥/⇧) on macOS and as plain
10+
* text on other platforms.
11+
* - `Enter` always renders as ⏎ (the symbol is widely recognized cross-platform).
12+
*/
13+
export const MODIFIER = Object.freeze({
14+
Meta: 'Meta',
15+
Ctrl: 'Ctrl',
16+
Alt: 'Alt',
17+
Shift: 'Shift',
18+
Enter: 'Enter',
19+
} as const);
20+
21+
export type ModifierKey = (typeof MODIFIER)[keyof typeof MODIFIER];
22+
423
export type KeycapHintProps = {
5-
keys: string[];
24+
/** Keys to render. Use `MODIFIER.*` for OS-aware modifiers; other strings render as-is. */
25+
keys: Array<ModifierKey | (string & {})>;
626
ariaLabel: string;
727
};
828

9-
export const KeycapHint: FC<KeycapHintProps> = ({ keys, ariaLabel }) => (
10-
<span className="graphiql-keycap-hint" aria-label={ariaLabel}>
11-
{keys.map((k, i) => (
12-
<kbd key={`${k}-${i}`} className="graphiql-keycap">
13-
{k}
14-
</kbd>
15-
))}
16-
</span>
17-
);
29+
const MAC_DISPLAY: Record<string, string> = {
30+
Meta: '⌘',
31+
Ctrl: '⌃',
32+
Alt: '⌥',
33+
Shift: '⇧',
34+
Enter: '⏎',
35+
};
36+
37+
const NON_MAC_DISPLAY: Record<string, string> = {
38+
Meta: 'Ctrl',
39+
Enter: '⏎',
40+
};
41+
42+
export const KeycapHint: FC<KeycapHintProps> = ({ keys, ariaLabel }) => {
43+
const display = isMacOs ? MAC_DISPLAY : NON_MAC_DISPLAY;
44+
return (
45+
<span className="graphiql-keycap-hint" aria-label={ariaLabel}>
46+
{keys.map((k, i) => (
47+
<kbd key={`${k}-${i}`} className="graphiql-keycap">
48+
{display[k] ?? k}
49+
</kbd>
50+
))}
51+
</span>
52+
);
53+
};
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
'use no memo';
2+
3+
import { describe, it, expect, vi } from 'vitest';
4+
import { render, screen } from '@testing-library/react';
5+
6+
vi.mock('../../constants', async () => {
7+
const actual =
8+
await vi.importActual<typeof import('../../constants')>('../../constants');
9+
return { ...actual, isMacOs: true };
10+
});
11+
12+
import { KeycapHint, MODIFIER } from './';
13+
14+
describe('KeycapHint (macOS)', () => {
15+
it('renders MODIFIER.Meta as "⌘"', () => {
16+
render(
17+
<KeycapHint
18+
keys={[MODIFIER.Meta, 'K']}
19+
ariaLabel="Open command palette"
20+
/>,
21+
);
22+
expect(screen.getByText('⌘')).toBeInTheDocument();
23+
expect(screen.getByText('K')).toBeInTheDocument();
24+
});
25+
26+
it('renders other modifiers as their Mac glyphs', () => {
27+
render(
28+
<KeycapHint
29+
keys={[MODIFIER.Ctrl, MODIFIER.Alt, MODIFIER.Shift, MODIFIER.Enter]}
30+
ariaLabel="All modifiers"
31+
/>,
32+
);
33+
expect(screen.getByText('⌃')).toBeInTheDocument();
34+
expect(screen.getByText('⌥')).toBeInTheDocument();
35+
expect(screen.getByText('⇧')).toBeInTheDocument();
36+
expect(screen.getByText('⏎')).toBeInTheDocument();
37+
});
38+
});

packages/graphiql-react/src/components/keycap-hint/keycap-hint.stories.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { Meta, StoryObj } from '@storybook/react-vite';
2-
import { KeycapHint } from './';
2+
import { KeycapHint, MODIFIER } from './';
33

44
const meta: Meta<typeof KeycapHint> = {
55
title: 'Primitives/KeycapHint',
@@ -16,10 +16,12 @@ export const Single: Story = {
1616

1717
export const ChordWithModifier: Story = {
1818
render: () => (
19-
<KeycapHint keys={['⌘', 'K']} ariaLabel="Open command palette" />
19+
<KeycapHint keys={[MODIFIER.Meta, 'K']} ariaLabel="Open command palette" />
2020
),
2121
};
2222

2323
export const RunShortcut: Story = {
24-
render: () => <KeycapHint keys={['⌘', '⏎']} ariaLabel="Run query" />,
24+
render: () => (
25+
<KeycapHint keys={[MODIFIER.Meta, MODIFIER.Enter]} ariaLabel="Run query" />
26+
),
2527
};

packages/graphiql-react/src/components/keycap-hint/keycap-hint.test.tsx

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,55 @@
22

33
import { describe, it, expect } from 'vitest';
44
import { render, screen } from '@testing-library/react';
5-
import { KeycapHint } from './';
5+
import { KeycapHint, MODIFIER } from './';
66

7-
describe('KeycapHint', () => {
8-
it('renders the provided keys', () => {
9-
render(<KeycapHint keys={['⌘', 'K']} ariaLabel="Open command palette" />);
10-
expect(screen.getByText('⌘')).toBeInTheDocument();
7+
// jsdom's default userAgent does not include "Mac", so `isMacOs` is false here.
8+
describe('KeycapHint (non-macOS)', () => {
9+
it('renders MODIFIER.Meta as "Ctrl"', () => {
10+
render(
11+
<KeycapHint
12+
keys={[MODIFIER.Meta, 'K']}
13+
ariaLabel="Open command palette"
14+
/>,
15+
);
16+
expect(screen.getByText('Ctrl')).toBeInTheDocument();
1117
expect(screen.getByText('K')).toBeInTheDocument();
1218
});
1319

20+
it('renders other modifiers as plain text', () => {
21+
render(
22+
<KeycapHint keys={[MODIFIER.Shift, MODIFIER.Alt]} ariaLabel="Combo" />,
23+
);
24+
expect(screen.getByText('Shift')).toBeInTheDocument();
25+
expect(screen.getByText('Alt')).toBeInTheDocument();
26+
});
27+
28+
it('always renders Enter as ⏎', () => {
29+
render(<KeycapHint keys={[MODIFIER.Enter]} ariaLabel="Submit" />);
30+
expect(screen.getByText('⏎')).toBeInTheDocument();
31+
});
32+
33+
it('passes unknown strings through unchanged', () => {
34+
render(<KeycapHint keys={['F1']} ariaLabel="Command palette" />);
35+
expect(screen.getByText('F1')).toBeInTheDocument();
36+
});
37+
1438
it('exposes the shortcut as an accessible name', () => {
1539
render(
16-
<KeycapHint keys={['⌘', 'Enter']} ariaLabel="Run query (Cmd+Enter)" />,
40+
<KeycapHint
41+
keys={[MODIFIER.Meta, MODIFIER.Enter]}
42+
ariaLabel="Run query (Ctrl+Enter)"
43+
/>,
1744
);
1845
expect(screen.getByLabelText(/Run query/i)).toBeInTheDocument();
1946
});
2047

2148
it('renders a <kbd> element for each key', () => {
2249
const { container } = render(
23-
<KeycapHint keys={['⌘', 'K']} ariaLabel="Open command palette" />,
50+
<KeycapHint
51+
keys={[MODIFIER.Meta, 'K']}
52+
ariaLabel="Open command palette"
53+
/>,
2454
);
2555
expect(container.querySelectorAll('kbd')).toHaveLength(2);
2656
});

packages/graphiql-react/src/components/top-bar/index.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import type { FC } from 'react';
66
import { useGraphiQL, useGraphiQLActions } from '../provider';
7-
import { KeycapHint } from '../keycap-hint';
7+
import { KeycapHint, MODIFIER } from '../keycap-hint';
88
import './index.css';
99

1010
export type TopBarProps = {
@@ -60,7 +60,10 @@ export const TopBarView: FC<TopBarViewProps> = ({
6060

6161
<button type="button" className="graphiql-top-bar-cmd">
6262
<span>Jump to schema</span>
63-
<KeycapHint keys={['⌘', 'K']} ariaLabel="Open command palette" />
63+
<KeycapHint
64+
keys={[MODIFIER.Meta, 'K']}
65+
ariaLabel="Open command palette"
66+
/>
6467
</button>
6568

6669
<button
@@ -71,7 +74,10 @@ export const TopBarView: FC<TopBarViewProps> = ({
7174
aria-label="Run query"
7275
>
7376
Run
74-
<KeycapHint keys={['⌘', '⏎']} ariaLabel="Run query shortcut" />
77+
<KeycapHint
78+
keys={[MODIFIER.Meta, MODIFIER.Enter]}
79+
ariaLabel="Run query shortcut"
80+
/>
7581
</button>
7682
</header>
7783
);

0 commit comments

Comments
 (0)