11import React , {
2- useCallback , useRef , useState ,
2+ useCallback , useEffect , useRef , useState ,
33} from 'react' ;
4+ import ReactDOM from 'react-dom' ;
45import PropTypes from 'prop-types' ;
56
67import {
78 Button , Dropdown , Icon , IconButton , ModalPopup , useToggle ,
89} from '@openedx/paragon' ;
9- import { MoreHoriz } from '@openedx/paragon/icons' ;
10+ import { ChevronRight , MoreHoriz } from '@openedx/paragon/icons' ;
1011
1112import { useIntl } from '@edx/frontend-platform/i18n' ;
1213
13- import { useLearnerActions } from './utils' ;
14+ import { useLearnerActionsMenu } from './utils' ;
1415
1516const LearnerActionsDropdown = ( {
1617 actionHandlers,
@@ -21,14 +22,16 @@ const LearnerActionsDropdown = ({
2122 const intl = useIntl ( ) ;
2223 const [ isOpen , open , close ] = useToggle ( false ) ;
2324 const [ target , setTarget ] = useState ( null ) ;
24- const actions = useLearnerActions ( userHasBulkDeletePrivileges ) ;
25+ const [ activeSubmenu , setActiveSubmenu ] = useState ( null ) ;
26+ const menuItems = useLearnerActionsMenu ( intl , userHasBulkDeletePrivileges ) ;
2527
2628 const handleActions = useCallback ( ( action ) => {
2729 const actionFunction = actionHandlers [ action ] ;
2830 if ( actionFunction ) {
2931 actionFunction ( ) ;
32+ close ( ) ;
3033 }
31- } , [ actionHandlers ] ) ;
34+ } , [ actionHandlers , close ] ) ;
3235
3336 const onClickButton = useCallback ( ( event ) => {
3437 event . preventDefault ( ) ;
@@ -39,8 +42,15 @@ const LearnerActionsDropdown = ({
3942 const onCloseModal = useCallback ( ( ) => {
4043 close ( ) ;
4144 setTarget ( null ) ;
45+ setActiveSubmenu ( null ) ;
4246 } , [ close ] ) ;
4347
48+ // Cleanup portal on unmount to prevent memory leaks and orphaned DOM nodes
49+ useEffect ( ( ) => ( ) => {
50+ setTarget ( null ) ;
51+ setActiveSubmenu ( null ) ;
52+ } , [ ] ) ;
53+
4454 return (
4555 < >
4656 < IconButton
@@ -53,41 +63,69 @@ const LearnerActionsDropdown = ({
5363 iconClassNames = { dropDownIconSize ? 'dropdown-icon-dimensions' : '' }
5464 />
5565 < div className = "actions-dropdown" >
56- < ModalPopup
57- onClose = { onCloseModal }
58- positionRef = { target }
59- isOpen = { isOpen }
60- placement = "bottom-start"
61- >
62- < div
63- className = "bg-white shadow d-flex flex-column mt-1"
64- data-testid = "learner-actions-dropdown-modal-popup"
66+ { isOpen && ReactDOM . createPortal (
67+ < ModalPopup
68+ onClose = { onCloseModal }
69+ positionRef = { target }
70+ isOpen = { isOpen }
71+ placement = "bottom-start"
72+ style = { { zIndex : 9998 } }
6573 >
66- { actions . map ( action => (
67- < React . Fragment key = { action . id } >
68- < Dropdown . Item
69- as = { Button }
70- variant = "tertiary"
71- size = "inline"
72- onClick = { ( ) => {
73- close ( ) ;
74- handleActions ( action . action ) ;
75- } }
76- className = "d-flex justify-content-start actions-dropdown-item"
77- data-testId = { action . id }
74+ < div
75+ className = "bg-white shadow d-flex flex-column mt-1"
76+ data-testid = "learner-actions-dropdown-modal-popup"
77+ style = { { position : 'relative' , zIndex : 9998 } }
78+ >
79+ { menuItems . map ( item => (
80+ < div
81+ key = { item . id }
82+ className = "position-relative"
83+ onMouseEnter = { ( ) => setActiveSubmenu ( item . id ) }
84+ onMouseLeave = { ( ) => setActiveSubmenu ( null ) }
85+ style = { { zIndex : 2 } }
7886 >
79- < Icon
80- src = { action . icon }
81- className = "icon-size-24"
82- />
83- < span className = "font-weight-normal ml-2" >
84- { action . label . defaultMessage }
85- </ span >
86- </ Dropdown . Item >
87- </ React . Fragment >
88- ) ) }
89- </ div >
90- </ ModalPopup >
87+ < Dropdown . Item
88+ as = { Button }
89+ variant = "tertiary"
90+ size = "inline"
91+ className = "d-flex justify-content-between align-items-center actions-dropdown-item"
92+ data-testid = { item . id }
93+ >
94+ < div className = "d-flex align-items-center" >
95+ < span className = "font-weight-normal" >
96+ { item . label }
97+ </ span >
98+ </ div >
99+ < Icon
100+ src = { ChevronRight }
101+ className = "icon-size-16"
102+ />
103+ </ Dropdown . Item >
104+ { activeSubmenu === item . id && (
105+ < div className = "bg-white learner-submenu-container" >
106+ { item . submenu . map ( subItem => (
107+ < Dropdown . Item
108+ key = { subItem . id }
109+ as = { Button }
110+ variant = "tertiary"
111+ size = "inline"
112+ onClick = { ( ) => handleActions ( subItem . action ) }
113+ className = "d-flex justify-content-start actions-dropdown-item"
114+ data-testid = { subItem . id }
115+ >
116+ < span className = "font-weight-normal" >
117+ { subItem . label }
118+ </ span >
119+ </ Dropdown . Item >
120+ ) ) }
121+ </ div >
122+ ) }
123+ </ div >
124+ ) ) }
125+ </ div >
126+ </ ModalPopup > ,
127+ document . body ,
128+ ) }
91129 </ div >
92130 </ >
93131 ) ;
0 commit comments