11import * as React from 'react' ;
22import * as ReactDOM from 'react-dom' ;
33import Trigger from 'rc-trigger' ;
4+ import raf from 'rc-util/lib/raf' ;
45import KeyCode from 'rc-util/lib/KeyCode' ;
56import CSSMotion , { CSSMotionProps } from 'rc-motion' ;
67import classNames from 'classnames' ;
@@ -103,7 +104,12 @@ export interface SubMenuProps {
103104 direction ?: 'ltr' | 'rtl' ;
104105}
105106
106- export class SubMenu extends React . Component < SubMenuProps > {
107+ interface SubMenuState {
108+ mode : MenuMode ;
109+ isOpen : boolean ;
110+ }
111+
112+ export class SubMenu extends React . Component < SubMenuProps , SubMenuState > {
107113 static defaultProps = {
108114 onMouseEnter : noop ,
109115 onMouseLeave : noop ,
@@ -129,6 +135,11 @@ export class SubMenu extends React.Component<SubMenuProps> {
129135 }
130136
131137 updateDefaultActiveFirst ( store , eventKey , value ) ;
138+
139+ this . state = {
140+ mode : props . mode ,
141+ isOpen : props . isOpen ,
142+ } ;
132143 }
133144
134145 isRootMenu : boolean ;
@@ -143,6 +154,8 @@ export class SubMenu extends React.Component<SubMenuProps> {
143154
144155 haveOpened : boolean ;
145156
157+ updateStateRaf : number ;
158+
146159 /**
147160 * Follow timeout should be `number`.
148161 * Current is only convert code into TS,
@@ -159,6 +172,26 @@ export class SubMenu extends React.Component<SubMenuProps> {
159172 componentDidUpdate ( ) {
160173 const { mode, parentMenu, manualRef, isOpen } = this . props ;
161174
175+ const updateState = ( ) => {
176+ this . setState ( {
177+ mode,
178+ isOpen,
179+ } ) ;
180+ } ;
181+
182+ // Delay sync when mode changed in case openKeys change not sync
183+ const isOpenChanged = isOpen !== this . state . isOpen ;
184+ const isModeChanged = mode !== this . state . mode ;
185+ if ( isModeChanged || isOpenChanged ) {
186+ raf . cancel ( this . updateStateRaf ) ;
187+
188+ if ( isModeChanged ) {
189+ this . updateStateRaf = raf ( updateState ) ;
190+ } else {
191+ updateState ( ) ;
192+ }
193+ }
194+
162195 // invoke customized ref to expose component to mixin
163196 if ( manualRef ) {
164197 manualRef ( this ) ;
@@ -186,6 +219,8 @@ export class SubMenu extends React.Component<SubMenuProps> {
186219 if ( this . mouseenterTimeout ) {
187220 clearTimeout ( this . mouseenterTimeout ) ;
188221 }
222+
223+ raf . cancel ( this . updateStateRaf ) ;
189224 }
190225
191226 onDestroy = ( key : string ) => {
@@ -197,10 +232,11 @@ export class SubMenu extends React.Component<SubMenuProps> {
197232 * This legacy code that `onKeyDown` is called by parent instead of dom self.
198233 * which need return code to check if this event is handled
199234 */
200- onKeyDown : React . KeyboardEventHandler < HTMLElement > = ( e ) => {
235+ onKeyDown : React . KeyboardEventHandler < HTMLElement > = e => {
201236 const { keyCode } = e ;
202237 const menu = this . menuInstance ;
203- const { isOpen, store } = this . props ;
238+ const { store } = this . props ;
239+ const visible = this . getVisible ( ) ;
204240
205241 if ( keyCode === KeyCode . ENTER ) {
206242 this . onTitleClick ( e ) ;
@@ -209,7 +245,7 @@ export class SubMenu extends React.Component<SubMenuProps> {
209245 }
210246
211247 if ( keyCode === KeyCode . RIGHT ) {
212- if ( isOpen ) {
248+ if ( visible ) {
213249 menu . onKeyDown ( e ) ;
214250 } else {
215251 this . triggerOpenChange ( true ) ;
@@ -220,7 +256,7 @@ export class SubMenu extends React.Component<SubMenuProps> {
220256 }
221257 if ( keyCode === KeyCode . LEFT ) {
222258 let handled : boolean ;
223- if ( isOpen ) {
259+ if ( visible ) {
224260 handled = menu . onKeyDown ( e ) ;
225261 } else {
226262 return undefined ;
@@ -232,22 +268,22 @@ export class SubMenu extends React.Component<SubMenuProps> {
232268 return handled ;
233269 }
234270
235- if ( isOpen && ( keyCode === KeyCode . UP || keyCode === KeyCode . DOWN ) ) {
271+ if ( visible && ( keyCode === KeyCode . UP || keyCode === KeyCode . DOWN ) ) {
236272 return menu . onKeyDown ( e ) ;
237273 }
238274
239275 return undefined ;
240276 } ;
241277
242- onOpenChange : OpenEventHandler = ( e ) => {
278+ onOpenChange : OpenEventHandler = e => {
243279 this . props . onOpenChange ( e ) ;
244280 } ;
245281
246282 onPopupVisibleChange = ( visible : boolean ) => {
247283 this . triggerOpenChange ( visible , visible ? 'mouseenter' : 'mouseleave' ) ;
248284 } ;
249285
250- onMouseEnter : React . MouseEventHandler < HTMLElement > = ( e ) => {
286+ onMouseEnter : React . MouseEventHandler < HTMLElement > = e => {
251287 const { eventKey : key , onMouseEnter, store } = this . props ;
252288 updateDefaultActiveFirst ( store , this . props . eventKey , false ) ;
253289 onMouseEnter ( {
@@ -256,7 +292,7 @@ export class SubMenu extends React.Component<SubMenuProps> {
256292 } ) ;
257293 } ;
258294
259- onMouseLeave : React . MouseEventHandler < HTMLElement > = ( e ) => {
295+ onMouseLeave : React . MouseEventHandler < HTMLElement > = e => {
260296 const { parentMenu, eventKey, onMouseLeave } = this . props ;
261297 parentMenu . subMenuInstance = this ;
262298 onMouseLeave ( {
@@ -265,7 +301,7 @@ export class SubMenu extends React.Component<SubMenuProps> {
265301 } ) ;
266302 } ;
267303
268- onTitleMouseEnter : React . MouseEventHandler < HTMLElement > = ( domEvent ) => {
304+ onTitleMouseEnter : React . MouseEventHandler < HTMLElement > = domEvent => {
269305 const { eventKey : key , onItemHover, onTitleMouseEnter } = this . props ;
270306 onItemHover ( {
271307 key,
@@ -277,7 +313,7 @@ export class SubMenu extends React.Component<SubMenuProps> {
277313 } ) ;
278314 } ;
279315
280- onTitleMouseLeave : React . MouseEventHandler < HTMLElement > = ( e ) => {
316+ onTitleMouseLeave : React . MouseEventHandler < HTMLElement > = e => {
281317 const { parentMenu, eventKey, onItemHover, onTitleMouseLeave } = this . props ;
282318 parentMenu . subMenuInstance = this ;
283319 onItemHover ( {
@@ -301,7 +337,7 @@ export class SubMenu extends React.Component<SubMenuProps> {
301337 if ( props . triggerSubMenuAction === 'hover' ) {
302338 return ;
303339 }
304- this . triggerOpenChange ( ! props . isOpen , 'click' ) ;
340+ this . triggerOpenChange ( ! this . getVisible ( ) , 'click' ) ;
305341 updateDefaultActiveFirst ( props . store , this . props . eventKey , false ) ;
306342 } ;
307343
@@ -313,11 +349,11 @@ export class SubMenu extends React.Component<SubMenuProps> {
313349 }
314350 } ;
315351
316- onSelect : SelectEventHandler = ( info ) => {
352+ onSelect : SelectEventHandler = info => {
317353 this . props . onSelect ( info ) ;
318354 } ;
319355
320- onDeselect : SelectEventHandler = ( info ) => {
356+ onDeselect : SelectEventHandler = info => {
321357 this . props . onDeselect ( info ) ;
322358 } ;
323359
@@ -331,6 +367,10 @@ export class SubMenu extends React.Component<SubMenuProps> {
331367
332368 getOpenClassName = ( ) => `${ this . props . rootPrefixCls } -submenu-open` ;
333369
370+ getVisible = ( ) => this . state . isOpen ;
371+
372+ getMode = ( ) => this . state . mode ;
373+
334374 saveMenuInstance = ( c : MenuItem ) => {
335375 // children menu instance
336376 this . menuInstance = c ;
@@ -367,9 +407,7 @@ export class SubMenu extends React.Component<SubMenuProps> {
367407 return ret . find ;
368408 } ;
369409
370- isOpen = ( ) => this . props . openKeys . indexOf ( this . props . eventKey ) !== - 1 ;
371-
372- isInlineMode = ( ) => this . props . mode === 'inline' ;
410+ isInlineMode = ( ) => this . getMode ( ) === 'inline' ;
373411
374412 adjustWidth = ( ) => {
375413 /* istanbul ignore if */
@@ -391,9 +429,11 @@ export class SubMenu extends React.Component<SubMenuProps> {
391429
392430 getBaseProps = ( ) : SubPopupMenuProps => {
393431 const { props } = this ;
432+ const mergedMode = this . getMode ( ) ;
433+
394434 return {
395- mode : props . mode === 'horizontal' ? 'vertical' : props . mode ,
396- visible : this . props . isOpen ,
435+ mode : mergedMode === 'horizontal' ? 'vertical' : mergedMode ,
436+ visible : this . getVisible ( ) ,
397437 level : props . level + 1 ,
398438 inlineIndent : props . inlineIndent ,
399439 focusable : false ,
@@ -498,13 +538,14 @@ export class SubMenu extends React.Component<SubMenuProps> {
498538
499539 render ( ) {
500540 const props = { ...this . props } ;
501- const { isOpen } = props ;
541+ const visible = this . getVisible ( ) ;
502542 const prefixCls = this . getPrefixCls ( ) ;
503543 const inline = this . isInlineMode ( ) ;
504- const className = classNames ( prefixCls , `${ prefixCls } -${ props . mode } ` , {
544+ const mergedMode = this . getMode ( ) ;
545+ const className = classNames ( prefixCls , `${ prefixCls } -${ mergedMode } ` , {
505546 [ props . className ] : ! ! props . className ,
506- [ this . getOpenClassName ( ) ] : isOpen ,
507- [ this . getActiveClassName ( ) ] : props . active || ( isOpen && ! inline ) ,
547+ [ this . getOpenClassName ( ) ] : visible ,
548+ [ this . getActiveClassName ( ) ] : props . active || ( visible && ! inline ) ,
508549 [ this . getDisabledClassName ( ) ] : props . disabled ,
509550 [ this . getSelectedClassName ( ) ] : this . isChildrenSelected ( ) ,
510551 } ) ;
@@ -554,15 +595,15 @@ export class SubMenu extends React.Component<SubMenuProps> {
554595 // only set aria-owns when menu is open
555596 // otherwise it would be an invalid aria-owns value
556597 // since corresponding node cannot be found
557- if ( this . props . isOpen ) {
598+ if ( this . getVisible ( ) ) {
558599 ariaOwns = {
559600 'aria-owns' : this . internalMenuId ,
560601 } ;
561602 }
562603
563604 // expand custom icon should NOT be displayed in menu with horizontal mode.
564605 let icon = null ;
565- if ( props . mode !== 'horizontal' ) {
606+ if ( mergedMode !== 'horizontal' ) {
566607 icon = this . props . expandIcon ; // ReactNode
567608 if ( typeof this . props . expandIcon === 'function' ) {
568609 icon = React . createElement ( this . props . expandIcon as any , {
@@ -579,7 +620,7 @@ export class SubMenu extends React.Component<SubMenuProps> {
579620 role = "button"
580621 { ...titleMouseEvents }
581622 { ...titleClickEvents }
582- aria-expanded = { isOpen }
623+ aria-expanded = { visible }
583624 { ...ariaOwns }
584625 aria-haspopup = "true"
585626 title = { typeof props . title === 'string' ? props . title : undefined }
@@ -594,7 +635,7 @@ export class SubMenu extends React.Component<SubMenuProps> {
594635 const getPopupContainer = props . parentMenu ?. isRootMenu
595636 ? props . parentMenu . props . getPopupContainer
596637 : ( triggerNode : HTMLElement ) => triggerNode . parentNode ;
597- const popupPlacement = popupPlacementMap [ props . mode ] ;
638+ const popupPlacement = popupPlacementMap [ mergedMode ] ;
598639 const popupAlign = props . popupOffset ? { offset : props . popupOffset } : { } ;
599640 const popupClassName = classNames ( {
600641 [ props . popupClassName ] : props . popupClassName && ! inline ,
@@ -608,7 +649,7 @@ export class SubMenu extends React.Component<SubMenuProps> {
608649 subMenuCloseDelay,
609650 builtinPlacements,
610651 } = props ;
611- menuAllProps . forEach ( ( key ) => delete props [ key ] ) ;
652+ menuAllProps . forEach ( key => delete props [ key ] ) ;
612653 // Set onClick to null, to ignore propagated onClick event
613654 delete props . onClick ;
614655 const placement = isRTL
@@ -619,7 +660,7 @@ export class SubMenu extends React.Component<SubMenuProps> {
619660 // [Legacy] It's a fast fix,
620661 // but we should check if we can refactor this to make code more easy to understand
621662 const baseProps = this . getBaseProps ( ) ;
622- const mergedMotion = inline
663+ const mergedMotion : CSSMotionProps = inline
623664 ? null
624665 : this . getMotion ( baseProps . mode , baseProps . visible ) ;
625666
@@ -636,7 +677,7 @@ export class SubMenu extends React.Component<SubMenuProps> {
636677 getPopupContainer = { getPopupContainer }
637678 builtinPlacements = { placement }
638679 popupPlacement = { popupPlacement }
639- popupVisible = { inline ? false : isOpen }
680+ popupVisible = { inline ? false : visible }
640681 popupAlign = { popupAlign }
641682 popup = { inline ? null : children }
642683 action = { disabled || inline ? [ ] : [ triggerSubMenuAction ] }
0 commit comments