11import React , { useState , useRef , useEffect } from 'react' ;
2+ import ReactDOM from 'react-dom' ; // Import ReactDOM
23import { CaretDown , Check } from '@phosphor-icons/react' ;
34import { motion , AnimatePresence } from 'framer-motion' ;
45
56const CustomDropdown = ( { options, value, onChange, icon : Icon , label } ) => {
67 const [ isOpen , setIsOpen ] = useState ( false ) ;
7- const dropdownRef = useRef ( null ) ;
8+ const dropdownRef = useRef ( null ) ; // Ref for the button
9+ const menuRef = useRef ( null ) ; // Ref for the dropdown menu
10+ const [ dropdownMenuPosition , setDropdownMenuPosition ] = useState ( { } ) ;
811
912 useEffect ( ( ) => {
1013 const handleClickOutside = ( event ) => {
11- if ( dropdownRef . current && ! dropdownRef . current . contains ( event . target ) ) {
14+ const isClickInsideButton =
15+ dropdownRef . current && dropdownRef . current . contains ( event . target ) ;
16+ const isClickInsideMenu =
17+ menuRef . current && menuRef . current . contains ( event . target ) ;
18+
19+ if ( ! isClickInsideButton && ! isClickInsideMenu ) {
1220 setIsOpen ( false ) ;
1321 }
1422 } ;
@@ -19,17 +27,72 @@ const CustomDropdown = ({ options, value, onChange, icon: Icon, label }) => {
1927 } ;
2028 } , [ ] ) ;
2129
30+ useEffect ( ( ) => {
31+ if ( isOpen && dropdownRef . current ) {
32+ const rect = dropdownRef . current . getBoundingClientRect ( ) ;
33+ setDropdownMenuPosition ( {
34+ top : rect . bottom + window . scrollY + 8 , // 8px for mt-2
35+ left : rect . left + window . scrollX ,
36+ width : rect . width ,
37+ } ) ;
38+ }
39+ } , [ isOpen ] ) ;
40+
2241 const handleSelect = ( optionValue ) => {
2342 onChange ( optionValue ) ;
2443 setIsOpen ( false ) ;
2544 } ;
2645
2746 const selectedOption = options . find ( ( opt ) => opt . value === value ) ;
2847
48+ // Render the dropdown menu (options list) using a Portal
49+ const renderDropdownMenu = ( ) => {
50+ if ( ! isOpen ) return null ;
51+
52+ return ReactDOM . createPortal (
53+ < motion . div
54+ ref = { menuRef }
55+ initial = { { opacity : 0 , y : - 10 , scale : 0.95 } }
56+ animate = { { opacity : 1 , y : 0 , scale : 1 } }
57+ exit = { { opacity : 0 , y : - 10 , scale : 0.95 } }
58+ transition = { { duration : 0.1 } }
59+ className = "bg-gray-800 border border-gray-700 rounded-md shadow-lg z-50 origin-top-left max-h-80 overflow-y-auto" // Added max-h-80 and overflow-y-auto
60+ style = { {
61+ position : 'absolute' ,
62+ top : dropdownMenuPosition . top ,
63+ left : dropdownMenuPosition . left ,
64+ minWidth : dropdownMenuPosition . width , // Set minWidth to button width
65+ width : 'max-content' , // Allow content to determine width, but respect minWidth
66+ } }
67+ >
68+ < div className = "py-1" >
69+ { options . map ( ( option ) => (
70+ < button
71+ key = { option . value }
72+ onClick = { ( ) => handleSelect ( option . value ) }
73+ className = { `flex items-center justify-between w-full px-4 py-2 text-sm text-left transition-colors ${
74+ value === option . value
75+ ? 'bg-primary-500/10 text-primary-400'
76+ : 'text-gray-300 hover:bg-gray-700 hover:text-white'
77+ } `}
78+ >
79+ < span > { option . label } </ span >
80+ { value === option . value && (
81+ < Check size = { 16 } className = "text-primary-400" />
82+ ) }
83+ </ button >
84+ ) ) }
85+ </ div >
86+ </ motion . div > ,
87+ document . body ,
88+ ) ;
89+ } ;
90+
2991 return (
30- < div className = "relative inline-block text-left" ref = { dropdownRef } >
92+ < div className = "relative inline-block text-left" >
3193 < button
3294 type = "button"
95+ ref = { dropdownRef } // Attach ref to the button
3396 onClick = { ( ) => setIsOpen ( ! isOpen ) }
3497 className = "flex items-center gap-2 px-4 py-2 bg-gray-800 hover:bg-gray-700 border border-gray-700 rounded-md text-sm font-medium text-gray-200 transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-900 focus:ring-primary-500"
3598 >
@@ -41,36 +104,7 @@ const CustomDropdown = ({ options, value, onChange, icon: Icon, label }) => {
41104 />
42105 </ button >
43106
44- < AnimatePresence >
45- { isOpen && (
46- < motion . div
47- initial = { { opacity : 0 , y : - 10 , scale : 0.95 } }
48- animate = { { opacity : 1 , y : 0 , scale : 1 } }
49- exit = { { opacity : 0 , y : - 10 , scale : 0.95 } }
50- transition = { { duration : 0.1 } }
51- className = "absolute right-0 mt-2 w-48 bg-gray-800 border border-gray-700 rounded-md shadow-lg z-50 origin-top-right"
52- >
53- < div className = "py-1" >
54- { options . map ( ( option ) => (
55- < button
56- key = { option . value }
57- onClick = { ( ) => handleSelect ( option . value ) }
58- className = { `flex items-center justify-between w-full px-4 py-2 text-sm text-left transition-colors ${
59- value === option . value
60- ? 'bg-primary-500/10 text-primary-400'
61- : 'text-gray-300 hover:bg-gray-700 hover:text-white'
62- } `}
63- >
64- < span > { option . label } </ span >
65- { value === option . value && (
66- < Check size = { 16 } className = "text-primary-400" />
67- ) }
68- </ button >
69- ) ) }
70- </ div >
71- </ motion . div >
72- ) }
73- </ AnimatePresence >
107+ { renderDropdownMenu ( ) }
74108 </ div >
75109 ) ;
76110} ;
0 commit comments