refactor(ui): move CopyableBadge copy affordance into the hover tooltip

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
ryan 2026-07-23 00:00:44 +00:00
parent 0d163101cb
commit a652454144
2 changed files with 45 additions and 18 deletions

View file

@ -1,6 +1,8 @@
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { TooltipProvider } from "@/components/ui/tooltip";
import { CopyableBadge } from "./copyable_badge";
const writeText = vi.fn().mockResolvedValue(undefined);
@ -13,29 +15,45 @@ beforeEach(() => {
});
});
const renderBadge = (value: string, maxWidthClassName?: string) =>
render(
<TooltipProvider delay={0}>
<CopyableBadge value={value} maxWidthClassName={maxWidthClassName} />
</TooltipProvider>,
);
const openTooltip = (value: string) => {
const trigger = screen.getByText(value).parentElement!;
fireEvent.pointerEnter(trigger, { pointerType: "mouse" });
fireEvent.mouseEnter(trigger);
};
describe("CopyableBadge", () => {
it("renders the value and truncates it", () => {
render(<CopyableBadge value="app-aigateway-inference-producttech-product-default" />);
it("renders the value truncated in the badge", () => {
renderBadge("app-aigateway-inference-producttech-product-default");
const label = screen.getByText("app-aigateway-inference-producttech-product-default");
expect(label.className).toContain("truncate");
expect(label.className).toContain("max-w-[220px]");
});
it("copies the full value when the copy button is clicked", async () => {
render(<CopyableBadge value="team-alias-long-name" />);
it("reveals the full value and copies it from the hover tooltip", async () => {
renderBadge("team-alias-long-name");
fireEvent.click(screen.getByRole("button", { name: "Copy team-alias-long-name" }));
openTooltip("team-alias-long-name");
const copyButton = await screen.findByRole("button", { name: "Copy team-alias-long-name" });
fireEvent.click(copyButton);
await waitFor(() => expect(writeText).toHaveBeenCalledWith("team-alias-long-name"));
});
it("exposes a copy affordance via accessible label", () => {
render(<CopyableBadge value="my-team" />);
expect(screen.getByRole("button", { name: "Copy my-team" })).toBeInTheDocument();
it("does not expose the copy affordance until hovered", () => {
renderBadge("my-team");
expect(screen.queryByRole("button", { name: "Copy my-team" })).not.toBeInTheDocument();
});
it("honors a custom max width", () => {
render(<CopyableBadge value="short" maxWidthClassName="max-w-[120px]" />);
renderBadge("short", "max-w-[120px]");
expect(screen.getByText("short").className).toContain("max-w-[120px]");
});
});

View file

@ -20,14 +20,23 @@ export function CopyableBadge({
dataTestId,
}: CopyableBadgeProps) {
return (
<Tag color={color} className="m-0 inline-flex max-w-full items-center gap-1" data-testid={dataTestId}>
<Tooltip>
<TooltipTrigger className="max-w-full cursor-default border-0 bg-transparent p-0">
<span className={cn("block truncate", maxWidthClassName)}>{value}</span>
</TooltipTrigger>
<TooltipContent>{value}</TooltipContent>
</Tooltip>
<CopyButton value={value} label={`Copy ${value}`} iconClassName="size-3" />
</Tag>
<Tooltip>
<TooltipTrigger
render={
<Tag color={color} className="m-0 max-w-full cursor-default" data-testid={dataTestId}>
<span className={cn("block truncate", maxWidthClassName)}>{value}</span>
</Tag>
}
/>
<TooltipContent className="max-w-sm">
<span className="break-all">{value}</span>
<CopyButton
value={value}
label={`Copy ${value}`}
iconClassName="size-3"
className="text-background/70 hover:bg-transparent hover:text-background"
/>
</TooltipContent>
</Tooltip>
);
}