@@ -12,7 +12,11 @@ import {
1212} from '@/features/rooms/livekit/components/ReactionPortal'
1313import { getEmojiLabel } from '@/features/rooms/livekit/utils/reactionUtils'
1414import { useRegisterKeyboardShortcut } from '@/features/shortcuts/useRegisterKeyboardShortcut'
15- import { Popover as RACPopover , Toolbar } from 'react-aria-components'
15+ import {
16+ Popover as RACPopover ,
17+ Dialog ,
18+ DialogTrigger ,
19+ } from 'react-aria-components'
1620import { FocusScope } from '@react-aria/focus'
1721import { Participant } from 'livekit-client'
1822import useRateLimiter from '@/hooks/useRateLimiter'
@@ -40,21 +44,12 @@ export const ReactionsToggle = () => {
4044 const [ reactions , setReactions ] = useState < Reaction [ ] > ( [ ] )
4145 const instanceIdRef = useRef ( 0 )
4246 const room = useRoomContext ( )
43- const triggerRef = useRef < HTMLButtonElement > ( null )
4447
4548 const [ isOpen , setIsOpen ] = useState ( false )
4649
4750 useRegisterKeyboardShortcut ( {
4851 id : 'reaction' ,
49- handler : ( ) => {
50- if ( isOpen ) {
51- document
52- . querySelector < HTMLElement > ( '[role="toolbar"] button' )
53- ?. focus ( )
54- } else {
55- setIsOpen ( true )
56- }
57- } ,
52+ handler : ( ) => setIsOpen ( ( prev ) => ! prev ) ,
5853 } )
5954
6055 const sendReaction = async ( emoji : string ) => {
@@ -83,37 +78,6 @@ export const ReactionsToggle = () => {
8378 } , ANIMATION_DURATION )
8479 }
8580
86- const handleToolbarKeyDown = ( e : React . KeyboardEvent ) => {
87- if ( e . key === 'Tab' ) {
88- e . preventDefault ( )
89- document . getElementById ( 'reaction-toggle' ) ?. focus ( )
90- return
91- }
92-
93- const buttons = Array . from (
94- ( e . currentTarget as HTMLElement ) . querySelectorAll < HTMLElement > (
95- '[role="toolbar"] button'
96- )
97- )
98- if ( buttons . length === 0 ) return
99-
100- if (
101- e . key === 'ArrowRight' &&
102- document . activeElement === buttons [ buttons . length - 1 ]
103- ) {
104- e . preventDefault ( )
105- e . stopPropagation ( )
106- buttons [ 0 ] . focus ( )
107- } else if (
108- e . key === 'ArrowLeft' &&
109- document . activeElement === buttons [ 0 ]
110- ) {
111- e . preventDefault ( )
112- e . stopPropagation ( )
113- buttons [ buttons . length - 1 ] . focus ( )
114- }
115- }
116-
11781 const debouncedSendReaction = useRateLimiter ( {
11882 callback : sendReaction ,
11983 maxCalls : 10 ,
@@ -123,76 +87,73 @@ export const ReactionsToggle = () => {
12387 return (
12488 < >
12589 < div className = { css ( { position : 'relative' } ) } >
126- < ToggleButton
127- ref = { triggerRef }
128- id = "reaction-toggle"
129- square
130- variant = "primaryDark"
131- aria-label = { t ( 'button' ) }
132- tooltip = { t ( 'button' ) }
133- isSelected = { isOpen }
134- onChange = { setIsOpen }
135- >
136- < RiEmotionLine />
137- </ ToggleButton >
138- < RACPopover
139- triggerRef = { triggerRef }
140- isOpen = { isOpen }
141- onOpenChange = { setIsOpen }
142- placement = "top"
143- offset = { 8 }
144- isNonModal
145- shouldCloseOnInteractOutside = { ( ) => false }
146- className = { css ( {
147- borderRadius : '8px' ,
148- padding : '0.35rem' ,
149- backgroundColor : 'primaryDark.50' ,
150- '&[data-entering]' : {
151- animation : 'fade 200ms ease' ,
152- } ,
153- '&[data-exiting]' : {
154- animation : 'fade 200ms ease-in reverse' ,
155- } ,
156- } ) }
157- >
158- { /* eslint-disable-next-line jsx-a11y/no-autofocus -- FocusScope autoFocus is programmatic focus for overlays, not the HTML autofocus attribute */ }
159- < FocusScope autoFocus restoreFocus >
160- { /* eslint-disable-next-line jsx-a11y/no-static-element-interactions -- handles Tab exit and arrow wrapping for the portal-rendered toolbar */ }
161- < div onKeyDownCapture = { handleToolbarKeyDown } >
162- < Toolbar
163- orientation = "horizontal"
164- aria-label = { t ( 'button' ) }
165- className = { css ( {
166- display : 'flex' ,
167- gap : '0.5rem' ,
168- } ) }
169- >
170- { Object . values ( Emoji ) . map ( ( emoji , index ) => (
171- < Button
172- key = { index }
173- onPress = { ( ) => debouncedSendReaction ( emoji ) }
174- aria-label = { t ( 'send' , { emoji : getEmojiLabel ( emoji , t ) } ) }
175- variant = "primaryTextDark"
176- size = "sm"
177- square
178- data-attr = { `send-reaction-${ emoji } ` }
179- >
180- < img
181- src = { `/assets/reactions/${ emoji } .pPng` }
182- alt = ""
183- className = { css ( {
184- width : '28px' ,
185- height : '28px' ,
186- pointerEvents : 'none' ,
187- userSelect : 'none' ,
188- } ) }
189- />
190- </ Button >
191- ) ) }
192- </ Toolbar >
193- </ div >
194- </ FocusScope >
195- </ RACPopover >
90+ < DialogTrigger isOpen = { isOpen } onOpenChange = { setIsOpen } >
91+ < ToggleButton
92+ square
93+ variant = "primaryDark"
94+ aria-label = { t ( 'button' ) }
95+ tooltip = { t ( 'button' ) }
96+ isSelected = { isOpen }
97+ onChange = { setIsOpen }
98+ >
99+ < RiEmotionLine />
100+ </ ToggleButton >
101+ < RACPopover
102+ placement = "top"
103+ offset = { 8 }
104+ isNonModal
105+ shouldCloseOnInteractOutside = { ( ) => false }
106+ className = { css ( {
107+ borderRadius : '8px' ,
108+ padding : '0.35rem' ,
109+ backgroundColor : 'primaryDark.50' ,
110+ '&[data-entering]' : {
111+ animation : 'fade 200ms ease' ,
112+ } ,
113+ '&[data-exiting]' : {
114+ animation : 'fade 200ms ease-in reverse' ,
115+ } ,
116+ } ) }
117+ >
118+ < Dialog className = { css ( { outline : 'none' } ) } >
119+ { /* eslint-disable-next-line jsx-a11y/no-autofocus -- FocusScope autoFocus is programmatic focus for overlays, not the HTML autofocus attribute */ }
120+ < FocusScope contain autoFocus restoreFocus >
121+ < div
122+ role = "toolbar"
123+ aria-orientation = "horizontal"
124+ aria-label = { t ( 'button' ) }
125+ className = { css ( {
126+ display : 'flex' ,
127+ gap : '0.5rem' ,
128+ } ) }
129+ >
130+ { Object . values ( Emoji ) . map ( ( emoji , index ) => (
131+ < Button
132+ key = { index }
133+ onPress = { ( ) => debouncedSendReaction ( emoji ) }
134+ aria-label = { t ( 'send' , { emoji : getEmojiLabel ( emoji , t ) } ) }
135+ variant = "primaryTextDark"
136+ size = "sm"
137+ square
138+ data-attr = { `send-reaction-${ emoji } ` }
139+ >
140+ < img
141+ src = { `/assets/reactions/${ emoji } .png` }
142+ alt = ""
143+ className = { css ( {
144+ width : '28px' ,
145+ height : '28px' ,
146+ pointerEvents : 'none' ,
147+ userSelect : 'none' ,
148+ } ) }
149+ />
150+ </ Button >
151+ ) ) }
152+ </ div >
153+ </ FocusScope >
154+ </ Dialog >
155+ </ RACPopover >
156+ </ DialogTrigger >
196157 </ div >
197158 < ReactionPortals reactions = { reactions } />
198159 </ >
0 commit comments