Skip to content

Commit 272722d

Browse files
committed
feat: add useWindowClickDismiss hook and integrate it into InstructionsAnswerCard for tooltip management
1 parent 94eaefa commit 272722d

File tree

3 files changed

+54
-3
lines changed

3 files changed

+54
-3
lines changed

agents.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,4 @@
4646
- If adding new output formats, update both the data definition in `data/files.json` and any generation logic (currently beyond this repo).
4747
- When page-level components need reusable calculations or datasets, extract those helpers into `lib/utils.ts` and import them instead of defining inline.
4848
- For complex UI blocks reused across the wizard, lift them into dedicated components (e.g., `components/instructions-answer-card.tsx`) and consume them in the wizard instead of duplicating markup.
49+
- When you identify reusable effect logic, wrap it in a custom hook under `hooks/` (e.g., `use-window-click-dismiss.ts`) and consume that hook rather than repeating `useEffect` blocks.

components/instructions-answer-card.tsx

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
import { cn } from "@/lib/utils"
44
import { Check, ExternalLink, Info } from "lucide-react"
5-
import type { ComponentPropsWithoutRef } from "react"
5+
import { useCallback, useState, type ComponentPropsWithoutRef, type MouseEvent } from "react"
6+
import { useWindowClickDismiss } from "@/hooks/use-window-click-dismiss"
67

78
export type InstructionsAnswerCardProps = {
89
label: string
@@ -32,6 +33,19 @@ export function InstructionsAnswerCard({
3233
onClick,
3334
...buttonProps
3435
}: InstructionsAnswerCardProps) {
36+
const [isTooltipOpen, setIsTooltipOpen] = useState(false)
37+
38+
const closeTooltip = useCallback(() => {
39+
setIsTooltipOpen(false)
40+
}, [])
41+
42+
useWindowClickDismiss(isTooltipOpen, closeTooltip)
43+
44+
const toggleTooltip = (event: MouseEvent<HTMLSpanElement>) => {
45+
event.stopPropagation()
46+
setIsTooltipOpen((prev) => !prev)
47+
}
48+
3549
return (
3650
<button
3751
type="button"
@@ -49,9 +63,24 @@ export function InstructionsAnswerCard({
4963
<div className="flex items-center gap-2">
5064
<span className="text-base font-medium text-foreground">{label}</span>
5165
{hasTooltipContent ? (
52-
<span className="relative flex items-center group/icon">
66+
<span
67+
className={cn(
68+
"relative flex items-center group/icon",
69+
isTooltipOpen && "pointer-events-auto"
70+
)}
71+
onClick={toggleTooltip}
72+
>
5373
<Info className="h-4 w-4 cursor-pointer text-muted-foreground transition-colors group-hover/icon:text-primary" />
54-
<div className="pointer-events-none absolute left-0 top-full z-20 hidden w-60 rounded-xl border border-border/70 bg-popover p-3 text-xs leading-relaxed text-popover-foreground shadow-xl transition-all duration-150 ease-out group-hover/icon:flex group-hover/icon:flex-col group-hover/icon:pointer-events-auto group-hover/icon:opacity-100 group-hover/icon:translate-y-0 opacity-0 translate-y-2">
74+
<div
75+
className={cn(
76+
"pointer-events-none absolute left-0 top-full z-20 hidden w-60 rounded-xl border border-border/70 bg-popover p-3 text-xs leading-relaxed text-popover-foreground shadow-xl transition-all duration-150 ease-out md:left-1/2 md:-translate-x-1/2",
77+
(isTooltipOpen || undefined) &&
78+
"pointer-events-auto !flex !flex-col opacity-100 translate-y-0",
79+
!isTooltipOpen && "opacity-0 translate-y-2",
80+
"group-hover/icon:flex group-hover/icon:flex-col group-hover/icon:pointer-events-auto group-hover/icon:opacity-100 group-hover/icon:translate-y-0"
81+
)}
82+
onClick={(event) => event.stopPropagation()}
83+
>
5584
{infoLines?.map((line) => (
5685
<span key={line} className="text-foreground">
5786
{line}

hooks/use-window-click-dismiss.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
"use client"
2+
3+
import { useEffect } from "react"
4+
5+
export function useWindowClickDismiss(isActive: boolean, onDismiss: () => void) {
6+
useEffect(() => {
7+
if (!isActive) {
8+
return
9+
}
10+
11+
const handleClick = () => {
12+
onDismiss()
13+
}
14+
15+
window.addEventListener("click", handleClick)
16+
17+
return () => {
18+
window.removeEventListener("click", handleClick)
19+
}
20+
}, [isActive, onDismiss])
21+
}

0 commit comments

Comments
 (0)