Skip to content

Commit 0ebbc50

Browse files
authored
fix: inlineCollapsed cause unexpected fade animation (#351)
* fix: Tmp check of the currect status * fix: Move all to raf * chore: Only mode change need asnyc update * fix test case * tests: Fix all
1 parent e008cf8 commit 0ebbc50

File tree

7 files changed

+454
-346
lines changed

7 files changed

+454
-346
lines changed

examples/antd-switch.js

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/* eslint-disable no-console, react/require-default-props, no-param-reassign */
2+
3+
import React from 'react';
4+
import { CommonMenu, inlineMotion } from './antd';
5+
import '../assets/index.less';
6+
7+
const Demo = () => {
8+
const [inline, setInline] = React.useState(false);
9+
const [openKeys, setOpenKey] = React.useState(['1']);
10+
11+
let restProps = {};
12+
if (inline) {
13+
restProps = { motion: inlineMotion };
14+
} else {
15+
restProps = { openAnimation: 'zoom' };
16+
}
17+
18+
return (
19+
<div style={{ margin: 20, width: 200 }}>
20+
<label>
21+
<input
22+
type="checkbox"
23+
checked={inline}
24+
onChange={() => setInline(!inline)}
25+
/>{' '}
26+
Inline
27+
</label>
28+
<CommonMenu
29+
mode="inline"
30+
openKeys={openKeys}
31+
collapsedWidth={80}
32+
onOpenChange={keys => {
33+
console.error('Open Keys Changed:', keys);
34+
setOpenKey(keys);
35+
}}
36+
inlineCollapsed={!inline}
37+
{...restProps}
38+
/>
39+
</div>
40+
);
41+
};
42+
43+
export default Demo;
44+
/* eslint-enable */

examples/antd.js

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ function handleClick(info) {
1212
const collapseNode = () => ({ height: 0 });
1313
const expandNode = node => ({ height: node.scrollHeight });
1414

15-
const inlineMotion = {
15+
export const inlineMotion = {
1616
motionName: 'rc-menu-collapse',
1717
onAppearStart: collapseNode,
1818
onAppearActive: expandNode,
@@ -95,7 +95,7 @@ const children2 = [
9595

9696
const customizeIndicator = <span>Add More Items</span>;
9797

98-
class CommonMenu extends React.Component {
98+
export class CommonMenu extends React.Component {
9999
state = {
100100
children: children1,
101101
overflowedIndicator: undefined,
@@ -134,11 +134,8 @@ class CommonMenu extends React.Component {
134134
triggerSubMenuAction={triggerSubMenuAction}
135135
onOpenChange={onOpenChange}
136136
selectedKeys={['3']}
137-
mode={this.props.mode}
138-
openAnimation={this.props.openAnimation}
139-
defaultOpenKeys={this.props.defaultOpenKeys}
140137
overflowedIndicator={overflowedIndicator}
141-
motion={this.props.motion}
138+
{...this.props}
142139
>
143140
{children}
144141
</Menu>

src/SubMenu.tsx

Lines changed: 71 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as React from 'react';
22
import * as ReactDOM from 'react-dom';
33
import Trigger from 'rc-trigger';
4+
import raf from 'rc-util/lib/raf';
45
import KeyCode from 'rc-util/lib/KeyCode';
56
import CSSMotion, { CSSMotionProps } from 'rc-motion';
67
import 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]}

src/SubPopupMenu.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -471,9 +471,9 @@ export class SubPopupMenu extends React.Component<SubPopupMenuProps> {
471471
);
472472
}
473473
}
474-
const connected = connect()(SubPopupMenu as any) as React.ComponentClass<
475-
SubPopupMenuProps
476-
> & {
474+
const connected = (connect()(
475+
SubPopupMenu as any,
476+
) as unknown) as React.ComponentClass<SubPopupMenuProps> & {
477477
getWrappedInstance: () => SubPopupMenu;
478478
};
479479

0 commit comments

Comments
 (0)