1+ 'use client' ;
2+
3+ import React , { useState , useRef , useEffect } from 'react' ;
4+ import { createPortal } from 'react-dom' ;
5+
6+ const Popover = ( { children, content } : { children : React . ReactNode , content : React . ReactNode } ) => {
7+ const [ isVisible , setIsVisible ] = useState ( false ) ;
8+ const [ popoverStyle , setPopoverStyle ] = useState < React . CSSProperties > ( { } ) ;
9+ const popoverRef = useRef < HTMLDivElement > ( null ) ;
10+ const triggerRef = useRef < HTMLButtonElement > ( null ) ;
11+
12+ const toggleVisibility = ( ) => {
13+ setIsVisible ( v => {
14+ const next = ! v ;
15+ if ( next && triggerRef . current ) {
16+ const rect = triggerRef . current . getBoundingClientRect ( ) ;
17+ setPopoverStyle ( {
18+ position : 'absolute' ,
19+ top : rect . bottom + window . scrollY + 4 , // 4px gap
20+ left : rect . left + window . scrollX ,
21+ zIndex : 1000
22+ } ) ;
23+ }
24+ return next ;
25+ } ) ;
26+ } ;
27+
28+ // Focus management and Escape key
29+ useEffect ( ( ) => {
30+ if ( isVisible && popoverRef . current ) {
31+ popoverRef . current . focus ( ) ;
32+ }
33+ } , [ isVisible ] ) ;
34+
35+ useEffect ( ( ) => {
36+ if ( ! isVisible && triggerRef . current ) {
37+ triggerRef . current . focus ( ) ;
38+ }
39+ } , [ isVisible ] ) ;
40+
41+ useEffect ( ( ) => {
42+ if ( ! isVisible ) return ;
43+ const handleKeyDown = ( event : KeyboardEvent ) => {
44+ if ( event . key === 'Escape' ) {
45+ setIsVisible ( false ) ;
46+ }
47+ } ;
48+ document . addEventListener ( 'keydown' , handleKeyDown ) ;
49+ return ( ) => document . removeEventListener ( 'keydown' , handleKeyDown ) ;
50+ } , [ isVisible ] ) ;
51+
52+ useEffect ( ( ) => {
53+ const handleClickOutside = ( event : MouseEvent ) => {
54+ if (
55+ popoverRef . current &&
56+ ! popoverRef . current . contains ( event . target as Node ) &&
57+ triggerRef . current &&
58+ ! triggerRef . current . contains ( event . target as Node )
59+ ) {
60+ setIsVisible ( false ) ;
61+ }
62+ } ;
63+ document . addEventListener ( 'mousedown' , handleClickOutside ) ;
64+ return ( ) => document . removeEventListener ( 'mousedown' , handleClickOutside ) ;
65+ } , [ ] ) ;
66+
67+ return (
68+ < span className = "relative inline-block" >
69+ < button
70+ ref = { triggerRef }
71+ onClick = { toggleVisibility }
72+ className = "p-0 underline decoration-dotted text-inherit border-none background-none z-10"
73+ aria-haspopup = "true"
74+ aria-expanded = { isVisible }
75+ aria-controls = "popover-content"
76+ type = "button"
77+ >
78+ { children }
79+ </ button >
80+ { isVisible && typeof window !== 'undefined' && createPortal (
81+ < div
82+ id = "popover-content"
83+ ref = { popoverRef }
84+ className = { `absolute top-0 left-1/2 transform -translate-x-1/2 bg-fd-popover text-fd-popover-foreground border border-fd-border shadow-lg rounded-md p-4 z-10 whitespace-normal min-w-[250px] max-w-[320px] text-[13px]` }
85+ role = "dialog"
86+ aria-modal = "true"
87+ aria-label = "Popover dialog"
88+ tabIndex = { - 1 }
89+ style = { popoverStyle }
90+ dangerouslySetInnerHTML = { { __html : content as string } } // Use dangerouslySetInnerHTML to set HTML content
91+ /> ,
92+ document . body
93+ ) }
94+ </ span >
95+ ) ;
96+ } ;
97+
98+ export default Popover ;
0 commit comments