1- import React , { ReactElement , useEffect , useRef , useState } from 'react' ;
2- import { Dialog } from '@headlessui/react' ;
1+ import React , { ReactElement , useEffect , useRef , useState , Fragment } from 'react' ;
2+ import { Dialog , Transition } from '@headlessui/react' ;
3+ import { XMarkIcon } from '@heroicons/react/24/outline' ;
34import Button from './tavern-base-ui/button/Button' ;
45
56type Position = {
@@ -14,36 +15,49 @@ export const ButtonDialogPopover = ({ children, label, leftIcon }: {
1415} ) => {
1516 const [ isOpen , setIsOpen ] = useState ( false )
1617 const [ position , setPosition ] = useState < Position > ( { top : 0 , left : 0 } )
18+ const [ isMobile , setIsMobile ] = useState ( false )
1719
1820 const buttonRef = useRef < HTMLButtonElement | null > ( null ) ;
1921 const panelRef = useRef < HTMLDivElement | null > ( null ) ;
2022
23+ useEffect ( ( ) => {
24+ const checkMobile = ( ) => {
25+ setIsMobile ( window . innerWidth < 768 ) // md breakpoint
26+ }
27+
28+ checkMobile ( )
29+ window . addEventListener ( 'resize' , checkMobile )
30+ return ( ) => window . removeEventListener ( 'resize' , checkMobile )
31+ } , [ ] )
32+
2133 const openDialog = ( ) : void => {
22- const dialogWidth = 384 ;
23- const spacing = 8 ;
24-
25- const button = buttonRef . current
26- if ( button ) {
27- const rect = button . getBoundingClientRect ( )
28- const viewportWidth = window . innerWidth
29-
30- const calculatedLeft = Math . min (
31- rect . left + window . scrollX ,
32- viewportWidth - dialogWidth - 16 // 16px right margin
33- )
34-
35- setPosition ( {
36- top : rect . bottom + window . scrollY + spacing ,
37- left : calculatedLeft ,
38- } )
34+ if ( ! isMobile ) {
35+ const dialogWidth = 384 ;
36+ const spacing = 8 ;
37+
38+ const button = buttonRef . current
39+ if ( button ) {
40+ const rect = button . getBoundingClientRect ( )
41+ const viewportWidth = window . innerWidth
42+
43+ const calculatedLeft = Math . min (
44+ rect . left ,
45+ viewportWidth - dialogWidth - 16
46+ )
47+
48+ setPosition ( {
49+ top : rect . bottom + spacing ,
50+ left : calculatedLeft ,
51+ } )
52+ }
3953 }
4054 setIsOpen ( true )
4155 }
4256
4357 const closeDialog = ( ) : void => setIsOpen ( false )
4458
4559 useEffect ( ( ) => {
46- if ( ! isOpen ) return
60+ if ( ! isOpen || isMobile ) return
4761
4862 const handleClickOutside = ( event : MouseEvent ) : void => {
4963 const target = event . target as Node
@@ -59,26 +73,95 @@ export const ButtonDialogPopover = ({ children, label, leftIcon }: {
5973
6074 document . addEventListener ( 'mousedown' , handleClickOutside )
6175 return ( ) => document . removeEventListener ( 'mousedown' , handleClickOutside )
62- } , [ isOpen ] )
76+ } , [ isOpen , isMobile ] )
6377
6478 return (
6579 < div className = 'flex justify-end' >
66- < Button ref = { buttonRef } leftIcon = { leftIcon } buttonVariant = 'ghost' buttonStyle = { { color : "gray" , size : "md" } } onClick = { openDialog } > { label } </ Button >
67- < Dialog open = { isOpen } onClose = { closeDialog } >
68- < div >
69- < div className = "fixed inset-0 bg-black/30 z-40" aria-hidden = "true" />
70- < div
71- ref = { panelRef }
72- className = "absolute z-50 bg-white border rounded-lg shadow-lg w-96 p-4 flex flex-col gap-4"
73- style = { {
74- top : position . top ,
75- left : position . left ,
76- } }
80+ < Button
81+ ref = { buttonRef }
82+ leftIcon = { leftIcon }
83+ buttonVariant = 'ghost'
84+ buttonStyle = { { color : "gray" , size : "md" } }
85+ onClick = { openDialog }
86+ >
87+ { label }
88+ </ Button >
89+
90+ < Transition . Root show = { isOpen } as = { Fragment } >
91+ < Dialog as = "div" className = "relative z-50" onClose = { closeDialog } >
92+ { /* Backdrop */ }
93+ < Transition . Child
94+ as = { Fragment }
95+ enter = "ease-out duration-300"
96+ enterFrom = "opacity-0"
97+ enterTo = "opacity-100"
98+ leave = "ease-in duration-200"
99+ leaveFrom = "opacity-100"
100+ leaveTo = "opacity-0"
77101 >
78- { children }
79- </ div >
80- </ div >
81- </ Dialog >
102+ < div className = "fixed inset-0 bg-black/30 z-40" aria-hidden = "true" />
103+ </ Transition . Child >
104+
105+ { isMobile ? (
106+ /* Mobile: Full-screen modal */
107+ < div className = "fixed inset-0 z-50 overflow-y-auto" >
108+ < Transition . Child
109+ as = { Fragment }
110+ enter = "ease-out duration-300"
111+ enterFrom = "opacity-0 scale-95"
112+ enterTo = "opacity-100 scale-100"
113+ leave = "ease-in duration-200"
114+ leaveFrom = "opacity-100 scale-100"
115+ leaveTo = "opacity-0 scale-95"
116+ >
117+ < Dialog . Panel className = "min-h-full bg-white" >
118+ { /* Header with close button */ }
119+ < div className = "sticky top-0 z-10 bg-white border-b border-gray-200 px-4 py-3 flex items-center justify-between" >
120+ < Dialog . Title className = "text-lg font-medium text-gray-900" >
121+ { label }
122+ </ Dialog . Title >
123+ < button
124+ onClick = { closeDialog }
125+ className = "text-gray-400 hover:text-gray-500"
126+ >
127+ < XMarkIcon className = "h-6 w-6" />
128+ </ button >
129+ </ div >
130+
131+ { /* Content */ }
132+ < div className = "px-4 py-4" >
133+ { children }
134+ </ div >
135+ </ Dialog . Panel >
136+ </ Transition . Child >
137+ </ div >
138+ ) : (
139+ /* Desktop: Positioned popover */
140+ < div className = "fixed inset-0 z-50 overflow-y-auto" >
141+ < Transition . Child
142+ as = { Fragment }
143+ enter = "ease-out duration-200"
144+ enterFrom = "opacity-0 scale-95"
145+ enterTo = "opacity-100 scale-100"
146+ leave = "ease-in duration-150"
147+ leaveFrom = "opacity-100 scale-100"
148+ leaveTo = "opacity-0 scale-95"
149+ >
150+ < div
151+ ref = { panelRef }
152+ className = "fixed bg-white border rounded-lg shadow-lg w-96 p-4 flex flex-col gap-4"
153+ style = { {
154+ top : position . top ,
155+ left : position . left ,
156+ } }
157+ >
158+ { children }
159+ </ div >
160+ </ Transition . Child >
161+ </ div >
162+ ) }
163+ </ Dialog >
164+ </ Transition . Root >
82165 </ div >
83166 ) ;
84167}
0 commit comments