// Copyright (c) Tailscale Inc & AUTHORS // SPDX-License-Identifier: BSD-3-Clause import cx from "classnames" import React, { useEffect, useRef, useState } from "react" import useToaster from "src/hooks/toaster" import { copyText } from "src/utils/clipboard" type Props = { className?: string hideAffordance?: boolean /** * primaryActionSubject is the subject of the toast confirmation message * "Copied to clipboard" */ primaryActionSubject: string primaryActionValue: string secondaryActionName?: string secondaryActionValue?: string /** * secondaryActionSubject is the subject of the toast confirmation message * prompted by the secondary action "Copied to clipboard" */ secondaryActionSubject?: string children?: React.ReactNode /** * onSecondaryAction is used to trigger events when the secondary copy * function is used. It is not used when the secondary action is hidden. */ onSecondaryAction?: () => void } /** * QuickCopy is a UI component that allows for copying textual content in one click. */ export default function QuickCopy(props: Props) { const { className, hideAffordance, primaryActionSubject, primaryActionValue, secondaryActionValue, secondaryActionName, secondaryActionSubject, onSecondaryAction, children, } = props const toaster = useToaster() const containerRef = useRef(null) const buttonRef = useRef(null) const [showButton, setShowButton] = useState(false) useEffect(() => { if (!showButton) { return } if (!containerRef.current || !buttonRef.current) { return } // We don't need to watch any `resize` event because it's pretty unlikely // the browser will resize while their cursor is over one of these items. const rect = containerRef.current.getBoundingClientRect() const maximumPossibleWidth = window.innerWidth - rect.left + 4 // We add the border-width (1px * 2 sides) and the padding (0.5rem * 2 sides) // and add 1px for rounding up the calculation in order to get the final // maxWidth value. This should be kept in sync with the CSS classes below. buttonRef.current.style.maxWidth = `${maximumPossibleWidth}px` buttonRef.current.style.visibility = "visible" }, [showButton]) const handlePrimaryAction = () => { copyText(primaryActionValue) toaster.show({ message: `Copied ${primaryActionSubject} to the clipboard`, }) } const handleSecondaryAction = () => { if (!secondaryActionValue) { return } copyText(secondaryActionValue) toaster.show({ message: `Copied ${ secondaryActionSubject || secondaryActionName } to the clipboard`, }) onSecondaryAction?.() } return (
setShowButton(false)} >
setShowButton(true)} className={cx("truncate", className)} > {children}
{!hideAffordance && ( )} {showButton && (
{children}
{secondaryActionValue && (
{secondaryActionName}
)}
)}
) }