mirror of
https://gitee.com/ant-design/ant-design.git
synced 2024-11-30 19:19:26 +08:00
fix: Refactor Menu related components life cycle methods (#15868)
* use state instead * rollback demo markdown * clean test * sider context should be clean * update test case * fix test * update debug desc * add siderCollapsed into omit * rm useless code
This commit is contained in:
parent
50deee55c3
commit
89dd827b7f
@ -1,4 +1,6 @@
|
||||
import createContext, { Context } from 'create-react-context';
|
||||
import { ConfigConsumer, ConfigConsumerProps } from '../config-provider';
|
||||
import { LayoutContext, LayoutContextProps } from './layout';
|
||||
|
||||
// matchMedia polyfill for
|
||||
// https://github.com/WickyNilliams/enquire.js/issues/82
|
||||
@ -18,7 +20,6 @@ import * as React from 'react';
|
||||
import { polyfill } from 'react-lifecycles-compat';
|
||||
import classNames from 'classnames';
|
||||
import omit from 'omit.js';
|
||||
import * as PropTypes from 'prop-types';
|
||||
import Icon from '../icon';
|
||||
import isNumeric from '../_util/isNumeric';
|
||||
|
||||
@ -31,6 +32,13 @@ const dimensionMap = {
|
||||
xxl: '1600px',
|
||||
};
|
||||
|
||||
export interface SiderContextProps {
|
||||
siderCollapsed?: boolean;
|
||||
collapsedWidth?: string | number;
|
||||
}
|
||||
|
||||
export const SiderContext: Context<SiderContextProps> = createContext({});
|
||||
|
||||
export type CollapseType = 'clickTrigger' | 'responsive';
|
||||
|
||||
export type SiderTheme = 'light' | 'dark';
|
||||
@ -50,16 +58,14 @@ export interface SiderProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
onBreakpoint?: (broken: boolean) => void;
|
||||
}
|
||||
|
||||
type InternalSideProps = SiderProps & LayoutContextProps;
|
||||
|
||||
export interface SiderState {
|
||||
collapsed?: boolean;
|
||||
below: boolean;
|
||||
belowShow?: boolean;
|
||||
}
|
||||
|
||||
export interface SiderContext {
|
||||
siderCollapsed: boolean;
|
||||
}
|
||||
|
||||
const generateId = (() => {
|
||||
let i = 0;
|
||||
return (prefix: string = '') => {
|
||||
@ -68,9 +74,7 @@ const generateId = (() => {
|
||||
};
|
||||
})();
|
||||
|
||||
class Sider extends React.Component<SiderProps, SiderState> {
|
||||
static __ANT_LAYOUT_SIDER: any = true;
|
||||
|
||||
class InternalSider extends React.Component<InternalSideProps, SiderState> {
|
||||
static defaultProps = {
|
||||
collapsible: false,
|
||||
defaultCollapsed: false,
|
||||
@ -81,16 +85,7 @@ class Sider extends React.Component<SiderProps, SiderState> {
|
||||
theme: 'dark' as SiderTheme,
|
||||
};
|
||||
|
||||
static childContextTypes = {
|
||||
siderCollapsed: PropTypes.bool,
|
||||
collapsedWidth: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
siderHook: PropTypes.object,
|
||||
};
|
||||
|
||||
static getDerivedStateFromProps(nextProps: SiderProps) {
|
||||
static getDerivedStateFromProps(nextProps: InternalSideProps) {
|
||||
if ('collapsed' in nextProps) {
|
||||
return {
|
||||
collapsed: nextProps.collapsed,
|
||||
@ -99,12 +94,10 @@ class Sider extends React.Component<SiderProps, SiderState> {
|
||||
return null;
|
||||
}
|
||||
|
||||
context: any;
|
||||
|
||||
private mql: MediaQueryList;
|
||||
private uniqueId: string;
|
||||
|
||||
constructor(props: SiderProps) {
|
||||
constructor(props: InternalSideProps) {
|
||||
super(props);
|
||||
this.uniqueId = generateId('ant-sider-');
|
||||
let matchMedia;
|
||||
@ -126,21 +119,14 @@ class Sider extends React.Component<SiderProps, SiderState> {
|
||||
};
|
||||
}
|
||||
|
||||
getChildContext() {
|
||||
return {
|
||||
siderCollapsed: this.state.collapsed,
|
||||
collapsedWidth: this.props.collapsedWidth,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.mql) {
|
||||
this.mql.addListener(this.responsiveHandler);
|
||||
this.responsiveHandler(this.mql);
|
||||
}
|
||||
|
||||
if (this.context.siderHook) {
|
||||
this.context.siderHook.addSider(this.uniqueId);
|
||||
if (this.props.siderHook) {
|
||||
this.props.siderHook.addSider(this.uniqueId);
|
||||
}
|
||||
}
|
||||
|
||||
@ -149,8 +135,8 @@ class Sider extends React.Component<SiderProps, SiderState> {
|
||||
this.mql.removeListener(this.responsiveHandler as any);
|
||||
}
|
||||
|
||||
if (this.context.siderHook) {
|
||||
this.context.siderHook.removeSider(this.uniqueId);
|
||||
if (this.props.siderHook) {
|
||||
this.props.siderHook.removeSider(this.uniqueId);
|
||||
}
|
||||
}
|
||||
|
||||
@ -206,6 +192,7 @@ class Sider extends React.Component<SiderProps, SiderState> {
|
||||
'onCollapse',
|
||||
'breakpoint',
|
||||
'onBreakpoint',
|
||||
'siderHook',
|
||||
]);
|
||||
const rawWidth = this.state.collapsed ? collapsedWidth : width;
|
||||
// use "px" as fallback unit for width
|
||||
@ -262,10 +249,29 @@ class Sider extends React.Component<SiderProps, SiderState> {
|
||||
};
|
||||
|
||||
render() {
|
||||
return <ConfigConsumer>{this.renderSider}</ConfigConsumer>;
|
||||
const { collapsed } = this.state;
|
||||
const { collapsedWidth } = this.props;
|
||||
return (
|
||||
<SiderContext.Provider
|
||||
value={{
|
||||
siderCollapsed: collapsed,
|
||||
collapsedWidth,
|
||||
}}
|
||||
>
|
||||
<ConfigConsumer>{this.renderSider}</ConfigConsumer>
|
||||
</SiderContext.Provider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
polyfill(Sider);
|
||||
polyfill(InternalSider);
|
||||
|
||||
export default Sider;
|
||||
export default class Sider extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<LayoutContext.Consumer>
|
||||
{(context: LayoutContextProps) => <InternalSider {...context} {...this.props} />}
|
||||
</LayoutContext.Consumer>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,3 @@
|
||||
import demoTest from '../../../tests/shared/demoTest';
|
||||
|
||||
demoTest('layout');
|
||||
demoTest('layout', { skip: ['custom-trigger-debug.md'] });
|
||||
|
@ -91,9 +91,9 @@ describe('Layout', () => {
|
||||
|
||||
it('should be controlled by collapsed', () => {
|
||||
const wrapper = mount(<Sider>Sider</Sider>);
|
||||
expect(wrapper.instance().state.collapsed).toBe(false);
|
||||
expect(wrapper.find('InternalSider').instance().state.collapsed).toBe(false);
|
||||
wrapper.setProps({ collapsed: true });
|
||||
expect(wrapper.instance().state.collapsed).toBe(true);
|
||||
expect(wrapper.find('InternalSider').instance().state.collapsed).toBe(true);
|
||||
});
|
||||
|
||||
it('should not add ant-layout-has-sider when `hasSider` is `false`', () => {
|
||||
|
125
components/layout/demo/custom-trigger-debug.md
Normal file
125
components/layout/demo/custom-trigger-debug.md
Normal file
@ -0,0 +1,125 @@
|
||||
---
|
||||
order: 99
|
||||
title:
|
||||
zh-CN: 自定义触发器 Debug
|
||||
en-US: Custom trigger debug
|
||||
debug: true
|
||||
---
|
||||
|
||||
## zh-CN
|
||||
|
||||
修改内容前,请尝试此 Demo 查看样式是否抖动。
|
||||
|
||||
````jsx
|
||||
import { Layout, Menu, Icon } from 'antd';
|
||||
|
||||
const { Header, Sider, Content } = Layout;
|
||||
const SubMenu = Menu.SubMenu;
|
||||
|
||||
class SiderDemo extends React.Component {
|
||||
state = {
|
||||
collapsed: true,
|
||||
};
|
||||
|
||||
toggle = () => {
|
||||
this.setState({
|
||||
collapsed: !this.state.collapsed,
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Layout>
|
||||
<Sider trigger={null} collapsible collapsed={this.state.collapsed}>
|
||||
<div className="logo" />
|
||||
<Menu
|
||||
theme="dark"
|
||||
mode="inline"
|
||||
defaultSelectedKeys={['3']}
|
||||
defaultOpenKeys={['sub1']}
|
||||
>
|
||||
<Menu.Item key="1">
|
||||
<Icon type="pie-chart" />
|
||||
<span>Option 1</span>
|
||||
</Menu.Item>
|
||||
<Menu.Item key="2">
|
||||
<Icon type="desktop" />
|
||||
<span>Option 2</span>
|
||||
</Menu.Item>
|
||||
<SubMenu
|
||||
key="sub1"
|
||||
title={
|
||||
<span>
|
||||
<Icon type="user" />
|
||||
<span>User</span>
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<Menu.Item key="3">Tom</Menu.Item>
|
||||
<Menu.Item key="4">Bill</Menu.Item>
|
||||
<Menu.Item key="5">Alex</Menu.Item>
|
||||
</SubMenu>
|
||||
<SubMenu
|
||||
key="sub2"
|
||||
title={
|
||||
<span>
|
||||
<Icon type="team" />
|
||||
<span>Team</span>
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<Menu.Item key="6">Team 1</Menu.Item>
|
||||
<Menu.Item key="8">Team 2</Menu.Item>
|
||||
</SubMenu>
|
||||
<Menu.Item key="9">
|
||||
<Icon type="file" />
|
||||
<span>File</span>
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
</Sider>
|
||||
<Layout>
|
||||
<Header style={{ background: '#fff', padding: 0 }}>
|
||||
<Icon
|
||||
className="trigger"
|
||||
type={this.state.collapsed ? 'menu-unfold' : 'menu-fold'}
|
||||
onClick={this.toggle}
|
||||
/>
|
||||
</Header>
|
||||
<Content
|
||||
style={{
|
||||
margin: '24px 16px',
|
||||
padding: 24,
|
||||
background: '#fff',
|
||||
minHeight: 280,
|
||||
}}
|
||||
>
|
||||
Content
|
||||
</Content>
|
||||
</Layout>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ReactDOM.render(<SiderDemo />, mountNode);
|
||||
````
|
||||
|
||||
````css
|
||||
#components-layout-demo-custom-trigger .trigger {
|
||||
font-size: 18px;
|
||||
line-height: 64px;
|
||||
padding: 0 24px;
|
||||
cursor: pointer;
|
||||
transition: color .3s;
|
||||
}
|
||||
|
||||
#components-layout-demo-custom-trigger .trigger:hover {
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
#components-layout-demo-custom-trigger .logo {
|
||||
height: 32px;
|
||||
background: rgba(255,255,255,.2);
|
||||
margin: 16px;
|
||||
}
|
||||
````
|
@ -1,6 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import * as PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import createContext, { Context } from 'create-react-context';
|
||||
import { SiderProps } from './Sider';
|
||||
import { ConfigConsumer, ConfigConsumerProps } from '../config-provider';
|
||||
|
||||
@ -13,6 +13,19 @@ export interface BasicProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
hasSider?: boolean;
|
||||
}
|
||||
|
||||
export interface LayoutContextProps {
|
||||
siderHook: {
|
||||
addSider: (id: string) => void;
|
||||
removeSider: (id: string) => void;
|
||||
};
|
||||
}
|
||||
export const LayoutContext: Context<LayoutContextProps> = createContext({
|
||||
siderHook: {
|
||||
addSider: () => null,
|
||||
removeSider: () => null,
|
||||
},
|
||||
});
|
||||
|
||||
interface BasicPropsWithTagName extends BasicProps {
|
||||
tagName: 'header' | 'footer' | 'main' | 'section';
|
||||
}
|
||||
@ -52,35 +65,37 @@ interface BasicLayoutState {
|
||||
}
|
||||
|
||||
class BasicLayout extends React.Component<BasicPropsWithTagName, BasicLayoutState> {
|
||||
static childContextTypes = {
|
||||
siderHook: PropTypes.object,
|
||||
};
|
||||
state = { siders: [] };
|
||||
|
||||
getChildContext() {
|
||||
getSiderHook() {
|
||||
return {
|
||||
siderHook: {
|
||||
addSider: (id: string) => {
|
||||
this.setState(state => ({
|
||||
siders: [...state.siders, id],
|
||||
}));
|
||||
},
|
||||
removeSider: (id: string) => {
|
||||
this.setState(state => ({
|
||||
siders: state.siders.filter(currentId => currentId !== id),
|
||||
}));
|
||||
},
|
||||
addSider: (id: string) => {
|
||||
this.setState(state => ({
|
||||
siders: [...state.siders, id],
|
||||
}));
|
||||
},
|
||||
removeSider: (id: string) => {
|
||||
this.setState(state => ({
|
||||
siders: state.siders.filter(currentId => currentId !== id),
|
||||
}));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
const { prefixCls, className, children, hasSider, tagName, ...others } = this.props;
|
||||
const { prefixCls, className, children, hasSider, tagName: Tag, ...others } = this.props;
|
||||
const classString = classNames(className, prefixCls, {
|
||||
[`${prefixCls}-has-sider`]:
|
||||
typeof hasSider === 'boolean' ? hasSider : this.state.siders.length > 0,
|
||||
});
|
||||
return React.createElement(tagName, { className: classString, ...others }, children);
|
||||
|
||||
return (
|
||||
<LayoutContext.Provider value={{ siderHook: this.getSiderHook() }}>
|
||||
<Tag className={classString} {...others}>
|
||||
{children}
|
||||
</Tag>
|
||||
</LayoutContext.Provider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3,6 +3,8 @@ import * as PropTypes from 'prop-types';
|
||||
import { SubMenu as RcSubMenu } from 'rc-menu';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { MenuContext, MenuContextProps } from './index';
|
||||
|
||||
interface TitleEventEntity {
|
||||
key: string;
|
||||
domEvent: Event;
|
||||
@ -25,7 +27,6 @@ class SubMenu extends React.Component<SubMenuProps, any> {
|
||||
};
|
||||
// fix issue:https://github.com/ant-design/ant-design/issues/8666
|
||||
static isSubMenu = 1;
|
||||
context: any;
|
||||
private subMenu: any;
|
||||
|
||||
onKeyDown = (e: React.MouseEvent<HTMLElement>) => {
|
||||
@ -38,13 +39,16 @@ class SubMenu extends React.Component<SubMenuProps, any> {
|
||||
|
||||
render() {
|
||||
const { rootPrefixCls, className } = this.props;
|
||||
const theme = this.context.antdMenuTheme;
|
||||
return (
|
||||
<RcSubMenu
|
||||
{...this.props}
|
||||
ref={this.saveSubMenu}
|
||||
popupClassName={classNames(`${rootPrefixCls}-${theme}`, className)}
|
||||
/>
|
||||
<MenuContext.Consumer>
|
||||
{({ antdMenuTheme }: MenuContextProps) => (
|
||||
<RcSubMenu
|
||||
{...this.props}
|
||||
ref={this.saveSubMenu}
|
||||
popupClassName={classNames(`${rootPrefixCls}-${antdMenuTheme}`, className)}
|
||||
/>
|
||||
)}
|
||||
</MenuContext.Consumer>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
71
components/menu/__tests__/__snapshots__/index.test.js.snap
Normal file
71
components/menu/__tests__/__snapshots__/index.test.js.snap
Normal file
@ -0,0 +1,71 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Menu should controlled collapse work 1`] = `
|
||||
<ul
|
||||
class="ant-menu ant-menu-light ant-menu-root ant-menu-inline"
|
||||
role="menu"
|
||||
>
|
||||
<li
|
||||
class="ant-menu-item"
|
||||
role="menuitem"
|
||||
style="padding-left: 24px;"
|
||||
>
|
||||
<i
|
||||
aria-label="icon: pie-chart"
|
||||
class="anticon anticon-pie-chart"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class=""
|
||||
data-icon="pie-chart"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M864 518H506V160c0-4.4-3.6-8-8-8h-26a398.46 398.46 0 0 0-282.8 117.1 398.19 398.19 0 0 0-85.7 127.1A397.61 397.61 0 0 0 72 552a398.46 398.46 0 0 0 117.1 282.8c36.7 36.7 79.5 65.6 127.1 85.7A397.61 397.61 0 0 0 472 952a398.46 398.46 0 0 0 282.8-117.1c36.7-36.7 65.6-79.5 85.7-127.1A397.61 397.61 0 0 0 872 552v-26c0-4.4-3.6-8-8-8zM705.7 787.8A331.59 331.59 0 0 1 470.4 884c-88.1-.4-170.9-34.9-233.2-97.2C174.5 724.1 140 640.7 140 552c0-88.7 34.5-172.1 97.2-234.8 54.6-54.6 124.9-87.9 200.8-95.5V586h364.3c-7.7 76.3-41.3 147-96.6 201.8zM952 462.4l-2.6-28.2c-8.5-92.1-49.4-179-115.2-244.6A399.4 399.4 0 0 0 589 74.6L560.7 72c-4.7-.4-8.7 3.2-8.7 7.9V464c0 4.4 3.6 8 8 8l384-1c4.7 0 8.4-4 8-8.6zm-332.2-58.2V147.6a332.24 332.24 0 0 1 166.4 89.8c45.7 45.6 77 103.6 90 166.1l-256.4.7z"
|
||||
/>
|
||||
</svg>
|
||||
</i>
|
||||
<span>
|
||||
Option 1
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
`;
|
||||
|
||||
exports[`Menu should controlled collapse work 2`] = `
|
||||
<ul
|
||||
class="ant-menu ant-menu-light ant-menu-inline-collapsed ant-menu-root ant-menu-inline"
|
||||
role="menu"
|
||||
>
|
||||
<li
|
||||
class="ant-menu-item"
|
||||
role="menuitem"
|
||||
style="padding-left: 24px;"
|
||||
>
|
||||
<i
|
||||
aria-label="icon: pie-chart"
|
||||
class="anticon anticon-pie-chart"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class=""
|
||||
data-icon="pie-chart"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M864 518H506V160c0-4.4-3.6-8-8-8h-26a398.46 398.46 0 0 0-282.8 117.1 398.19 398.19 0 0 0-85.7 127.1A397.61 397.61 0 0 0 72 552a398.46 398.46 0 0 0 117.1 282.8c36.7 36.7 79.5 65.6 127.1 85.7A397.61 397.61 0 0 0 472 952a398.46 398.46 0 0 0 282.8-117.1c36.7-36.7 65.6-79.5 85.7-127.1A397.61 397.61 0 0 0 872 552v-26c0-4.4-3.6-8-8-8zM705.7 787.8A331.59 331.59 0 0 1 470.4 884c-88.1-.4-170.9-34.9-233.2-97.2C174.5 724.1 140 640.7 140 552c0-88.7 34.5-172.1 97.2-234.8 54.6-54.6 124.9-87.9 200.8-95.5V586h364.3c-7.7 76.3-41.3 147-96.6 201.8zM952 462.4l-2.6-28.2c-8.5-92.1-49.4-179-115.2-244.6A399.4 399.4 0 0 0 589 74.6L560.7 72c-4.7-.4-8.7 3.2-8.7 7.9V464c0 4.4 3.6 8 8 8l384-1c4.7 0 8.4-4 8-8.6zm-332.2-58.2V147.6a332.24 332.24 0 0 1 166.4 89.8c45.7 45.6 77 103.6 90 166.1l-256.4.7z"
|
||||
/>
|
||||
</svg>
|
||||
</i>
|
||||
<span>
|
||||
Option 1
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
`;
|
@ -556,37 +556,13 @@ describe('Menu', () => {
|
||||
it('get correct animation type when switched from inline', () => {
|
||||
const wrapper = mount(<Menu mode="inline" />);
|
||||
wrapper.setProps({ mode: 'horizontal' });
|
||||
expect(wrapper.instance().getMenuOpenAnimation('')).toBe('');
|
||||
expect(wrapper.instance().switchingModeFromInline).toBe(false);
|
||||
});
|
||||
|
||||
it('Menu should not shake when collapsed changed', () => {
|
||||
const wrapper = mount(
|
||||
<Menu
|
||||
defaultSelectedKeys={['5']}
|
||||
defaultOpenKeys={['sub1']}
|
||||
mode="inline"
|
||||
inlineCollapsed={false}
|
||||
>
|
||||
<SubMenu
|
||||
key="sub1"
|
||||
title={
|
||||
<span>
|
||||
<span>Navigation One</span>
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<Menu.Item key="5">Option 5</Menu.Item>
|
||||
<Menu.Item key="6">Option 6</Menu.Item>
|
||||
</SubMenu>
|
||||
</Menu>,
|
||||
);
|
||||
expect(wrapper.instance().contextSiderCollapsed).toBe(true);
|
||||
wrapper.setProps({ inlineCollapsed: true });
|
||||
expect(wrapper.instance().contextSiderCollapsed).toBe(false);
|
||||
jest.runAllTimers();
|
||||
wrapper.update();
|
||||
expect(wrapper.instance().contextSiderCollapsed).toBe(false);
|
||||
expect(
|
||||
wrapper
|
||||
.find('InternalMenu')
|
||||
.instance()
|
||||
.getMenuOpenAnimation(''),
|
||||
).toBe('');
|
||||
expect(wrapper.find('InternalMenu').state().switchingModeFromInline).toBe(false);
|
||||
});
|
||||
|
||||
it('MenuItem should not render Tooltip when inlineCollapsed is false', () => {
|
||||
@ -615,4 +591,21 @@ describe('Menu', () => {
|
||||
wrapper.update();
|
||||
expect(wrapper.find('.ant-tooltip-inner').length).toBe(0);
|
||||
});
|
||||
|
||||
it('should controlled collapse work', () => {
|
||||
const wrapper = mount(
|
||||
<Menu mode="inline" inlineCollapsed={false}>
|
||||
<Menu.Item key="1">
|
||||
<Icon type="pie-chart" />
|
||||
<span>Option 1</span>
|
||||
</Menu.Item>
|
||||
</Menu>,
|
||||
);
|
||||
|
||||
expect(wrapper.render()).toMatchSnapshot();
|
||||
|
||||
wrapper.setProps({ inlineCollapsed: true });
|
||||
|
||||
expect(wrapper.render()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
@ -1,13 +1,15 @@
|
||||
import * as React from 'react';
|
||||
import RcMenu, { Divider, ItemGroup } from 'rc-menu';
|
||||
import * as PropTypes from 'prop-types';
|
||||
import createContext, { Context } from 'create-react-context';
|
||||
import classNames from 'classnames';
|
||||
import omit from 'omit.js';
|
||||
import SubMenu from './SubMenu';
|
||||
import Item from './MenuItem';
|
||||
import { ConfigConsumer, ConfigConsumerProps } from '../config-provider';
|
||||
import animation from '../_util/openAnimation';
|
||||
import warning from '../_util/warning';
|
||||
import { polyfill } from 'react-lifecycles-compat';
|
||||
import { SiderContext, SiderContextProps } from '../layout/Sider';
|
||||
|
||||
export interface SelectParam {
|
||||
key: string;
|
||||
@ -57,42 +59,70 @@ export interface MenuProps {
|
||||
overflowedIndicator?: React.ReactNode;
|
||||
}
|
||||
|
||||
type InternalMenuProps = MenuProps & SiderContextProps;
|
||||
|
||||
export interface MenuState {
|
||||
openKeys: string[];
|
||||
|
||||
// This may be not best way since origin code use `this.switchingModeFromInline` to handle collapse management.
|
||||
// But for current test, seems it's OK just use state.
|
||||
switchingModeFromInline: boolean;
|
||||
inlineOpenKeys: string[];
|
||||
prevProps: InternalMenuProps;
|
||||
}
|
||||
|
||||
class Menu extends React.Component<MenuProps, MenuState> {
|
||||
static Divider = Divider;
|
||||
static Item = Item;
|
||||
static SubMenu = SubMenu;
|
||||
static ItemGroup = ItemGroup;
|
||||
export interface MenuContextProps {
|
||||
inlineCollapsed: boolean;
|
||||
antdMenuTheme?: MenuTheme;
|
||||
}
|
||||
|
||||
export const MenuContext: Context<MenuContextProps> = createContext({
|
||||
inlineCollapsed: false,
|
||||
});
|
||||
|
||||
class InternalMenu extends React.Component<InternalMenuProps, MenuState> {
|
||||
static defaultProps: Partial<MenuProps> = {
|
||||
className: '',
|
||||
theme: 'light', // or dark
|
||||
focusable: false,
|
||||
};
|
||||
static childContextTypes = {
|
||||
inlineCollapsed: PropTypes.bool,
|
||||
antdMenuTheme: PropTypes.string,
|
||||
};
|
||||
static contextTypes = {
|
||||
siderCollapsed: PropTypes.bool,
|
||||
collapsedWidth: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
||||
};
|
||||
|
||||
static getDerivedStateFromProps(nextProps: MenuProps) {
|
||||
if ('openKeys' in nextProps) {
|
||||
return { openKeys: nextProps.openKeys! };
|
||||
static getDerivedStateFromProps(nextProps: InternalMenuProps, prevState: MenuState) {
|
||||
const { prevProps } = prevState;
|
||||
const newState: Partial<MenuState> = {
|
||||
prevProps: nextProps,
|
||||
};
|
||||
if (prevProps.mode === 'inline' && nextProps.mode !== 'inline') {
|
||||
newState.switchingModeFromInline = true;
|
||||
}
|
||||
return null;
|
||||
|
||||
if ('openKeys' in nextProps) {
|
||||
newState.openKeys = nextProps.openKeys;
|
||||
} else {
|
||||
// [Legacy] Old code will return after `openKeys` changed.
|
||||
// Not sure the reason, we should keep this logic still.
|
||||
if (
|
||||
(nextProps.inlineCollapsed && !prevProps.inlineCollapsed) ||
|
||||
(nextProps.siderCollapsed && !prevProps.siderCollapsed)
|
||||
) {
|
||||
newState.switchingModeFromInline = true;
|
||||
newState.inlineOpenKeys = prevState.openKeys;
|
||||
newState.openKeys = [];
|
||||
}
|
||||
|
||||
if (
|
||||
(!nextProps.inlineCollapsed && prevProps.inlineCollapsed) ||
|
||||
(!nextProps.siderCollapsed && prevProps.siderCollapsed)
|
||||
) {
|
||||
newState.openKeys = prevState.inlineOpenKeys;
|
||||
newState.inlineOpenKeys = [];
|
||||
}
|
||||
}
|
||||
|
||||
return newState;
|
||||
}
|
||||
|
||||
context: any;
|
||||
switchingModeFromInline: boolean;
|
||||
inlineOpenKeys: string[] = [];
|
||||
contextSiderCollapsed: boolean = true;
|
||||
|
||||
constructor(props: MenuProps) {
|
||||
constructor(props: InternalMenuProps) {
|
||||
super(props);
|
||||
|
||||
warning(
|
||||
@ -117,43 +147,18 @@ class Menu extends React.Component<MenuProps, MenuState> {
|
||||
|
||||
this.state = {
|
||||
openKeys: openKeys || [],
|
||||
switchingModeFromInline: false,
|
||||
inlineOpenKeys: [],
|
||||
prevProps: props,
|
||||
};
|
||||
}
|
||||
|
||||
getChildContext() {
|
||||
return {
|
||||
inlineCollapsed: this.getInlineCollapsed(),
|
||||
antdMenuTheme: this.props.theme,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: MenuProps) {
|
||||
if (prevProps.mode === 'inline' && this.props.mode !== 'inline') {
|
||||
this.switchingModeFromInline = true;
|
||||
}
|
||||
if (
|
||||
(this.props.inlineCollapsed && !prevProps.inlineCollapsed) ||
|
||||
(this.getInlineCollapsed() && this.contextSiderCollapsed)
|
||||
) {
|
||||
this.contextSiderCollapsed = false;
|
||||
this.switchingModeFromInline = true;
|
||||
this.inlineOpenKeys = this.state.openKeys;
|
||||
this.setState({ openKeys: [] });
|
||||
}
|
||||
if (
|
||||
(!this.props.inlineCollapsed && prevProps.inlineCollapsed) ||
|
||||
(!this.getInlineCollapsed() && !this.contextSiderCollapsed)
|
||||
) {
|
||||
this.contextSiderCollapsed = true;
|
||||
this.setState({ openKeys: this.inlineOpenKeys });
|
||||
this.inlineOpenKeys = [];
|
||||
}
|
||||
}
|
||||
|
||||
restoreModeVerticalFromInline() {
|
||||
if (this.switchingModeFromInline) {
|
||||
this.switchingModeFromInline = false;
|
||||
this.setState({});
|
||||
const { switchingModeFromInline } = this.state;
|
||||
if (switchingModeFromInline) {
|
||||
this.setState({
|
||||
switchingModeFromInline: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -175,7 +180,7 @@ class Menu extends React.Component<MenuProps, MenuState> {
|
||||
|
||||
// Fix SVGElement e.target.className.indexOf is not a function
|
||||
// https://github.com/ant-design/ant-design/issues/15699
|
||||
const { className } = e.target as (HTMLElement | SVGElement);
|
||||
const { className } = e.target as HTMLElement | SVGElement;
|
||||
// SVGAnimatedString.animVal should be identical to SVGAnimatedString.baseVal, unless during an animation.
|
||||
const classNameValue =
|
||||
Object.prototype.toString.call(className) === '[object SVGAnimatedString]'
|
||||
@ -216,7 +221,7 @@ class Menu extends React.Component<MenuProps, MenuState> {
|
||||
|
||||
getRealMenuMode() {
|
||||
const inlineCollapsed = this.getInlineCollapsed();
|
||||
if (this.switchingModeFromInline && inlineCollapsed) {
|
||||
if (this.state.switchingModeFromInline && inlineCollapsed) {
|
||||
return 'inline';
|
||||
}
|
||||
const { mode } = this.props;
|
||||
@ -225,11 +230,8 @@ class Menu extends React.Component<MenuProps, MenuState> {
|
||||
|
||||
getInlineCollapsed() {
|
||||
const { inlineCollapsed } = this.props;
|
||||
if (this.context.siderCollapsed !== undefined) {
|
||||
return this.context.siderCollapsed;
|
||||
}
|
||||
if (this.contextSiderCollapsed) {
|
||||
return false;
|
||||
if (this.props.siderCollapsed !== undefined) {
|
||||
return this.props.siderCollapsed;
|
||||
}
|
||||
return inlineCollapsed;
|
||||
}
|
||||
@ -245,9 +247,12 @@ class Menu extends React.Component<MenuProps, MenuState> {
|
||||
} else {
|
||||
// When mode switch from inline
|
||||
// submenu should hide without animation
|
||||
if (this.switchingModeFromInline) {
|
||||
if (this.state.switchingModeFromInline) {
|
||||
menuOpenAnimation = '';
|
||||
this.switchingModeFromInline = false;
|
||||
this.setState({
|
||||
switchingModeFromInline: false,
|
||||
});
|
||||
// this.switchingModeFromInline = false;
|
||||
} else {
|
||||
menuOpenAnimation = 'zoom-big';
|
||||
}
|
||||
@ -257,7 +262,8 @@ class Menu extends React.Component<MenuProps, MenuState> {
|
||||
}
|
||||
|
||||
renderMenu = ({ getPopupContainer, getPrefixCls }: ConfigConsumerProps) => {
|
||||
const { prefixCls: customizePrefixCls, className, theme } = this.props;
|
||||
const { prefixCls: customizePrefixCls, className, theme, collapsedWidth } = this.props;
|
||||
const passProps = omit(this.props, ['collapsedWidth', 'siderCollapsed']);
|
||||
const menuMode = this.getRealMenuMode();
|
||||
const menuOpenAnimation = this.getMenuOpenAnimation(menuMode!);
|
||||
|
||||
@ -282,7 +288,6 @@ class Menu extends React.Component<MenuProps, MenuState> {
|
||||
}
|
||||
|
||||
// https://github.com/ant-design/ant-design/issues/8587
|
||||
const { collapsedWidth } = this.context;
|
||||
if (
|
||||
this.getInlineCollapsed() &&
|
||||
(collapsedWidth === 0 || collapsedWidth === '0' || collapsedWidth === '0px')
|
||||
@ -293,7 +298,7 @@ class Menu extends React.Component<MenuProps, MenuState> {
|
||||
return (
|
||||
<RcMenu
|
||||
getPopupContainer={getPopupContainer}
|
||||
{...this.props}
|
||||
{...passProps}
|
||||
{...menuProps}
|
||||
prefixCls={prefixCls}
|
||||
onTransitionEnd={this.handleTransitionEnd}
|
||||
@ -303,10 +308,33 @@ class Menu extends React.Component<MenuProps, MenuState> {
|
||||
};
|
||||
|
||||
render() {
|
||||
return <ConfigConsumer>{this.renderMenu}</ConfigConsumer>;
|
||||
return (
|
||||
<MenuContext.Provider
|
||||
value={{
|
||||
inlineCollapsed: this.getInlineCollapsed() || false,
|
||||
antdMenuTheme: this.props.theme,
|
||||
}}
|
||||
>
|
||||
<ConfigConsumer>{this.renderMenu}</ConfigConsumer>
|
||||
</MenuContext.Provider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
polyfill(Menu);
|
||||
polyfill(InternalMenu);
|
||||
|
||||
export default Menu;
|
||||
// We should keep this as ref-able
|
||||
export default class Menu extends React.Component<MenuProps, {}> {
|
||||
static Divider = Divider;
|
||||
static Item = Item;
|
||||
static SubMenu = SubMenu;
|
||||
static ItemGroup = ItemGroup;
|
||||
|
||||
render() {
|
||||
return (
|
||||
<SiderContext.Consumer>
|
||||
{(context: SiderContextProps) => <InternalMenu {...this.props} {...context} />}
|
||||
</SiderContext.Consumer>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -2,4 +2,5 @@ import '../../style/index.less';
|
||||
import './index.less';
|
||||
|
||||
// style dependencies
|
||||
// deps-lint-skip: layout
|
||||
import '../../tooltip/style';
|
||||
|
@ -483,6 +483,7 @@ exports[`Table.rowSelection render with default selection correctly 1`] = `
|
||||
<ul
|
||||
class="ant-dropdown-menu ant-table-selection-menu ant-dropdown-menu-light ant-dropdown-menu-root ant-dropdown-menu-vertical"
|
||||
role="menu"
|
||||
tabindex="0"
|
||||
>
|
||||
<li
|
||||
class="ant-dropdown-menu-item"
|
||||
|
Loading…
Reference in New Issue
Block a user