All files / src/components/display EventHoverRow.tsx

45.16% Statements 14/31
58.33% Branches 7/12
50% Functions 5/10
45.16% Lines 14/31

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170                                                                                                                      533x 533x     533x 323x 266x           533x                               533x                                 533x 79x       454x 169x                     6x     6x                         285x                                                                
/**
 * Wrapper component for events in the EventStream that provides:
 * - Hover action row (Copy + Select) in normal mode
 * - Checkbox + full-row click target in selection mode
 *
 * Presentational component decoupled from useGrackle(). Performs clipboard
 * side effects via navigator.clipboard when the user clicks Copy.
 */
 
import {
  useState,
  useCallback,
  useEffect,
  useRef,
  type JSX,
  type ReactNode,
  type MouseEvent,
} from "react";
import { Clipboard, Check, CheckSquare } from "lucide-react";
import { ICON_SM } from "../../utils/iconSize.js";
import styles from "./EventHoverRow.module.scss";
 
/** Props for the EventHoverRow component. */
export interface EventHoverRowProps {
  /** Text to copy when the hover Copy button is clicked. */
  copyText: string;
  /** Whether this event has copyable content (shows hover actions). */
  isContentBearing: boolean;
  /** Whether multi-select mode is active. */
  isSelecting: boolean;
  /** Whether this event is currently selected in multi-select mode. */
  isSelected: boolean;
  /** Accessible label for the selection checkbox (e.g. "Select message from assistant at 2:34 PM"). */
  checkboxLabel?: string;
  /** Called when the Select button in the hover row is clicked (enters selection mode). */
  onSelect: () => void;
  /** Called when the row is clicked in selection mode. Receives the shiftKey state. */
  onToggle: (shiftKey: boolean) => void;
  /** Called after a successful single-event copy from the hover row. */
  onCopied?: () => void;
  /** The event content to wrap. */
  children: ReactNode;
}
 
/**
 * Wraps an event in the EventStream with hover actions (normal mode) or
 * selection affordances (multi-select mode).
 */
export function EventHoverRow({
  copyText,
  isContentBearing,
  isSelecting,
  isSelected,
  checkboxLabel,
  onSelect,
  onToggle,
  onCopied,
  children,
}: EventHoverRowProps): JSX.Element {
  const [copied, setCopied] = useState(false);
  const copiedTimerRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
 
  // Clear the "copied" feedback timer on unmount
  useEffect(() => {
    return () => {
      Iif (copiedTimerRef.current !== undefined) {
        clearTimeout(copiedTimerRef.current);
      }
    };
  }, []);
 
  const handleCopy = useCallback(async () => {
    try {
      await navigator.clipboard.writeText(copyText);
      setCopied(true);
      onCopied?.();
      Iif (copiedTimerRef.current !== undefined) {
        clearTimeout(copiedTimerRef.current);
      }
      copiedTimerRef.current = setTimeout(() => {
        setCopied(false);
      }, 2000);
    } catch {
      // Clipboard write failed silently
    }
  }, [copyText, onCopied]);
 
  const handleRowClick = useCallback(
    (e: MouseEvent<HTMLDivElement>) => {
      Iif (!isSelecting) {
        return;
      }
      // Don't intercept if the user clicked inside an interactive element
      const target = e.target as HTMLElement;
      Iif (target.closest("a, button, input, textarea, select, [role=button]")) {
        return;
      }
      e.preventDefault();
      onToggle(e.shiftKey);
    },
    [isSelecting, onToggle],
  );
 
  // Non-content-bearing events: render plain, no interactivity
  if (!isContentBearing) {
    return <div className={styles.row}>{children}</div>;
  }
 
  // Selection mode: checkbox + clickable row
  if (isSelecting) {
    return (
      <div
        className={`${styles.row} ${styles.selectingRow} ${isSelected ? styles.selected : ""}`}
        onClick={handleRowClick}
        data-testid="event-selectable-row"
      >
        <div className={styles.checkboxArea}>
          <input
            type="checkbox"
            checked={isSelected}
            onChange={(e) => {
              onToggle((e.nativeEvent as globalThis.MouseEvent).shiftKey);
            }}
            onClick={(e) => {
              e.stopPropagation();
            }}
            className={styles.checkbox}
            aria-label={checkboxLabel ?? "Select this event"}
            data-testid="event-select-checkbox"
          />
        </div>
        <div className={styles.contentArea}>{children}</div>
      </div>
    );
  }
 
  // Normal mode: hover action row
  return (
    <div className={styles.row} data-testid="event-hover-row">
      <div className={styles.hoverActions} data-testid="event-hover-actions">
        <button
          type="button"
          className={styles.hoverButton}
          onClick={() => {
            handleCopy().catch(() => {});
          }}
          aria-label="Copy event content"
          data-testid="event-hover-copy"
        >
          {copied ? (
            <Check size={ICON_SM} aria-hidden="true" />
          ) : (
            <Clipboard size={ICON_SM} aria-hidden="true" />
          )}
        </button>
        <button
          type="button"
          className={styles.hoverButton}
          onClick={onSelect}
          aria-label="Select this event"
          data-testid="event-hover-select"
        >
          <CheckSquare size={ICON_SM} aria-hidden="true" />
        </button>
      </div>
      {children}
    </div>
  );
}