mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
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:
co-authored by
Danielle Maywood
parent
edcee32ab9
commit
193e4bd73b
@@ -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>
|
||||
),
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
Reference in New Issue
Block a user