Skip to content

Commit c6cca88

Browse files
tyler-daneclaude
andcommitted
refactor: ShortcutKeys accepts a key or key array directly
Callers no longer need a helper or a single-element array: ShortcutKeys takes `keys: string | string[]` and normalizes internally, so a one-key hint is just `keys="?"` and a combo is `keys={["Mod", "K"]}`. - toKeyArray is now private to ShortcutKeys (un-exported). - TooltipWrapper / MenuItem / ShortcutTip pass their `string | string[]` shortcut straight through — no toKeyArray call at the call site. - SelectView passes `keys={option.key}` instead of `keys={[option.key]}`. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent a596234 commit c6cca88

7 files changed

Lines changed: 35 additions & 32 deletions

File tree

packages/web/src/components/SelectView/SelectView.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ export const SelectView = ({
179179
)}
180180
>
181181
<span>{option.label}</span>
182-
<ShortcutKeys keys={[option.key]} />
182+
<ShortcutKeys keys={option.key} />
183183
</div>
184184
);
185185
})}

packages/web/src/components/Shortcuts/ShortcutKeys.test.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,14 @@ describe("ShortcutKeys", () => {
1515
expect(chips[1]?.textContent).toBe("W");
1616
});
1717

18+
it("accepts a single key as a bare string (one chip)", () => {
19+
const { container } = render(<ShortcutKeys keys="?" />);
20+
21+
const chips = keycaps(container);
22+
expect(chips).toHaveLength(1);
23+
expect(chips[0]?.textContent).toBe("?");
24+
});
25+
1826
it("resolves the `cmd` alias to the Meta (Command) icon", () => {
1927
render(<ShortcutKeys keys={["cmd", "K"]} />);
2028

@@ -27,7 +35,10 @@ describe("ShortcutKeys", () => {
2735
const { container } = render(<ShortcutKeys keys={[]} />);
2836
expect(keycaps(container)).toHaveLength(0);
2937

30-
// Blank/whitespace entries are dropped, leaving nothing to render.
38+
// Blank/whitespace entries (string or array) are dropped.
39+
const { container: emptyString } = render(<ShortcutKeys keys="" />);
40+
expect(keycaps(emptyString)).toHaveLength(0);
41+
3142
const { container: whitespace } = render(
3243
<ShortcutKeys keys={["", " "]} />,
3344
);

packages/web/src/components/Shortcuts/ShortcutKeys.tsx

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,16 @@ function normalizeKeyToken(key: string): string {
2323

2424
/**
2525
* Normalizes a `string | string[]` shortcut to a key array. A lone string is
26-
* treated as a single key (never split), so callers can pass `"?"` for a
27-
* one-key hint and `["Mod", "K"]` for a combo — no `"+"` parsing involved.
26+
* treated as a single key (never split), so callers pass `"?"` for a one-key
27+
* hint and `["Mod", "K"]` for a combo — no `"+"` parsing involved.
2828
*/
29-
export function toKeyArray(keys: string | string[]): string[] {
29+
function toKeyArray(keys: string | string[]): string[] {
3030
return Array.isArray(keys) ? keys : [keys];
3131
}
3232

3333
interface Props {
34-
/** One key per entry, e.g. `["Mod", "K"]`, `["Shift", "W"]`, `["?"]`. */
35-
keys: string[];
34+
/** A single key (`"?"`) or one key per entry (`["Mod", "K"]`). */
35+
keys: string | string[];
3636
title?: string;
3737
className?: string;
3838
}
@@ -42,7 +42,9 @@ interface Props {
4242
* read as distinct keys (`[⌘] [K]`) rather than a single `"+"`-joined string.
4343
*/
4444
export function ShortcutKeys({ keys, title, className }: Props) {
45-
const cleaned = keys.map((key) => key.trim()).filter(Boolean);
45+
const cleaned = toKeyArray(keys)
46+
.map((key) => key.trim())
47+
.filter(Boolean);
4648

4749
// Nothing to render -> emit nothing, rather than an empty chip row.
4850
if (cleaned.length === 0) return null;

packages/web/src/components/Shortcuts/ShortcutOverlay/ShortcutsOverlay.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export const ShortcutsOverlay = ({
3636
className={classNames(
3737
"fixed top-24 left-3 border-border-primary bg-bg-secondary",
3838
"border p-3 shadow-lg backdrop-blur-sm md:block",
39-
`hidden w-[240px] rounded-lg ${className}`,
39+
`hidden w-60 rounded-lg ${className}`,
4040
)}
4141
style={{ zIndex: maxZIndex }}
4242
>

packages/web/src/components/Tooltip/TooltipWrapper.tsx

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
import type React from "react";
22
import { type ReactNode } from "react";
33
import { AlignItems, Flex } from "@web/components/Flex/Flex";
4-
import {
5-
ShortcutKeys,
6-
toKeyArray,
7-
} from "@web/components/Shortcuts/ShortcutKeys";
4+
import { ShortcutKeys } from "@web/components/Shortcuts/ShortcutKeys";
85
import {
96
Tooltip,
107
TooltipContent,
@@ -46,7 +43,7 @@ export const TooltipWrapper: React.FC<Props> = ({
4643
{description && <TooltipDescription description={description} />}
4744
{shortcut &&
4845
(typeof shortcut === "string" || Array.isArray(shortcut) ? (
49-
<ShortcutKeys keys={toKeyArray(shortcut)} />
46+
<ShortcutKeys keys={shortcut} />
5047
) : (
5148
<ShortcutHint variant="keycap">{shortcut}</ShortcutHint>
5249
))}
Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
import { type ReactNode, useState } from "react";
2-
import {
3-
ShortcutKeys,
4-
toKeyArray,
5-
} from "@web/components/Shortcuts/ShortcutKeys";
2+
import { ShortcutKeys } from "@web/components/Shortcuts/ShortcutKeys";
63

74
interface ShortcutProps {
85
shortcut: string | string[];
@@ -11,19 +8,18 @@ interface ShortcutProps {
118
}
129

1310
const ShortcutBadge = ({
14-
keys,
11+
shortcut,
1512
ariaLabel,
1613
}: {
17-
keys: string[];
14+
shortcut: string | string[];
1815
ariaLabel?: string;
19-
}) => <ShortcutKeys keys={keys} title={ariaLabel} />;
16+
}) => <ShortcutKeys keys={shortcut} title={ariaLabel} />;
2017

2118
export const ShortcutTip = ({
2219
shortcut,
2320
"aria-label": ariaLabel,
2421
children,
2522
}: ShortcutProps) => {
26-
const keys = toKeyArray(shortcut);
2723
const [isHovered, setIsHovered] = useState(false);
2824

2925
if (children != null) {
@@ -35,10 +31,12 @@ export const ShortcutTip = ({
3531
onMouseLeave={() => setIsHovered(false)}
3632
>
3733
{children}
38-
{isHovered && <ShortcutBadge keys={keys} ariaLabel={ariaLabel} />}
34+
{isHovered && (
35+
<ShortcutBadge shortcut={shortcut} ariaLabel={ariaLabel} />
36+
)}
3937
</span>
4038
);
4139
}
4240

43-
return <ShortcutBadge keys={keys} ariaLabel={ariaLabel} />;
41+
return <ShortcutBadge shortcut={shortcut} ariaLabel={ariaLabel} />;
4442
};

packages/web/src/views/Forms/ActionsMenu/MenuItem.tsx

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
import type React from "react";
22
import { useEffect, useRef, useState } from "react";
3-
import {
4-
ShortcutKeys,
5-
toKeyArray,
6-
} from "@web/components/Shortcuts/ShortcutKeys";
3+
import { ShortcutKeys } from "@web/components/Shortcuts/ShortcutKeys";
74
import {
85
Tooltip,
96
TooltipContent,
@@ -93,10 +90,8 @@ const MenuItem: React.FC<MenuItemProps> = ({
9390
</button>
9491
);
9592

96-
const tooltipKeys = tooltipContent ? toKeyArray(tooltipContent) : [];
97-
9893
// No keys to show -> render the bare button (no empty tooltip surface).
99-
if (tooltipKeys.length === 0) {
94+
if (!tooltipContent || tooltipContent.length === 0) {
10095
return button;
10196
}
10297

@@ -108,7 +103,7 @@ const MenuItem: React.FC<MenuItemProps> = ({
108103
>
109104
<TooltipTrigger asChild>{button}</TooltipTrigger>
110105
<TooltipContent>
111-
<ShortcutKeys keys={tooltipKeys} />
106+
<ShortcutKeys keys={tooltipContent} />
112107
</TooltipContent>
113108
</Tooltip>
114109
);

0 commit comments

Comments
 (0)