From 0c31ada67ae7fbb9673d6ee16f343ccb76163d4e Mon Sep 17 00:00:00 2001 From: ajuner <106791576@qq.com> Date: Mon, 27 Sep 2021 17:09:18 +0800 Subject: [PATCH] refactor(drawer): use compositionAPI --- components/drawer/demo/basic.vue | 3 +- components/drawer/demo/extra.vue | 65 +++ components/drawer/demo/form-in-drawer.vue | 19 +- components/drawer/demo/index.vue | 6 + components/drawer/demo/multi-level-drawer.vue | 47 +- components/drawer/demo/render-in-current.vue | 4 +- components/drawer/demo/size.vue | 61 +++ components/drawer/demo/user-profile.vue | 2 +- components/drawer/index.en-US.md | 51 +- components/drawer/index.tsx | 479 +++++++++------- components/drawer/index.zh-CN.md | 51 +- components/drawer/style/drawer.less | 98 ++-- components/drawer/style/index.less | 3 + components/drawer/style/rtl.less | 16 + components/style/themes/default.less | 10 +- components/vc-drawer/assets/index.less | 89 +-- components/vc-drawer/index.ts | 3 + components/vc-drawer/src/DrawerChild.tsx | 513 ++++++++++++++++++ components/vc-drawer/src/DrawerWrapper.tsx | 118 ++++ ...DrawerPropTypes.js => IDrawerPropTypes.ts} | 43 +- components/vc-drawer/src/index.js | 5 - .../vc-drawer/src/{utils.js => utils.ts} | 59 +- 22 files changed, 1338 insertions(+), 407 deletions(-) create mode 100644 components/drawer/demo/extra.vue create mode 100644 components/drawer/demo/size.vue create mode 100644 components/drawer/style/rtl.less create mode 100644 components/vc-drawer/index.ts create mode 100644 components/vc-drawer/src/DrawerChild.tsx create mode 100644 components/vc-drawer/src/DrawerWrapper.tsx rename components/vc-drawer/src/{IDrawerPropTypes.js => IDrawerPropTypes.ts} (51%) delete mode 100644 components/vc-drawer/src/index.js rename components/vc-drawer/src/{utils.js => utils.ts} (57%) diff --git a/components/drawer/demo/basic.vue b/components/drawer/demo/basic.vue index c446dfd4e..7a203d6ce 100644 --- a/components/drawer/demo/basic.vue +++ b/components/drawer/demo/basic.vue @@ -22,8 +22,7 @@ Basic drawer. v-model:visible="visible" title="Basic Drawer" placement="right" - :closable="false" - :after-visible-change="afterVisibleChange" + @after-visible-change="afterVisibleChange" >

Some contents...

Some contents...

diff --git a/components/drawer/demo/extra.vue b/components/drawer/demo/extra.vue new file mode 100644 index 000000000..e940d5310 --- /dev/null +++ b/components/drawer/demo/extra.vue @@ -0,0 +1,65 @@ + +--- +order: 2 +title: + zh-CN: 额外操作 + en-US: Extra Actions +--- + +## zh-CN + +在 Ant Design 规范中,操作按钮建议放在抽屉的右上角,可以使用 extra 属性来实现。 + +## en-US + +Extra actions should be placed at corner of drawer in Ant Design, you can using `extra` prop for that. + + + + + diff --git a/components/drawer/demo/form-in-drawer.vue b/components/drawer/demo/form-in-drawer.vue index 482078cdb..6d92bd650 100644 --- a/components/drawer/demo/form-in-drawer.vue +++ b/components/drawer/demo/form-in-drawer.vue @@ -1,6 +1,6 @@ --- -order: 3 +order: 4 title: zh-CN: 抽屉表单 en-US: Submit form in drawer @@ -26,6 +26,7 @@ Use form in drawer with submit button. :width="720" :visible="visible" :body-style="{ paddingBottom: '80px' }" + :footer-style="{ textAlign: 'right' }" @close="onClose" > @@ -96,22 +97,10 @@ Use form in drawer with submit button. -
+ diff --git a/components/drawer/demo/user-profile.vue b/components/drawer/demo/user-profile.vue index bbe0817ec..2036f31f5 100644 --- a/components/drawer/demo/user-profile.vue +++ b/components/drawer/demo/user-profile.vue @@ -1,6 +1,6 @@ --- -order: 4 +order: 5 title: zh-CN: 信息预览抽屉 en-US: Preview drawer diff --git a/components/drawer/index.en-US.md b/components/drawer/index.en-US.md index 30bbe553a..c775d33fe 100644 --- a/components/drawer/index.en-US.md +++ b/components/drawer/index.en-US.md @@ -15,34 +15,41 @@ A Drawer is a panel that is typically overlaid on top of a page and slides in fr - Processing subtasks. When subtasks are too heavy for a Popover and we still want to keep the subtasks in the context of the main task, Drawer comes very handy. - When the same Form is needed in multiple places. - ## API -| Property | Description | Type | Default | Version | -| --- | --- | --- | --- | --- | -| closable | Whether a close (x) button is visible on top right of the Drawer dialog or not. | boolean | true | | -| destroyOnClose | Whether to unmount child components on closing drawer or not. | boolean | false | | -| getContainer | Return the mounted node for Drawer. | HTMLElement \| `() => HTMLElement` \| Selectors | 'body' | | -| mask | Whether to show mask or not. | Boolean | true | | -| maskClosable | Clicking on the mask (area outside the Drawer) to close the Drawer or not. | boolean | true | | -| maskStyle | Style for Drawer's mask element. | object | {} | | -| title | The title for Drawer. | string\|slot | - | | -| visible(v-model) | Whether the Drawer dialog is visible or not. | boolean | false | | -| wrapClassName | The class name of the container of the Drawer dialog. | string | - | | -| wrapStyle | Style of wrapper element which **contains mask** compare to `drawerStyle` | object | - | | +| Props | Description | Type | Default | Version | +| --- | --- | --- | --- | --- | --- | +| autoFocus | Whether Drawer should get focused after open | boolean | true | 3.0.0 | +| bodyStyle | Style of the drawer content part | CSSProperties | - | | +| className(old: wrapClassName) | The class name of the container of the Drawer dialog | string | - | 3.0.0 | +| closable | Whether a close (x) button is visible on top right of the Drawer dialog or not | boolean | true | | +| closeIcon | Custom close icon | VNode \| slot | | 3.0.0 | +| contentWrapperStyle | Style of the drawer wrapper of content part | CSSProperties | 3.0.0 | +| destroyOnClose | Whether to unmount child components on closing drawer or not | boolean | false | | | drawerStyle | Style of the popup layer element | object | - | | -| headerStyle | Style of the drawer header part | object | - | | -| bodyStyle | Style of the drawer content part | object | - | | -| width | Width of the Drawer dialog. | string\|number | 256 | | -| height | placement is `top` or `bottom`, height of the Drawer dialog. | string\|number | - | | -| zIndex | The `z-index` of the Drawer. | Number | 1000 | | -| placement | The placement of the Drawer. | 'top' \| 'right' \| 'bottom' \| 'left' | 'right' | | -| handle | After setting, the drawer is directly mounted on the DOM, and you can control the drawer to open or close through this `handle`. | VNode \| slot | - | | -| afterVisibleChange | Callback after the animation ends when switching drawers. | function(visible) | - | | -| keyboard | Whether support press esc to close | Boolean | true | | +| extra | Extra actions area at corner | VNode \| slot | - | 3.0.0 | +| footer | The footer for Drawer | VNode \| slot | - | 3.0.0 | +| footerStyle | Style of the drawer footer part | CSSProperties | - | 3.0.0 | +| forceRender | Prerender Drawer component forcely | boolean | - | false | 3.0.0 | +| getContainer | Return the mounted node for Drawer | HTMLElement \| `() => HTMLElement` \| Selectors | 'body' | | +| headerStyle | Style of the drawer header part | CSSProperties | - | 3.0.0 | +| height | Placement is `top` or `bottom`, height of the Drawer dialog | string \| number | 378 | | +| keyboard | Whether support press esc to close | boolean | true | | +| mask | Whether to show mask or not | Boolean | true | | +| maskClosable | Clicking on the mask (area outside the Drawer) to close the Drawer or not | boolean | true | | +| maskStyle | Style for Drawer's mask element | CSSProperties | {} | | +| placement | The placement of the Drawer | 'top' \| 'right' \| 'bottom' \| 'left' | 'right' | | +| push | Nested drawers push behavior | boolean \| {distance: string \| number} | { distance: 180} | 3.0.0 | +| size | presetted size of drawer, default `378px` and large `736px` | `default` \| `large` | `default` | 3.0.0 | +| style(old: wrapStyle) | Style of wrapper element which contains mask compare to drawerStyle | CSSProperties | - | 3.0.0 | +| title | The title for Drawer | string \| slot | - | | +| visible(v-model) | Whether the Drawer dialog is visible or not | boolean | - | | +| width | Width of the Drawer dialog | string \| number | 378 | | +| zIndex | The `z-index` of the Drawer | Number | 1000 | | ## Methods | Name | Description | Type | Default | Version | | --- | --- | --- | --- | --- | +| afterVisibleChange | Callback after the animation ends when switching drawers. | function(visible) | - | | | close | Specify a callback that will be called when a user clicks mask, close button or Cancel button. | function(e) | - | | diff --git a/components/drawer/index.tsx b/components/drawer/index.tsx index c401f9693..9827a7136 100644 --- a/components/drawer/index.tsx +++ b/components/drawer/index.tsx @@ -1,189 +1,237 @@ -import type { CSSProperties } from 'vue'; -import { inject, provide, nextTick, defineComponent } from 'vue'; +import type { CSSProperties, ExtractPropTypes, PropType } from 'vue'; +import { + inject, + nextTick, + defineComponent, + ref, + onMounted, + provide, + onBeforeMount, + onUpdated, + onUnmounted, +} from 'vue'; +import { getPropsSlot } from '../_util/props-util'; import classnames from '../_util/classNames'; -import VcDrawer from '../vc-drawer/src'; +import VcDrawer from '../vc-drawer'; import PropTypes from '../_util/vue-types'; -import BaseMixin from '../_util/BaseMixin'; import CloseOutlined from '@ant-design/icons-vue/CloseOutlined'; -import { getComponent, getOptionProps } from '../_util/props-util'; import { defaultConfigProvider } from '../config-provider'; import { tuple, withInstall } from '../_util/type'; import omit from '../_util/omit'; const PlacementTypes = tuple('top', 'right', 'bottom', 'left'); -type placementType = typeof PlacementTypes[number]; +export type placementType = typeof PlacementTypes[number]; + +const SizeTypes = tuple('default', 'large'); +export type sizeType = typeof SizeTypes[number]; + +export interface PushState { + distance: string | number; +} + +const defaultPushState: PushState = { distance: 180 }; + +const drawerProps = { + autoFocus: PropTypes.looseBool, + closable: PropTypes.looseBool.def(true), + closeIcon: PropTypes.VNodeChild.def(), + destroyOnClose: PropTypes.looseBool, + forceRender: PropTypes.looseBool, + getContainer: PropTypes.any, + maskClosable: PropTypes.looseBool.def(true), + mask: PropTypes.looseBool.def(true), + maskStyle: PropTypes.object, + style: PropTypes.object, + size: { + type: String as PropType, + }, + drawerStyle: PropTypes.object, + headerStyle: PropTypes.object, + bodyStyle: PropTypes.object, + contentWrapperStyle: PropTypes.object, + title: PropTypes.VNodeChild, + visible: PropTypes.looseBool, + className: PropTypes.string, + width: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + height: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + zIndex: PropTypes.number, + prefixCls: PropTypes.string, + push: PropTypes.oneOfType([PropTypes.looseBool, { type: Object as PropType }]).def( + defaultPushState, + ), + placement: PropTypes.oneOf(PlacementTypes).def('right'), + keyboard: PropTypes.looseBool.def(true), + extra: PropTypes.VNodeChild, + footer: PropTypes.VNodeChild, + footerStyle: PropTypes.object, + level: PropTypes.any.def(null), + levelMove: PropTypes.any, +}; + +export type DrawerProps = Partial>; + const Drawer = defineComponent({ name: 'ADrawer', - mixins: [BaseMixin], inheritAttrs: false, - props: { - closable: PropTypes.looseBool.def(true), - destroyOnClose: PropTypes.looseBool, - getContainer: PropTypes.any, - maskClosable: PropTypes.looseBool.def(true), - mask: PropTypes.looseBool.def(true), - maskStyle: PropTypes.object, - wrapStyle: PropTypes.object, - bodyStyle: PropTypes.object, - headerStyle: PropTypes.object, - drawerStyle: PropTypes.object, - title: PropTypes.VNodeChild, - visible: PropTypes.looseBool, - width: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).def(256), - height: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).def(256), - zIndex: PropTypes.number, - prefixCls: PropTypes.string, - placement: PropTypes.oneOf(PlacementTypes).def('right'), - level: PropTypes.any.def(null), - wrapClassName: PropTypes.string, // not use class like react, vue will add class to root dom - handle: PropTypes.VNodeChild, - afterVisibleChange: PropTypes.func, - keyboard: PropTypes.looseBool.def(true), - onClose: PropTypes.func, - 'onUpdate:visible': PropTypes.func, - }, - setup(props) { + props: drawerProps, + emits: ['update:visible', 'close', 'afterVisibleChange'], + setup(props, { emit, slots, attrs }) { + const sPush = ref(false); + const preVisible = ref(props.visible); + const destroyClose = ref(false); + const vcDrawer = ref(null); const configProvider = inject('configProvider', defaultConfigProvider); - return { - configProvider, - destroyClose: false, - preVisible: props.visible, - parentDrawer: inject('parentDrawer', null), - }; - }, - data() { - return { - sPush: false, - }; - }, - beforeCreate() { - provide('parentDrawer', this); - }, - mounted() { - // fix: delete drawer in child and re-render, no push started. - // {show && } - const { visible } = this; - if (visible && this.parentDrawer) { - this.parentDrawer.push(); - } - }, - updated() { - nextTick(() => { - if (this.preVisible !== this.visible && this.parentDrawer) { - if (this.visible) { - this.parentDrawer.push(); - } else { - this.parentDrawer.pull(); - } - } - this.preVisible = this.visible; - }); - }, - beforeUnmount() { - // unmount drawer in child, clear push. - if (this.parentDrawer) { - this.parentDrawer.pull(); - } - }, - methods: { - domFocus() { - if (this.$refs.vcDrawer) { - (this.$refs.vcDrawer as any).domFocus(); - } - }, - close(e: Event) { - this.$emit('update:visible', false); - this.$emit('close', e); - }, - // onMaskClick(e) { - // if (!this.maskClosable) { - // return; - // } - // this.close(e); - // }, - push() { - this.setState({ - sPush: true, + const parentDrawerOpts = inject('parentDrawerOpts', null); + + onBeforeMount(() => { + provide('parentDrawerOpts', { + setPush, + setPull, }); - }, - pull() { - this.setState( - { - sPush: false, - }, - () => { - this.domFocus(); - }, - ); - }, - onDestroyTransitionEnd() { - const isDestroyOnClose = this.getDestroyOnClose(); + }); + + onMounted(() => { + const { visible } = props; + if (visible && parentDrawerOpts) { + parentDrawerOpts.setPush(); + } + }); + + onUnmounted(() => { + if (parentDrawerOpts) { + parentDrawerOpts.setPull(); + } + }); + + onUpdated(() => { + const { visible } = props; + nextTick(() => { + if (preVisible.value !== visible && parentDrawerOpts) { + if (visible) { + parentDrawerOpts.setPush(); + } else { + parentDrawerOpts.setPull(); + } + } + preVisible.value = visible; + }); + }); + + const domFocus = () => { + vcDrawer.value?.domFocus?.(); + }; + + const close = (e: Event) => { + emit('update:visible', false); + emit('close', e); + }; + + const afterVisibleChange = (visible: boolean) => { + emit('afterVisibleChange', visible); + }; + + const setPush = () => { + sPush.value = true; + }; + + const setPull = () => { + sPush.value = false; + nextTick(() => { + domFocus(); + }); + }; + + const onDestroyTransitionEnd = () => { + const isDestroyOnClose = getDestroyOnClose(); if (!isDestroyOnClose) { return; } - if (!this.visible) { - this.destroyClose = true; - (this as any).$forceUpdate(); + if (!props.visible) { + destroyClose.value = true; } - }, + }; + + const getDestroyOnClose = () => { + return props.destroyOnClose && !props.visible; + }; + + const getPushTransform = (placement?: placementType) => { + const { push } = props; + let distance: number | string; + if (typeof push === 'boolean') { + distance = push ? defaultPushState.distance : 0; + } else { + distance = push!.distance; + } + + distance = parseFloat(String(distance || 0)); - getDestroyOnClose() { - return this.destroyOnClose && !this.visible; - }, - // get drawar push width or height - getPushTransform(placement?: placementType) { if (placement === 'left' || placement === 'right') { - return `translateX(${placement === 'left' ? 180 : -180}px)`; + return `translateX(${placement === 'left' ? distance : -distance}px)`; } if (placement === 'top' || placement === 'bottom') { - return `translateY(${placement === 'top' ? 180 : -180}px)`; + return `translateY(${placement === 'top' ? distance : -distance}px)`; } - }, - getRcDrawerStyle() { - const { zIndex, placement, wrapStyle } = this.$props; - const { sPush: push } = this.$data; + }; + + const getRcDrawerStyle = () => { + const { zIndex, placement, style, mask } = props; + const offsetStyle = mask ? {} : getOffsetStyle(); return { zIndex, - transform: push ? this.getPushTransform(placement) : undefined, - ...wrapStyle, + transform: sPush.value ? getPushTransform(placement) : undefined, + ...offsetStyle, + ...style, }; - }, - renderHeader(prefixCls: string) { - const { closable, headerStyle } = this.$props; - const title = getComponent(this, 'title'); + }; + + const renderHeader = (prefixCls: string) => { + const { closable, headerStyle } = props; + const extra = getPropsSlot(slots, props, 'extra'); + const title = getPropsSlot(slots, props, 'title'); if (!title && !closable) { return null; } - const headerClassName = title ? `${prefixCls}-header` : `${prefixCls}-header-no-title`; return ( -
- {title &&
{title}
} - {closable ? this.renderCloseIcon(prefixCls) : null} +
+
+ {renderCloseIcon(prefixCls)} + {title &&
{title}
} +
+ {extra &&
{extra}
}
); - }, - renderCloseIcon(prefixCls: string) { - const { closable } = this; + }; + + const renderCloseIcon = (prefixCls: string) => { + const { closable } = props; + const $closeIcon = getPropsSlot(slots, props, 'closeIcon'); return ( closable && ( - ) ); - }, - // render drawer body dom - renderBody(prefixCls: string) { - if (this.destroyClose && !this.visible) { + }; + + const renderBody = (prefixCls: string) => { + if (destroyClose.value && !props.visible) { return null; } - this.destroyClose = false; - const { bodyStyle, drawerStyle } = this.$props; + destroyClose.value = false; + + const { bodyStyle, drawerStyle } = props; const containerStyle: CSSProperties = {}; - const isDestroyOnClose = this.getDestroyOnClose(); + const isDestroyOnClose = getDestroyOnClose(); if (isDestroyOnClose) { // Increase the opacity transition, delete children after closing. containerStyle.opacity = 0; @@ -194,74 +242,93 @@ const Drawer = defineComponent({
- {this.renderHeader(prefixCls)} + {renderHeader(prefixCls)}
- {this.$slots.default?.()} + {slots.default?.()}
+ {renderFooter(prefixCls)}
); - }, - }, - render() { - const props: any = getOptionProps(this); - const { - prefixCls: customizePrefixCls, - width, - height, - visible, - placement, - wrapClassName, - mask, - ...rest - } = props; - const haveMask = mask ? '' : 'no-mask'; - const offsetStyle: CSSProperties = {}; - if (placement === 'left' || placement === 'right') { - offsetStyle.width = typeof width === 'number' ? `${width}px` : width; - } else { - offsetStyle.height = typeof height === 'number' ? `${height}px` : height; - } - const handler = getComponent(this, 'handle') || false; - const getPrefixCls = this.configProvider.getPrefixCls; - const prefixCls = getPrefixCls('drawer', customizePrefixCls); - const { class: className } = this.$attrs; - const vcDrawerProps: any = { - ...this.$attrs, - ...omit(rest, [ - 'closable', - 'destroyOnClose', - 'drawerStyle', - 'headerStyle', - 'bodyStyle', - 'title', - 'push', - 'visible', - 'getPopupContainer', - 'rootPrefixCls', - 'getPrefixCls', - 'renderEmpty', - 'csp', - 'pageHeader', - 'autoInsertSpaceInButton', - ]), - onClose: this.close, - handler, - ...offsetStyle, - prefixCls, - open: visible, - showMask: mask, - placement, - class: classnames({ - [className as string]: !!className, - [wrapClassName]: !!wrapClassName, - [haveMask]: !!haveMask, - }), - wrapStyle: this.getRcDrawerStyle(), - ref: 'vcDrawer', }; - return {this.renderBody(prefixCls)}; + + const renderFooter = (prefixCls: string) => { + const footer = getPropsSlot(slots, props, 'footer'); + if (!footer) { + return null; + } + + const footerClassName = `${prefixCls}-footer`; + return ( +
+ {footer} +
+ ); + }; + + const getOffsetStyle = () => { + // https://github.com/ant-design/ant-design/issues/24287 + const { visible, mask, placement, size, width, height } = props; + if (!visible && !mask) { + return {}; + } + const offsetStyle: CSSProperties = {}; + if (placement === 'left' || placement === 'right') { + const defaultWidth = size === 'large' ? 736 : 378; + offsetStyle.width = typeof width === 'undefined' ? defaultWidth : width; + } else { + const defaultHeight = size === 'large' ? 736 : 378; + offsetStyle.height = typeof height === 'undefined' ? defaultHeight : height; + } + return offsetStyle; + }; + + return () => { + const { + prefixCls: customizePrefixCls, + width, + height, + visible, + placement, + mask, + className, + ...rest + } = props; + const offsetStyle = mask ? getOffsetStyle() : {}; + const haveMask = mask ? '' : 'no-mask'; + const getPrefixCls = configProvider.getPrefixCls; + const prefixCls = getPrefixCls('drawer', customizePrefixCls); + const vcDrawerProps: any = { + ...attrs, + ...omit(rest, [ + 'size', + 'closeIcon', + 'closable', + 'destroyOnClose', + 'drawerStyle', + 'headerStyle', + 'bodyStyle', + 'title', + 'push', + ]), + ...offsetStyle, + onClose: close, + afterVisibleChange, + handler: false, + prefixCls, + open: visible, + showMask: mask, + placement, + wrapperClassName: classnames({ + [className as string]: className, + [haveMask]: !!haveMask, + }), + style: getRcDrawerStyle(), + ref: vcDrawer, + }; + return {renderBody(prefixCls)}; + }; }, }); diff --git a/components/drawer/index.zh-CN.md b/components/drawer/index.zh-CN.md index bec03bb6c..9d45349b0 100644 --- a/components/drawer/index.zh-CN.md +++ b/components/drawer/index.zh-CN.md @@ -6,33 +6,50 @@ subtitle: 抽屉 cover: https://gw.alipayobjects.com/zos/alicdn/7z8NJQhFb/Drawer.svg --- +屏幕边缘滑出的浮层面板。 + +## 何时使用 + +抽屉从父窗体边缘滑入,覆盖住部分父窗体内容。用户在抽屉内操作时不必离开当前任务,操作完成后,可以平滑地回到原任务。 + +- 当需要一个附加的面板来控制父窗体内容,这个面板在需要时呼出。比如,控制界面展示样式,往界面中添加内容。 +- 当需要在当前任务流中插入临时任务,创建或预览附加内容。比如展示协议条款,创建子对象。 + ## API | 参数 | 说明 | 类型 | 默认值 | 版本 | -| --- | --- | --- | --- | --- | +| --- | --- | --- | --- | --- | --- | +| autoFocus | 抽屉展开后是否将焦点切换至其 Dom 节点 | boolean | true | 3.0.0 | +| bodyStyle | 可用于设置 Drawer 内容部分的样式 | CSSProperties | - | | +| className(原 wrapClassName) | 对话框外层容器的类名 | string | - | 3.0.0 | | closable | 是否显示右上角的关闭按钮 | boolean | true | | +| closeIcon | 自定义关闭图标 | VNode \| slot | | 3.0.0 | +| contentWrapperStyle | 可用于设置 Drawer 包裹内容部分的样式 | CSSProperties | 3.0.0 | | destroyOnClose | 关闭时销毁 Drawer 里的子元素 | boolean | false | | +| drawerStyle | 用于设置 Drawer 弹出层的样式 | object | - | | +| extra | 抽屉右上角的操作区域 | VNode \| slot | - | 3.0.0 | +| footer | 抽屉的页脚 | VNode \| slot | - | 3.0.0 | +| footerStyle | 抽屉页脚部件的样式 | CSSProperties | - | 3.0.0 | +| forceRender | 预渲染 Drawer 内元素 | boolean | - | false | 3.0.0 | | getContainer | 指定 Drawer 挂载的 HTML 节点 | HTMLElement \| `() => HTMLElement` \| Selectors | 'body' | | -| maskClosable | 点击蒙层是否允许关闭 | boolean | true | | +| headerStyle | 用于设置 Drawer 头部的样式 | CSSProperties | - | 3.0.0 | +| height | 高度, 在 `placement` 为 `top` 或 `bottom` 时使用 | string \| number | 378 | | +| keyboard | 是否支持键盘 esc 关闭 | boolean | true | | | mask | 是否展示遮罩 | Boolean | true | | -| maskStyle | 遮罩样式 | object | {} | | +| maskClosable | 点击蒙层是否允许关闭 | boolean | true | | +| maskStyle | 遮罩样式 | CSSProperties | {} | | +| placement | 抽屉的方向 | 'top' \| 'right' \| 'bottom' \| 'left' | 'right' | | +| push | 用于设置多层 Drawer 的推动行为 | boolean \| {distance: string \| number} | { distance: 180} | 3.0.0 | +| size | 预设抽屉宽度(或高度),default `378px` 和 large `736px` | `default` \| `large` | `default` | 3.0.0 | +| style(原 wrapStyle) | 可用于设置 Drawer 最外层容器的样式,和 `drawerStyle` 的区别是作用节点包括 `mask` | CSSProperties | - | 3.0.0 | | title | 标题 | string \| slot | - | | | visible(v-model) | Drawer 是否可见 | boolean | - | | -| wrapClassName | 对话框外层容器的类名 | string | - | | -| wrapStyle | 可用于设置 Drawer 最外层容器的样式,和 `drawerStyle` 的区别是作用节点包括 `mask` | object | - | | -| drawerStyle | 用于设置 Drawer 弹出层的样式 | object | - | | -| headerStyle | 用于设置 Drawer 头部的样式 | object | - | | -| bodyStyle | 可用于设置 Drawer 内容部分的样式 | object | - | | -| width | 宽度 | string \| number | 256 | | -| height | 高度, 在 `placement` 为 `top` 或 `bottom` 时使用 | string \| number | 256 | | +| width | 宽度 | string \| number | 378 | | | zIndex | 设置 Drawer 的 `z-index` | Number | 1000 | | -| placement | 抽屉的方向 | 'top' \| 'right' \| 'bottom' \| 'left' | 'right' | | -| handle | 设置后抽屉直接挂载到 DOM 上,你可以通过该 handle 控制抽屉打开关闭 | VNode \| slot | - | | -| afterVisibleChange | 切换抽屉时动画结束后的回调 | function(visible) | 无 | | -| keyboard | 是否支持键盘 esc 关闭 | boolean | true | | ## 方法 -| 名称 | 描述 | 类型 | 默认值 | 版本 | -| ----- | ------------------------------------ | ----------- | ------ | ---- | -| close | 点击遮罩层或右上角叉或取消按钮的回调 | function(e) | 无 | | +| 名称 | 描述 | 类型 | 默认值 | 版本 | +| ------------------ | ------------------------------------ | ----------------- | ------ | ---- | +| afterVisibleChange | 切换抽屉时动画结束后的回调 | function(visible) | 无 | | +| close | 点击遮罩层或右上角叉或取消按钮的回调 | function(e) | 无 | | diff --git a/components/drawer/style/drawer.less b/components/drawer/style/drawer.less index f28ad981b..eab0002fd 100644 --- a/components/drawer/style/drawer.less +++ b/components/drawer/style/drawer.less @@ -1,12 +1,11 @@ -@import '../../style/themes/index'; +@import '../../style/themes/index.less'; -// Preserve the typo for compatibility -// https://github.com/ant-design/ant-design/issues/14628 -@dawer-prefix-cls: ~'@{ant-prefix}-drawer'; - -@drawer-prefix-cls: @dawer-prefix-cls; +@drawer-prefix-cls: ~'@{ant-prefix}-drawer'; +@picker-prefix-cls: ~'@{ant-prefix}-picker'; .@{drawer-prefix-cls} { + @drawer-header-close-padding: ceil(((@drawer-header-close-size - @font-size-lg) / 2)); + position: fixed; z-index: @zindex-modal; width: 0%; @@ -20,7 +19,10 @@ &-content-wrapper { position: absolute; + width: 100%; + height: 100%; } + .@{drawer-prefix-cls}-content { width: 100%; height: 100%; @@ -38,12 +40,17 @@ width: 100%; transition: transform @animation-duration-slow @ease-base-out; } - &.@{drawer-prefix-cls}-open.no-mask { - width: 0%; - } } &-left { + left: 0; + + .@{drawer-prefix-cls} { + &-content-wrapper { + left: 0; + } + } + &.@{drawer-prefix-cls}-open { .@{drawer-prefix-cls}-content-wrapper { box-shadow: @shadow-1-right; @@ -84,9 +91,6 @@ height: 100%; transition: transform @animation-duration-slow @ease-base-out; } - &.@{drawer-prefix-cls}-open.no-mask { - height: 0%; - } } &-top { @@ -118,15 +122,12 @@ } } - &.@{drawer-prefix-cls}-open { - .@{drawer-prefix-cls} { - &-mask { - height: 100%; - opacity: 1; - transition: none; - animation: antdDrawerFadeIn @animation-duration-slow @ease-base-out; - } - } + &.@{drawer-prefix-cls}-open .@{drawer-prefix-cls}-mask { + height: 100%; + opacity: 1; + transition: none; + animation: antdDrawerFadeIn @animation-duration-slow @ease-base-out; + pointer-events: auto; } &-title { @@ -147,19 +148,13 @@ } &-close { - position: absolute; - top: 0; - right: 0; - z-index: @zindex-popup-close; - display: block; - width: 56px; - height: 56px; - padding: 0; - color: @text-color-secondary; + display: inline-block; + margin-right: 12px; + color: @modal-close-color; font-weight: 700; font-size: @font-size-lg; font-style: normal; - line-height: 56px; + line-height: 1; text-align: center; text-transform: none; text-decoration: none; @@ -179,27 +174,48 @@ &-header { position: relative; + display: flex; + align-items: center; + justify-content: space-between; padding: @drawer-header-padding; color: @text-color; background: @drawer-bg; border-bottom: @border-width-base @border-style-base @border-color-split; border-radius: @border-radius-base @border-radius-base 0 0; + + &-title { + display: flex; + align-items: center; + justify-content: space-between; + } + + &-close-only { + padding-bottom: 0; + border: none; + } } - &-header-no-title { - color: @text-color; - background: @drawer-bg; + &-wrapper-body { + display: flex; + flex-direction: column; + flex-wrap: nowrap; + width: 100%; + height: 100%; } &-body { + flex-grow: 1; padding: @drawer-body-padding; + overflow: auto; font-size: @font-size-base; line-height: @line-height-base; word-wrap: break-word; } - &-wrapper-body { - height: 100%; - overflow: auto; + + &-footer { + flex-shrink: 0; + padding: @drawer-footer-padding-vertical @drawer-footer-padding-horizontal; + border-top: @border-width-base @border-style-base @border-color-split; } &-mask { @@ -212,12 +228,20 @@ opacity: 0; filter: ~'alpha(opacity=45)'; transition: opacity @animation-duration-slow linear, height 0s ease @animation-duration-slow; + pointer-events: none; } + &-open { &-content { box-shadow: @shadow-2; } } + + .@{picker-prefix-cls} { + &-clear { + background: @popover-background; + } + } } @keyframes antdDrawerFadeIn { diff --git a/components/drawer/style/index.less b/components/drawer/style/index.less index 79d170c75..a36039cda 100644 --- a/components/drawer/style/index.less +++ b/components/drawer/style/index.less @@ -1,3 +1,6 @@ @import '../../style/themes/index'; @import '../../style/mixins/index'; @import './drawer'; +@import './rtl'; + +// .popover-customize-bg(@drawer-prefix-cls, @popover-background); diff --git a/components/drawer/style/rtl.less b/components/drawer/style/rtl.less new file mode 100644 index 000000000..f710bfa7d --- /dev/null +++ b/components/drawer/style/rtl.less @@ -0,0 +1,16 @@ +@import '../../style/themes/index'; + +@drawer-prefix-cls: ~'@{ant-prefix}-drawer'; + +.@{drawer-prefix-cls} { + &-rtl { + direction: rtl; + } + + &-close { + .@{drawer-prefix-cls}-rtl & { + margin-right: 0; + margin-left: 12px; + } + } +} diff --git a/components/style/themes/default.less b/components/style/themes/default.less index 41d2c4932..93b238b7b 100644 --- a/components/style/themes/default.less +++ b/components/style/themes/default.less @@ -494,6 +494,9 @@ @modal-footer-bg: transparent; @modal-footer-border-color-split: @border-color-split; @modal-mask-bg: fade(@black, 45%); +@modal-close-color: @text-color-secondary; +@modal-footer-padding-vertical: 10px; +@modal-footer-padding-horizontal: 16px; // Progress // -- @@ -876,9 +879,12 @@ // Drawer // --- -@drawer-header-padding: 16px 24px; -@drawer-body-padding: 24px; +@drawer-header-padding: @padding-md @padding-lg; +@drawer-body-padding: @padding-lg; @drawer-bg: @component-background; +@drawer-footer-padding-vertical: @modal-footer-padding-vertical; +@drawer-footer-padding-horizontal: @modal-footer-padding-horizontal; +@drawer-header-close-size: 56px; // Timeline // --- diff --git a/components/vc-drawer/assets/index.less b/components/vc-drawer/assets/index.less index 2af70fb46..97acc4284 100644 --- a/components/vc-drawer/assets/index.less +++ b/components/vc-drawer/assets/index.less @@ -3,25 +3,27 @@ @drawer: drawer; .@{drawer} { position: fixed; - top: 0; z-index: 9999; - > * { - transition: transform @duration @ease-in-out-circ, opacity @duration @ease-in-out-circ, - box-shaow @duration @ease-in-out-circ; + transition: width 0s ease @duration, height 0s ease @duration, transform @duration @ease-in-out-circ; + >* { + transition: transform @duration @ease-in-out-circ, opacity @duration @ease-in-out-circ, box-shadow @duration @ease-in-out-circ; + } + &.@{drawer}-open { + transition: transform @duration @ease-in-out-circ; } & &-mask { background: #000; opacity: 0; - width: 0; + width: 100%; height: 0; - position: fixed; + position: absolute; top: 0; - transition: opacity @duration @ease-in-out-circ, width 0s ease @duration, + left: 0; + transition: opacity @duration @ease-in-out-circ, height 0s ease @duration; - display: block !important; } &-content-wrapper { - position: fixed; + position: absolute; background: #fff; } &-content { @@ -77,41 +79,53 @@ } &.@{drawer}-open { width: 100%; + &.no-mask { + width: 0%; + } } } &-left { + top: 0; + left: 0; .@{drawer} { &-handle { right: -40px; - box-shadow: 2px 0 8px rgba(0, 0, 0, 0.15); + box-shadow: 2px 0 8px rgba(0, 0, 0, .15); border-radius: 0 4px 4px 0; } } &.@{drawer}-open { .@{drawer} { &-content-wrapper { - box-shadow: 2px 0 8px rgba(0, 0, 0, 0.15); + box-shadow: 2px 0 8px rgba(0, 0, 0, .15); } } } } &-right { + top: 0; + right: 0; .@{drawer} { &-content-wrapper { right: 0; } &-handle { left: -40px; - box-shadow: -2px 0 8px rgba(0, 0, 0, 0.15); + box-shadow: -2px 0 8px rgba(0, 0, 0, .15); border-radius: 4px 0 0 4px; } } &.@{drawer}-open { & .@{drawer} { &-content-wrapper { - box-shadow: -2px 0 8px rgba(0, 0, 0, 0.15); + box-shadow: -2px 0 8px rgba(0, 0, 0, .15); } } + &.no-mask { + // https://github.com/ant-design/ant-design/issues/18607 + right: 1px; + transform: translateX(1px); + } } } &-top, @@ -122,60 +136,74 @@ .@{drawer}-content { width: 100%; } + .@{drawer}-content { + height: 100%; + } + &.@{drawer}-open { + height: 100%; + &.no-mask { + height: 0%; + } + } + .@{drawer} { &-handle { left: 50%; margin-left: -20px; } } - &.@{drawer}-open { - height: 100%; - } } &-top { + top: 0; + left: 0; .@{drawer} { &-handle { top: auto; bottom: -40px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); + box-shadow: 0 2px 8px rgba(0, 0, 0, .15); border-radius: 0 0 4px 4px; } } &.@{drawer}-open { .@{drawer} { - &-wrapper { - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); + &-content-wrapper { + box-shadow: 0 2px 8px rgba(0, 0, 0, .15); } } } } &-bottom { + bottom: 0; + left: 0; .@{drawer} { &-content-wrapper { bottom: 0; } &-handle { top: -40px; - box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.15); + box-shadow: 0 -2px 8px rgba(0, 0, 0, .15); border-radius: 4px 4px 0 0; } } &.@{drawer}-open { .@{drawer} { &-content-wrapper { - box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.15); + box-shadow: 0 -2px 8px rgba(0, 0, 0, .15); } } + &.no-mask { + // https://github.com/ant-design/ant-design/issues/18607 + bottom: 1px; + transform: translateY(1px); + } } } &.@{drawer}-open { .@{drawer} { &-mask { - opacity: 0.3; - width: 100%; + opacity: .3; height: 100%; - animation: fadeIn 0.3s @ease-in-out-circ; - transition: none; + transition: opacity 0.3s @ease-in-out-circ; } &-handle { &-icon { @@ -190,13 +218,4 @@ } } } -} - -@keyframes fadeIn { - 0% { - opacity: 0; - } - 100% { - opacity: 0.3; - } -} +} \ No newline at end of file diff --git a/components/vc-drawer/index.ts b/components/vc-drawer/index.ts new file mode 100644 index 000000000..2049a56b9 --- /dev/null +++ b/components/vc-drawer/index.ts @@ -0,0 +1,3 @@ +import Drawer from './src/DrawerWrapper'; + +export default Drawer; diff --git a/components/vc-drawer/src/DrawerChild.tsx b/components/vc-drawer/src/DrawerChild.tsx new file mode 100644 index 000000000..8ad088195 --- /dev/null +++ b/components/vc-drawer/src/DrawerChild.tsx @@ -0,0 +1,513 @@ +import { defineComponent, reactive, onMounted, onUpdated, onUnmounted, nextTick, watch } from 'vue'; +import classnames from '../../_util/classNames'; +import getScrollBarSize from '../../_util/getScrollBarSize'; +import KeyCode from '../../_util/KeyCode'; +import omit from '../../_util/omit'; +import supportsPassive from '../../_util/supportsPassive'; +import { DrawerChildProps } from './IDrawerPropTypes'; +import type { IDrawerChildProps } from './IDrawerPropTypes'; + +import { + addEventListener, + dataToArray, + getTouchParentScroll, + isNumeric, + removeEventListener, + transformArguments, + transitionEndFun, + transitionStr, + windowIsUndefined, +} from './utils'; + +const currentDrawer: Record = {}; + +const DrawerChild = defineComponent({ + inheritAttrs: false, + props: DrawerChildProps, + emits: ['close', 'handleClick', 'change'], + setup(props, { emit, slots, expose }) { + const state = reactive({ + levelDom: [], + dom: null, + contentWrapper: null, + contentDom: null, + maskDom: null, + handlerDom: null, + drawerId: null, + timeout: null, + passive: null, + startPos: { + x: null, + y: null, + }, + }); + + onMounted(() => { + nextTick(() => { + if (!windowIsUndefined) { + state.passive = supportsPassive ? { passive: false } : false; + } + const { open, getContainer, showMask, autoFocus } = props; + const container = getContainer?.(); + state.drawerId = `drawer_id_${Number( + (Date.now() + Math.random()) + .toString() + .replace('.', Math.round(Math.random() * 9).toString()), + ).toString(16)}`; + getLevelDom(props); + if (open) { + if (container && container.parentNode === document.body) { + currentDrawer[state.drawerId] = open; + } + // 默认打开状态时推出 level; + openLevelTransition(); + nextTick(() => { + if (autoFocus) { + domFocus(); + } + }); + if (showMask) { + props.scrollLocker?.lock(); + } + } + }); + }); + + onUpdated(() => { + const { open, getContainer, scrollLocker, showMask, autoFocus } = props; + const container = getContainer?.(); + if (container && container.parentNode === document.body) { + currentDrawer[state.drawerId] = !!open; + } + openLevelTransition(); + if (open) { + if (autoFocus) { + domFocus(); + } + if (showMask) { + scrollLocker?.lock(); + } + } else { + scrollLocker?.unLock(); + } + }); + + onUnmounted(() => { + const { open, scrollLocker } = props; + delete currentDrawer[state.drawerId]; + if (open) { + setLevelTransform(false); + document.body.style.touchAction = ''; + } + scrollLocker?.unLock(); + }); + + watch( + () => props.placement, + val => { + if (val) { + // test 的 bug, 有动画过场,删除 dom + state.contentDom = null; + if (state.contentWrapper) { + state.contentWrapper.style.transition = `none`; + setTimeout(() => { + state.contentWrapper.style.transition = ``; + }); + } + } + }, + ); + + const domFocus = () => { + state.dom?.focus?.(); + }; + + const removeStartHandler = (e: TouchEvent) => { + if (e.touches.length > 1) { + return; + } + state.startPos = { + x: e.touches[0].clientX, + y: e.touches[0].clientY, + }; + }; + + const removeMoveHandler = (e: TouchEvent) => { + if (e.changedTouches.length > 1) { + return; + } + const currentTarget = e.currentTarget as HTMLElement; + const differX = e.changedTouches[0].clientX - state.startPos.x; + const differY = e.changedTouches[0].clientY - state.startPos.y; + if ( + (currentTarget === state.maskDom || + currentTarget === state.handlerDom || + (currentTarget === state.contentDom && + getTouchParentScroll(currentTarget, e.target as HTMLElement, differX, differY))) && + e.cancelable + ) { + e.preventDefault(); + } + }; + + const transitionEnd = (e: TransitionEvent) => { + const dom: HTMLElement = e.target as HTMLElement; + removeEventListener(dom, transitionEndFun, transitionEnd); + dom.style.transition = ''; + }; + + const onKeyDown = (e: KeyboardEvent) => { + if (e.keyCode === KeyCode.ESC) { + e.stopPropagation(); + onClose(e); + } + }; + + const onClose = (e: Event) => { + emit('close', e); + }; + + const onWrapperTransitionEnd = (e: TransitionEvent) => { + const { open, afterVisibleChange } = props; + if (e.target === state.contentWrapper && e.propertyName.match(/transform$/)) { + state.dom.style.transition = ''; + if (!open && getCurrentDrawerSome()) { + document.body.style.overflowX = ''; + if (state.maskDom) { + state.maskDom.style.left = ''; + state.maskDom.style.width = ''; + } + } + if (afterVisibleChange) { + afterVisibleChange(!!open); + } + } + }; + + const openLevelTransition = () => { + const { open, width, height } = props; + const { isHorizontal, placementName } = getHorizontalBoolAndPlacementName(); + const contentValue = state.contentDom + ? state.contentDom.getBoundingClientRect()[isHorizontal ? 'width' : 'height'] + : 0; + const value = (isHorizontal ? width : height) || contentValue; + setLevelAndScrolling(open, placementName, value); + }; + + const setLevelTransform = ( + open?: boolean, + placementName?: string, + value?: string | number, + right?: number, + ) => { + const { placement, levelMove, duration, ease, showMask } = props; + // router 切换时可能会导至页面失去滚动条,所以需要时时获取。 + state.levelDom.forEach(dom => { + dom.style.transition = `transform ${duration} ${ease}`; + addEventListener(dom, transitionEndFun, transitionEnd); + let levelValue = open ? value : 0; + if (levelMove) { + const $levelMove = transformArguments(levelMove, { target: dom, open }); + levelValue = open ? $levelMove[0] : $levelMove[1] || 0; + } + const $value = typeof levelValue === 'number' ? `${levelValue}px` : levelValue; + let placementPos = placement === 'left' || placement === 'top' ? $value : `-${$value}`; + placementPos = + showMask && placement === 'right' && right + ? `calc(${placementPos} + ${right}px)` + : placementPos; + dom.style.transform = levelValue ? `${placementName}(${placementPos})` : ''; + }); + }; + + const setLevelAndScrolling = ( + open?: boolean, + placementName?: string, + value?: string | number, + ) => { + if (!windowIsUndefined) { + const right = + document.body.scrollHeight > + (window.innerHeight || document.documentElement.clientHeight) && + window.innerWidth > document.body.offsetWidth + ? getScrollBarSize(true) + : 0; + setLevelTransform(open, placementName, value, right); + toggleScrollingToDrawerAndBody(right); + } + emit('change', open); + }; + + const toggleScrollingToDrawerAndBody = (right: number) => { + const { getContainer, showMask, open } = props; + const container = getContainer?.(); + // 处理 body 滚动 + if (container && container.parentNode === document.body && showMask) { + const eventArray = ['touchstart']; + const domArray = [document.body, state.maskDom, state.handlerDom, state.contentDom]; + if (open && document.body.style.overflow !== 'hidden') { + if (right) { + addScrollingEffect(right); + } + document.body.style.touchAction = 'none'; + // 手机禁滚 + domArray.forEach((item, i) => { + if (!item) { + return; + } + addEventListener( + item, + eventArray[i] || 'touchmove', + i ? removeMoveHandler : removeStartHandler, + state.passive, + ); + }); + } else if (getCurrentDrawerSome()) { + document.body.style.touchAction = ''; + if (right) { + remScrollingEffect(right); + } + // 恢复事件 + domArray.forEach((item, i) => { + if (!item) { + return; + } + removeEventListener( + item, + eventArray[i] || 'touchmove', + i ? removeMoveHandler : removeStartHandler, + state.passive, + ); + }); + } + } + }; + + const addScrollingEffect = (right: number) => { + const { placement, duration, ease } = props; + const widthTransition = `width ${duration} ${ease}`; + const transformTransition = `transform ${duration} ${ease}`; + state.dom.style.transition = 'none'; + switch (placement) { + case 'right': + state.dom.style.transform = `translateX(-${right}px)`; + break; + case 'top': + case 'bottom': + state.dom.style.width = `calc(100% - ${right}px)`; + state.dom.style.transform = 'translateZ(0)'; + break; + default: + break; + } + clearTimeout(state.timeout); + state.timeout = setTimeout(() => { + if (state.dom) { + state.dom.style.transition = `${transformTransition},${widthTransition}`; + state.dom.style.width = ''; + state.dom.style.transform = ''; + } + }); + }; + + const remScrollingEffect = (right: number) => { + const { placement, duration, ease } = props; + + if (transitionStr) { + document.body.style.overflowX = 'hidden'; + } + state.dom.style.transition = 'none'; + let heightTransition: string; + let widthTransition = `width ${duration} ${ease}`; + const transformTransition = `transform ${duration} ${ease}`; + switch (placement) { + case 'left': { + state.dom.style.width = '100%'; + widthTransition = `width 0s ${ease} ${duration}`; + break; + } + case 'right': { + state.dom.style.transform = `translateX(${right}px)`; + state.dom.style.width = '100%'; + widthTransition = `width 0s ${ease} ${duration}`; + if (state.maskDom) { + state.maskDom.style.left = `-${right}px`; + state.maskDom.style.width = `calc(100% + ${right}px)`; + } + break; + } + case 'top': + case 'bottom': { + state.dom.style.width = `calc(100% + ${right}px)`; + state.dom.style.height = '100%'; + state.dom.style.transform = 'translateZ(0)'; + heightTransition = `height 0s ${ease} ${duration}`; + break; + } + default: + break; + } + clearTimeout(state.timeout); + state.timeout = setTimeout(() => { + if (state.dom) { + state.dom.style.transition = `${transformTransition},${ + heightTransition ? `${heightTransition},` : '' + }${widthTransition}`; + state.dom.style.transform = ''; + state.dom.style.width = ''; + state.dom.style.height = ''; + } + }); + }; + + const getCurrentDrawerSome = () => !Object.keys(currentDrawer).some(key => currentDrawer[key]); + + const getLevelDom = ({ level, getContainer }: IDrawerChildProps) => { + if (windowIsUndefined) { + return; + } + const container = getContainer?.(); + const parent = container ? (container.parentNode as HTMLElement) : null; + state.levelDom = []; + if (level === 'all') { + const children: HTMLElement[] = parent ? Array.prototype.slice.call(parent.children) : []; + children.forEach((child: HTMLElement) => { + if ( + child.nodeName !== 'SCRIPT' && + child.nodeName !== 'STYLE' && + child.nodeName !== 'LINK' && + child !== container + ) { + state.levelDom.push(child); + } + }); + } else if (level) { + dataToArray(level).forEach(key => { + document.querySelectorAll(key).forEach(item => { + state.levelDom.push(item); + }); + }); + } + }; + + const getHorizontalBoolAndPlacementName = () => { + const { placement } = props; + const isHorizontal = placement === 'left' || placement === 'right'; + const placementName = `translate${isHorizontal ? 'X' : 'Y'}`; + return { + isHorizontal, + placementName, + }; + }; + + const getDerivedStateFromProps = ( + props: IDrawerChildProps, + { prevProps }: { prevProps: IDrawerChildProps }, + ) => { + const nextState = { + prevProps: props, + }; + if (prevProps !== undefined) { + const { placement, level } = props; + if (placement !== prevProps.placement) { + // test 的 bug, 有动画过场,删除 dom + state.contentDom = null; + } + if (level !== prevProps.level) { + getLevelDom(props); + } + } + return nextState; + }; + + expose({ getDerivedStateFromProps }); + + return () => { + const { + width, + height, + open: $open, + prefixCls, + placement, + level, + levelMove, + ease, + duration, + getContainer, + onChange, + afterVisibleChange, + showMask, + maskClosable, + maskStyle, + keyboard, + getOpenCount, + scrollLocker, + contentWrapperStyle, + style, + ...otherProps + } = props; + // 首次渲染都将是关闭状态。 + const open = state.dom ? $open : false; + const wrapperClassName = classnames(prefixCls, { + [`${prefixCls}-${placement}`]: true, + [`${prefixCls}-open`]: open, + 'no-mask': !showMask, + }); + + const { placementName } = getHorizontalBoolAndPlacementName(); + // 百分比与像素动画不同步,第一次打用后全用像素动画。 + // const defaultValue = !this.contentDom || !level ? '100%' : `${value}px`; + const placementPos = placement === 'left' || placement === 'top' ? '-100%' : '100%'; + const transform = open ? '' : `${placementName}(${placementPos})`; + + return ( +
{ + state.dom = c as HTMLElement; + }} + onKeydown={open && keyboard ? onKeyDown : undefined} + onTransitionend={onWrapperTransitionEnd} + > + {showMask && ( +
{ + state.maskDom = c as HTMLElement; + }} + /> + )} +
{ + state.contentWrapper = c as HTMLElement; + }} + > +
{ + state.contentDom = c as HTMLElement; + }} + > + {slots.children?.()} +
+
+
+ ); + }; + }, +}); + +export default DrawerChild; diff --git a/components/vc-drawer/src/DrawerWrapper.tsx b/components/vc-drawer/src/DrawerWrapper.tsx new file mode 100644 index 000000000..855eddecc --- /dev/null +++ b/components/vc-drawer/src/DrawerWrapper.tsx @@ -0,0 +1,118 @@ +import Child from './DrawerChild'; +import { initDefaultProps } from '../../_util/props-util'; +import { Teleport, defineComponent, ref, watch } from 'vue'; +import { DrawerProps } from './IDrawerPropTypes'; +import type { IDrawerProps } from './IDrawerPropTypes'; + +const DrawerWrapper = defineComponent({ + inheritAttrs: false, + props: initDefaultProps(DrawerProps, { + prefixCls: 'drawer', + placement: 'left', + getContainer: 'body', + level: 'all', + duration: '.3s', + ease: 'cubic-bezier(0.78, 0.14, 0.15, 0.86)', + afterVisibleChange: () => {}, + showMask: true, + maskClosable: true, + maskStyle: {}, + wrapperClassName: null, + keyboard: true, + forceRender: false, + autoFocus: true, + }), + emits: ['handleClick', 'close'], + + setup(props, { emit, expose, slots }) { + const dom = ref(null); + + const container = ref(props.getContainer || null); + + const open = ref(props.open); + + const $forceRender = ref(props.forceRender); + + watch( + () => props.open, + val => { + if (!dom.value) { + $forceRender.value = true; + open.value = false; + setTimeout(() => { + open.value = true; + }); + } else { + open.value = val; + } + }, + ); + + const getDerivedStateFromProps = ( + props: IDrawerProps, + { prevProps }: { prevProps: IDrawerProps }, + ) => { + const newState: { + open?: boolean; + prevProps: IDrawerProps; + } = { + prevProps: props, + }; + if (typeof prevProps !== 'undefined' && props.open !== prevProps.open) { + newState.open = props.open; + } + return newState; + }; + + expose({ getDerivedStateFromProps }); + + const onHandleClick = (e: MouseEvent | KeyboardEvent) => { + emit('handleClick', e); + }; + + const onClose = (e: MouseEvent | KeyboardEvent) => { + emit('close', e); + }; + + return () => { + const { afterVisibleChange, getContainer, wrapperClassName, forceRender, ...otherProps } = + props; + + let portal = null; + if (!getContainer) { + return ( +
+ dom.value} + onClose={onClose} + onHandleClick={onHandleClick} + > +
+ ); + } + if ($forceRender.value || open.value || dom.value) { + portal = ( + +
+ dom.value} + afterVisibleChange={afterVisibleChange} + onClose={onClose} + onHandleClick={onHandleClick} + /> +
+
+ ); + } + return portal; + }; + }, +}); + +export default DrawerWrapper; diff --git a/components/vc-drawer/src/IDrawerPropTypes.js b/components/vc-drawer/src/IDrawerPropTypes.ts similarity index 51% rename from components/vc-drawer/src/IDrawerPropTypes.js rename to components/vc-drawer/src/IDrawerPropTypes.ts index 44867cb16..bbf9f35f0 100644 --- a/components/vc-drawer/src/IDrawerPropTypes.js +++ b/components/vc-drawer/src/IDrawerPropTypes.ts @@ -1,29 +1,33 @@ import PropTypes from '../../_util/vue-types'; +import { PropType, ExtractPropTypes } from 'vue'; -const IProps = { - width: PropTypes.any, - height: PropTypes.any, - defaultOpen: PropTypes.looseBool, - firstEnter: PropTypes.looseBool, - open: PropTypes.looseBool, +export type IPlacement = 'left' | 'top' | 'right' | 'bottom'; + +const Props = { prefixCls: PropTypes.string, - placement: PropTypes.string, + width: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + height: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + style: PropTypes.object, + placement: { + type: String as PropType, + }, + class: PropTypes.string, level: PropTypes.oneOfType([PropTypes.string, PropTypes.array]), levelMove: PropTypes.oneOfType([PropTypes.number, PropTypes.func, PropTypes.array]), - ease: PropTypes.string, duration: PropTypes.string, - handler: PropTypes.any, + ease: PropTypes.string, showMask: PropTypes.looseBool, - maskStyle: PropTypes.object, - className: PropTypes.string, - wrapStyle: PropTypes.object, maskClosable: PropTypes.looseBool, + maskStyle: PropTypes.object, afterVisibleChange: PropTypes.func, keyboard: PropTypes.looseBool, + contentWrapperStyle: PropTypes.object, + autoFocus: PropTypes.looseBool, + open: PropTypes.looseBool, }; -const IDrawerProps = { - ...IProps, +const DrawerProps = { + ...Props, wrapperClassName: PropTypes.string, forceRender: PropTypes.looseBool, getContainer: PropTypes.oneOfType([ @@ -34,11 +38,16 @@ const IDrawerProps = { ]), }; -const IDrawerChildProps = { - ...IProps, +type IDrawerProps = Partial>; + +const DrawerChildProps = { + ...Props, getContainer: PropTypes.func, getOpenCount: PropTypes.func, + scrollLocker: PropTypes.any, switchScrollingEffect: PropTypes.func, }; -export { IDrawerProps, IDrawerChildProps }; +type IDrawerChildProps = Partial>; + +export { DrawerProps, DrawerChildProps, IDrawerProps, IDrawerChildProps }; diff --git a/components/vc-drawer/src/index.js b/components/vc-drawer/src/index.js deleted file mode 100644 index 3c5531745..000000000 --- a/components/vc-drawer/src/index.js +++ /dev/null @@ -1,5 +0,0 @@ -// base in 1.7.7 -// export this package's api -import Drawer from './Drawer'; - -export default Drawer; diff --git a/components/vc-drawer/src/utils.js b/components/vc-drawer/src/utils.ts similarity index 57% rename from components/vc-drawer/src/utils.js rename to components/vc-drawer/src/utils.ts index 7da5b91e3..b8ba8b3d8 100644 --- a/components/vc-drawer/src/utils.js +++ b/components/vc-drawer/src/utils.ts @@ -1,47 +1,54 @@ -export function dataToArray(vars) { +export function dataToArray(vars: any) { if (Array.isArray(vars)) { return vars; } return [vars]; } -const transitionEndObject = { +const transitionEndObject: Record = { transition: 'transitionend', WebkitTransition: 'webkitTransitionEnd', MozTransition: 'transitionend', OTransition: 'oTransitionEnd otransitionend', }; -export const transitionStr = Object.keys(transitionEndObject).filter(key => { +export const transitionStr: string = Object.keys(transitionEndObject).filter(key => { if (typeof document === 'undefined') { return false; } const html = document.getElementsByTagName('html')[0]; return key in (html ? html.style : {}); })[0]; -export const transitionEnd = transitionEndObject[transitionStr]; +export const transitionEndFun: string = transitionEndObject[transitionStr]; -export function addEventListener(target, eventType, callback, options) { +export function addEventListener( + target: HTMLElement, + eventType: string, + callback: (e: TouchEvent | Event) => void, + options?: any, +) { if (target.addEventListener) { target.addEventListener(eventType, callback, options); - } else if (target.attachEvent) { - target.attachEvent(`on${eventType}`, callback); + } else if ((target as any).attachEvent) { + // tslint:disable-line + (target as any).attachEvent(`on${eventType}`, callback); // tslint:disable-line } } -export function removeEventListener(target, eventType, callback, options) { +export function removeEventListener( + target: HTMLElement, + eventType: string, + callback: (e: TouchEvent | Event) => void, + options?: any, +) { if (target.removeEventListener) { target.removeEventListener(eventType, callback, options); - } else if (target.attachEvent) { - target.detachEvent(`on${eventType}`, callback); + } else if ((target as any).attachEvent) { + // tslint:disable-line + (target as any).detachEvent(`on${eventType}`, callback); // tslint:disable-line } } -export function transformArguments(arg, cb) { - let result; - if (typeof arg === 'function') { - result = arg(cb); - } else { - result = arg; - } +export function transformArguments(arg: any, cb: any) { + const result = typeof arg === 'function' ? arg(cb) : arg; if (Array.isArray(result)) { if (result.length === 2) { return result; @@ -51,9 +58,8 @@ export function transformArguments(arg, cb) { return [result]; } -export const isNumeric = value => { - return !isNaN(parseFloat(value)) && isFinite(value); // eslint-disable-line -}; +export const isNumeric = (value: string | number | undefined) => + !isNaN(parseFloat(value as string)) && isFinite(value as number); export const windowIsUndefined = !( typeof window !== 'undefined' && @@ -61,7 +67,12 @@ export const windowIsUndefined = !( window.document.createElement ); -export const getTouchParentScroll = (root, currentTarget, differX, differY) => { +export const getTouchParentScroll = ( + root: HTMLElement, + currentTarget: HTMLElement | Document | null, + differX: number, + differY: number, +): boolean => { if (!currentTarget || currentTarget === document || currentTarget instanceof Document) { return false; } @@ -92,10 +103,10 @@ export const getTouchParentScroll = (root, currentTarget, differX, differY) => { (isX && (!x || (x && - ((currentTarget.scrollLeft >= scrollX && scrollX < 0) || - (currentTarget.scrollLeft <= 0 && scrollX > 0))))) + ((currentTarget.scrollLeft >= scrollX && differX < 0) || + (currentTarget.scrollLeft <= 0 && differX > 0))))) ) { - return getTouchParentScroll(root, currentTarget.parentNode, differX, differY); + return getTouchParentScroll(root, currentTarget.parentNode as HTMLElement, differX, differY); } return false; };