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 <danielle@themaywoods.com>
This commit is contained in:
Jake Howell
2026-02-06 21:28:57 +11:00
committed by GitHub
co-authored by Danielle Maywood
parent edcee32ab9
commit 193e4bd73b
4 changed files with 114 additions and 17 deletions
+43
View File
@@ -0,0 +1,43 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { CommandIcon } from "lucide-react";
import { Kbd, KbdGroup } from "./Kbd";
const meta: Meta<typeof Kbd> = {
title: "components/Kbd",
component: Kbd,
args: {
children: "K",
},
};
export default meta;
type Story = StoryObj<typeof Kbd>;
export const Default: Story = {};
export const WithIcon: Story = {
args: {
children: <CommandIcon />,
},
};
export const Group: Story = {
render: () => (
<KbdGroup>
<Kbd>
<CommandIcon />
</Kbd>
<Kbd>Enter</Kbd>
</KbdGroup>
),
};
export const MultipleKeys: Story = {
render: () => (
<KbdGroup>
<Kbd>Ctrl</Kbd>
<Kbd>Shift</Kbd>
<Kbd>P</Kbd>
</KbdGroup>
),
};
+27
View File
@@ -0,0 +1,27 @@
import { cn } from "utils/cn";
function Kbd({ className, ...props }: React.ComponentProps<"kbd">) {
return (
<kbd
data-slot="kbd"
className={cn(
"inline-flex items-center justify-center gap-1",
"h-5 w-fit min-w-5 font-sans text-xs font-medium select-none",
"bg-surface-tertiary text-content-secondary rounded-sm px-1",
"[&_svg:not([class*='size-'])]:size-3 pointer-events-none",
className,
)}
{...props}
/>
);
}
function KbdGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<kbd
data-slot="kbd-group"
className={cn("gap-1 inline-flex items-center", className)}
{...props}
/>
);
}
export { Kbd, KbdGroup };
@@ -11,6 +11,7 @@ import { ErrorAlert } from "components/Alert/ErrorAlert";
import { Button } from "components/Button/Button";
import { ExternalImage } from "components/ExternalImage/ExternalImage";
import { displayError, displaySuccess } from "components/GlobalSnackbar/utils";
import { Kbd, KbdGroup } from "components/Kbd/Kbd";
import { Link } from "components/Link/Link";
import {
Select,
@@ -34,6 +35,7 @@ import TextareaAutosize, {
type TextareaAutosizeProps,
} from "react-textarea-autosize";
import { docs } from "utils/docs";
import { getOSKey } from "utils/platform";
import { PromptSelectTrigger } from "./PromptSelectTrigger";
import { TemplateVersionSelect } from "./TemplateVersionSelect";
@@ -373,23 +375,34 @@ const CreateTaskForm: FC<CreateTaskFormProps> = ({ templates, onSuccess }) => {
/>
)}
<Button
size="icon"
type="submit"
disabled={prompt.trim().length === 0 || isMissingExternalAuth}
className="rounded-full disabled:bg-surface-invert-primary disabled:opacity-70"
>
<Spinner
loading={
isLoadingExternalAuth ||
isPollingExternalAuth ||
createTaskMutation.isPending
}
>
<ArrowUpIcon />
</Spinner>
<span className="sr-only">Run task</span>
</Button>
<Tooltip>
<TooltipTrigger asChild>
<Button
size="icon"
type="submit"
disabled={prompt.trim().length === 0 || isMissingExternalAuth}
className="rounded-full disabled:bg-surface-invert-primary disabled:opacity-70"
>
<Spinner
loading={
isLoadingExternalAuth ||
isPollingExternalAuth ||
createTaskMutation.isPending
}
>
<ArrowUpIcon />
</Spinner>
<span className="sr-only">Run task</span>
</Button>
</TooltipTrigger>
<TooltipContent align="end">
<KbdGroup>
<Kbd>{getOSKey()}</Kbd>
<span>+</span>
<Kbd>Enter</Kbd>
</KbdGroup>
</TooltipContent>
</Tooltip>
</div>
</div>
</fieldset>
+14
View File
@@ -0,0 +1,14 @@
/**
* Returns true if the current platform is macOS.
*/
function isMac(): boolean {
return !!navigator.platform.match("Mac");
}
/**
* Returns the platform-appropriate modifier key label: ⌘ on macOS,
* Ctrl on everything else.
*/
export function getOSKey(): string {
return isMac() ? "⌘" : "Ctrl";
}