diff --git a/components/drawer/__tests__/MultiDrawer.test.js b/components/drawer/__tests__/MultiDrawer.test.js index 6380569d72..1a56b9b7ac 100644 --- a/components/drawer/__tests__/MultiDrawer.test.js +++ b/components/drawer/__tests__/MultiDrawer.test.js @@ -38,14 +38,14 @@ const MultiDrawer = { width: 520, visible: this.visible, getContainer: false, - wrapClassName: 'test_drawer', + className: 'test_drawer', placement: this.placement, onClose: this.onClose, }; const childrenDrawerProps = { title: 'Two-level Drawer', width: 320, - wrapClassName: 'Two-level', + className: 'Two-level', visible: this.childrenDrawer, getContainer: false, placement: this.placement, @@ -112,7 +112,7 @@ describe('Drawer', () => { wrapper.find('#open_two_drawer').trigger('click'); }, 0); await asyncExpect(() => { - const translateX = wrapper.find('.ant-drawer.test_drawer').element.style.transform; + const translateX = wrapper.find('.test_drawer').find('.ant-drawer').element.style.transform; expect(translateX).toEqual('translateX(-180px)'); expect(wrapper.find('#two_drawer_text').exists()).toBe(true); }, 1000); @@ -133,7 +133,7 @@ describe('Drawer', () => { wrapper.find('#open_two_drawer').trigger('click'); }, 0); await asyncExpect(() => { - const translateX = wrapper.find('.ant-drawer.test_drawer').element.style.transform; + const translateX = wrapper.find('.test_drawer').find('.ant-drawer').element.style.transform; expect(translateX).toEqual('translateX(180px)'); expect(wrapper.find('#two_drawer_text').exists()).toBe(true); }, 1000); @@ -153,7 +153,7 @@ describe('Drawer', () => { wrapper.find('#open_two_drawer').trigger('click'); }, 0); await asyncExpect(() => { - const translateY = wrapper.find('.ant-drawer.test_drawer').element.style.transform; + const translateY = wrapper.find('.test_drawer').find('.ant-drawer').element.style.transform; expect(translateY).toEqual('translateY(180px)'); expect(wrapper.find('#two_drawer_text').exists()).toBe(true); }, 1000); diff --git a/components/drawer/__tests__/__snapshots__/Drawer.test.js.snap b/components/drawer/__tests__/__snapshots__/Drawer.test.js.snap index be9a172016..0e5591ff4c 100644 --- a/components/drawer/__tests__/__snapshots__/Drawer.test.js.snap +++ b/components/drawer/__tests__/__snapshots__/Drawer.test.js.snap @@ -1,110 +1,122 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Drawer class is test_drawer 1`] = ` -
-
+
+
-
+
-
- +
+
+ +
+
Here is content of Drawer
+
-
`; exports[`Drawer closable is false 1`] = ` -
-
+
+
-
+
Here is content of Drawer
+
-
`; exports[`Drawer destroyOnClose is true 1`] = ` -
+
-
+
Here is content of Drawer
+
-
`; exports[`Drawer have a title 1`] = ` -
-
+
+
-
+
-
Test Title
+
+
Test Title
+
+
Here is content of Drawer
+
-
`; exports[`Drawer render correctly 1`] = ` -
-
+
+
-
+
-
- +
+
+ +
+
Here is content of Drawer
+
-
`; exports[`Drawer render top drawer 1`] = ` -
-
+
+
-
+
-
- +
+
+ +
+
Here is content of Drawer
+
-
diff --git a/components/drawer/__tests__/__snapshots__/DrawerEvent.test.js.snap b/components/drawer/__tests__/__snapshots__/DrawerEvent.test.js.snap index 2f0490811c..5336ced19c 100644 --- a/components/drawer/__tests__/__snapshots__/DrawerEvent.test.js.snap +++ b/components/drawer/__tests__/__snapshots__/DrawerEvent.test.js.snap @@ -4,19 +4,22 @@ exports[`Drawer render correctly 1`] = `
-
+
-
+
-
- +
+
+ +
+
Here is content of Drawer
+
-
diff --git a/components/drawer/__tests__/__snapshots__/demo.test.js.snap b/components/drawer/__tests__/__snapshots__/demo.test.js.snap index 125aa94514..cc47ae3b95 100644 --- a/components/drawer/__tests__/__snapshots__/demo.test.js.snap +++ b/components/drawer/__tests__/__snapshots__/demo.test.js.snap @@ -7,6 +7,13 @@ exports[`renders ./components/drawer/demo/basic.vue correctly 1`] = ` `; +exports[`renders ./components/drawer/demo/extra.vue correctly 1`] = ` +
+ +`; + exports[`renders ./components/drawer/demo/form-in-drawer.vue correctly 1`] = `
-
-
+
+
-
+
-
Basic Drawer
+
+ +
Basic Drawer
+

Some contents...

+
-
`; +exports[`renders ./components/drawer/demo/size.vue correctly 1`] = ` + + +`; + exports[`renders ./components/drawer/demo/user-profile.vue correctly 1`] = `
diff --git a/components/drawer/demo/basic.vue b/components/drawer/demo/basic.vue index c446dfd4e4..7a203d6ce9 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 0000000000..e940d5310e --- /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 482078cdb2..6d92bd650a 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 bbe0817ec1..4de4e4d110 100644 --- a/components/drawer/demo/user-profile.vue +++ b/components/drawer/demo/user-profile.vue @@ -1,6 +1,6 @@ --- -order: 4 +order: 6 title: zh-CN: 信息预览抽屉 en-US: Preview drawer diff --git a/components/drawer/index.en-US.md b/components/drawer/index.en-US.md index 30bbe553a1..6a7e459811 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 | +| Props | 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 | - | | +| 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 c401f96936..4382b37dc4 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 useConfigInject from '../_util/hooks/useConfigInject'; 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) { - 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(); - } + 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 parentDrawerOpts = inject('parentDrawerOpts', null); + const { prefixCls } = useConfigInject('drawer', props); + + onBeforeMount(() => { + provide('parentDrawerOpts', { + setPush, + setPull, + }); + }); + + onMounted(() => { + const { visible } = props; + if (visible && parentDrawerOpts) { + parentDrawerOpts.setPush(); } - 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(); + + onUnmounted(() => { + if (parentDrawerOpts) { + parentDrawerOpts.setPull(); } - }, - 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, + }); + + onUpdated(() => { + const { visible } = props; + nextTick(() => { + if (preVisible.value !== visible && parentDrawerOpts) { + if (visible) { + parentDrawerOpts.setPush(); + } else { + parentDrawerOpts.setPull(); + } + } + preVisible.value = visible; }); - }, - pull() { - this.setState( - { - sPush: false, - }, - () => { - this.domFocus(); - }, - ); - }, - onDestroyTransitionEnd() { - const isDestroyOnClose = this.getDestroyOnClose(); + }); + + 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; } - }, - getDestroyOnClose() { - return this.destroyOnClose && !this.visible; - }, - // get drawar push width or height - getPushTransform(placement?: placementType) { + distance = parseFloat(String(distance || 0)); + 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,83 @@ 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 { width, height, visible, placement, mask, className, ...rest } = props; + + const offsetStyle = mask ? getOffsetStyle() : {}; + const haveMask = mask ? '' : 'no-mask'; + const vcDrawerProps: any = { + ...attrs, + ...omit(rest, [ + 'size', + 'closeIcon', + 'closable', + 'destroyOnClose', + 'drawerStyle', + 'headerStyle', + 'bodyStyle', + 'title', + 'push', + ]), + ...offsetStyle, + onClose: close, + afterVisibleChange, + handler: false, + prefixCls: prefixCls.value, + open: visible, + showMask: mask, + placement, + wrapperClassName: classnames({ + [className as string]: className, + [haveMask]: !!haveMask, + }), + style: getRcDrawerStyle(), + ref: vcDrawer, + }; + return {renderBody(prefixCls.value)}; + }; }, }); diff --git a/components/drawer/index.zh-CN.md b/components/drawer/index.zh-CN.md index bec03bb6c6..d6af45da28 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 f28ad981b2..eab0002fd5 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 79d170c750..a36039cdaa 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 0000000000..f710bfa7dd --- /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 41d2c49322..93b238b7bc 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 2af70fb469..97acc42846 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 0000000000..2049a56b93 --- /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/Drawer.jsx b/components/vc-drawer/src/Drawer.jsx deleted file mode 100644 index 8e1c9c8f2b..0000000000 --- a/components/vc-drawer/src/Drawer.jsx +++ /dev/null @@ -1,620 +0,0 @@ -import classnames from '../../_util/classNames'; -import { Teleport, nextTick, defineComponent } from 'vue'; -import BaseMixin from '../../_util/BaseMixin'; -import { initDefaultProps, getSlot } from '../../_util/props-util'; -import getScrollBarSize from '../../_util/getScrollBarSize'; -import { IDrawerProps } from './IDrawerPropTypes'; -import KeyCode from '../../_util/KeyCode'; -import { - dataToArray, - transitionEnd, - transitionStr, - addEventListener, - removeEventListener, - transformArguments, - isNumeric, -} from './utils'; -import supportsPassive from '../../_util/supportsPassive'; -import { cloneElement } from '../../_util/vnode'; - -function noop() {} - -const currentDrawer = {}; -const windowIsUndefined = !( - typeof window !== 'undefined' && - window.document && - window.document.createElement -); - -const Drawer = defineComponent({ - name: 'Drawer', - mixins: [BaseMixin], - inheritAttrs: false, - props: initDefaultProps(IDrawerProps, { - prefixCls: 'drawer', - placement: 'left', - getContainer: 'body', - level: 'all', - duration: '.3s', - ease: 'cubic-bezier(0.78, 0.14, 0.15, 0.86)', - firstEnter: false, // 记录首次进入. - showMask: true, - handler: true, - maskStyle: {}, - wrapperClassName: '', - }), - data() { - this.levelDom = []; - this.contentDom = null; - this.maskDom = null; - this.handlerdom = null; - this.mousePos = null; - this.sFirstEnter = this.firstEnter; - this.timeout = null; - this.children = null; - this.dom = null; - this.drawerId = Number( - (Date.now() + Math.random()).toString().replace('.', Math.round(Math.random() * 9)), - ).toString(16); - const open = this.open !== undefined ? this.open : !!this.defaultOpen; - currentDrawer[this.drawerId] = open; - this.orignalOpen = this.open; - this.preProps = { ...this.$props }; - return { - sOpen: open, - isOpenChange: undefined, - passive: undefined, - container: undefined, - }; - }, - watch: { - open(val) { - if (val !== undefined && val !== this.preProps.open) { - this.isOpenChange = true; - // 没渲染 dom 时,获取默认数据; - if (!this.container) { - this.getDefault(this.$props); - } - this.setState({ - sOpen: open, - }); - } - this.preProps.open = val; - if (val) { - setTimeout(() => { - this.domFocus(); - }); - } - }, - placement(val) { - if (val !== this.preProps.placement) { - // test 的 bug, 有动画过场,删除 dom - this.contentDom = null; - } - this.preProps.placement = val; - }, - level(val) { - if (this.preProps.level !== val) { - this.getParentAndLevelDom(this.$props); - } - this.preProps.level = val; - }, - }, - mounted() { - nextTick(() => { - if (!windowIsUndefined) { - this.passive = supportsPassive ? { passive: false } : false; - } - const open = this.getOpen(); - if (this.handler || open || this.sFirstEnter) { - this.getDefault(this.$props); - if (open) { - this.isOpenChange = true; - nextTick(() => { - this.domFocus(); - }); - } - this.$forceUpdate(); - } - }); - }, - updated() { - nextTick(() => { - // dom 没渲染时,重走一遍。 - if (!this.sFirstEnter && this.container) { - this.$forceUpdate(); - this.sFirstEnter = true; - } - }); - }, - beforeUnmount() { - delete currentDrawer[this.drawerId]; - delete this.isOpenChange; - if (this.container) { - if (this.sOpen) { - this.setLevelDomTransform(false, true); - } - document.body.style.overflow = ''; - } - this.sFirstEnter = false; - clearTimeout(this.timeout); - }, - methods: { - domFocus() { - if (this.dom) { - this.dom.focus(); - } - }, - onKeyDown(e) { - if (e.keyCode === KeyCode.ESC) { - e.stopPropagation(); - this.__emit('close', e); - } - }, - onMaskTouchEnd(e) { - this.__emit('close', e); - this.onTouchEnd(e, true); - }, - onIconTouchEnd(e) { - this.__emit('handleClick', e); - this.onTouchEnd(e); - }, - onTouchEnd(e, close) { - if (this.open !== undefined) { - return; - } - const open = close || this.sOpen; - this.isOpenChange = true; - this.setState({ - sOpen: !open, - }); - }, - onWrapperTransitionEnd(e) { - if (e.target === this.contentWrapper && e.propertyName.match(/transform$/)) { - const open = this.getOpen(); - this.dom.style.transition = ''; - if (!open && this.getCurrentDrawerSome()) { - document.body.style.overflowX = ''; - if (this.maskDom) { - this.maskDom.style.left = ''; - this.maskDom.style.width = ''; - } - } - if (this.afterVisibleChange) { - this.afterVisibleChange(!!open); - } - } - }, - getDefault(props) { - this.getParentAndLevelDom(props); - if (props.getContainer || props.parent) { - this.container = this.defaultGetContainer(); - } - }, - getCurrentDrawerSome() { - return !Object.keys(currentDrawer).some(key => currentDrawer[key]); - }, - getSelfContainer() { - return this.container; - }, - getParentAndLevelDom(props) { - if (windowIsUndefined) { - return; - } - const { level, getContainer } = props; - this.levelDom = []; - if (getContainer) { - if (typeof getContainer === 'string') { - const dom = document.querySelectorAll(getContainer)[0]; - this.parent = dom; - } - if (typeof getContainer === 'function') { - this.parent = getContainer(); - } - if (typeof getContainer === 'object' && getContainer instanceof window.HTMLElement) { - this.parent = getContainer; - } - } - if (!getContainer && this.container) { - this.parent = this.container.parentNode; - } - if (level === 'all') { - const children = Array.prototype.slice.call(this.parent.children); - children.forEach(child => { - if ( - child.nodeName !== 'SCRIPT' && - child.nodeName !== 'STYLE' && - child.nodeName !== 'LINK' && - child !== this.container - ) { - this.levelDom.push(child); - } - }); - } else if (level) { - dataToArray(level).forEach(key => { - document.querySelectorAll(key).forEach(item => { - this.levelDom.push(item); - }); - }); - } - }, - setLevelDomTransform(open, openTransition, placementName, value) { - const { placement, levelMove, duration, ease, getContainer } = this.$props; - if (!windowIsUndefined) { - this.levelDom.forEach(dom => { - if (dom && (this.isOpenChange || openTransition)) { - /* eslint no-param-reassign: "error" */ - dom.style.transition = `transform ${duration} ${ease}`; - addEventListener(dom, transitionEnd, this.trnasitionEnd); - 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; - const placementPos = - placement === 'left' || placement === 'top' ? $value : `-${$value}`; - dom.style.transform = levelValue ? `${placementName}(${placementPos})` : ''; - dom.style.msTransform = levelValue ? `${placementName}(${placementPos})` : ''; - } - }); - // 处理 body 滚动 - if (getContainer === 'body') { - const eventArray = ['touchstart']; - const domArray = [document.body, this.maskDom, this.handlerdom, this.contentDom]; - const right = - document.body.scrollHeight > - (window.innerHeight || document.documentElement.clientHeight) && - window.innerWidth > document.body.offsetWidth - ? getScrollBarSize(1) - : 0; - let widthTransition = `width ${duration} ${ease}`; - const trannsformTransition = `transform ${duration} ${ease}`; - if (open && document.body.style.overflow !== 'hidden') { - document.body.style.overflow = 'hidden'; - if (right) { - document.body.style.position = 'relative'; - document.body.style.width = `calc(100% - ${right}px)`; - clearTimeout(this.timeout); - if (this.dom) { - this.dom.style.transition = 'none'; - switch (placement) { - case 'right': - this.dom.style.transform = `translateX(-${right}px)`; - this.dom.style.msTransform = `translateX(-${right}px)`; - break; - case 'top': - case 'bottom': - this.dom.style.width = `calc(100% - ${right}px)`; - this.dom.style.transform = 'translateZ(0)'; - break; - default: - break; - } - this.timeout = setTimeout(() => { - this.dom.style.transition = `${trannsformTransition},${widthTransition}`; - this.dom.style.width = ''; - this.dom.style.transform = ''; - this.dom.style.msTransform = ''; - }); - } - } - // 手机禁滚 - domArray.forEach((item, i) => { - if (!item) { - return; - } - addEventListener( - item, - eventArray[i] || 'touchmove', - i ? this.removeMoveHandler : this.removeStartHandler, - this.passive, - ); - }); - } else if (this.getCurrentDrawerSome()) { - document.body.style.overflow = ''; - if ((this.isOpenChange || openTransition) && right) { - document.body.style.position = ''; - document.body.style.width = ''; - if (transitionStr) { - document.body.style.overflowX = 'hidden'; - } - if (placement === 'right' && this.maskDom) { - this.maskDom.style.left = `-${right}px`; - this.maskDom.style.width = `calc(100% + ${right}px)`; - } - clearTimeout(this.timeout); - if (this.dom) { - this.dom.style.transition = 'none'; - let heightTransition; - switch (placement) { - case 'right': { - this.dom.style.transform = `translateX(${right}px)`; - this.dom.style.msTransform = `translateX(${right}px)`; - this.dom.style.width = '100%'; - widthTransition = `width 0s ${ease} ${duration}`; - break; - } - case 'top': - case 'bottom': { - this.dom.style.width = `calc(100% + ${right}px)`; - this.dom.style.height = '100%'; - this.dom.style.transform = 'translateZ(0)'; - heightTransition = `height 0s ${ease} ${duration}`; - break; - } - default: - break; - } - this.timeout = setTimeout(() => { - this.dom.style.transition = `${trannsformTransition},${ - heightTransition ? `${heightTransition},` : '' - }${widthTransition}`; - this.dom.style.transform = ''; - this.dom.style.msTransform = ''; - this.dom.style.width = ''; - this.dom.style.height = ''; - }); - } - } - domArray.forEach((item, i) => { - if (!item) { - return; - } - removeEventListener( - item, - eventArray[i] || 'touchmove', - i ? this.removeMoveHandler : this.removeStartHandler, - this.passive, - ); - }); - } - } - } - const { onChange } = this.$attrs; - if (onChange && this.isOpenChange && this.sFirstEnter) { - onChange(open); - this.isOpenChange = false; - } - }, - getChildToRender(open) { - const { - prefixCls, - placement, - handler, - showMask, - maskStyle, - width, - height, - wrapStyle, - keyboard, - maskClosable, - } = this.$props; - const { class: cls, style, ...restAttrs } = this.$attrs; - const children = getSlot(this); - const wrapperClassname = classnames(prefixCls, { - [`${prefixCls}-${placement}`]: true, - [`${prefixCls}-open`]: open, - 'no-mask': !showMask, - [cls]: cls, - }); - const isOpenChange = this.isOpenChange; - const isHorizontal = placement === 'left' || placement === 'right'; - const placementName = `translate${isHorizontal ? 'X' : 'Y'}`; - // 百分比与像素动画不同步,第一次打用后全用像素动画。 - // const defaultValue = !this.contentDom || !level ? '100%' : `${value}px`; - const placementPos = placement === 'left' || placement === 'top' ? '-100%' : '100%'; - const transform = open ? '' : `${placementName}(${placementPos})`; - if (isOpenChange === undefined || isOpenChange) { - const contentValue = this.contentDom - ? this.contentDom.getBoundingClientRect()[isHorizontal ? 'width' : 'height'] - : 0; - const value = (isHorizontal ? width : height) || contentValue; - this.setLevelDomTransform(open, false, placementName, value); - } - let handlerChildren; - if (handler !== false) { - const handlerDefalut = ( -
{}}> - -
- ); - const { handler: handlerSlot } = this; - const handlerSlotVnode = handlerSlot || handlerDefalut; - const handleIconClick = handlerSlotVnode.props && handlerSlotVnode.props.onClick; - handlerChildren = cloneElement(handlerSlotVnode, { - onClick: e => { - handleIconClick && handleIconClick(e); - this.onIconTouchEnd(e); - }, - ref: c => { - this.handlerdom = c; - }, - }); - } - const domContProps = { - ...restAttrs, - class: wrapperClassname, - onTransitionend: this.onWrapperTransitionEnd, - onKeydown: open && keyboard ? this.onKeyDown : noop, - style: { ...wrapStyle, ...style }, - }; - // 跑用例用 - const touchEvents = { - [supportsPassive ? 'onTouchstartPassive' : 'onTouchstart']: open - ? this.removeStartHandler - : noop, - [supportsPassive ? 'onTouchmovePassive' : 'onTouchmove']: open - ? this.removeMoveHandler - : noop, - }; - return ( -
{ - this.dom = c; - }} - {...domContProps} - tabindex={-1} - > - {showMask && ( -
{ - this.maskDom = c; - }} - /> - )} -
{ - this.contentWrapper = c; - }} - > -
{ - this.contentDom = c; - }} - {...touchEvents} - > - {children} -
- {handlerChildren} -
-
- ); - }, - getOpen() { - return this.open !== undefined ? this.open : this.sOpen; - }, - getTouchParentScroll(root, currentTarget, differX, differY) { - if (!currentTarget || currentTarget === document) { - return false; - } - // root 为 drawer-content 设定了 overflow, 判断为 root 的 parent 时结束滚动; - if (currentTarget === root.parentNode) { - return true; - } - - const isY = Math.max(Math.abs(differX), Math.abs(differY)) === Math.abs(differY); - const isX = Math.max(Math.abs(differX), Math.abs(differY)) === Math.abs(differX); - - const scrollY = currentTarget.scrollHeight - currentTarget.clientHeight; - const scrollX = currentTarget.scrollWidth - currentTarget.clientWidth; - /** - *
- *
- *
- * 在没设定 overflow: auto 或 scroll 时,currentTarget 里获取不到 scrollTop 或 scrollLeft, - * 预先用 scrollTo 来滚动,如果取出的值跟滚动前取出不同,则 currnetTarget 被设定了 overflow; 否则就是上面这种。 - */ - const t = currentTarget.scrollTop; - const l = currentTarget.scrollLeft; - if (currentTarget.scrollTo) { - currentTarget.scrollTo(currentTarget.scrollLeft + 1, currentTarget.scrollTop + 1); - } - const currentT = currentTarget.scrollTop; - const currentL = currentTarget.scrollLeft; - if (currentTarget.scrollTo) { - currentTarget.scrollTo(currentTarget.scrollLeft - 1, currentTarget.scrollTop - 1); - } - if ( - (isY && - (!scrollY || - !(currentT - t) || - (scrollY && - ((currentTarget.scrollTop >= scrollY && differY < 0) || - (currentTarget.scrollTop <= 0 && differY > 0))))) || - (isX && - (!scrollX || - !(currentL - l) || - (scrollX && - ((currentTarget.scrollLeft >= scrollX && differX < 0) || - (currentTarget.scrollLeft <= 0 && differX > 0))))) - ) { - return this.getTouchParentScroll(root, currentTarget.parentNode, differX, differY); - } - return false; - }, - removeStartHandler(e) { - if (e.touches.length > 1) { - return; - } - this.startPos = { - x: e.touches[0].clientX, - y: e.touches[0].clientY, - }; - }, - removeMoveHandler(e) { - if (e.changedTouches.length > 1) { - return; - } - const currentTarget = e.currentTarget; - const differX = e.changedTouches[0].clientX - this.startPos.x; - const differY = e.changedTouches[0].clientY - this.startPos.y; - if ( - currentTarget === this.maskDom || - currentTarget === this.handlerdom || - (currentTarget === this.contentDom && - this.getTouchParentScroll(currentTarget, e.target, differX, differY)) - ) { - e.preventDefault(); - } - }, - trnasitionEnd(e) { - removeEventListener(e.target, transitionEnd, this.trnasitionEnd); - e.target.style.transition = ''; - }, - defaultGetContainer() { - if (windowIsUndefined) { - return null; - } - const container = document.createElement('div'); - this.parent.appendChild(container); - if (this.wrapperClassName) { - container.className = this.wrapperClassName; - } - return container; - }, - }, - - render() { - const { getContainer, wrapperClassName, handler, forceRender } = this.$props; - const open = this.getOpen(); - let portal = null; - currentDrawer[this.drawerId] = open ? this.container : open; - const children = this.getChildToRender(this.sFirstEnter ? open : false); - if (!getContainer) { - return ( -
{ - this.container = c; - }} - > - {children} -
- ); - } - if (!this.container || (!open && !this.sFirstEnter)) { - return null; - } - // 如果有 handler 为内置强制渲染; - const $forceRender = !!handler || forceRender; - if ($forceRender || open || this.dom) { - portal = {children}; - } - return portal; - }, -}); - -export default Drawer; diff --git a/components/vc-drawer/src/DrawerChild.tsx b/components/vc-drawer/src/DrawerChild.tsx new file mode 100644 index 0000000000..c690a3229c --- /dev/null +++ b/components/vc-drawer/src/DrawerChild.tsx @@ -0,0 +1,534 @@ +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 setStyle from '../../_util/setStyle'; + +import { + addEventListener, + dataToArray, + getTouchParentScroll, + isNumeric, + removeEventListener, + transformArguments, + transitionEndFun, + windowIsUndefined, +} from './utils'; + +const currentDrawer: Record = {}; + +export interface scrollLockOptions { + container: HTMLElement; +} + +const createScrollLocker = (options: scrollLockOptions) => { + const scrollBarSize = 0; + const cacheStyleMap = new Map(); + return { + getContainer: () => { + return options?.container; + }, + getCacheStyleMap: () => { + return cacheStyleMap; + }, + lock: () => { + cacheStyleMap.set( + options.container, + setStyle( + { + width: scrollBarSize !== 0 ? `calc(100% - ${scrollBarSize}px)` : undefined, + overflow: 'hidden', + overflowX: 'hidden', + overflowY: 'hidden', + }, + { + element: options.container || document.body, + }, + ), + ); + }, + unLock: () => { + setStyle(cacheStyleMap.get(options.container) || {}, { element: options.container }); + cacheStyleMap.delete(options.container); + }, + }; +}; + +const DrawerChild = defineComponent({ + inheritAttrs: false, + props: DrawerChildProps, + emits: ['close', 'handleClick', 'change'], + setup(props, { emit, slots }) { + 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, + }, + scrollLocker: 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 (container) { + state.scrollLocker = createScrollLocker({ + container: container.parentNode, + }); + } + + if (open) { + if (container && container.parentNode === document.body) { + currentDrawer[state.drawerId] = open; + } + // 默认打开状态时推出 level; + openLevelTransition(); + nextTick(() => { + if (autoFocus) { + domFocus(); + } + }); + if (showMask) { + state.scrollLocker?.lock(); + } + } + }); + }); + + onUpdated(() => { + const { open, getContainer, showMask, autoFocus } = props; + + const container = getContainer?.(); + if (container && container.parentNode === document.body) { + currentDrawer[state.drawerId] = !!open; + } + openLevelTransition(); + if (open) { + if (autoFocus) { + domFocus(); + } + if (showMask) { + state.scrollLocker?.lock(); + } + } else { + state.scrollLocker?.unLock(); + } + }); + + onUnmounted(() => { + const { open } = props; + delete currentDrawer[state.drawerId]; + if (open) { + setLevelTransform(false); + document.body.style.touchAction = ''; + } + state.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; + + 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, + }; + }; + + 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 0000000000..855eddecc7 --- /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 50% rename from components/vc-drawer/src/IDrawerPropTypes.js rename to components/vc-drawer/src/IDrawerPropTypes.ts index 44867cb162..a23808d182 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 type { 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 3c55317452..0000000000 --- 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 7da5b91e32..b8ba8b3d82 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; };