1- import React , { useCallback , useMemo } from "react" ;
1+ import React , { useCallback , useMemo , memo , useState } from "react" ;
22import type { MenuProps } from "antd" ;
33import { Dropdown as AntdDropdown } from "antd" ;
4- import {
5- cloneDeep ,
6- isEmpty ,
7- isObject ,
8- isString ,
9- compact ,
10- join ,
11- tail ,
12- } from "lodash-es" ;
4+ import { isEmpty , isObject , isString , compact , join , tail } from "lodash-es" ;
135import {
146 CustomScopeProvider ,
157 unwrapWidget ,
@@ -61,6 +53,26 @@ export type PopupMenuProps = {
6153} & EnsembleWidgetProps < PopupMenuStyles & EnsembleWidgetStyles > &
6254 HasItemTemplate & { "item-template" ?: { value : Expression < string > } } ;
6355
56+ // memoized component for rendering menu item labels to prevent expensive re-renders
57+ const MenuItemLabel = memo < {
58+ label : Expression < string > | { [ key : string ] : unknown } ;
59+ hasBeenOpened : boolean ;
60+ isContextMenu : boolean ;
61+ } > ( ( { label, hasBeenOpened, isContextMenu } ) => {
62+ if ( isString ( label ) ) {
63+ return < span > { label } </ span > ;
64+ }
65+
66+ // for context menus, render immediately
67+ // for other triggers, only render complex widgets after menu has been opened
68+ if ( ! hasBeenOpened && ! isContextMenu ) {
69+ return < span style = { { opacity : 0.6 } } > ...</ span > ;
70+ }
71+
72+ return < > { EnsembleRuntime . render ( [ unwrapWidget ( label ) ] ) } </ > ;
73+ } ) ;
74+ MenuItemLabel . displayName = "MenuItemLabel" ;
75+
6476export const PopupMenu : React . FC < PopupMenuProps > = ( {
6577 onTriggered,
6678 onItemSelect,
@@ -74,6 +86,12 @@ export const PopupMenu: React.FC<PopupMenuProps> = ({
7486 const action = useEnsembleAction ( onItemSelect ) ;
7587 const onTriggerAction = useEnsembleAction ( onTriggered ) ;
7688
89+ // track if menu has been opened to enable lazy rendering
90+ const [ hasBeenOpened , setHasBeenOpened ] = useState ( false ) ;
91+
92+ // for context menus, we need to detect when they're opened differently
93+ const isContextMenu = values ?. trigger === "contextMenu" ;
94+
7795 const { namedData } = useTemplateData ( {
7896 data : itemTemplate ?. data ,
7997 name : itemTemplate ?. name ,
@@ -87,9 +105,13 @@ export const PopupMenu: React.FC<PopupMenuProps> = ({
87105
88106 const menuItem : ItemType = {
89107 key : `popupmenu_item_${ index } ` ,
90- label : isString ( rawItem . label )
91- ? rawItem . label
92- : EnsembleRuntime . render ( [ unwrapWidget ( rawItem . label ) ] ) ,
108+ label : (
109+ < MenuItemLabel
110+ label = { rawItem . label }
111+ hasBeenOpened = { hasBeenOpened }
112+ isContextMenu = { isContextMenu }
113+ />
114+ ) ,
93115 disabled : rawItem . enabled === false ,
94116 ...( rawItem . items && {
95117 children : rawItem . items . map ( ( itm , childIndex ) =>
@@ -100,77 +122,77 @@ export const PopupMenu: React.FC<PopupMenuProps> = ({
100122 } ;
101123 return menuItem ;
102124 } ,
103- [ ] ,
125+ [ hasBeenOpened , isContextMenu ] ,
104126 ) ;
105127
106- const popupMenuItems = useMemo ( ( ) => {
107- const popupItems : MenuProps [ "items" ] = [ ] ;
108-
109- const items = values ?. items ;
110- if ( items ) {
111- const tempItems = compact (
112- items . map ( ( rawItem , index ) => getMenuItem ( rawItem , index ) ) ,
113- ) ;
114-
115- popupItems . push ( ...tempItems ) ;
128+ const templateItems = useMemo ( ( ) => {
129+ if ( ! isObject ( itemTemplate ) || isEmpty ( namedData ) ) {
130+ return [ ] ;
116131 }
117132
118- if ( isObject ( itemTemplate ) && ! isEmpty ( namedData ) ) {
119- const tempItems = namedData . map ( ( item , index ) => {
120- const itm : ItemType = {
121- key : `popupmenu_itemTemplate_ ${ index } ` ,
122- label : (
133+ return namedData . map ( ( item , index ) => {
134+ const itm : ItemType = {
135+ key : `popupmenu_itemTemplate_ ${ index } ` ,
136+ label :
137+ hasBeenOpened || isContextMenu ? (
123138 < CustomScopeProvider value = { item as CustomScope } >
124139 { EnsembleRuntime . render ( [ itemTemplate . template ] ) }
125140 </ CustomScopeProvider >
141+ ) : (
142+ < span style = { { opacity : 0.6 } } > ...</ span >
126143 ) ,
127- } ;
128- return itm ;
129- } ) ;
144+ } ;
145+ return itm ;
146+ } ) ;
147+ } , [ itemTemplate , namedData , hasBeenOpened , isContextMenu ] ) ;
130148
131- popupItems . push ( ...tempItems ) ;
149+ const regularItems = useMemo ( ( ) => {
150+ const items = values ?. items ;
151+ if ( ! items || items . length === 0 ) {
152+ return [ ] ;
132153 }
133154
134- if ( values ?. showDivider ) {
155+ return compact ( items . map ( ( rawItem , index ) => getMenuItem ( rawItem , index ) ) ) ;
156+ } , [ values ?. items , getMenuItem ] ) ;
157+
158+ const popupMenuItems = useMemo ( ( ) => {
159+ const popupItems : MenuProps [ "items" ] = [ ...regularItems , ...templateItems ] ;
160+
161+ if ( values ?. showDivider && popupItems . length > 1 ) {
135162 for ( let i = 1 ; i < popupItems . length ; i += 2 ) {
136163 popupItems . splice ( i , 0 , { type : "divider" } ) ;
137164 }
138165 }
139166
140167 return popupItems ;
141- } , [
142- values ?. items ,
143- values ?. showDivider ,
144- itemTemplate ,
145- namedData ,
146- getMenuItem ,
147- ] ) ;
168+ } , [ regularItems , templateItems , values ?. showDivider ] ) ;
148169
149170 const widgetToRender = useMemo ( ( ) => {
150171 if ( ! values ?. widget ) {
151172 throw Error ( "PopupMenu requires a widget to render the anchor." ) ;
152173 }
153- const widget = cloneDeep ( values . widget ) ;
154- const actualWidget = unwrapWidget ( widget ) ;
174+ const actualWidget = unwrapWidget ( values . widget ) ;
155175 return EnsembleRuntime . render ( [ actualWidget ] ) ;
156176 } , [ values ?. widget ] ) ;
157177
158178 const itemsMap = useMemo ( ( ) => {
159179 const map = new Map < string , PopupMenuItem > ( ) ;
160180
161- namedData . forEach ( ( item , index ) => {
162- map . set ( `itemTemplate_${ index } ` , item as PopupMenuItem ) ;
163- } ) ;
181+ if ( namedData . length > 0 ) {
182+ namedData . forEach ( ( item , index ) => {
183+ map . set ( `itemTemplate_${ index } ` , item as PopupMenuItem ) ;
184+ } ) ;
185+ }
164186
165- if ( values ?. items ) {
187+ if ( values ?. items && values . items . length > 0 ) {
166188 const traverseItems = (
167189 items : PopupMenuItem [ ] ,
168190 path : number [ ] = [ ] ,
169191 ) : void => {
170192 items . forEach ( ( item , index ) => {
171193 const newPath = [ ...path , index ] ;
172194 map . set ( `item_${ newPath . join ( "_" ) } ` , item ) ;
173- if ( item . items ) {
195+ if ( item . items && item . items . length > 0 ) {
174196 // handle nested items
175197 traverseItems ( item . items , newPath ) ;
176198 }
@@ -194,10 +216,13 @@ export const PopupMenu: React.FC<PopupMenuProps> = ({
194216 const handleOnOpenChange = useCallback (
195217 ( open : boolean ) => {
196218 if ( open ) {
197- onTriggerAction ?. callback ( { open } ) ;
219+ setHasBeenOpened ( true ) ;
220+ if ( onTriggerAction ?. callback ) {
221+ onTriggerAction . callback ( { open } ) ;
222+ }
198223 }
199224 } ,
200- [ onTriggerAction ] ,
225+ [ onTriggerAction ?. callback ] ,
201226 ) ;
202227
203228 return (
0 commit comments