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 | /** * 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 () => { if (copiedTimerRef.current !== undefined) { clearTimeout(copiedTimerRef.current); } }; }, []); const handleCopy = useCallback(async () => { try { await navigator.clipboard.writeText(copyText); setCopied(true); onCopied?.(); if (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>) => { if (!isSelecting) { return; } // Don't intercept if the user clicked inside an interactive element const target = e.target as HTMLElement; if (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> ); } |