11"use client" ;
22
3- import React , { useState , useEffect , useCallback } from "react" ;
3+ import React , { useState , useEffect , useCallback , useRef } from "react" ;
44import { useRouter } from "next/navigation" ;
55import { useStreamEvents } from "@/hooks/useStreamEvents" ;
66import { formatAmount } from "@/utils/amount" ;
@@ -24,6 +24,7 @@ export const NotificationDropdown: React.FC<NotificationDropdownProps> = ({ publ
2424 const [ isOpen , setIsOpen ] = useState ( false ) ;
2525 const [ notifications , setNotifications ] = useState < NotificationItem [ ] > ( [ ] ) ;
2626 const [ unreadCount , setUnreadCount ] = useState ( 0 ) ;
27+ const containerRef = useRef < HTMLDivElement > ( null ) ;
2728
2829 const { events : streamEvents , connected } = useStreamEvents ( {
2930 userPublicKeys : [ publicKey ] ,
@@ -111,6 +112,53 @@ export const NotificationDropdown: React.FC<NotificationDropdownProps> = ({ publ
111112 } ) ;
112113 } , [ streamEvents , isOpen , formatEventMessage ] ) ;
113114
115+ useEffect ( ( ) => {
116+ const handleClickOutside = ( event : MouseEvent ) => {
117+ if ( containerRef . current && ! containerRef . current . contains ( event . target as Node ) ) {
118+ setIsOpen ( false ) ;
119+ }
120+ } ;
121+
122+ const handleKeyDown = ( event : KeyboardEvent ) => {
123+ if ( event . key === "Escape" ) {
124+ setIsOpen ( false ) ;
125+ return ;
126+ }
127+
128+ if ( event . key === "Tab" && containerRef . current && isOpen ) {
129+ const focusableElements = containerRef . current . querySelectorAll < HTMLElement > (
130+ 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
131+ ) ;
132+ if ( focusableElements . length === 0 ) return ;
133+
134+ const firstElement = focusableElements [ 0 ] ;
135+ const lastElement = focusableElements [ focusableElements . length - 1 ] ;
136+
137+ if ( event . shiftKey ) {
138+ if ( document . activeElement === firstElement ) {
139+ event . preventDefault ( ) ;
140+ lastElement ?. focus ( ) ;
141+ }
142+ } else {
143+ if ( document . activeElement === lastElement ) {
144+ event . preventDefault ( ) ;
145+ firstElement ?. focus ( ) ;
146+ }
147+ }
148+ }
149+ } ;
150+
151+ if ( isOpen ) {
152+ document . addEventListener ( "mousedown" , handleClickOutside ) ;
153+ document . addEventListener ( "keydown" , handleKeyDown ) ;
154+ }
155+
156+ return ( ) => {
157+ document . removeEventListener ( "mousedown" , handleClickOutside ) ;
158+ document . removeEventListener ( "keydown" , handleKeyDown ) ;
159+ } ;
160+ } , [ isOpen ] ) ;
161+
114162 const handleDropdownOpen = useCallback ( ( ) => {
115163 setIsOpen ( ( prev ) => ! prev ) ;
116164
@@ -121,7 +169,7 @@ export const NotificationDropdown: React.FC<NotificationDropdownProps> = ({ publ
121169 } , [ isOpen ] ) ;
122170
123171 return (
124- < div className = "relative" >
172+ < div className = "relative" ref = { containerRef } >
125173 < button
126174 onClick = { handleDropdownOpen }
127175 aria-label = { `Notifications${ unreadCount > 0 ? `, ${ unreadCount } unread` : "" } ` }
0 commit comments