LCOV - code coverage report
Current view: top level - dashql-app/src/ui/foundations - clipboard.tsx Coverage Total Hit
Test: DashQL 915828b Lines: 93.2 % 44 41
Test Date: 2026-11-06 19:30:32

            Line data    Source code
       1              : import * as React from 'react';
       2              : 
       3              : import { CheckIcon, CopyIcon } from './symbol_icon.js';
       4              : import type { Icon } from './symbol_icon.js';
       5              : 
       6              : import { useLogger } from '../../platform/logger/logger_provider.js';
       7              : import { stringifyError } from '../../platform/logger/logger.js';
       8              : import { ButtonSize, ButtonVariant, IconButton } from './button.js';
       9              : 
      10            1 : const DEFAULT_COPY_TIMEOUT = 2000;
      11              : 
      12              : interface Props {
      13              :     variant: ButtonVariant;
      14              :     size: ButtonSize;
      15              :     className?: string;
      16              :     value?: string;
      17              :     getValue?: () => string;
      18              :     timeoutMs?: number;
      19              :     logContext: string;
      20              :     disabled?: boolean;
      21              :     icon?: Icon;
      22              :     ['aria-label']: string;
      23              :     ['aria-labelledby']: string;
      24              :     ['data-testid']?: string;
      25              : }
      26              : /// The icon to render inside the button
      27            1 : export function CopyToClipboardButton(props: Props): React.ReactElement {
      28            1 :     const logger = useLogger();
      29            1 :     const [lastCopied, setLastCopied] = React.useState<number | null>(null);
      30            1 :     const [wasRecentlyCopied, setWasRecentlyCopied] = React.useState<boolean>(false);
      31            1 :     const timeoutMs = props.timeoutMs ?? DEFAULT_COPY_TIMEOUT;
      32              : 
      33            1 :     const copy = React.useCallback(async () => {
      34              :         // Read the value when copying, getValue may return text that changes without a re-render
      35            1 :         const value = props.value ?? (props.getValue ? props.getValue() : "");
      36            1 :         try {
      37            1 :             await navigator.clipboard.writeText(value);
      38            1 :             logger.info("Copied to clipboard", { "chars": value.length.toString() }, props.logContext);
      39            1 :             setLastCopied(Date.now());
      40            1 :         } catch (e: any) {
      41            0 :             logger.error("Copying failed", { "error": stringifyError(e) }, props.logContext);
      42            0 :         }
      43            1 :     }, [setLastCopied, props.value, props.getValue]);
      44              : 
      45            1 :     React.useEffect(() => {
      46            1 :         if (lastCopied == null) {
      47            1 :             return;
      48              :         }
      49            1 :         setWasRecentlyCopied(true);
      50            1 :         const timeoutId = setTimeout(() => {
      51            0 :             setWasRecentlyCopied(false);
      52            1 :         }, timeoutMs);
      53            1 :         return () => clearTimeout(timeoutId);
      54            1 :     }, [lastCopied]);
      55              : 
      56            1 :     let InnerIcon: Icon;
      57            1 :     if (wasRecentlyCopied) {
      58            1 :         InnerIcon = CheckIcon;
      59              :     } else {
      60            1 :         InnerIcon = props.icon ?? CopyIcon;
      61              :     }
      62              : 
      63            1 :     const ariaLabel = props['aria-label'];
      64            1 :     const ariaLabelledBy = props['aria-labelledby'];
      65            1 :     return (
      66            1 :         <IconButton
      67            1 :             className={props.className}
      68            1 :             variant={props.variant}
      69            1 :             size={props.size}
      70            1 :             onClick={props.disabled ? undefined : copy}
      71            1 :             aria-label={ariaLabel}
      72            1 :             aria-labelledby={ariaLabelledBy}
      73            1 :             data-testid={props['data-testid']}
      74            1 :             disabled={props.disabled}
      75              :         >
      76            1 :             <InnerIcon />
      77            1 :         </IconButton>
      78              :     );
      79            1 : }
        

Generated by: LCOV version 2.0-1