**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" />
133 lines
3.9 KiB
TypeScript
133 lines
3.9 KiB
TypeScript
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;
|