5
5
* This source code is licensed under the license found in the LICENSE file in
6
6
* the root directory of this source tree.
7
7
*/
8
+
8
9
import PropTypes from 'lib/PropTypes' ;
9
10
import React , { useState , useEffect , useRef } from 'react' ;
10
11
import styles from 'components/ContextMenu/ContextMenu.scss' ;
11
12
12
- const getPositionToFitVisibleScreen = ( ref , offset = 0 , mainItemCount = 0 , subItemCount = 0 ) => {
13
- if ( ref . current ) {
14
- const elBox = ref . current . getBoundingClientRect ( ) ;
15
- let y = 0 ;
16
-
17
- const footerHeight = 50 ;
18
- const lowerLimit = window . innerHeight - footerHeight ;
19
- const upperLimit = 0 ;
20
-
21
- if ( elBox . bottom > lowerLimit ) {
22
- y = lowerLimit - elBox . bottom ;
23
- } else if ( elBox . top < upperLimit ) {
24
- y = upperLimit - elBox . top ;
25
- }
26
-
27
- const projectedTop = elBox . top + y + offset ;
28
- const projectedBottom = projectedTop + elBox . height ;
13
+ const getPositionToFitVisibleScreen = (
14
+ ref ,
15
+ offset = 0 ,
16
+ mainItemCount = 0 ,
17
+ subItemCount = 0
18
+ ) => {
19
+ if ( ! ref . current ) return ;
29
20
30
- const shouldApplyOffset = mainItemCount === 0 || subItemCount > mainItemCount ;
31
- if ( shouldApplyOffset && projectedTop >= upperLimit && projectedBottom <= lowerLimit ) {
32
- y += offset ;
33
- }
21
+ const elBox = ref . current . getBoundingClientRect ( ) ;
22
+ const menuHeight = elBox . height ;
23
+ const footerHeight = 50 ;
24
+ const lowerLimit = window . innerHeight - footerHeight ;
25
+ const upperLimit = 0 ;
34
26
35
- const prevEl = ref . current . previousSibling ;
36
- if ( prevEl ) {
37
- const prevElBox = prevEl . getBoundingClientRect ( ) ;
38
- const prevElStyle = window . getComputedStyle ( prevEl ) ;
39
- const rawTop = prevElStyle . top ;
27
+ const shouldApplyOffset = mainItemCount === 0 || subItemCount > mainItemCount ;
28
+ const prevEl = ref . current . previousSibling ;
40
29
41
- const parsedTop = parseInt ( rawTop , 10 ) ;
42
- const prevElTop = Number . isFinite ( parsedTop ) ? parsedTop : prevElBox . top ;
30
+ if ( prevEl ) {
31
+ const prevElBox = prevEl . getBoundingClientRect ( ) ;
32
+ const showOnRight = prevElBox . x + prevElBox . width + elBox . width < window . innerWidth ;
43
33
44
- if ( ! shouldApplyOffset ) {
45
- y = prevElTop + offset ;
46
- }
34
+ let proposedTop = shouldApplyOffset
35
+ ? prevElBox . top + offset
36
+ : prevElBox . top ;
47
37
48
- const showOnRight = prevElBox . x + prevElBox . width + elBox . width < window . innerWidth ;
49
- return {
50
- x : showOnRight ? prevElBox . width : - elBox . width ,
51
- y
52
- } ;
53
- }
38
+ proposedTop = Math . max ( upperLimit , Math . min ( proposedTop , lowerLimit - menuHeight ) ) ;
54
39
55
- return { x : 0 , y } ;
40
+ return {
41
+ x : showOnRight ? prevElBox . width : - elBox . width ,
42
+ y : proposedTop - elBox . top ,
43
+ } ;
56
44
}
45
+
46
+ const proposedTop = elBox . top + offset ;
47
+ const clampedTop = Math . max ( upperLimit , Math . min ( proposedTop , lowerLimit - menuHeight ) ) ;
48
+ return {
49
+ x : 0 ,
50
+ y : clampedTop - elBox . top ,
51
+ } ;
57
52
} ;
58
53
59
54
const MenuSection = ( { level, items, path, setPath, hide, parentItemCount = 0 } ) => {
60
55
const sectionRef = useRef ( null ) ;
61
- const [ position , setPosition ] = useState ( ) ;
56
+ const [ position , setPosition ] = useState ( null ) ;
57
+ const hasPositioned = useRef ( false ) ;
62
58
63
59
useEffect ( ( ) => {
64
- const newPosition = getPositionToFitVisibleScreen (
65
- sectionRef ,
66
- path [ level ] * 30 ,
67
- parentItemCount ,
68
- items . length
69
- ) ;
70
- newPosition && setPosition ( newPosition ) ;
71
- } , [ sectionRef , path , level , items . length , parentItemCount ] ) ;
60
+ if ( ! hasPositioned . current ) {
61
+ const newPosition = getPositionToFitVisibleScreen (
62
+ sectionRef ,
63
+ path [ level ] * 30 ,
64
+ parentItemCount ,
65
+ items . length
66
+ ) ;
67
+ if ( newPosition ) {
68
+ setPosition ( newPosition ) ;
69
+ hasPositioned . current = true ;
70
+ }
71
+ }
72
+ } , [ ] ) ;
72
73
73
74
const style = position
74
75
? {
75
- left : position . x ,
76
- top : position . y ,
77
- maxHeight : '80vh ' ,
78
- overflowY : 'scroll' ,
79
- opacity : 1 ,
80
- }
76
+ transform : `translate( ${ position . x } px, ${ position . y } px)` ,
77
+ maxHeight : '80vh' ,
78
+ overflowY : 'auto ' ,
79
+ opacity : 1 ,
80
+ position : 'absolute' ,
81
+ }
81
82
: { } ;
82
83
83
84
return (
84
85
< ul ref = { sectionRef } className = { styles . category } style = { style } >
85
86
{ items . map ( ( item , index ) => {
86
- if ( item . items ) {
87
- return (
88
- < li
89
- key = { `menu-section-${ level } -${ index } ` }
90
- className = { styles . item }
91
- onMouseEnter = { ( ) => {
92
- const newPath = path . slice ( 0 , level + 1 ) ;
93
- newPath . push ( index ) ;
94
- setPath ( newPath ) ;
95
- } }
96
- >
97
- { item . text }
98
- </ li >
99
- ) ;
100
- }
87
+ const handleHover = ( ) => {
88
+ const newPath = path . slice ( 0 , level + 1 ) ;
89
+ newPath . push ( index ) ;
90
+ setPath ( newPath ) ;
91
+ } ;
92
+
101
93
return (
102
94
< li
103
95
key = { `menu-section-${ level } -${ index } ` }
104
- className = { styles . option }
96
+ className = { item . items ? styles . item : styles . option }
105
97
style = { item . disabled ? { opacity : 0.5 , cursor : 'not-allowed' } : { } }
106
98
onClick = { ( ) => {
107
- if ( item . disabled === true ) {
108
- return ;
99
+ if ( ! item . disabled ) {
100
+ item . callback ?. ( ) ;
101
+ hide ( ) ;
109
102
}
110
- item . callback && item . callback ( ) ;
111
- hide ( ) ;
112
- } }
113
- onMouseEnter = { ( ) => {
114
- const newPath = path . slice ( 0 , level + 1 ) ;
115
- setPath ( newPath ) ;
116
103
} }
104
+ onMouseEnter = { handleHover }
117
105
>
118
106
{ item . text }
119
107
{ item . subtext && < span > - { item . subtext } </ span > }
@@ -138,27 +126,24 @@ const ContextMenu = ({ x, y, items }) => {
138
126
setPath ( [ 0 ] ) ;
139
127
} ;
140
128
141
- function handleClickOutside ( event ) {
142
- if ( menuRef . current && ! menuRef . current . contains ( event . target ) ) {
143
- hide ( ) ;
144
- }
145
- }
146
-
147
129
useEffect ( ( ) => {
130
+ const handleClickOutside = event => {
131
+ if ( menuRef . current && ! menuRef . current . contains ( event . target ) ) {
132
+ hide ( ) ;
133
+ }
134
+ } ;
148
135
document . addEventListener ( 'mousedown' , handleClickOutside ) ;
149
136
return ( ) => {
150
137
document . removeEventListener ( 'mousedown' , handleClickOutside ) ;
151
138
} ;
152
- } ) ;
139
+ } , [ ] ) ;
153
140
154
- if ( ! visible ) {
155
- return null ;
156
- }
141
+ if ( ! visible ) return null ;
157
142
158
143
const getItemsFromLevel = level => {
159
144
let result = items ;
160
- for ( let index = 1 ; index <= level ; index ++ ) {
161
- result = result [ path [ index ] ] . items ;
145
+ for ( let i = 1 ; i <= level ; i ++ ) {
146
+ result = result [ path [ i ] ] ? .items || [ ] ;
162
147
}
163
148
return result ;
164
149
} ;
@@ -167,19 +152,16 @@ const ContextMenu = ({ x, y, items }) => {
167
152
< div
168
153
className = { styles . menu }
169
154
ref = { menuRef }
170
- style = { {
171
- left : x ,
172
- top : y ,
173
- } }
155
+ style = { { left : x , top : y , position : 'absolute' } }
174
156
>
175
- { path . map ( ( position , level ) => {
157
+ { path . map ( ( _ , level ) => {
176
158
const itemsForLevel = getItemsFromLevel ( level ) ;
177
159
const parentItemCount =
178
160
level === 0 ? items . length : getItemsFromLevel ( level - 1 ) . length ;
179
161
180
162
return (
181
163
< MenuSection
182
- key = { `section-${ position } -${ level } ` }
164
+ key = { `section-${ path [ level ] } -${ level } ` }
183
165
path = { path }
184
166
setPath = { setPath }
185
167
level = { level }
@@ -196,9 +178,7 @@ const ContextMenu = ({ x, y, items }) => {
196
178
ContextMenu . propTypes = {
197
179
x : PropTypes . number . isRequired . describe ( 'X context menu position.' ) ,
198
180
y : PropTypes . number . isRequired . describe ( 'Y context menu position.' ) ,
199
- items : PropTypes . array . isRequired . describe (
200
- 'Array with tree representation of context menu items.'
201
- ) ,
181
+ items : PropTypes . array . isRequired . describe ( 'Array with tree representation of context menu items.' ) ,
202
182
} ;
203
183
204
184
export default ContextMenu ;
0 commit comments