refactor(drawer): use compositionAPI

refactor-drawer
ajuner 2021-09-27 17:09:18 +08:00 committed by tangjinzhou
parent 4fe3c7a919
commit 0c31ada67a
22 changed files with 1338 additions and 407 deletions

View File

@ -22,8 +22,7 @@ Basic drawer.
v-model:visible="visible" v-model:visible="visible"
title="Basic Drawer" title="Basic Drawer"
placement="right" placement="right"
:closable="false" @after-visible-change="afterVisibleChange"
:after-visible-change="afterVisibleChange"
> >
<p>Some contents...</p> <p>Some contents...</p>
<p>Some contents...</p> <p>Some contents...</p>

View File

@ -0,0 +1,65 @@
<docs>
---
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.
</docs>
<template>
<a-radio-group v-model:value="placement" style="margin-right: 8px">
<a-radio value="top">top</a-radio>
<a-radio value="right">right</a-radio>
<a-radio value="bottom">bottom</a-radio>
<a-radio value="left">left</a-radio>
</a-radio-group>
<a-button type="primary" @click="showDrawer">Open</a-button>
<a-drawer
:width="500"
title="Basic Drawer"
:placement="placement"
:visible="visible"
@close="onClose"
>
<template #extra>
<a-button style="margin-right: 8px" @click="onClose">Cancel</a-button>
<a-button type="primary" @click="onClose">Submit</a-button>
</template>
<p>Some contents...</p>
<p>Some contents...</p>
<p>Some contents...</p>
</a-drawer>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue';
export default defineComponent({
setup() {
const placement = ref<string>('left');
const visible = ref<boolean>(false);
const showDrawer = () => {
visible.value = true;
};
const onClose = () => {
visible.value = false;
};
return {
placement,
visible,
showDrawer,
onClose,
};
},
});
</script>

View File

@ -1,6 +1,6 @@
<docs> <docs>
--- ---
order: 3 order: 4
title: title:
zh-CN: 抽屉表单 zh-CN: 抽屉表单
en-US: Submit form in drawer en-US: Submit form in drawer
@ -26,6 +26,7 @@ Use form in drawer with submit button.
:width="720" :width="720"
:visible="visible" :visible="visible"
:body-style="{ paddingBottom: '80px' }" :body-style="{ paddingBottom: '80px' }"
:footer-style="{ textAlign: 'right' }"
@close="onClose" @close="onClose"
> >
<a-form :model="form" :rules="rules" layout="vertical"> <a-form :model="form" :rules="rules" layout="vertical">
@ -96,22 +97,10 @@ Use form in drawer with submit button.
</a-col> </a-col>
</a-row> </a-row>
</a-form> </a-form>
<div <template #footer>
:style="{
position: 'absolute',
right: 0,
bottom: 0,
width: '100%',
borderTop: '1px solid #e9e9e9',
padding: '10px 16px',
background: '#fff',
textAlign: 'right',
zIndex: 1,
}"
>
<a-button style="margin-right: 8px" @click="onClose">Cancel</a-button> <a-button style="margin-right: 8px" @click="onClose">Cancel</a-button>
<a-button type="primary" @click="onClose">Submit</a-button> <a-button type="primary" @click="onClose">Submit</a-button>
</div> </template>
</a-drawer> </a-drawer>
</template> </template>
<script lang="ts"> <script lang="ts">

View File

@ -1,20 +1,24 @@
<template> <template>
<demo-sort> <demo-sort>
<basic /> <basic />
<extra />
<placement /> <placement />
<render-in-current /> <render-in-current />
<form-in-drawer /> <form-in-drawer />
<user-profile /> <user-profile />
<multi-level-drawer /> <multi-level-drawer />
<size />
</demo-sort> </demo-sort>
</template> </template>
<script lang="ts"> <script lang="ts">
import Basic from './basic.vue'; import Basic from './basic.vue';
import Extra from './extra.vue';
import Placement from './placement.vue'; import Placement from './placement.vue';
import UserProfile from './user-profile.vue'; import UserProfile from './user-profile.vue';
import MultiLevelDrawer from './multi-level-drawer.vue'; import MultiLevelDrawer from './multi-level-drawer.vue';
import FormInDrawer from './form-in-drawer.vue'; import FormInDrawer from './form-in-drawer.vue';
import RenderInCurrent from './render-in-current.vue'; import RenderInCurrent from './render-in-current.vue';
import Size from './size.vue';
import CN from '../index.zh-CN.md'; import CN from '../index.zh-CN.md';
import US from '../index.en-US.md'; import US from '../index.en-US.md';
@ -25,11 +29,13 @@ export default defineComponent({
US, US,
components: { components: {
Basic, Basic,
Extra,
Placement, Placement,
UserProfile, UserProfile,
MultiLevelDrawer, MultiLevelDrawer,
FormInDrawer, FormInDrawer,
RenderInCurrent, RenderInCurrent,
Size,
}, },
setup() { setup() {
return {}; return {};

View File

@ -1,6 +1,24 @@
<docs> <docs>
--- ---
order: 5 order: 6
title:
zh-CN: 多层抽屉
en-US: Multi-level drawer
---
## zh-CN
在抽屉内打开新的抽屉用以解决多分支任务的复杂状况
## en-US
Open a new drawer on top of an existing drawer to handle multi branch tasks.
</docs>
<docs>
---
order: 6
title: title:
zh-CN: 多层抽屉 zh-CN: 多层抽屉
en-US: Multi-level drawer en-US: Multi-level drawer
@ -22,7 +40,8 @@ Open a new drawer on top of an existing drawer to handle multi branch tasks.
title="Multi-level drawer" title="Multi-level drawer"
width="520" width="520"
:closable="false" :closable="false"
:visible="visible" v-model:visible="visible"
:footer-style="{ textAlign: 'right' }"
@close="onClose" @close="onClose"
> >
<a-button type="primary" @click="showChildrenDrawer">Two-level drawer</a-button> <a-button type="primary" @click="showChildrenDrawer">Two-level drawer</a-button>
@ -30,27 +49,15 @@ Open a new drawer on top of an existing drawer to handle multi branch tasks.
title="Two-level Drawer" title="Two-level Drawer"
width="320" width="320"
:closable="false" :closable="false"
:visible="childrenDrawer" v-model:visible="childrenDrawer"
@close="onChildrenDrawerClose"
> >
<a-button type="primary" @click="showChildrenDrawer">This is two-level drawer</a-button> <a-button type="primary" @click="showChildrenDrawer">This is two-level drawer</a-button>
</a-drawer> </a-drawer>
<div
:style="{ <template #footer>
position: 'absolute',
bottom: 0,
width: '100%',
borderTop: '1px solid #e8e8e8',
padding: '10px 16px',
textAlign: 'right',
left: 0,
background: '#fff',
borderRadius: '0 0 4px 4px',
}"
>
<a-button style="margin-right: 8px" @click="onClose">Cancel</a-button> <a-button style="margin-right: 8px" @click="onClose">Cancel</a-button>
<a-button type="primary" @click="onClose">Submit</a-button> <a-button type="primary" @click="onClose">Submit</a-button>
</div> </template>
</a-drawer> </a-drawer>
</template> </template>
<script lang="ts"> <script lang="ts">
@ -71,16 +78,12 @@ export default defineComponent({
const showChildrenDrawer = () => { const showChildrenDrawer = () => {
childrenDrawer.value = true; childrenDrawer.value = true;
}; };
const onChildrenDrawerClose = () => {
childrenDrawer.value = false;
};
return { return {
visible, visible,
childrenDrawer, childrenDrawer,
showDrawer, showDrawer,
onClose, onClose,
showChildrenDrawer, showChildrenDrawer,
onChildrenDrawerClose,
}; };
}, },
}); });

View File

@ -1,6 +1,6 @@
<docs> <docs>
--- ---
order: 2 order: 3
title: title:
zh-CN: 渲染在当前 DOM zh-CN: 渲染在当前 DOM
en-US: Render in current dom en-US: Render in current dom
@ -40,7 +40,7 @@ Render in current dom. custom container, check getContainer.
:closable="false" :closable="false"
:visible="visible" :visible="visible"
:get-container="false" :get-container="false"
:wrap-style="{ position: 'absolute' }" :style="{ position: 'absolute' }"
@close="onClose" @close="onClose"
> >
<p>Some contents...</p> <p>Some contents...</p>

View File

@ -0,0 +1,61 @@
<docs>
---
order: 7
title:
zh-CN: 预设宽度
en-US: Presetted size
---
## zh-CN
抽屉的默认宽度为 `378px`另外还提供一个大号抽屉 `736px`可以用 size 属性来设置
## en-US
The default width (or height) of Drawer is `378px`, and there is a presetted large size `736px`.
</docs>
<template>
<a-button type="primary" @click="showDrawer('default')">Open Default Size (378px)</a-button>
<a-button type="primary" @click="showDrawer('large')">Open Large Size (736px)</a-button>
<a-drawer
title="Basic Drawer"
:size="size"
:placement="placement"
:visible="visible"
@close="onClose"
>
<template #extra>
<a-button style="margin-right: 8px" @click="onClose">Cancel</a-button>
<a-button type="primary" @click="onClose">Submit</a-button>
</template>
<p>Some contents...</p>
<p>Some contents...</p>
<p>Some contents...</p>
</a-drawer>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue';
export default defineComponent({
setup() {
const visible = ref<boolean>(false);
const size = ref<string>('default');
const showDrawer = (val: string) => {
size.value = val;
visible.value = true;
};
const onClose = () => {
visible.value = false;
};
return {
visible,
size,
showDrawer,
onClose,
};
},
});
</script>

View File

@ -1,6 +1,6 @@
<docs> <docs>
--- ---
order: 4 order: 5
title: title:
zh-CN: 信息预览抽屉 zh-CN: 信息预览抽屉
en-US: Preview drawer en-US: Preview drawer

View File

@ -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. - 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. - When the same Form is needed in multiple places.
## API ## 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 | | | autoFocus | Whether Drawer should get focused after open | boolean | true | 3.0.0 |
| destroyOnClose | Whether to unmount child components on closing drawer or not. | boolean | false | | | bodyStyle | Style of the drawer content part | CSSProperties | - | |
| getContainer | Return the mounted node for Drawer. | HTMLElement \| `() => HTMLElement` \| Selectors | 'body' | | | className(old: wrapClassName) | The class name of the container of the Drawer dialog | string | - | 3.0.0 |
| mask | Whether to show mask or not. | Boolean | true | | | closable | Whether a close (x) button is visible on top right of the Drawer dialog or not | boolean | true | |
| maskClosable | Clicking on the mask (area outside the Drawer) to close the Drawer or not. | boolean | true | | | closeIcon | Custom close icon | VNode \| slot | <CloseOutlined /> | 3.0.0 |
| maskStyle | Style for Drawer's mask element. | object | {} | | | contentWrapperStyle | Style of the drawer wrapper of content part | CSSProperties | 3.0.0 |
| title | The title for Drawer. | string\|slot | - | | | destroyOnClose | Whether to unmount child components on closing drawer or not | boolean | false | |
| 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 | - | |
| drawerStyle | Style of the popup layer element | object | - | | | drawerStyle | Style of the popup layer element | object | - | |
| headerStyle | Style of the drawer header part | object | - | | | extra | Extra actions area at corner | VNode \| slot | - | 3.0.0 |
| bodyStyle | Style of the drawer content part | object | - | | | footer | The footer for Drawer | VNode \| slot | - | 3.0.0 |
| width | Width of the Drawer dialog. | string\|number | 256 | | | footerStyle | Style of the drawer footer part | CSSProperties | - | 3.0.0 |
| height | placement is `top` or `bottom`, height of the Drawer dialog. | string\|number | - | | | forceRender | Prerender Drawer component forcely | boolean | - | false | 3.0.0 |
| zIndex | The `z-index` of the Drawer. | Number | 1000 | | | getContainer | Return the mounted node for Drawer | HTMLElement \| `() => HTMLElement` \| Selectors | 'body' | |
| placement | The placement of the Drawer. | 'top' \| 'right' \| 'bottom' \| 'left' | 'right' | | | headerStyle | Style of the drawer header part | CSSProperties | - | 3.0.0 |
| 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 | - | | | height | Placement is `top` or `bottom`, height of the Drawer dialog | string \| number | 378 | |
| afterVisibleChange | Callback after the animation ends when switching drawers. | function(visible) | - | | | keyboard | Whether support press esc to close | boolean | true | |
| 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 ## Methods
| Name | Description | Type | Default | Version | | 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) | - | | | close | Specify a callback that will be called when a user clicks mask, close button or Cancel button. | function(e) | - | |

View File

@ -1,189 +1,237 @@
import type { CSSProperties } from 'vue'; import type { CSSProperties, ExtractPropTypes, PropType } from 'vue';
import { inject, provide, nextTick, defineComponent } 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 classnames from '../_util/classNames';
import VcDrawer from '../vc-drawer/src'; import VcDrawer from '../vc-drawer';
import PropTypes from '../_util/vue-types'; import PropTypes from '../_util/vue-types';
import BaseMixin from '../_util/BaseMixin';
import CloseOutlined from '@ant-design/icons-vue/CloseOutlined'; import CloseOutlined from '@ant-design/icons-vue/CloseOutlined';
import { getComponent, getOptionProps } from '../_util/props-util';
import { defaultConfigProvider } from '../config-provider'; import { defaultConfigProvider } from '../config-provider';
import { tuple, withInstall } from '../_util/type'; import { tuple, withInstall } from '../_util/type';
import omit from '../_util/omit'; import omit from '../_util/omit';
const PlacementTypes = tuple('top', 'right', 'bottom', 'left'); 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(<CloseOutlined />),
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<sizeType>,
},
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<PushState> }]).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<ExtractPropTypes<typeof drawerProps>>;
const Drawer = defineComponent({ const Drawer = defineComponent({
name: 'ADrawer', name: 'ADrawer',
mixins: [BaseMixin],
inheritAttrs: false, inheritAttrs: false,
props: { props: drawerProps,
closable: PropTypes.looseBool.def(true), emits: ['update:visible', 'close', 'afterVisibleChange'],
destroyOnClose: PropTypes.looseBool, setup(props, { emit, slots, attrs }) {
getContainer: PropTypes.any, const sPush = ref(false);
maskClosable: PropTypes.looseBool.def(true), const preVisible = ref(props.visible);
mask: PropTypes.looseBool.def(true), const destroyClose = ref(false);
maskStyle: PropTypes.object, const vcDrawer = ref(null);
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); const configProvider = inject('configProvider', defaultConfigProvider);
return { const parentDrawerOpts = inject('parentDrawerOpts', null);
configProvider,
destroyClose: false, onBeforeMount(() => {
preVisible: props.visible, provide('parentDrawerOpts', {
parentDrawer: inject('parentDrawer', null), setPush,
}; setPull,
},
data() {
return {
sPush: false,
};
},
beforeCreate() {
provide('parentDrawer', this);
},
mounted() {
// fix: delete drawer in child and re-render, no push started.
// <Drawer>{show && <Drawer />}</Drawer>
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,
}); });
}, });
pull() {
this.setState( onMounted(() => {
{ const { visible } = props;
sPush: false, if (visible && parentDrawerOpts) {
}, parentDrawerOpts.setPush();
() => { }
this.domFocus(); });
},
); onUnmounted(() => {
}, if (parentDrawerOpts) {
onDestroyTransitionEnd() { parentDrawerOpts.setPull();
const isDestroyOnClose = this.getDestroyOnClose(); }
});
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) { if (!isDestroyOnClose) {
return; return;
} }
if (!this.visible) { if (!props.visible) {
this.destroyClose = true; destroyClose.value = true;
(this as any).$forceUpdate();
} }
}, };
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') { if (placement === 'left' || placement === 'right') {
return `translateX(${placement === 'left' ? 180 : -180}px)`; return `translateX(${placement === 'left' ? distance : -distance}px)`;
} }
if (placement === 'top' || placement === 'bottom') { 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 getRcDrawerStyle = () => {
const { sPush: push } = this.$data; const { zIndex, placement, style, mask } = props;
const offsetStyle = mask ? {} : getOffsetStyle();
return { return {
zIndex, zIndex,
transform: push ? this.getPushTransform(placement) : undefined, transform: sPush.value ? getPushTransform(placement) : undefined,
...wrapStyle, ...offsetStyle,
...style,
}; };
}, };
renderHeader(prefixCls: string) {
const { closable, headerStyle } = this.$props; const renderHeader = (prefixCls: string) => {
const title = getComponent(this, 'title'); const { closable, headerStyle } = props;
const extra = getPropsSlot(slots, props, 'extra');
const title = getPropsSlot(slots, props, 'title');
if (!title && !closable) { if (!title && !closable) {
return null; return null;
} }
const headerClassName = title ? `${prefixCls}-header` : `${prefixCls}-header-no-title`;
return ( return (
<div class={headerClassName} style={headerStyle}> <div
{title && <div class={`${prefixCls}-title`}>{title}</div>} class={classnames(`${prefixCls}-header`, {
{closable ? this.renderCloseIcon(prefixCls) : null} [`${prefixCls}-header-close-only`]: closable && !title && !extra,
})}
style={headerStyle}
>
<div class={`${prefixCls}-header-title`}>
{renderCloseIcon(prefixCls)}
{title && <div class={`${prefixCls}-title`}>{title}</div>}
</div>
{extra && <div class={`${prefixCls}-extra`}>{extra}</div>}
</div> </div>
); );
}, };
renderCloseIcon(prefixCls: string) {
const { closable } = this; const renderCloseIcon = (prefixCls: string) => {
const { closable } = props;
const $closeIcon = getPropsSlot(slots, props, 'closeIcon');
return ( return (
closable && ( closable && (
<button key="closer" onClick={this.close} aria-label="Close" class={`${prefixCls}-close`}> <button key="closer" onClick={close} aria-label="Close" class={`${prefixCls}-close`}>
<CloseOutlined /> {$closeIcon}
</button> </button>
) )
); );
}, };
// render drawer body dom
renderBody(prefixCls: string) { const renderBody = (prefixCls: string) => {
if (this.destroyClose && !this.visible) { if (destroyClose.value && !props.visible) {
return null; return null;
} }
this.destroyClose = false; destroyClose.value = false;
const { bodyStyle, drawerStyle } = this.$props;
const { bodyStyle, drawerStyle } = props;
const containerStyle: CSSProperties = {}; const containerStyle: CSSProperties = {};
const isDestroyOnClose = this.getDestroyOnClose(); const isDestroyOnClose = getDestroyOnClose();
if (isDestroyOnClose) { if (isDestroyOnClose) {
// Increase the opacity transition, delete children after closing. // Increase the opacity transition, delete children after closing.
containerStyle.opacity = 0; containerStyle.opacity = 0;
@ -194,74 +242,93 @@ const Drawer = defineComponent({
<div <div
class={`${prefixCls}-wrapper-body`} class={`${prefixCls}-wrapper-body`}
style={{ ...containerStyle, ...drawerStyle }} style={{ ...containerStyle, ...drawerStyle }}
onTransitionend={this.onDestroyTransitionEnd} onTransitionend={onDestroyTransitionEnd}
> >
{this.renderHeader(prefixCls)} {renderHeader(prefixCls)}
<div key="body" class={`${prefixCls}-body`} style={bodyStyle}> <div key="body" class={`${prefixCls}-body`} style={bodyStyle}>
{this.$slots.default?.()} {slots.default?.()}
</div> </div>
{renderFooter(prefixCls)}
</div> </div>
); );
},
},
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 <VcDrawer {...vcDrawerProps}>{this.renderBody(prefixCls)}</VcDrawer>;
const renderFooter = (prefixCls: string) => {
const footer = getPropsSlot(slots, props, 'footer');
if (!footer) {
return null;
}
const footerClassName = `${prefixCls}-footer`;
return (
<div class={footerClassName} style={props.footerStyle}>
{footer}
</div>
);
};
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 <VcDrawer {...vcDrawerProps}>{renderBody(prefixCls)}</VcDrawer>;
};
}, },
}); });

View File

@ -6,33 +6,50 @@ subtitle: 抽屉
cover: https://gw.alipayobjects.com/zos/alicdn/7z8NJQhFb/Drawer.svg cover: https://gw.alipayobjects.com/zos/alicdn/7z8NJQhFb/Drawer.svg
--- ---
屏幕边缘滑出的浮层面板。
## 何时使用
抽屉从父窗体边缘滑入,覆盖住部分父窗体内容。用户在抽屉内操作时不必离开当前任务,操作完成后,可以平滑地回到原任务。
- 当需要一个附加的面板来控制父窗体内容,这个面板在需要时呼出。比如,控制界面展示样式,往界面中添加内容。
- 当需要在当前任务流中插入临时任务,创建或预览附加内容。比如展示协议条款,创建子对象。
## API ## API
| 参数 | 说明 | 类型 | 默认值 | 版本 | | 参数 | 说明 | 类型 | 默认值 | 版本 |
| --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- |
| autoFocus | 抽屉展开后是否将焦点切换至其 Dom 节点 | boolean | true | 3.0.0 |
| bodyStyle | 可用于设置 Drawer 内容部分的样式 | CSSProperties | - | |
| className(原 wrapClassName) | 对话框外层容器的类名 | string | - | 3.0.0 |
| closable | 是否显示右上角的关闭按钮 | boolean | true | | | closable | 是否显示右上角的关闭按钮 | boolean | true | |
| closeIcon | 自定义关闭图标 | VNode \| slot | <CloseOutlined /> | 3.0.0 |
| contentWrapperStyle | 可用于设置 Drawer 包裹内容部分的样式 | CSSProperties | 3.0.0 |
| destroyOnClose | 关闭时销毁 Drawer 里的子元素 | boolean | false | | | 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' | | | 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 | | | 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 | - | | | title | 标题 | string \| slot | - | |
| visible(v-model) | Drawer 是否可见 | boolean | - | | | visible(v-model) | Drawer 是否可见 | boolean | - | |
| wrapClassName | 对话框外层容器的类名 | string | - | | | width | 宽度 | string \| number | 378 | |
| 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 | |
| zIndex | 设置 Drawer 的 `z-index` | Number | 1000 | | | 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) | 无 | |

View File

@ -1,12 +1,11 @@
@import '../../style/themes/index'; @import '../../style/themes/index.less';
// Preserve the typo for compatibility @drawer-prefix-cls: ~'@{ant-prefix}-drawer';
// https://github.com/ant-design/ant-design/issues/14628 @picker-prefix-cls: ~'@{ant-prefix}-picker';
@dawer-prefix-cls: ~'@{ant-prefix}-drawer';
@drawer-prefix-cls: @dawer-prefix-cls;
.@{drawer-prefix-cls} { .@{drawer-prefix-cls} {
@drawer-header-close-padding: ceil(((@drawer-header-close-size - @font-size-lg) / 2));
position: fixed; position: fixed;
z-index: @zindex-modal; z-index: @zindex-modal;
width: 0%; width: 0%;
@ -20,7 +19,10 @@
&-content-wrapper { &-content-wrapper {
position: absolute; position: absolute;
width: 100%;
height: 100%;
} }
.@{drawer-prefix-cls}-content { .@{drawer-prefix-cls}-content {
width: 100%; width: 100%;
height: 100%; height: 100%;
@ -38,12 +40,17 @@
width: 100%; width: 100%;
transition: transform @animation-duration-slow @ease-base-out; transition: transform @animation-duration-slow @ease-base-out;
} }
&.@{drawer-prefix-cls}-open.no-mask {
width: 0%;
}
} }
&-left { &-left {
left: 0;
.@{drawer-prefix-cls} {
&-content-wrapper {
left: 0;
}
}
&.@{drawer-prefix-cls}-open { &.@{drawer-prefix-cls}-open {
.@{drawer-prefix-cls}-content-wrapper { .@{drawer-prefix-cls}-content-wrapper {
box-shadow: @shadow-1-right; box-shadow: @shadow-1-right;
@ -84,9 +91,6 @@
height: 100%; height: 100%;
transition: transform @animation-duration-slow @ease-base-out; transition: transform @animation-duration-slow @ease-base-out;
} }
&.@{drawer-prefix-cls}-open.no-mask {
height: 0%;
}
} }
&-top { &-top {
@ -118,15 +122,12 @@
} }
} }
&.@{drawer-prefix-cls}-open { &.@{drawer-prefix-cls}-open .@{drawer-prefix-cls}-mask {
.@{drawer-prefix-cls} { height: 100%;
&-mask { opacity: 1;
height: 100%; transition: none;
opacity: 1; animation: antdDrawerFadeIn @animation-duration-slow @ease-base-out;
transition: none; pointer-events: auto;
animation: antdDrawerFadeIn @animation-duration-slow @ease-base-out;
}
}
} }
&-title { &-title {
@ -147,19 +148,13 @@
} }
&-close { &-close {
position: absolute; display: inline-block;
top: 0; margin-right: 12px;
right: 0; color: @modal-close-color;
z-index: @zindex-popup-close;
display: block;
width: 56px;
height: 56px;
padding: 0;
color: @text-color-secondary;
font-weight: 700; font-weight: 700;
font-size: @font-size-lg; font-size: @font-size-lg;
font-style: normal; font-style: normal;
line-height: 56px; line-height: 1;
text-align: center; text-align: center;
text-transform: none; text-transform: none;
text-decoration: none; text-decoration: none;
@ -179,27 +174,48 @@
&-header { &-header {
position: relative; position: relative;
display: flex;
align-items: center;
justify-content: space-between;
padding: @drawer-header-padding; padding: @drawer-header-padding;
color: @text-color; color: @text-color;
background: @drawer-bg; background: @drawer-bg;
border-bottom: @border-width-base @border-style-base @border-color-split; border-bottom: @border-width-base @border-style-base @border-color-split;
border-radius: @border-radius-base @border-radius-base 0 0; 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 { &-wrapper-body {
color: @text-color; display: flex;
background: @drawer-bg; flex-direction: column;
flex-wrap: nowrap;
width: 100%;
height: 100%;
} }
&-body { &-body {
flex-grow: 1;
padding: @drawer-body-padding; padding: @drawer-body-padding;
overflow: auto;
font-size: @font-size-base; font-size: @font-size-base;
line-height: @line-height-base; line-height: @line-height-base;
word-wrap: break-word; word-wrap: break-word;
} }
&-wrapper-body {
height: 100%; &-footer {
overflow: auto; flex-shrink: 0;
padding: @drawer-footer-padding-vertical @drawer-footer-padding-horizontal;
border-top: @border-width-base @border-style-base @border-color-split;
} }
&-mask { &-mask {
@ -212,12 +228,20 @@
opacity: 0; opacity: 0;
filter: ~'alpha(opacity=45)'; filter: ~'alpha(opacity=45)';
transition: opacity @animation-duration-slow linear, height 0s ease @animation-duration-slow; transition: opacity @animation-duration-slow linear, height 0s ease @animation-duration-slow;
pointer-events: none;
} }
&-open { &-open {
&-content { &-content {
box-shadow: @shadow-2; box-shadow: @shadow-2;
} }
} }
.@{picker-prefix-cls} {
&-clear {
background: @popover-background;
}
}
} }
@keyframes antdDrawerFadeIn { @keyframes antdDrawerFadeIn {

View File

@ -1,3 +1,6 @@
@import '../../style/themes/index'; @import '../../style/themes/index';
@import '../../style/mixins/index'; @import '../../style/mixins/index';
@import './drawer'; @import './drawer';
@import './rtl';
// .popover-customize-bg(@drawer-prefix-cls, @popover-background);

View File

@ -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;
}
}
}

View File

@ -494,6 +494,9 @@
@modal-footer-bg: transparent; @modal-footer-bg: transparent;
@modal-footer-border-color-split: @border-color-split; @modal-footer-border-color-split: @border-color-split;
@modal-mask-bg: fade(@black, 45%); @modal-mask-bg: fade(@black, 45%);
@modal-close-color: @text-color-secondary;
@modal-footer-padding-vertical: 10px;
@modal-footer-padding-horizontal: 16px;
// Progress // Progress
// -- // --
@ -876,9 +879,12 @@
// Drawer // Drawer
// --- // ---
@drawer-header-padding: 16px 24px; @drawer-header-padding: @padding-md @padding-lg;
@drawer-body-padding: 24px; @drawer-body-padding: @padding-lg;
@drawer-bg: @component-background; @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 // Timeline
// --- // ---

View File

@ -3,25 +3,27 @@
@drawer: drawer; @drawer: drawer;
.@{drawer} { .@{drawer} {
position: fixed; position: fixed;
top: 0;
z-index: 9999; z-index: 9999;
> * { 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-shaow @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 { & &-mask {
background: #000; background: #000;
opacity: 0; opacity: 0;
width: 0; width: 100%;
height: 0; height: 0;
position: fixed; position: absolute;
top: 0; 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; height 0s ease @duration;
display: block !important;
} }
&-content-wrapper { &-content-wrapper {
position: fixed; position: absolute;
background: #fff; background: #fff;
} }
&-content { &-content {
@ -77,41 +79,53 @@
} }
&.@{drawer}-open { &.@{drawer}-open {
width: 100%; width: 100%;
&.no-mask {
width: 0%;
}
} }
} }
&-left { &-left {
top: 0;
left: 0;
.@{drawer} { .@{drawer} {
&-handle { &-handle {
right: -40px; 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; border-radius: 0 4px 4px 0;
} }
} }
&.@{drawer}-open { &.@{drawer}-open {
.@{drawer} { .@{drawer} {
&-content-wrapper { &-content-wrapper {
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.15); box-shadow: 2px 0 8px rgba(0, 0, 0, .15);
} }
} }
} }
} }
&-right { &-right {
top: 0;
right: 0;
.@{drawer} { .@{drawer} {
&-content-wrapper { &-content-wrapper {
right: 0; right: 0;
} }
&-handle { &-handle {
left: -40px; 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; border-radius: 4px 0 0 4px;
} }
} }
&.@{drawer}-open { &.@{drawer}-open {
& .@{drawer} { & .@{drawer} {
&-content-wrapper { &-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, &-top,
@ -122,60 +136,74 @@
.@{drawer}-content { .@{drawer}-content {
width: 100%; width: 100%;
} }
.@{drawer}-content {
height: 100%;
}
&.@{drawer}-open {
height: 100%;
&.no-mask {
height: 0%;
}
}
.@{drawer} { .@{drawer} {
&-handle { &-handle {
left: 50%; left: 50%;
margin-left: -20px; margin-left: -20px;
} }
} }
&.@{drawer}-open {
height: 100%;
}
} }
&-top { &-top {
top: 0;
left: 0;
.@{drawer} { .@{drawer} {
&-handle { &-handle {
top: auto; top: auto;
bottom: -40px; 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; border-radius: 0 0 4px 4px;
} }
} }
&.@{drawer}-open { &.@{drawer}-open {
.@{drawer} { .@{drawer} {
&-wrapper { &-content-wrapper {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); box-shadow: 0 2px 8px rgba(0, 0, 0, .15);
} }
} }
} }
} }
&-bottom { &-bottom {
bottom: 0;
left: 0;
.@{drawer} { .@{drawer} {
&-content-wrapper { &-content-wrapper {
bottom: 0; bottom: 0;
} }
&-handle { &-handle {
top: -40px; 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; border-radius: 4px 4px 0 0;
} }
} }
&.@{drawer}-open { &.@{drawer}-open {
.@{drawer} { .@{drawer} {
&-content-wrapper { &-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}-open {
.@{drawer} { .@{drawer} {
&-mask { &-mask {
opacity: 0.3; opacity: .3;
width: 100%;
height: 100%; height: 100%;
animation: fadeIn 0.3s @ease-in-out-circ; transition: opacity 0.3s @ease-in-out-circ;
transition: none;
} }
&-handle { &-handle {
&-icon { &-icon {
@ -190,13 +218,4 @@
} }
} }
} }
} }
@keyframes fadeIn {
0% {
opacity: 0;
}
100% {
opacity: 0.3;
}
}

View File

@ -0,0 +1,3 @@
import Drawer from './src/DrawerWrapper';
export default Drawer;

View File

@ -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<string, boolean> = {};
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 (
<div
{...omit(otherProps, ['switchScrollingEffect', 'autoFocus'])}
tabindex={-1}
class={wrapperClassName}
style={style}
ref={(c: HTMLElement | null) => {
state.dom = c as HTMLElement;
}}
onKeydown={open && keyboard ? onKeyDown : undefined}
onTransitionend={onWrapperTransitionEnd}
>
{showMask && (
<div
class={`${prefixCls}-mask`}
onClick={maskClosable ? onClose : undefined}
style={maskStyle}
ref={c => {
state.maskDom = c as HTMLElement;
}}
/>
)}
<div
class={`${prefixCls}-content-wrapper`}
style={{
transform,
msTransform: transform,
width: isNumeric(width) ? `${width}px` : width,
height: isNumeric(height) ? `${height}px` : height,
...contentWrapperStyle,
}}
ref={c => {
state.contentWrapper = c as HTMLElement;
}}
>
<div
class={`${prefixCls}-content`}
ref={c => {
state.contentDom = c as HTMLElement;
}}
>
{slots.children?.()}
</div>
</div>
</div>
);
};
},
});
export default DrawerChild;

View File

@ -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<HTMLElement>(null);
const container = ref(props.getContainer || null);
const open = ref<boolean>(props.open);
const $forceRender = ref<boolean>(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 (
<div class={wrapperClassName || null} ref={dom}>
<Child
v-slots={{ children: slots.default }}
{...otherProps}
open={open.value}
getContainer={() => dom.value}
onClose={onClose}
onHandleClick={onHandleClick}
></Child>
</div>
);
}
if ($forceRender.value || open.value || dom.value) {
portal = (
<Teleport to={container.value}>
<div class={wrapperClassName || null} ref={dom}>
<Child
v-slots={{ children: slots.default }}
{...props}
open={open.value}
getContainer={() => dom.value}
afterVisibleChange={afterVisibleChange}
onClose={onClose}
onHandleClick={onHandleClick}
/>
</div>
</Teleport>
);
}
return portal;
};
},
});
export default DrawerWrapper;

View File

@ -1,29 +1,33 @@
import PropTypes from '../../_util/vue-types'; import PropTypes from '../../_util/vue-types';
import { PropType, ExtractPropTypes } from 'vue';
const IProps = { export type IPlacement = 'left' | 'top' | 'right' | 'bottom';
width: PropTypes.any,
height: PropTypes.any, const Props = {
defaultOpen: PropTypes.looseBool,
firstEnter: PropTypes.looseBool,
open: PropTypes.looseBool,
prefixCls: PropTypes.string, 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<IPlacement>,
},
class: PropTypes.string,
level: PropTypes.oneOfType([PropTypes.string, PropTypes.array]), level: PropTypes.oneOfType([PropTypes.string, PropTypes.array]),
levelMove: PropTypes.oneOfType([PropTypes.number, PropTypes.func, PropTypes.array]), levelMove: PropTypes.oneOfType([PropTypes.number, PropTypes.func, PropTypes.array]),
ease: PropTypes.string,
duration: PropTypes.string, duration: PropTypes.string,
handler: PropTypes.any, ease: PropTypes.string,
showMask: PropTypes.looseBool, showMask: PropTypes.looseBool,
maskStyle: PropTypes.object,
className: PropTypes.string,
wrapStyle: PropTypes.object,
maskClosable: PropTypes.looseBool, maskClosable: PropTypes.looseBool,
maskStyle: PropTypes.object,
afterVisibleChange: PropTypes.func, afterVisibleChange: PropTypes.func,
keyboard: PropTypes.looseBool, keyboard: PropTypes.looseBool,
contentWrapperStyle: PropTypes.object,
autoFocus: PropTypes.looseBool,
open: PropTypes.looseBool,
}; };
const IDrawerProps = { const DrawerProps = {
...IProps, ...Props,
wrapperClassName: PropTypes.string, wrapperClassName: PropTypes.string,
forceRender: PropTypes.looseBool, forceRender: PropTypes.looseBool,
getContainer: PropTypes.oneOfType([ getContainer: PropTypes.oneOfType([
@ -34,11 +38,16 @@ const IDrawerProps = {
]), ]),
}; };
const IDrawerChildProps = { type IDrawerProps = Partial<ExtractPropTypes<typeof DrawerProps>>;
...IProps,
const DrawerChildProps = {
...Props,
getContainer: PropTypes.func, getContainer: PropTypes.func,
getOpenCount: PropTypes.func, getOpenCount: PropTypes.func,
scrollLocker: PropTypes.any,
switchScrollingEffect: PropTypes.func, switchScrollingEffect: PropTypes.func,
}; };
export { IDrawerProps, IDrawerChildProps }; type IDrawerChildProps = Partial<ExtractPropTypes<typeof DrawerChildProps>>;
export { DrawerProps, DrawerChildProps, IDrawerProps, IDrawerChildProps };

View File

@ -1,5 +0,0 @@
// base in 1.7.7
// export this package's api
import Drawer from './Drawer';
export default Drawer;

View File

@ -1,47 +1,54 @@
export function dataToArray(vars) { export function dataToArray(vars: any) {
if (Array.isArray(vars)) { if (Array.isArray(vars)) {
return vars; return vars;
} }
return [vars]; return [vars];
} }
const transitionEndObject = { const transitionEndObject: Record<string, string> = {
transition: 'transitionend', transition: 'transitionend',
WebkitTransition: 'webkitTransitionEnd', WebkitTransition: 'webkitTransitionEnd',
MozTransition: 'transitionend', MozTransition: 'transitionend',
OTransition: 'oTransitionEnd otransitionend', OTransition: 'oTransitionEnd otransitionend',
}; };
export const transitionStr = Object.keys(transitionEndObject).filter(key => { export const transitionStr: string = Object.keys(transitionEndObject).filter(key => {
if (typeof document === 'undefined') { if (typeof document === 'undefined') {
return false; return false;
} }
const html = document.getElementsByTagName('html')[0]; const html = document.getElementsByTagName('html')[0];
return key in (html ? html.style : {}); return key in (html ? html.style : {});
})[0]; })[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) { if (target.addEventListener) {
target.addEventListener(eventType, callback, options); target.addEventListener(eventType, callback, options);
} else if (target.attachEvent) { } else if ((target as any).attachEvent) {
target.attachEvent(`on${eventType}`, callback); // 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) { if (target.removeEventListener) {
target.removeEventListener(eventType, callback, options); target.removeEventListener(eventType, callback, options);
} else if (target.attachEvent) { } else if ((target as any).attachEvent) {
target.detachEvent(`on${eventType}`, callback); // tslint:disable-line
(target as any).detachEvent(`on${eventType}`, callback); // tslint:disable-line
} }
} }
export function transformArguments(arg, cb) { export function transformArguments(arg: any, cb: any) {
let result; const result = typeof arg === 'function' ? arg(cb) : arg;
if (typeof arg === 'function') {
result = arg(cb);
} else {
result = arg;
}
if (Array.isArray(result)) { if (Array.isArray(result)) {
if (result.length === 2) { if (result.length === 2) {
return result; return result;
@ -51,9 +58,8 @@ export function transformArguments(arg, cb) {
return [result]; return [result];
} }
export const isNumeric = value => { export const isNumeric = (value: string | number | undefined) =>
return !isNaN(parseFloat(value)) && isFinite(value); // eslint-disable-line !isNaN(parseFloat(value as string)) && isFinite(value as number);
};
export const windowIsUndefined = !( export const windowIsUndefined = !(
typeof window !== 'undefined' && typeof window !== 'undefined' &&
@ -61,7 +67,12 @@ export const windowIsUndefined = !(
window.document.createElement 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) { if (!currentTarget || currentTarget === document || currentTarget instanceof Document) {
return false; return false;
} }
@ -92,10 +103,10 @@ export const getTouchParentScroll = (root, currentTarget, differX, differY) => {
(isX && (isX &&
(!x || (!x ||
(x && (x &&
((currentTarget.scrollLeft >= scrollX && scrollX < 0) || ((currentTarget.scrollLeft >= scrollX && differX < 0) ||
(currentTarget.scrollLeft <= 0 && scrollX > 0))))) (currentTarget.scrollLeft <= 0 && differX > 0)))))
) { ) {
return getTouchParentScroll(root, currentTarget.parentNode, differX, differY); return getTouchParentScroll(root, currentTarget.parentNode as HTMLElement, differX, differY);
} }
return false; return false;
}; };