refactor: migrate CopyableValue from MUI to Radix UI (#20261)

## Summary

Migrates the `CopyableValue` component from Material-UI Tooltip to Radix
UI Tooltip.

## Changes

### CopyableValue Component
- **Removed unused prop**: `PopperProps` (never used in any call site)
- **Renamed prop**: `placement` → `side` (Radix naming convention)
- **Updated pattern**: Now uses
TooltipProvider/Tooltip/TooltipTrigger/TooltipContent composition

**Minimal API surface change:**
- `placement` prop renamed to `side` (only affects 1 call site - already
updated)
- `PopperProps` removed (was never used)

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Jaayden Halko
2025-11-10 22:15:40 +00:00
committed by GitHub
co-authored by Claude
parent f2a1a7e8c3
commit 47c703aa4d
4 changed files with 85 additions and 52 deletions
@@ -1,35 +1,91 @@
import Tooltip, { type TooltipProps } from "@mui/material/Tooltip";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "components/Tooltip/Tooltip";
import { useClickable } from "hooks/useClickable";
import { useClipboard } from "hooks/useClipboard";
import type { FC, HTMLAttributes } from "react";
import { type FC, type HTMLAttributes, useState } from "react";
import { cn } from "utils/cn";
type TooltipSide = "top" | "right" | "bottom" | "left";
interface CopyableValueProps extends HTMLAttributes<HTMLSpanElement> {
value: string;
placement?: TooltipProps["placement"];
PopperProps?: TooltipProps["PopperProps"];
side?: TooltipSide;
}
export const CopyableValue: FC<CopyableValueProps> = ({
value,
placement = "bottom-start",
PopperProps,
side = "bottom",
children,
className,
role,
tabIndex,
onClick,
onKeyDown,
onKeyUp,
...attrs
}) => {
const { showCopiedSuccess, copyToClipboard } = useClipboard();
const [tooltipOpen, setTooltipOpen] = useState(false);
const [isFocused, setIsFocused] = useState(false);
const clickableProps = useClickable<HTMLSpanElement>(() => {
copyToClipboard(value);
setTooltipOpen(true);
});
return (
<Tooltip
title={showCopiedSuccess ? "Copied!" : "Click to copy"}
placement={placement}
PopperProps={PopperProps}
>
<span {...attrs} {...clickableProps} css={{ cursor: "pointer" }}>
{children}
</span>
</Tooltip>
<TooltipProvider delayDuration={100}>
<Tooltip
open={tooltipOpen}
onOpenChange={(shouldBeOpen) => {
// Always keep the tooltip open when in focus to handle issues when onOpenChange is unexpectedly false
if (!shouldBeOpen && isFocused) return;
setTooltipOpen(shouldBeOpen);
}}
>
<TooltipTrigger asChild>
<span
ref={clickableProps.ref}
{...attrs}
className={cn("cursor-pointer", className)}
role={role ?? clickableProps.role}
tabIndex={tabIndex ?? clickableProps.tabIndex}
onClick={(event) => {
clickableProps.onClick(event);
onClick?.(event);
}}
onKeyDown={(event) => {
clickableProps.onKeyDown(event);
onKeyDown?.(event);
}}
onKeyUp={(event) => {
clickableProps.onKeyUp(event);
onKeyUp?.(event);
}}
onMouseEnter={() => {
setIsFocused(true);
setTooltipOpen(true);
}}
onMouseLeave={() => {
setTooltipOpen(false);
}}
onFocus={() => {
setIsFocused(true);
}}
onBlur={() => {
setTooltipOpen(false);
}}
>
{children}
</span>
</TooltipTrigger>
<TooltipContent side={side}>
{showCopiedSuccess ? "Copied!" : "Click to copy"}
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
};
@@ -32,7 +32,10 @@ export const SensitiveValue: FC<SensitiveValueProps> = ({ value }) => {
gap: 4,
}}
>
<CopyableValue value={value} css={styles.value}>
<CopyableValue
value={value}
className="w-[calc(100%-22px)] overflow-hidden whitespace-nowrap text-ellipsis"
>
{displayValue}
</CopyableValue>
<Tooltip title={buttonLabel}>
@@ -52,14 +55,6 @@ export const SensitiveValue: FC<SensitiveValueProps> = ({ value }) => {
};
const styles = {
value: {
// 22px is the button width
width: "calc(100% - 22px)",
overflow: "hidden",
whiteSpace: "nowrap",
textOverflow: "ellipsis",
},
button: css`
color: inherit;
`,
@@ -150,20 +150,20 @@ export const EditOAuth2AppPageView: FC<EditOAuth2AppProps> = ({
<dl css={styles.dataList}>
<dt>Client ID</dt>
<dd>
<CopyableValue value={app.id}>
<CopyableValue value={app.id} side="right">
{app.id} <CopyIcon className="size-icon-xs" />
</CopyableValue>
</dd>
<dt>Authorization URL</dt>
<dd>
<CopyableValue value={app.endpoints.authorization}>
<CopyableValue value={app.endpoints.authorization} side="right">
{app.endpoints.authorization}{" "}
<CopyIcon className="size-icon-xs" />
</CopyableValue>
</dd>
<dt>Token URL</dt>
<dd>
<CopyableValue value={app.endpoints.token}>
<CopyableValue value={app.endpoints.token} side="right">
{app.endpoints.token} <CopyIcon className="size-icon-xs" />
</CopyableValue>
</dd>
+7 -25
View File
@@ -12,7 +12,6 @@ import {
PageHeaderSubtitle,
PageHeaderTitle,
} from "components/PageHeader/PageHeader";
import { Stack } from "components/Stack/Stack";
import { SearchIcon, XIcon } from "lucide-react";
import { type FC, type ReactNode, useMemo, useState } from "react";
import {
@@ -79,13 +78,7 @@ const IconsPage: FC = () => {
<Tooltip
placement="bottom-end"
title={
<p
css={{
padding: 8,
fontSize: 13,
lineHeight: 1.5,
}}
>
<p className="p-2 leading-6 text-sm">
You can suggest a new icon by submitting a Pull Request to our
public GitHub repository. Just keep in mind that it should be
relevant to many Coder users, and redistributable under a
@@ -124,12 +117,7 @@ const IconsPage: FC = () => {
},
startAdornment: (
<InputAdornment position="start">
<SearchIcon
className="size-icon-xs"
css={{
color: theme.palette.text.secondary,
}}
/>
<SearchIcon className="size-icon-xs text-content-secondary" />
</InputAdornment>
),
endAdornment: searchInputText && (
@@ -147,19 +135,13 @@ const IconsPage: FC = () => {
}}
/>
<Stack
direction="row"
wrap="wrap"
spacing={1}
justifyContent="center"
css={{ marginTop: 32 }}
>
<div className="flex flex-row gap-2 justify-center flex-wrap max-w-full mt-8">
{searchedIcons.length === 0 && (
<EmptyState message="No results matched your search" />
)}
{searchedIcons.map((icon) => (
<CopyableValue key={icon.url} value={icon.url} placement="bottom">
<Stack alignItems="center" css={{ margin: 12 }}>
<CopyableValue key={icon.url} value={icon.url}>
<div className="flex flex-col gap-4 items-center max-w-full p-3">
<img
alt={icon.url}
src={icon.url}
@@ -189,10 +171,10 @@ const IconsPage: FC = () => {
>
{icon.description}
</figcaption>
</Stack>
</div>
</CopyableValue>
))}
</Stack>
</div>
</Margins>
</>
);