mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
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:
@@ -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;
|
||||
`,
|
||||
|
||||
+3
-3
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user