55 * This source code is licensed under the license found in the LICENSE file in
66 * the root directory of this source tree.
77 */
8+
89import PropTypes from 'lib/PropTypes' ;
910import React , { useState , useEffect , useRef } from 'react' ;
1011import styles from 'components/ContextMenu/ContextMenu.scss' ;
1112
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 ;
2920
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 ;
3426
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 ;
4029
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 ;
4333
44- if ( ! shouldApplyOffset ) {
45- y = prevElTop + offset ;
46- }
34+ let proposedTop = shouldApplyOffset
35+ ? prevElBox . top + offset
36+ : prevElBox . top ;
4737
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 ) ) ;
5439
55- return { x : 0 , y } ;
40+ return {
41+ x : showOnRight ? prevElBox . width : - elBox . width ,
42+ y : proposedTop - elBox . top ,
43+ } ;
5644 }
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+ } ;
5752} ;
5853
5954const MenuSection = ( { level, items, path, setPath, hide, parentItemCount = 0 } ) => {
6055 const sectionRef = useRef ( null ) ;
61- const [ position , setPosition ] = useState ( ) ;
56+ const [ position , setPosition ] = useState ( null ) ;
57+ const hasPositioned = useRef ( false ) ;
6258
6359 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+ } , [ ] ) ;
7273
7374 const style = position
7475 ? {
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+ }
8182 : { } ;
8283
8384 return (
8485 < ul ref = { sectionRef } className = { styles . category } style = { style } >
8586 { 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+
10193 return (
10294 < li
10395 key = { `menu-section-${ level } -${ index } ` }
104- className = { styles . option }
96+ className = { item . items ? styles . item : styles . option }
10597 style = { item . disabled ? { opacity : 0.5 , cursor : 'not-allowed' } : { } }
10698 onClick = { ( ) => {
107- if ( item . disabled === true ) {
108- return ;
99+ if ( ! item . disabled ) {
100+ item . callback ?. ( ) ;
101+ hide ( ) ;
109102 }
110- item . callback && item . callback ( ) ;
111- hide ( ) ;
112- } }
113- onMouseEnter = { ( ) => {
114- const newPath = path . slice ( 0 , level + 1 ) ;
115- setPath ( newPath ) ;
116103 } }
104+ onMouseEnter = { handleHover }
117105 >
118106 { item . text }
119107 { item . subtext && < span > - { item . subtext } </ span > }
@@ -138,27 +126,24 @@ const ContextMenu = ({ x, y, items }) => {
138126 setPath ( [ 0 ] ) ;
139127 } ;
140128
141- function handleClickOutside ( event ) {
142- if ( menuRef . current && ! menuRef . current . contains ( event . target ) ) {
143- hide ( ) ;
144- }
145- }
146-
147129 useEffect ( ( ) => {
130+ const handleClickOutside = event => {
131+ if ( menuRef . current && ! menuRef . current . contains ( event . target ) ) {
132+ hide ( ) ;
133+ }
134+ } ;
148135 document . addEventListener ( 'mousedown' , handleClickOutside ) ;
149136 return ( ) => {
150137 document . removeEventListener ( 'mousedown' , handleClickOutside ) ;
151138 } ;
152- } ) ;
139+ } , [ ] ) ;
153140
154- if ( ! visible ) {
155- return null ;
156- }
141+ if ( ! visible ) return null ;
157142
158143 const getItemsFromLevel = level => {
159144 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 || [ ] ;
162147 }
163148 return result ;
164149 } ;
@@ -167,19 +152,16 @@ const ContextMenu = ({ x, y, items }) => {
167152 < div
168153 className = { styles . menu }
169154 ref = { menuRef }
170- style = { {
171- left : x ,
172- top : y ,
173- } }
155+ style = { { left : x , top : y , position : 'absolute' } }
174156 >
175- { path . map ( ( position , level ) => {
157+ { path . map ( ( _ , level ) => {
176158 const itemsForLevel = getItemsFromLevel ( level ) ;
177159 const parentItemCount =
178160 level === 0 ? items . length : getItemsFromLevel ( level - 1 ) . length ;
179161
180162 return (
181163 < MenuSection
182- key = { `section-${ position } -${ level } ` }
164+ key = { `section-${ path [ level ] } -${ level } ` }
183165 path = { path }
184166 setPath = { setPath }
185167 level = { level }
@@ -196,9 +178,7 @@ const ContextMenu = ({ x, y, items }) => {
196178ContextMenu . propTypes = {
197179 x : PropTypes . number . isRequired . describe ( 'X context menu position.' ) ,
198180 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.' ) ,
202182} ;
203183
204184export default ContextMenu ;
0 commit comments