1- import { useMemo , useState } from "react" ;
1+ import { useMemo , useState , useRef , useEffect } from "react" ;
22
33import { useLocale } from "@calcom/lib/hooks/useLocale" ;
44import useMediaQuery from "@calcom/lib/hooks/useMediaQuery" ;
@@ -20,6 +20,28 @@ export const AddVariablesDropdown = (props: IAddVariablesDropdown) => {
2020 const { t } = useLocale ( ) ;
2121 const [ query , setQuery ] = useState ( "" ) ;
2222 const isMobile = useMediaQuery ( "(max-width: 640px)" ) ;
23+ const [ isOpen , setisOpen ] = useState ( false ) ;
24+ const [ selectedIndex , setSelectedIndex ] = useState < number > ( - 1 ) ;
25+ const itemRefs = useRef < ( HTMLButtonElement | null ) [ ] > ( [ ] ) ;
26+ const dropdownContainerRef = useRef < HTMLDivElement > ( null ) ;
27+
28+ useEffect ( ( ) => {
29+ if ( selectedIndex >= 0 && dropdownContainerRef . current && itemRefs . current [ selectedIndex ] ) {
30+ const container = dropdownContainerRef . current ;
31+ const selectedItem = itemRefs . current [ selectedIndex ] ;
32+
33+ if ( selectedItem ) {
34+ const containerRect = container . getBoundingClientRect ( ) ;
35+ const itemRect = selectedItem . getBoundingClientRect ( ) ;
36+
37+ if ( itemRect . bottom > containerRect . bottom ) {
38+ container . scrollTop += itemRect . bottom - containerRect . bottom ;
39+ } else if ( itemRect . top < containerRect . top ) {
40+ container . scrollTop -= containerRect . top - itemRect . top ;
41+ }
42+ }
43+ }
44+ } , [ selectedIndex ] ) ;
2345
2446 const filteredVariables = useMemo ( ( ) => {
2547 const q = query . trim ( ) . toLowerCase ( ) ;
@@ -32,12 +54,55 @@ export const AddVariablesDropdown = (props: IAddVariablesDropdown) => {
3254 } ) ;
3355 } , [ props . variables , query , t ] ) ;
3456
57+ const handleOnOpen = ( open : boolean ) => {
58+ setisOpen ( open ) ;
59+ if ( ! open ) setQuery ( "" ) ;
60+ setSelectedIndex ( open && filteredVariables . length > 0 ? 0 : - 1 ) ;
61+ } ;
62+
63+ const handleKeyDown = ( e : React . KeyboardEvent ) => {
64+ if ( filteredVariables . length === 0 || ! isOpen ) return ;
65+
66+ switch ( e . key ) {
67+ case "Enter" :
68+ e . preventDefault ( ) ;
69+ if ( selectedIndex >= 0 && selectedIndex < filteredVariables . length ) {
70+ props . addVariable ( t ( `${ filteredVariables [ selectedIndex ] } _variable` ) ) ;
71+ }
72+ setisOpen ( false ) ;
73+ setQuery ( "" ) ;
74+ setSelectedIndex ( - 1 ) ;
75+ break ;
76+ case "ArrowUp" :
77+ e . preventDefault ( ) ;
78+ setSelectedIndex ( ( prev ) => {
79+ if ( filteredVariables . length === 0 ) return - 1 ;
80+ if ( prev <= 0 || prev === - 1 ) return filteredVariables . length - 1 ;
81+ return prev - 1 ;
82+ } ) ;
83+ break ;
84+ case "ArrowDown" :
85+ e . preventDefault ( ) ;
86+ setSelectedIndex ( ( prev ) => {
87+ if ( filteredVariables . length === 0 ) return - 1 ;
88+ if ( prev >= filteredVariables . length - 1 ) return 0 ;
89+ return prev + 1 ;
90+ } ) ;
91+ break ;
92+ case "Escape" :
93+ e . preventDefault ( ) ;
94+ setisOpen ( false ) ;
95+ setQuery ( "" ) ;
96+ setSelectedIndex ( - 1 ) ;
97+ break ;
98+ }
99+ } ;
100+
35101 return (
36- < Dropdown
37- onOpenChange = { ( open ) => {
38- if ( ! open ) setQuery ( "" ) ;
39- } } >
40- < DropdownMenuTrigger aria-label = "Add variable" className = "focus:bg-cal-muted pt-[6px]" >
102+ < Dropdown onOpenChange = { handleOnOpen } open = { isOpen } >
103+ < DropdownMenuTrigger
104+ aria-label = "Add variable"
105+ className = "focus:bg-cal-muted pt-[6px] focus:outline-none" >
41106 < div className = "items-center" >
42107 { props . isTextEditor ? (
43108 < >
@@ -64,7 +129,7 @@ export const AddVariablesDropdown = (props: IAddVariablesDropdown) => {
64129 ) }
65130 </ div >
66131 </ DropdownMenuTrigger >
67- < DropdownMenuContent className = "w-52" >
132+ < DropdownMenuContent className = "w-52" onKeyDown = { handleKeyDown } >
68133 < div className = "stack-y-2 p-1" >
69134 < div className = "text-muted ml-1 text-left text-xs font-medium tracking-wide" >
70135 { t ( "add_dynamic_variables" ) }
@@ -80,15 +145,21 @@ export const AddVariablesDropdown = (props: IAddVariablesDropdown) => {
80145 className = "border-subtle bg-default focus:ring-brand-800 w-full rounded-md border px-3 py-2 text-sm outline-none focus:ring-1"
81146 />
82147 </ div >
83- < div className = "max-h-64 overflow-y-auto overflow-x-hidden md:max-h-80" >
148+ < div className = "max-h-64 overflow-y-auto overflow-x-hidden md:max-h-80" ref = { dropdownContainerRef } >
84149 { filteredVariables . length === 0 ? (
85150 < div className = "text-subtle px-4 py-2 text-center text-sm" > { t ( "no_variables_found" ) } </ div >
86151 ) : (
87- filteredVariables . map ( ( variable ) => (
88- < DropdownMenuItem key = { variable } className = "w-full p-1 hover:ring-0" >
89- < div
152+ filteredVariables . map ( ( variable , index ) => (
153+ < DropdownMenuItem key = { variable } className = "w-full p-1 hover:ring-0 focus:outline-none" >
154+ < button
155+ ref = { ( el ) => ( itemRefs . current [ index ] = el ) }
90156 key = { variable }
91- className = "w-full cursor-pointer rounded-md text-left transition-colors"
157+ type = "button"
158+ className = { `hover:bg-muted w-full rounded-md px-3 py-2 text-left transition-colors focus:outline-none ${
159+ selectedIndex === index ? "bg-muted" : ""
160+ } `}
161+ onMouseEnter = { ( ) => setSelectedIndex ( index ) }
162+ data-active = { selectedIndex === index }
92163 onClick = { ( ) => {
93164 props . addVariable ( t ( `${ variable } _variable` ) ) ;
94165 setQuery ( "" ) ;
@@ -103,7 +174,7 @@ export const AddVariablesDropdown = (props: IAddVariablesDropdown) => {
103174 </ div >
104175 < div className = "text-muted hidden text-xs sm:block" > { t ( `${ variable } _info` ) } </ div >
105176 </ div >
106- </ div >
177+ </ button >
107178 </ DropdownMenuItem >
108179 ) )
109180 ) }
0 commit comments