Browse Source

refactor(drawer): rewrite with hook from @hosseinmd (#29229)

* refactor: drawer to functional component and  hooks

* chore: clean code default value

* perf: remove doesn't necessary useCallback

* chore: useContext instead of Consumer

* fix ref

* Add Drawer displayName

* fix tsc

* fix tsc

Co-authored-by: Hossein Mohammadi <hosseinm.developer@gmail.com>
pull/29226/head
afc163 4 years ago
committed by GitHub
parent
commit
4ee9253467
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 11
      components/drawer/__tests__/Drawer.test.js
  2. 23
      components/drawer/__tests__/type.test.tsx
  3. 474
      components/drawer/index.tsx

11
components/drawer/__tests__/Drawer.test.js

@ -147,4 +147,15 @@ describe('Drawer', () => {
errorSpy.mockRestore();
});
it('should support ref', () => {
const ref = React.createRef();
mount(
<Drawer visible ref={ref} width={400}>
Here is content of Drawer
</Drawer>,
);
expect(typeof ref.current.push).toBe('function');
expect(typeof ref.current.pull).toBe('function');
});
});

23
components/drawer/__tests__/type.test.tsx

@ -0,0 +1,23 @@
import * as React from 'react';
import Drawer from '..';
describe('Drawer.typescript', () => {
it('Drawer', () => {
const onClose = jest.fn();
const wrapper = (
<Drawer
title="Basic Drawer"
placement="right"
closable={false}
onClose={onClose}
visible={false}
>
<p>Some contents...</p>
<p>Some contents...</p>
<p>Some contents...</p>
</Drawer>
);
expect(wrapper).toBeTruthy();
});
});

474
components/drawer/index.tsx

@ -3,11 +3,16 @@ import RcDrawer from 'rc-drawer';
import getScrollBarSize from 'rc-util/lib/getScrollBarSize';
import CloseOutlined from '@ant-design/icons/CloseOutlined';
import classNames from 'classnames';
import omit from 'rc-util/lib/omit';
import { ConfigContext, DirectionType } from '../config-provider';
import { tuple } from '../_util/type';
import useForceUpdate from '../_util/hooks/useForceUpdate';
const DrawerContext = React.createContext<Drawer | null>(null);
type DrawerRef = {
push(): void;
pull(): void;
};
const DrawerContext = React.createContext<DrawerRef | null>(null);
type EventType =
| React.KeyboardEvent<HTMLDivElement>
@ -50,6 +55,7 @@ export interface DrawerProps {
keyboard?: boolean;
footer?: React.ReactNode;
footerStyle?: React.CSSProperties;
level?: string | string[] | null | undefined;
}
export interface IDrawerState {
@ -61,223 +67,222 @@ interface InternalDrawerProps extends DrawerProps {
}
const defaultPushState: PushState = { distance: 180 };
class Drawer extends React.Component<InternalDrawerProps, IDrawerState> {
static defaultProps = {
width: 256,
height: 256,
closable: true,
placement: 'right' as placementType,
maskClosable: true,
mask: true,
level: null,
keyboard: true,
push: defaultPushState,
};
readonly state = {
push: false,
};
parentDrawer: Drawer | null;
destroyClose: boolean;
public componentDidMount() {
// fix: delete drawer in child and re-render, no push started.
// <Drawer>{show && <Drawer />}</Drawer>
const { visible } = this.props;
if (visible && this.parentDrawer) {
this.parentDrawer.push();
}
}
public componentDidUpdate(preProps: DrawerProps) {
const { visible } = this.props;
if (preProps.visible !== visible && this.parentDrawer) {
if (visible) {
this.parentDrawer.push();
} else {
this.parentDrawer.pull();
const Drawer = React.forwardRef<DrawerRef, InternalDrawerProps>(
(
{
width = 256,
height = 256,
closable = true,
placement = 'right' as placementType,
maskClosable = true,
mask = true,
level = null,
keyboard = true,
push = defaultPushState,
closeIcon = <CloseOutlined />,
bodyStyle,
drawerStyle,
prefixCls,
className,
direction,
visible,
children,
zIndex,
destroyOnClose,
style,
title,
headerStyle,
onClose,
footer,
footerStyle,
...rest
},
ref,
) => {
const forceUpdate = useForceUpdate();
const [internalPush, setPush] = React.useState(false);
const parentDrawer = React.useContext(DrawerContext);
const destroyClose = React.useRef<boolean>(false);
React.useEffect(() => {
// fix: delete drawer in child and re-render, no push started.
// <Drawer>{show && <Drawer />}</Drawer>
if (visible && parentDrawer) {
parentDrawer.push();
}
}
}
public componentWillUnmount() {
// unmount drawer in child, clear push.
if (this.parentDrawer) {
this.parentDrawer.pull();
this.parentDrawer = null;
}
}
push = () => {
if (this.props.push) {
this.setState({ push: true });
}
};
pull = () => {
if (this.props.push) {
this.setState({ push: false });
}
};
return () => {
if (parentDrawer) {
parentDrawer.pull();
// parentDrawer = null;
}
};
}, []);
React.useEffect(() => {
if (parentDrawer) {
if (visible) {
parentDrawer.push();
} else {
parentDrawer.pull();
}
}
}, [visible]);
onDestroyTransitionEnd = () => {
const isDestroyOnClose = this.getDestroyOnClose();
if (!isDestroyOnClose) {
return;
}
if (!this.props.visible) {
this.destroyClose = true;
this.forceUpdate();
}
};
const operations = React.useMemo(
() => ({
push() {
if (push) {
setPush(true);
}
},
pull() {
if (push) {
setPush(false);
}
},
}),
[push],
);
getDestroyOnClose = () => this.props.destroyOnClose && !this.props.visible;
React.useImperativeHandle(ref, () => operations, [operations]);
getPushDistance = () => {
const { push } = this.props;
let distance: number | string;
if (typeof push === 'boolean') {
distance = push ? defaultPushState.distance : 0;
} else {
distance = push!.distance;
}
return parseFloat(String(distance || 0));
};
const isDestroyOnClose = destroyOnClose && !visible;
// get drawer push width or height
getPushTransform = (placement?: placementType) => {
const distance = this.getPushDistance();
const onDestroyTransitionEnd = () => {
if (!isDestroyOnClose) {
return;
}
if (!visible) {
destroyClose.current = true;
forceUpdate();
}
};
if (placement === 'left' || placement === 'right') {
return `translateX(${placement === 'left' ? distance : -distance}px)`;
}
if (placement === 'top' || placement === 'bottom') {
return `translateY(${placement === 'top' ? distance : -distance}px)`;
}
};
const getOffsetStyle = () => {
// https://github.com/ant-design/ant-design/issues/24287
if (!visible && !mask) {
return {};
}
const offsetStyle: any = {};
if (placement === 'left' || placement === 'right') {
offsetStyle.width = width;
} else {
offsetStyle.height = height;
}
return offsetStyle;
};
getOffsetStyle() {
const { placement, width, height, visible, mask } = this.props;
// https://github.com/ant-design/ant-design/issues/24287
if (!visible && !mask) {
return {};
}
const offsetStyle: any = {};
if (placement === 'left' || placement === 'right') {
offsetStyle.width = width;
} else {
offsetStyle.height = height;
}
return offsetStyle;
}
getRcDrawerStyle = () => {
const { zIndex, placement, mask, style } = this.props;
const { push } = this.state;
// 当无 mask 时,将 width 应用到外层容器上
// 解决 https://github.com/ant-design/ant-design/issues/12401 的问题
const offsetStyle = mask ? {} : this.getOffsetStyle();
return {
zIndex,
transform: push ? this.getPushTransform(placement) : undefined,
...offsetStyle,
...style,
const getRcDrawerStyle = () => {
// get drawer push width or height
const getPushTransform = (_placement?: placementType) => {
let distance: number | string;
if (typeof push === 'boolean') {
distance = push ? defaultPushState.distance : 0;
} else {
distance = push!.distance;
}
distance = parseFloat(String(distance || 0));
if (_placement === 'left' || _placement === 'right') {
return `translateX(${_placement === 'left' ? distance : -distance}px)`;
}
if (_placement === 'top' || _placement === 'bottom') {
return `translateY(${_placement === 'top' ? distance : -distance}px)`;
}
};
// 当无 mask 时,将 width 应用到外层容器上
// 解决 https://github.com/ant-design/ant-design/issues/12401 的问题
const offsetStyle = mask ? {} : getOffsetStyle();
return {
zIndex,
transform: internalPush ? getPushTransform(placement) : undefined,
...offsetStyle,
...style,
};
};
};
renderHeader() {
const { title, prefixCls, closable, headerStyle } = this.props;
if (!title && !closable) {
return null;
function renderCloseIcon() {
return (
closable && (
<button
type="button"
onClick={onClose}
aria-label="Close"
className={`${prefixCls}-close`}
style={
{
'--scroll-bar': `${getScrollBarSize()}px`,
} as any
}
>
{closeIcon}
</button>
)
);
}
const headerClassName = title ? `${prefixCls}-header` : `${prefixCls}-header-no-title`;
return (
<div className={headerClassName} style={headerStyle}>
{title && <div className={`${prefixCls}-title`}>{title}</div>}
{closable && this.renderCloseIcon()}
</div>
);
}
function renderHeader() {
if (!title && !closable) {
return null;
}
renderFooter() {
const { footer, footerStyle, prefixCls } = this.props;
if (!footer) {
return null;
const headerClassName = title ? `${prefixCls}-header` : `${prefixCls}-header-no-title`;
return (
<div className={headerClassName} style={headerStyle}>
{title && <div className={`${prefixCls}-title`}>{title}</div>}
{closable && renderCloseIcon()}
</div>
);
}
const footerClassName = `${prefixCls}-footer`;
return (
<div className={footerClassName} style={footerStyle}>
{footer}
</div>
);
}
renderCloseIcon() {
const { closable, closeIcon = <CloseOutlined />, prefixCls, onClose } = this.props;
return (
closable && (
<button
type="button"
onClick={onClose}
aria-label="Close"
className={`${prefixCls}-close`}
style={
{
'--scroll-bar': `${getScrollBarSize()}px`,
} as any
}
>
{closeIcon}
</button>
)
);
}
function renderFooter() {
if (!footer) {
return null;
}
// render drawer body dom
renderBody = () => {
const { bodyStyle, drawerStyle, prefixCls, visible } = this.props;
if (this.destroyClose && !visible) {
return null;
const footerClassName = `${prefixCls}-footer`;
return (
<div className={footerClassName} style={footerStyle}>
{footer}
</div>
);
}
this.destroyClose = false;
const containerStyle: React.CSSProperties = {};
// render drawer body dom
const renderBody = () => {
if (destroyClose.current && !visible) {
return null;
}
destroyClose.current = false;
const isDestroyOnClose = this.getDestroyOnClose();
const containerStyle: React.CSSProperties = {};
if (isDestroyOnClose) {
// Increase the opacity transition, delete children after closing.
containerStyle.opacity = 0;
containerStyle.transition = 'opacity .3s';
}
if (isDestroyOnClose) {
// Increase the opacity transition, delete children after closing.
containerStyle.opacity = 0;
containerStyle.transition = 'opacity .3s';
}
return (
<div
className={`${prefixCls}-wrapper-body`}
style={{
...containerStyle,
...drawerStyle,
}}
onTransitionEnd={this.onDestroyTransitionEnd}
>
{this.renderHeader()}
<div className={`${prefixCls}-body`} style={bodyStyle}>
{this.props.children}
return (
<div
className={`${prefixCls}-wrapper-body`}
style={{
...containerStyle,
...drawerStyle,
}}
onTransitionEnd={onDestroyTransitionEnd}
>
{renderHeader()}
<div className={`${prefixCls}-body`} style={bodyStyle}>
{children}
</div>
{renderFooter()}
</div>
{this.renderFooter()}
</div>
);
};
// render Provider for Multi-level drawer
renderProvider = (value: Drawer) => {
this.parentDrawer = value;
const { prefixCls, placement, className, mask, direction, visible, ...rest } = this.props;
);
};
const drawerClassName = classNames(
{
@ -286,61 +291,60 @@ class Drawer extends React.Component<InternalDrawerProps, IDrawerState> {
},
className,
);
const offsetStyle = mask ? this.getOffsetStyle() : {};
const offsetStyle = mask ? getOffsetStyle() : {};
return (
<DrawerContext.Provider value={this}>
<DrawerContext.Provider value={operations}>
<RcDrawer
handler={false}
{...omit(rest, [
'zIndex',
'closable',
'closeIcon',
'destroyOnClose',
'drawerStyle',
'headerStyle',
'bodyStyle',
'footerStyle',
'footer',
'title',
'push',
'width',
'height',
])}
{...{
placement,
prefixCls,
maskClosable,
level,
keyboard,
children,
onClose,
...rest,
}}
{...offsetStyle}
prefixCls={prefixCls}
open={visible}
showMask={mask}
placement={placement}
style={this.getRcDrawerStyle()}
style={getRcDrawerStyle()}
className={drawerClassName}
>
{this.renderBody()}
{renderBody()}
</RcDrawer>
</DrawerContext.Provider>
);
};
},
);
render() {
return <DrawerContext.Consumer>{this.renderProvider}</DrawerContext.Consumer>;
}
}
Drawer.displayName = 'Drawer';
const DrawerWrapper: React.FC<DrawerProps> = props => {
const { prefixCls: customizePrefixCls, getContainer: customizeGetContainer } = props;
const { getPopupContainer, getPrefixCls, direction } = React.useContext(ConfigContext);
const DrawerWrapper: React.FC<DrawerProps> = React.forwardRef<DrawerRef, DrawerProps>(
(props, ref) => {
const { prefixCls: customizePrefixCls, getContainer: customizeGetContainer } = props;
const { getPopupContainer, getPrefixCls, direction } = React.useContext(ConfigContext);
const prefixCls = getPrefixCls('drawer', customizePrefixCls);
const getContainer =
// 有可能为 false,所以不能直接判断
customizeGetContainer === undefined && getPopupContainer
? () => getPopupContainer(document.body)
: customizeGetContainer;
const prefixCls = getPrefixCls('drawer', customizePrefixCls);
const getContainer =
// 有可能为 false,所以不能直接判断
customizeGetContainer === undefined && getPopupContainer
? () => getPopupContainer(document.body)
: customizeGetContainer;
return (
<Drawer {...props} prefixCls={prefixCls} getContainer={getContainer} direction={direction} />
);
};
return (
<Drawer
{...props}
ref={ref}
prefixCls={prefixCls}
getContainer={getContainer}
direction={direction}
/>
);
},
);
DrawerWrapper.displayName = 'DrawerWrapper';

Loading…
Cancel
Save