refactor(layout.sider): rewrite with hook (#27719)

* refactor(sider): rewrite with hook

* Update layout.tsx

* Update layout.tsx
This commit is contained in:
Tom Xu 2020-11-20 08:56:12 +08:00 committed by GitHub
parent 1f935803f8
commit 050185bce1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 154 additions and 161 deletions

View File

@ -5,8 +5,8 @@ import BarsOutlined from '@ant-design/icons/BarsOutlined';
import RightOutlined from '@ant-design/icons/RightOutlined';
import LeftOutlined from '@ant-design/icons/LeftOutlined';
import { LayoutContext, LayoutContextProps } from './layout';
import { ConfigConsumer, ConfigConsumerProps } from '../config-provider';
import { LayoutContext } from './layout';
import { ConfigContext } from '../config-provider';
import isNumeric from '../_util/isNumeric';
const dimensionMaxMap = {
@ -45,8 +45,6 @@ export interface SiderProps extends React.HTMLAttributes<HTMLDivElement> {
onBreakpoint?: (broken: boolean) => void;
}
type InternalSideProps = SiderProps & LayoutContextProps;
export interface SiderState {
collapsed?: boolean;
below: boolean;
@ -60,129 +58,94 @@ const generateId = (() => {
};
})();
class InternalSider extends React.Component<InternalSideProps, SiderState> {
static defaultProps = {
collapsible: false,
defaultCollapsed: false,
reverseArrow: false,
width: 200,
collapsedWidth: 80,
style: {},
theme: 'dark' as SiderTheme,
};
const Sider: React.FC<SiderProps> = ({
prefixCls: customizePrefixCls,
className,
trigger,
children,
defaultCollapsed = false,
theme = 'dark',
style = {},
collapsible = false,
reverseArrow = false,
width = 200,
collapsedWidth = 80,
zeroWidthTriggerStyle,
breakpoint,
onCollapse,
onBreakpoint,
...props
}) => {
const { siderHook } = React.useContext(LayoutContext);
static getDerivedStateFromProps(nextProps: InternalSideProps) {
if ('collapsed' in nextProps) {
return {
collapsed: nextProps.collapsed,
};
}
return null;
}
const [collapsed, setCollapsed] = React.useState(
'collapsed' in props ? props.collapsed : defaultCollapsed,
);
const [below, setBelow] = React.useState(false);
private mql: MediaQueryList;
private uniqueId: string;
constructor(props: InternalSideProps) {
super(props);
this.uniqueId = generateId('ant-sider-');
let matchMedia;
if (typeof window !== 'undefined') {
matchMedia = window.matchMedia;
}
if (matchMedia && props.breakpoint && props.breakpoint in dimensionMaxMap) {
this.mql = matchMedia(`(max-width: ${dimensionMaxMap[props.breakpoint]})`);
}
let collapsed;
React.useEffect(() => {
if ('collapsed' in props) {
collapsed = props.collapsed;
} else {
collapsed = props.defaultCollapsed;
setCollapsed(props.collapsed);
}
this.state = {
collapsed,
below: false,
};
}
}, [props.collapsed]);
componentDidMount() {
if (this.mql) {
try {
this.mql.addEventListener('change', this.responsiveHandler);
} catch (error) {
this.mql.addListener(this.responsiveHandler);
}
this.responsiveHandler(this.mql);
const handleSetCollapsed = (value: boolean, type: CollapseType) => {
if (!('collapsed' in props)) {
setCollapsed(value);
}
this.props?.siderHook.addSider(this.uniqueId);
}
componentWillUnmount() {
try {
this.mql?.removeEventListener('change', this.responsiveHandler);
} catch (error) {
this.mql?.removeListener(this.responsiveHandler);
}
this.props?.siderHook.removeSider(this.uniqueId);
}
responsiveHandler = (mql: MediaQueryListEvent | MediaQueryList) => {
this.setState({ below: mql.matches });
const { onBreakpoint } = this.props;
const { collapsed } = this.state;
if (onBreakpoint) {
onBreakpoint(mql.matches);
}
if (collapsed !== mql.matches) {
this.setCollapsed(mql.matches, 'responsive');
}
};
setCollapsed = (collapsed: boolean, type: CollapseType) => {
if (!('collapsed' in this.props)) {
this.setState({
collapsed,
});
}
const { onCollapse } = this.props;
if (onCollapse) {
onCollapse(collapsed, type);
onCollapse(value, type);
}
};
toggle = () => {
const collapsed = !this.state.collapsed;
this.setCollapsed(collapsed, 'clickTrigger');
React.useEffect(() => {
const responsiveHandler = (mql: MediaQueryListEvent | MediaQueryList) => {
setBelow(mql.matches);
if (onBreakpoint) {
onBreakpoint(mql.matches);
}
if (collapsed !== mql.matches) {
handleSetCollapsed(mql.matches, 'responsive');
}
};
let mql: MediaQueryList;
if (typeof window !== 'undefined') {
const { matchMedia } = window;
if (matchMedia && breakpoint && breakpoint in dimensionMaxMap) {
mql = matchMedia(`(max-width: ${dimensionMaxMap[breakpoint]})`);
try {
mql.addEventListener('change', responsiveHandler);
} catch (error) {
mql.addListener(responsiveHandler);
}
responsiveHandler(mql);
}
}
return () => {
try {
mql?.removeEventListener('change', responsiveHandler);
} catch (error) {
mql?.removeListener(responsiveHandler);
}
};
}, [onBreakpoint, collapsed]);
React.useEffect(() => {
const uniqueId = generateId('ant-sider-');
siderHook.addSider(uniqueId);
return () => siderHook.removeSider(uniqueId);
}, []);
const toggle = () => {
handleSetCollapsed(!collapsed, 'clickTrigger');
};
renderSider = ({ getPrefixCls }: ConfigConsumerProps) => {
const {
prefixCls: customizePrefixCls,
className,
theme,
collapsible,
reverseArrow,
trigger,
style,
width,
collapsedWidth,
zeroWidthTriggerStyle,
children,
...others
} = this.props;
const { collapsed, below } = this.state;
const { getPrefixCls } = React.useContext(ConfigContext);
const renderSider = () => {
const prefixCls = getPrefixCls('layout-sider', customizePrefixCls);
const divProps = omit(others, [
'collapsed',
'defaultCollapsed',
'onCollapse',
'breakpoint',
'onBreakpoint',
'siderHook',
'zeroWidthTriggerStyle',
]);
const divProps = omit(props, ['collapsed']);
const rawWidth = collapsed ? collapsedWidth : width;
// use "px" as fallback unit for width
const siderWidth = isNumeric(rawWidth) ? `${rawWidth}px` : String(rawWidth);
@ -190,7 +153,7 @@ class InternalSider extends React.Component<InternalSideProps, SiderState> {
const zeroWidthTrigger =
parseFloat(String(collapsedWidth || 0)) === 0 ? (
<span
onClick={this.toggle}
onClick={toggle}
className={classNames(
`${prefixCls}-zero-width-trigger`,
`${prefixCls}-zero-width-trigger-${reverseArrow ? 'right' : 'left'}`,
@ -209,11 +172,7 @@ class InternalSider extends React.Component<InternalSideProps, SiderState> {
const triggerDom =
trigger !== null
? zeroWidthTrigger || (
<div
className={`${prefixCls}-trigger`}
onClick={this.toggle}
style={{ width: siderWidth }}
>
<div className={`${prefixCls}-trigger`} onClick={toggle} style={{ width: siderWidth }}>
{trigger || defaultTrigger}
</div>
)
@ -244,29 +203,16 @@ class InternalSider extends React.Component<InternalSideProps, SiderState> {
);
};
render() {
const { collapsed } = this.state;
const { collapsedWidth } = this.props;
return (
<SiderContext.Provider
value={{
siderCollapsed: collapsed,
collapsedWidth,
}}
>
<ConfigConsumer>{this.renderSider}</ConfigConsumer>
</SiderContext.Provider>
);
}
}
return (
<SiderContext.Provider
value={{
siderCollapsed: collapsed,
collapsedWidth,
}}
>
{renderSider()}
</SiderContext.Provider>
);
};
// eslint-disable-next-line react/prefer-stateless-function
export default class Sider extends React.Component {
render() {
return (
<LayoutContext.Consumer>
{(context: LayoutContextProps) => <InternalSider {...context} {...this.props} />}
</LayoutContext.Consumer>
);
}
}
export default Sider;

View File

@ -35,3 +35,49 @@ exports[`Layout rtl render component should be rendered correctly in RTL directi
/>
</aside>
`;
exports[`Layout should be controlled by collapsed 1`] = `
<Sider>
<aside
className="ant-layout-sider ant-layout-sider-dark"
style={
Object {
"flex": "0 0 200px",
"maxWidth": "200px",
"minWidth": "200px",
"width": "200px",
}
}
>
<div
className="ant-layout-sider-children"
>
Sider
</div>
</aside>
</Sider>
`;
exports[`Layout should be controlled by collapsed 2`] = `
<Sider
collapsed={true}
>
<aside
className="ant-layout-sider ant-layout-sider-dark ant-layout-sider-collapsed"
style={
Object {
"flex": "0 0 80px",
"maxWidth": "80px",
"minWidth": "80px",
"width": "80px",
}
}
>
<div
className="ant-layout-sider-children"
>
Sider
</div>
</aside>
</Sider>
`;

View File

@ -127,9 +127,10 @@ describe('Layout', () => {
it('should be controlled by collapsed', () => {
const wrapper = mount(<Sider>Sider</Sider>);
expect(wrapper.find('InternalSider').instance().state.collapsed).toBe(false);
expect(wrapper).toMatchSnapshot();
wrapper.setProps({ collapsed: true });
expect(wrapper.find('InternalSider').instance().state.collapsed).toBe(true);
wrapper.update();
expect(wrapper).toMatchSnapshot();
});
it('should not add ant-layout-has-sider when `hasSider` is `false`', () => {

View File

@ -1,5 +1,5 @@
import InternalLayout, { BasicProps, Content, Footer, Header } from './layout';
import Sider, { SiderProps } from './Sider';
import Sider from './Sider';
export { BasicProps as LayoutProps } from './layout';
export { SiderProps } from './Sider';
@ -8,7 +8,7 @@ interface LayoutType extends React.FC<BasicProps> {
Header: typeof Header;
Footer: typeof Footer;
Content: typeof Content;
Sider: React.ComponentClass<SiderProps>;
Sider: typeof Sider;
}
const Layout = InternalLayout as LayoutType;

View File

@ -54,17 +54,6 @@ const BasicLayout: React.FC<BasicPropsWithTagName> = props => {
const [siders, setSiders] = React.useState<string[]>([]);
const getSiderHook = () => {
return {
addSider: (id: string) => {
setSiders([...siders, id]);
},
removeSider: (id: string) => {
setSiders(siders.filter(currentId => currentId !== id));
},
};
};
const { prefixCls, className, children, hasSider, tagName: Tag, ...others } = props;
const classString = classNames(
prefixCls,
@ -76,7 +65,18 @@ const BasicLayout: React.FC<BasicPropsWithTagName> = props => {
);
return (
<LayoutContext.Provider value={{ siderHook: getSiderHook() }}>
<LayoutContext.Provider
value={{
siderHook: {
addSider: (id: string) => {
setSiders(prev => [...prev, id]);
},
removeSider: (id: string) => {
setSiders(prev => prev.filter(currentId => currentId !== id));
},
},
}}
>
<Tag className={classString} {...others}>
{children}
</Tag>