@@ -6,6 +6,7 @@ import { HeaderActionBarItemButton, HeaderActionBarItemButtonProps } from './Hea
66import { useHeaderContext } from '../../HeaderContext' ;
77import classNames from '../../../../utils/classNames' ;
88import { AllElementPropsWithRef } from '../../../../utils/elementTypings' ;
9+ import { addDocumentFocusPrevention } from '../../utils/focusPrevention' ;
910
1011export type HeaderActionBarItemProps = React . PropsWithChildren <
1112 AllElementPropsWithRef < 'div' > & {
@@ -87,6 +88,8 @@ export const HeaderActionBarItem = (properties: HeaderActionBarItemProps) => {
8788 } = properties ;
8889 const dropdownContentElementRef = useRef < HTMLDivElement > ( null ) ;
8990 const containerElementRef = useRef < HTMLDivElement > ( null ) ;
91+ const backdropRef = useRef < HTMLDivElement > ( null ) ;
92+ const originalTabIndexes = useRef < Map < Element , string | null > > ( new Map ( ) ) ;
9093 const [ hasContent , setHasContent ] = useState ( false ) ;
9194 const { isSmallScreen } = useHeaderContext ( ) ;
9295 const [ visible , setDisplayProperty ] = useState ( false ) ;
@@ -101,25 +104,68 @@ export const HeaderActionBarItem = (properties: HeaderActionBarItemProps) => {
101104 if ( ! visible ) return ;
102105 const container = getContainer ( ) ;
103106 const eventTargetNode = event . target ;
104- if ( ! container . contains ( eventTargetNode ) ) {
105- setDisplayProperty ( false ) ;
107+
108+ // Check if click is inside the container
109+ if ( container . contains ( eventTargetNode ) ) {
110+ return ;
111+ }
112+
113+ // For Menu item, also check if click is inside the controlled element (mobile menu)
114+ if ( id === 'Menu' ) {
115+ const controlledElement = document . getElementById ( 'hds-mobile-menu' ) ;
116+ if ( controlledElement && controlledElement . contains ( eventTargetNode ) ) {
117+ return ;
118+ }
106119 }
120+
121+ // Click is outside - close the dropdown and prevent this click from doing anything else
122+ event . preventDefault ( ) ;
123+ event . stopPropagation ( ) ;
124+ event . stopImmediatePropagation ( ) ;
125+ setDisplayProperty ( false ) ;
107126 } ;
108127
109128 const handleBlur = ( event ) => {
110129 if ( ! visible ) return ;
111130 const container = getContainer ( ) ;
131+ const backdrop = backdropRef . current ;
112132 const eventTargetNode = event . relatedTarget ;
113133 // close the dropdown if the focus is outside the container on large screens
114- if ( ! container . contains ( eventTargetNode ) && ! isSmallScreen ) {
134+ // but not if focus moved to the backdrop
135+ if ( ! container . contains ( eventTargetNode ) && eventTargetNode !== backdrop && ! isSmallScreen ) {
136+ setDisplayProperty ( false ) ;
137+ }
138+ } ;
139+
140+ const handleKeyDown = ( event ) => {
141+ if ( event . key === 'Escape' && visible ) {
142+ event . preventDefault ( ) ;
115143 setDisplayProperty ( false ) ;
116144 }
117145 } ;
118146
119147 useEffect ( ( ) => {
120- document . addEventListener ( 'click' , handleDocumentClick ) ;
121- return ( ) => document . removeEventListener ( 'click' , handleDocumentClick ) ;
122- } , [ containerElementRef . current ] ) ;
148+ if ( visible ) {
149+ // Only add the listener when dropdown is visible
150+ document . addEventListener ( 'click' , handleDocumentClick , true ) ;
151+ document . addEventListener ( 'keydown' , handleKeyDown ) ;
152+
153+ // Use the shared utility function to prevent focus and pointer events on elements outside the header
154+ const header = containerElementRef . current ?. closest ( 'header' ) as HTMLElement ;
155+ const cleanupFocusPrevention = header ? addDocumentFocusPrevention ( header , originalTabIndexes , true ) : null ;
156+
157+ return ( ) => {
158+ document . removeEventListener ( 'click' , handleDocumentClick , true ) ;
159+ document . removeEventListener ( 'keydown' , handleKeyDown ) ;
160+
161+ // Cleanup focus prevention
162+ if ( cleanupFocusPrevention ) {
163+ cleanupFocusPrevention ( ) ;
164+ }
165+ } ;
166+ }
167+ return undefined ;
168+ } , [ visible ] ) ;
123169
124170 // Hide the component if there is no content
125171 useEffect ( ( ) => {
@@ -162,33 +208,51 @@ export const HeaderActionBarItem = (properties: HeaderActionBarItemProps) => {
162208
163209 /* eslint-disable jsx-a11y/no-noninteractive-tabindex */
164210 return (
165- < div { ...rest } id = { id } className = { className } ref = { containerElementRef } onBlur = { handleBlur } >
166- < HeaderActionBarItemButton
167- className = { iconClassName }
168- onClick = { handleButtonClick }
169- label = { iconLabel }
170- icon = { iconClass }
171- aria-expanded = { visible }
172- aria-label = { ariaLabel !== undefined ? ariaLabel : String ( label ) }
173- aria-controls = { id === 'Menu' ? `hds-mobile-menu` : `${ id } -dropdown` }
174- labelOnRight = { labelOnRight }
175- fixedRightPosition = { fixedRightPosition }
176- isActive = { visible }
177- fullWidth = { fullWidth }
178- hasSubItems = { hasSubItems }
179- avatar = { avatar }
180- preventButtonResize = { preventButtonResize }
181- { ...buttonOverlayProps }
182- />
183- { hasSubItems && (
184- < div className = { classes . dropdownWrapper } >
185- < div id = { `${ id } -dropdown` } className = { dropdownClassName } ref = { dropdownContentElementRef } >
186- { heading && < h3 > { label } </ h3 > }
187- < ul > { children } </ ul >
188- </ div >
189- </ div >
211+ < >
212+ { visible && hasSubItems && (
213+ < div
214+ ref = { backdropRef }
215+ className = { classes . backdrop }
216+ aria-hidden = "true"
217+ style = { {
218+ position : 'fixed' ,
219+ top : 0 ,
220+ left : 0 ,
221+ right : 0 ,
222+ bottom : 0 ,
223+ zIndex : 99 ,
224+ pointerEvents : 'none' ,
225+ } }
226+ />
190227 ) }
191- </ div >
228+ < div { ...rest } id = { id } className = { className } ref = { containerElementRef } onBlur = { handleBlur } >
229+ < HeaderActionBarItemButton
230+ className = { iconClassName }
231+ onClick = { handleButtonClick }
232+ label = { iconLabel }
233+ icon = { iconClass }
234+ aria-expanded = { visible }
235+ aria-label = { ariaLabel !== undefined ? ariaLabel : String ( label ) }
236+ aria-controls = { id === 'Menu' ? `hds-mobile-menu` : `${ id } -dropdown` }
237+ labelOnRight = { labelOnRight }
238+ fixedRightPosition = { fixedRightPosition }
239+ isActive = { visible }
240+ fullWidth = { fullWidth }
241+ hasSubItems = { hasSubItems }
242+ avatar = { avatar }
243+ preventButtonResize = { preventButtonResize }
244+ { ...buttonOverlayProps }
245+ />
246+ { hasSubItems && (
247+ < div className = { classes . dropdownWrapper } >
248+ < div id = { `${ id } -dropdown` } className = { dropdownClassName } ref = { dropdownContentElementRef } >
249+ { heading && < h3 > { label } </ h3 > }
250+ < ul > { children } </ ul >
251+ </ div >
252+ </ div >
253+ ) }
254+ </ div >
255+ </ >
192256 ) ;
193257} ;
194258
0 commit comments