Files
fleet/frontend/components/buttons/CopyButton/CopyButton.tsx
T
Steven Palmesano 62ed3583e6 Clear button styles (#49292)
**Related issue:** Resolves #49276

**New features**
- Added new "Secondary" (bordered, off-white fill) and "Subdued"
(borderless, low-emphasis) button variants to match the Figma spec,
alongside the existing Primary style.
- Allowed rows to be selected in Controls > OS updates.

**Cleanup**
- Once nothing referenced the old styles anymore, fully removed the old
`text-icon`, `brand-inverse-icon`, `inverse-alert`, `inverse`, and
`icon` button variants (type, styles, and Storybook entries) from the
shared `Button` component.
- Removed the `iconStroke` prop, which had become a no-op once the old
variants it supported were gone.
- Renamed `ActionsDropdown`'s variants
(`button`/`brand-button`/`small-button`) to
`subdued`/`primary`/`secondary` to match the same naming used everywhere
else.
- Replaced a one-off dropdown implementation on the Software title page
with the shared `ActionsDropdown` component, instead of maintaining
duplicate styling logic.
- Changed the button name on Host details > Reports > Report details
from "View data for all hosts" to "View report for all hosts" (to match
the previous page's Actions drop-down options).


# Checklist for submitter

- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.


## Testing

- [x] Added/updated automated tests
- [x] QA'd all new/changed functionality manually

<img width="1475" height="241" alt="Screenshot 2026-07-21 at 06 35 49"
src="https://github.com/user-attachments/assets/7cfbd444-7837-40e8-854e-bc5989d57d85"
/>
<img width="661" height="306" alt="Screenshot 2026-07-21 at 06 37 18"
src="https://github.com/user-attachments/assets/5d0c4873-8179-4089-b115-7e8cd3a53b4d"
/>
<img width="1427" height="423" alt="Screenshot 2026-07-21 at 06 37 30"
src="https://github.com/user-attachments/assets/a4850a60-f44a-4902-b45e-0094f23a52f8"
/>
<img width="1427" height="640" alt="Screenshot 2026-07-21 at 06 37 46"
src="https://github.com/user-attachments/assets/738a4a7f-cd7d-4162-b659-6f649c32204d"
/>
<img width="1445" height="479" alt="Screenshot 2026-07-22 at 07 03 22"
src="https://github.com/user-attachments/assets/4f672dc0-5c6d-4eb8-8465-ed5233fcd1b2"
/>
<img width="811" height="871" alt="Screenshot 2026-07-21 at 06 41 20"
src="https://github.com/user-attachments/assets/5421c96e-2dab-492a-af26-be0e5a7791ca"
/>
2026-07-23 07:11:59 -05:00

133 lines
3.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useEffect, useRef, useState } from "react";
import classnames from "classnames";
import { Tooltip as ReactTooltip5 } from "react-tooltip-5";
import { uniqueId } from "lodash";
import Button from "components/buttons/Button";
import Icon from "components/Icon";
import { stringToClipboard } from "utilities/copy_text";
type CopyButtonVariant = "secondary" | "subdued" | "compact";
interface ICopyButtonProps {
copyText: string;
/** Override the button's content. Defaults to `<Icon name="copy" />`. */
children?: React.ReactNode;
/** `"subdued"` (default) — borderless low-emphasis icon-only button —
* see #35329.
* `"secondary"` — bordered 36×36 icon-only button (the current preferred
* secondary style — see #35329).
* `"compact"` — icon collapsed to its natural size, no extra vertical
* chrome. Use for inline-with-text copy actions so the surrounding row
* doesn't grow to button height. */
variant?: CopyButtonVariant;
size?: "small" | "default";
className?: string;
ariaLabel?: string;
/** Distance in px from the anchor to the tooltip. Defaults to `4` — tight
* for inline-with-text copy actions. Pass `10` to match react-tooltip 5's
* own default when the trigger is a larger floating button. */
tooltipOffset?: number;
/** Table buttons show on row hover and tab focus only */
rowHover?: boolean;
}
const baseClass = "copy-button";
const HIDE_AFTER_MS = 1000;
const SUCCESS_MESSAGE = "Copied!";
const ERROR_MESSAGE = "Copy failed";
const CopyButton = ({
copyText,
children,
variant = "subdued",
size,
className,
ariaLabel = "Copy to clipboard",
tooltipOffset = 4,
rowHover = false,
}: ICopyButtonProps) => {
const [message, setMessage] = useState<string | null>(null);
const tipIdRef = useRef(uniqueId("copy-button-tooltip-"));
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
useEffect(() => {
return () => {
if (timerRef.current) {
clearTimeout(timerRef.current);
}
};
}, []);
const onClick = (evt: React.MouseEvent<HTMLButtonElement>) => {
evt.preventDefault();
// Drop focus only for mouse activations, so a lingering focus ring
// doesn't stay visible after a click — keyboard Enter/Space report
// `detail === 0` and must keep their tab position.
if (evt.detail !== 0) {
evt.currentTarget.blur();
}
// Cancel any previous click's hide timer so the new badge gets the full
// window — and so the hide doesn't race a slow `writeText()` (>1s would
// leave the message visible with no timer to clear it).
if (timerRef.current) {
clearTimeout(timerRef.current);
timerRef.current = null;
}
const scheduleHide = () => {
timerRef.current = setTimeout(() => setMessage(null), HIDE_AFTER_MS);
};
stringToClipboard(copyText)
.then(() => {
setMessage(SUCCESS_MESSAGE);
scheduleHide();
})
.catch(() => {
setMessage(ERROR_MESSAGE);
scheduleHide();
});
};
const isCompact = variant === "compact";
return (
<span className={baseClass} data-tooltip-id={tipIdRef.current}>
<Button
variant={isCompact ? "subdued" : variant}
size={size}
onClick={onClick}
className={classnames(
`${baseClass}__button`,
{
[`${baseClass}__button--compact`]: isCompact,
"row-hover-button": rowHover,
},
className
)}
ariaLabel={ariaLabel}
>
{children ?? (
<Icon name="copy" size={size === "small" ? "small" : undefined} />
)}
</Button>
<ReactTooltip5
id={tipIdRef.current}
isOpen={message !== null}
place="left"
offset={tooltipOffset}
opacity={1}
disableStyleInjection
noArrow
className={`${baseClass}__tooltip`}
>
{message}
</ReactTooltip5>
</span>
);
};
export default CopyButton;