1
1
import * as React from 'react' ;
2
2
import { classNames } from 'mo/common/className' ;
3
3
import { MenuItem } from './menuItem' ;
4
- import { ISubMenuProps , MenuMode , SubMenu } from './subMenu' ;
4
+ import { isHorizontal , ISubMenuProps , MenuMode , SubMenu } from './subMenu' ;
5
+ import { debounce } from 'lodash' ;
5
6
import {
7
+ activeClassName ,
6
8
defaultMenuClassName ,
9
+ defaultSubMenuClassName ,
7
10
horizontalMenuClassName ,
8
11
verticalMenuClassName ,
9
12
} from './base' ;
10
13
import { mergeFunctions } from 'mo/common/utils' ;
11
14
import { cloneReactChildren } from 'mo/react' ;
15
+ import { em2Px } from 'mo/common/css' ;
16
+ import { getRelativePosition , triggerEvent } from 'mo/common/dom' ;
12
17
13
- export interface IMenuProps extends ISubMenuProps { }
18
+ export type IMenuProps = ISubMenuProps ;
19
+
20
+ const visibleMenuItem = ( item ?: HTMLElement ) => {
21
+ if ( ! item ) return ;
22
+ if ( item ?. dataset . submenu ) {
23
+ const subMenu : HTMLElement = Array . prototype . find . call (
24
+ item . children ,
25
+ ( dom : HTMLElement ) => dom . nodeName === 'UL'
26
+ ) ;
27
+ subMenu . style . opacity = '1' ;
28
+ subMenu . style . pointerEvents = 'auto' ;
29
+ item . classList . add ( activeClassName ) ;
30
+ }
31
+ } ;
32
+
33
+ const setPositionForSubMenu = (
34
+ item ?: HTMLElement ,
35
+ subMenu ?: HTMLElement ,
36
+ isAlignHorizontal : boolean = false
37
+ ) => {
38
+ if ( ! item || ! subMenu ) return ;
39
+ const domRect = item . getBoundingClientRect ( ) ;
40
+ const pos = getRelativePosition ( subMenu , domRect ) ;
41
+
42
+ if ( isAlignHorizontal ) pos . y = pos . y + domRect . height ;
43
+ else {
44
+ pos . x = pos . x + domRect . width ;
45
+ // The vertical menu default has padding 0.5em so that need reduce the padding
46
+ const fontSize = getComputedStyle ( subMenu ) . getPropertyValue (
47
+ 'font-size'
48
+ ) ;
49
+ const paddingTop = em2Px ( 0.5 , parseInt ( fontSize . replace ( 'px' , '' ) , 10 ) ) ;
50
+ pos . y = pos . y - paddingTop ;
51
+ }
52
+
53
+ subMenu . style . cssText = `
54
+ left: ${ pos . x } px;
55
+ top: ${ pos . y } px;
56
+ ` ;
57
+ } ;
14
58
15
59
export function Menu ( props : React . PropsWithChildren < IMenuProps > ) {
16
60
const {
@@ -19,9 +63,14 @@ export function Menu(props: React.PropsWithChildren<IMenuProps>) {
19
63
data = [ ] ,
20
64
children,
21
65
onClick,
66
+ trigger = 'hover' ,
22
67
...custom
23
68
} = props ;
69
+ const menuRef = React . useRef < HTMLUListElement > ( null ) ;
70
+ const isMouseInMenu = React . useRef ( false ) ;
24
71
let content = cloneReactChildren ( children , { onClick } ) ;
72
+ // Only when the trigger is hover need to set the delay
73
+ const delay = trigger === 'hover' ? 200 : 0 ;
25
74
26
75
const modeClassName =
27
76
mode === MenuMode . Horizontal
@@ -55,8 +104,90 @@ export function Menu(props: React.PropsWithChildren<IMenuProps>) {
55
104
content = renderMenusByData ( data ) ;
56
105
}
57
106
107
+ const initialMenuStyle = ( ) => {
108
+ menuRef . current ?. querySelectorAll ( 'ul' ) . forEach ( ( ul ) => {
109
+ ul . style . opacity = '0' ;
110
+ ul . style . pointerEvents = 'none' ;
111
+ } ) ;
112
+ menuRef . current
113
+ ?. querySelectorAll ( `li.${ activeClassName } ` )
114
+ . forEach ( ( li ) => {
115
+ li . classList . remove ( activeClassName ) ;
116
+ } ) ;
117
+ } ;
118
+
119
+ const detectDomElementByEvent = debounce ( ( e ) => {
120
+ // ensure only when mouse in menu can the submenu toggle visibility
121
+ if ( isMouseInMenu . current ) {
122
+ const doms = document . elementsFromPoint (
123
+ e . pageX ,
124
+ e . pageY
125
+ ) as HTMLElement [ ] ;
126
+ const ulDom = doms . find ( ( dom ) => dom . nodeName === 'UL' ) ;
127
+ const liDom = doms . find ( ( dom ) => dom . nodeName === 'LI' ) ;
128
+ // clear current ul children style
129
+ if ( ulDom ) {
130
+ ulDom . querySelectorAll ( 'ul' ) . forEach ( ( ul ) => {
131
+ ul . style . opacity = '0' ;
132
+ ul . style . pointerEvents = 'none' ;
133
+ } ) ;
134
+ ulDom
135
+ . querySelectorAll ( `li.${ activeClassName } ` )
136
+ . forEach ( ( li ) => {
137
+ li . classList . remove ( activeClassName ) ;
138
+ } ) ;
139
+ }
140
+ visibleMenuItem ( liDom ) ;
141
+ const subMenu = liDom ?. querySelector ( 'ul' ) || undefined ;
142
+ setPositionForSubMenu ( liDom , subMenu , isHorizontal ( mode ) ) ;
143
+ }
144
+ } , delay ) ;
145
+
146
+ const handleTriggerEvent = ( e ) => {
147
+ e . preventDefault ( ) ;
148
+ e . persist ( ) ;
149
+ e . stopPropagation ( ) ;
150
+ isMouseInMenu . current = true ;
151
+ detectDomElementByEvent ( e ) ;
152
+ } ;
153
+
154
+ const handleMouseOut = ( ) => {
155
+ isMouseInMenu . current = false ;
156
+ } ;
157
+
158
+ const getEventListener = ( ) => {
159
+ // sub menu do not listen any event
160
+ if ( claNames ?. includes ( defaultSubMenuClassName ) ) return { } ;
161
+ return {
162
+ [ triggerEvent ( trigger ) ] : handleTriggerEvent ,
163
+ onMouseOut : handleMouseOut ,
164
+ } ;
165
+ } ;
166
+
167
+ const hideAfterLeftWindow = React . useCallback ( ( ) => {
168
+ if ( document . hidden ) {
169
+ initialMenuStyle ( ) ;
170
+ }
171
+ } , [ ] ) ;
172
+
173
+ React . useEffect ( ( ) => {
174
+ window . addEventListener ( 'contextmenu' , initialMenuStyle ) ;
175
+ window . addEventListener ( 'click' , initialMenuStyle ) ;
176
+ window . addEventListener ( 'visibilitychange' , hideAfterLeftWindow ) ;
177
+ return ( ) => {
178
+ document . removeEventListener ( 'contextmenu' , initialMenuStyle ) ;
179
+ window . removeEventListener ( 'click' , initialMenuStyle ) ;
180
+ window . removeEventListener ( 'visibilitychange' , hideAfterLeftWindow ) ;
181
+ } ;
182
+ } , [ ] ) ;
183
+
58
184
return (
59
- < ul className = { claNames } { ...custom } >
185
+ < ul
186
+ className = { claNames }
187
+ ref = { menuRef }
188
+ { ...getEventListener ( ) }
189
+ { ...custom }
190
+ >
60
191
{ content }
61
192
</ ul >
62
193
) ;
0 commit comments