@@ -19,6 +19,7 @@ limitations under the License.
19
19
import React , { CSSProperties , RefObject , SyntheticEvent , useRef , useState } from "react" ;
20
20
import ReactDOM from "react-dom" ;
21
21
import classNames from "classnames" ;
22
+ import FocusLock from "react-focus-lock" ;
22
23
23
24
import { Key } from "../../Keyboard" ;
24
25
import { Writeable } from "../../@types/common" ;
@@ -43,8 +44,6 @@ function getOrCreateContainer(): HTMLDivElement {
43
44
return container ;
44
45
}
45
46
46
- const ARIA_MENU_ITEM_ROLES = new Set ( [ "menuitem" , "menuitemcheckbox" , "menuitemradio" ] ) ;
47
-
48
47
export interface IPosition {
49
48
top ?: number ;
50
49
bottom ?: number ;
@@ -84,6 +83,10 @@ export interface IProps extends IPosition {
84
83
// it will be mounted to a container at the root of the DOM.
85
84
mountAsChild ?: boolean ;
86
85
86
+ // If specified, contents will be wrapped in a FocusLock, this is only needed if the context menu is being rendered
87
+ // within an existing FocusLock e.g inside a modal.
88
+ focusLock ?: boolean ;
89
+
87
90
// Function to be called on menu close
88
91
onFinished ( ) ;
89
92
// on resize callback
@@ -99,7 +102,7 @@ interface IState {
99
102
// this will allow the ContextMenu to manage its own focus using arrow keys as per the ARIA guidelines.
100
103
@replaceableComponent ( "structures.ContextMenu" )
101
104
export class ContextMenu extends React . PureComponent < IProps , IState > {
102
- private initialFocus : HTMLElement ;
105
+ private readonly initialFocus : HTMLElement ;
103
106
104
107
static defaultProps = {
105
108
hasBackground : true ,
@@ -108,6 +111,7 @@ export class ContextMenu extends React.PureComponent<IProps, IState> {
108
111
109
112
constructor ( props , context ) {
110
113
super ( props , context ) ;
114
+
111
115
this . state = {
112
116
contextMenuElem : null ,
113
117
} ;
@@ -121,14 +125,13 @@ export class ContextMenu extends React.PureComponent<IProps, IState> {
121
125
this . initialFocus . focus ( ) ;
122
126
}
123
127
124
- private collectContextMenuRect = ( element ) => {
128
+ private collectContextMenuRect = ( element : HTMLDivElement ) => {
125
129
// We don't need to clean up when unmounting, so ignore
126
130
if ( ! element ) return ;
127
131
128
- let first = element . querySelector ( '[role^="menuitem"]' ) ;
129
- if ( ! first ) {
130
- first = element . querySelector ( '[tab-index]' ) ;
131
- }
132
+ const first = element . querySelector < HTMLElement > ( '[role^="menuitem"]' )
133
+ || element . querySelector < HTMLElement > ( '[tab-index]' ) ;
134
+
132
135
if ( first ) {
133
136
first . focus ( ) ;
134
137
}
@@ -205,7 +208,7 @@ export class ContextMenu extends React.PureComponent<IProps, IState> {
205
208
descending = true ;
206
209
}
207
210
}
208
- } while ( element && ! ARIA_MENU_ITEM_ROLES . has ( element . getAttribute ( "role" ) ) ) ;
211
+ } while ( element && ! element . getAttribute ( "role" ) ?. startsWith ( "menuitem" ) ) ;
209
212
210
213
if ( element ) {
211
214
( element as HTMLElement ) . focus ( ) ;
@@ -383,6 +386,17 @@ export class ContextMenu extends React.PureComponent<IProps, IState> {
383
386
) ;
384
387
}
385
388
389
+ let body = < >
390
+ { chevron }
391
+ { props . children }
392
+ </ > ;
393
+
394
+ if ( props . focusLock ) {
395
+ body = < FocusLock >
396
+ { body }
397
+ </ FocusLock > ;
398
+ }
399
+
386
400
return (
387
401
< div
388
402
className = { classNames ( "mx_ContextualMenu_wrapper" , this . props . wrapperClassName ) }
@@ -397,8 +411,7 @@ export class ContextMenu extends React.PureComponent<IProps, IState> {
397
411
ref = { this . collectContextMenuRect }
398
412
role = { this . props . managed ? "menu" : undefined }
399
413
>
400
- { chevron }
401
- { props . children }
414
+ { body }
402
415
</ div >
403
416
{ background }
404
417
</ div >
0 commit comments