Skip to content

Commit 193e4bd

Browse files
feat: implement <Kbd /> and shortcut tooltip (#21971)
Closes #21650 This pull-request adds a `<Tooltip />` with `<Kbd />` modifiers to the `Run Task` button describing the shortcut how to submit the prompt quickly without having to navigate to the `↑` button. <img width="456" height="298" alt="CleanShot 2026-02-06 at 19 40 58@2x" src="https://github.com/user-attachments/assets/fa08a373-21c3-4620-9551-0c8a6b3547ab" /> It should be noted that the [keyboard shortcut already existed](https://github.com/coder/coder/blob/jakehwll/21650-submit-prompt-shortcut/site/src/modules/tasks/TaskPrompt/TaskPrompt.tsx#L222-L227) so we don't need to implement that here. ```ts // L222-L227 const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => { // Submit form on Cmd+Enter (Mac) or Ctrl+Enter (Windows/Linux) if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) { onSubmit(e); } }; ``` --------- Co-authored-by: Danielle Maywood <[email protected]>
1 parent edcee32 commit 193e4bd

4 files changed

Lines changed: 114 additions & 17 deletions

File tree

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import type { Meta, StoryObj } from "@storybook/react-vite";
2+
import { CommandIcon } from "lucide-react";
3+
import { Kbd, KbdGroup } from "./Kbd";
4+
5+
const meta: Meta<typeof Kbd> = {
6+
title: "components/Kbd",
7+
component: Kbd,
8+
args: {
9+
children: "K",
10+
},
11+
};
12+
13+
export default meta;
14+
type Story = StoryObj<typeof Kbd>;
15+
16+
export const Default: Story = {};
17+
18+
export const WithIcon: Story = {
19+
args: {
20+
children: <CommandIcon />,
21+
},
22+
};
23+
24+
export const Group: Story = {
25+
render: () => (
26+
<KbdGroup>
27+
<Kbd>
28+
<CommandIcon />
29+
</Kbd>
30+
<Kbd>Enter</Kbd>
31+
</KbdGroup>
32+
),
33+
};
34+
35+
export const MultipleKeys: Story = {
36+
render: () => (
37+
<KbdGroup>
38+
<Kbd>Ctrl</Kbd>
39+
<Kbd>Shift</Kbd>
40+
<Kbd>P</Kbd>
41+
</KbdGroup>
42+
),
43+
};

‎site/src/components/Kbd/Kbd.tsx‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { cn } from "utils/cn";
2+
3+
function Kbd({ className, ...props }: React.ComponentProps<"kbd">) {
4+
return (
5+
<kbd
6+
data-slot="kbd"
7+
className={cn(
8+
"inline-flex items-center justify-center gap-1",
9+
"h-5 w-fit min-w-5 font-sans text-xs font-medium select-none",
10+
"bg-surface-tertiary text-content-secondary rounded-sm px-1",
11+
"[&_svg:not([class*='size-'])]:size-3 pointer-events-none",
12+
className,
13+
)}
14+
{...props}
15+
/>
16+
);
17+
}
18+
function KbdGroup({ className, ...props }: React.ComponentProps<"div">) {
19+
return (
20+
<kbd
21+
data-slot="kbd-group"
22+
className={cn("gap-1 inline-flex items-center", className)}
23+
{...props}
24+
/>
25+
);
26+
}
27+
export { Kbd, KbdGroup };

‎site/src/modules/tasks/TaskPrompt/TaskPrompt.tsx‎

Lines changed: 30 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { ErrorAlert } from "components/Alert/ErrorAlert";
1111
import { Button } from "components/Button/Button";
1212
import { ExternalImage } from "components/ExternalImage/ExternalImage";
1313
import { displayError, displaySuccess } from "components/GlobalSnackbar/utils";
14+
import { Kbd, KbdGroup } from "components/Kbd/Kbd";
1415
import { Link } from "components/Link/Link";
1516
import {
1617
Select,
@@ -34,6 +35,7 @@ import TextareaAutosize, {
3435
type TextareaAutosizeProps,
3536
} from "react-textarea-autosize";
3637
import { docs } from "utils/docs";
38+
import { getOSKey } from "utils/platform";
3739
import { PromptSelectTrigger } from "./PromptSelectTrigger";
3840
import { TemplateVersionSelect } from "./TemplateVersionSelect";
3941

@@ -373,23 +375,34 @@ const CreateTaskForm: FC<CreateTaskFormProps> = ({ templates, onSuccess }) => {
373375
/>
374376
)}
375377

376-
<Button
377-
size="icon"
378-
type="submit"
379-
disabled={prompt.trim().length === 0 || isMissingExternalAuth}
380-
className="rounded-full disabled:bg-surface-invert-primary disabled:opacity-70"
381-
>
382-
<Spinner
383-
loading={
384-
isLoadingExternalAuth ||
385-
isPollingExternalAuth ||
386-
createTaskMutation.isPending
387-
}
388-
>
389-
<ArrowUpIcon />
390-
</Spinner>
391-
<span className="sr-only">Run task</span>
392-
</Button>
378+
<Tooltip>
379+
<TooltipTrigger asChild>
380+
<Button
381+
size="icon"
382+
type="submit"
383+
disabled={prompt.trim().length === 0 || isMissingExternalAuth}
384+
className="rounded-full disabled:bg-surface-invert-primary disabled:opacity-70"
385+
>
386+
<Spinner
387+
loading={
388+
isLoadingExternalAuth ||
389+
isPollingExternalAuth ||
390+
createTaskMutation.isPending
391+
}
392+
>
393+
<ArrowUpIcon />
394+
</Spinner>
395+
<span className="sr-only">Run task</span>
396+
</Button>
397+
</TooltipTrigger>
398+
<TooltipContent align="end">
399+
<KbdGroup>
400+
<Kbd>{getOSKey()}</Kbd>
401+
<span>+</span>
402+
<Kbd>Enter</Kbd>
403+
</KbdGroup>
404+
</TooltipContent>
405+
</Tooltip>
393406
</div>
394407
</div>
395408
</fieldset>

‎site/src/utils/platform.ts‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/**
2+
* Returns true if the current platform is macOS.
3+
*/
4+
function isMac(): boolean {
5+
return !!navigator.platform.match("Mac");
6+
}
7+
8+
/**
9+
* Returns the platform-appropriate modifier key label: ⌘ on macOS,
10+
* Ctrl on everything else.
11+
*/
12+
export function getOSKey(): string {
13+
return isMac() ? "⌘" : "Ctrl";
14+
}

0 commit comments

Comments
 (0)