diff --git a/components/drawer/demo/basic.vue b/components/drawer/demo/basic.vue
index c446dfd4e4..998684be93 100644
--- a/components/drawer/demo/basic.vue
+++ b/components/drawer/demo/basic.vue
@@ -20,10 +20,11 @@ Basic drawer.
Open
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.
+
+
+
+
+
+ top
+ right
+ bottom
+ left
+
+ Open
+
+
+ Cancel
+ Submit
+
+
Some contents...
+
Some contents...
+
Some contents...
+
+
+
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.
-
+
CancelSubmit
-
+
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..acd01e060e 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 | - | |
+| class | The class name of the container of the Drawer dialog | string | - | |
+| 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 | Style of wrapper element which contains mask compare to drawerStyle | CSSProperties | - | |
+| 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..7ade529059 100644
--- a/components/drawer/index.tsx
+++ b/components/drawer/index.tsx
@@ -1,189 +1,280 @@
-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,
+ onUnmounted,
+ watch,
+ computed,
+} from 'vue';
+import { getPropsSlot, initDefaultProps } 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';
+import devWarning from '../vc-util/devWarning';
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,
+ closeIcon: PropTypes.VNodeChild,
+ destroyOnClose: PropTypes.looseBool,
+ forceRender: PropTypes.looseBool,
+ getContainer: PropTypes.any,
+ maskClosable: PropTypes.looseBool,
+ mask: PropTypes.looseBool,
+ maskStyle: PropTypes.object,
+ /** @deprecated Use `style` instead */
+ wrapStyle: PropTypes.style,
+ style: PropTypes.style,
+ class: PropTypes.any,
+ /** @deprecated Use `class` instead */
+ wrapClassName: PropTypes.string,
+ size: {
+ type: String as PropType,
+ },
+ drawerStyle: PropTypes.object,
+ headerStyle: PropTypes.object,
+ bodyStyle: PropTypes.object,
+ contentWrapperStyle: PropTypes.object,
+ title: PropTypes.VNodeChild,
+ visible: PropTypes.looseBool.isRequired,
+ 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 }]),
+ placement: PropTypes.oneOf(PlacementTypes),
+ keyboard: PropTypes.looseBool,
+ extra: PropTypes.VNodeChild,
+ footer: PropTypes.VNodeChild,
+ footerStyle: PropTypes.object,
+ level: PropTypes.any,
+ levelMove: PropTypes.any,
+ handle: PropTypes.VNodeChild,
+ /** @deprecated Use `@afterVisibleChange` instead */
+ afterVisibleChange: PropTypes.func,
+});
+
+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),
+ props: initDefaultProps(drawerProps(), {
+ closable: true,
+ placement: 'right' as placementType,
+ maskClosable: true,
+ mask: true,
+ level: null,
+ keyboard: true,
+ push: defaultPushState,
+ }),
+ slots: ['closeIcon', 'title', 'extra', 'footer', 'handle'],
+ emits: ['update:visible', 'close', 'afterVisibleChange'],
+ setup(props, { emit, slots, attrs }) {
+ const sPush = ref(false);
+ const destroyClose = ref(false);
+ const vcDrawer = ref(null);
+ const parentDrawerOpts = inject('parentDrawerOpts', null);
+ const { prefixCls } = useConfigInject('drawer', props);
+ devWarning(
+ !props.afterVisibleChange,
+ 'Drawer',
+ '`afterVisibleChange` prop is deprecated, please use `@afterVisibleChange` event instead',
+ );
+ devWarning(
+ props.wrapStyle === undefined,
+ 'Drawer',
+ '`wrapStyle` prop is deprecated, please use `style` instead',
+ );
+ devWarning(
+ props.wrapClassName === undefined,
+ 'Drawer',
+ '`wrapClassName` prop is deprecated, please use `class` instead',
+ );
+ const setPush = () => {
+ sPush.value = true;
};
- },
- data() {
- return {
- sPush: false,
+
+ const setPull = () => {
+ sPush.value = false;
+ nextTick(() => {
+ domFocus();
+ });
};
- },
- 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();
- }
+ 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,
- });
- },
- pull() {
- this.setState(
- {
- sPush: false,
- },
- () => {
- this.domFocus();
- },
- );
- },
- onDestroyTransitionEnd() {
- const isDestroyOnClose = this.getDestroyOnClose();
+ });
+
+ watch(
+ () => props.visible,
+ visible => {
+ if (parentDrawerOpts) {
+ if (visible) {
+ parentDrawerOpts.setPush();
+ } else {
+ parentDrawerOpts.setPull();
+ }
+ }
+ },
+ { flush: 'post' },
+ );
+
+ const domFocus = () => {
+ vcDrawer.value?.domFocus?.();
+ };
+
+ const close = (e: Event) => {
+ emit('update:visible', false);
+ emit('close', e);
+ };
+
+ const afterVisibleChange = (visible: boolean) => {
+ props.afterVisibleChange?.(visible);
+ emit('afterVisibleChange', visible);
+ };
+ const destroyOnClose = computed(() => props.destroyOnClose && !props.visible);
+ const onDestroyTransitionEnd = () => {
+ const isDestroyOnClose = destroyOnClose.value;
if (!isDestroyOnClose) {
return;
}
- if (!this.visible) {
- this.destroyClose = true;
- (this as any).$forceUpdate();
+ if (!props.visible) {
+ destroyClose.value = true;
+ }
+ };
+
+ const pushTransform = computed(() => {
+ const { push, placement } = 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)`;
+ }
+ return null;
+ });
+
+ const offsetStyle = computed(() => {
+ // https://github.com/ant-design/ant-design/issues/24287
+ const { visible, mask, placement, size, width, height } = props;
+ if (!visible && !mask) {
+ return {};
}
- },
- getRcDrawerStyle() {
- const { zIndex, placement, wrapStyle } = this.$props;
- const { sPush: push } = this.$data;
+ const val: CSSProperties = {};
+ if (placement === 'left' || placement === 'right') {
+ const defaultWidth = size === 'large' ? 736 : 378;
+ val.width = typeof width === 'undefined' ? defaultWidth : width;
+ val.width = typeof val.width === 'string' ? val.width : `${val.width}px`;
+ } else {
+ const defaultHeight = size === 'large' ? 736 : 378;
+ val.height = typeof height === 'undefined' ? defaultHeight : height;
+ val.height = typeof val.height === 'string' ? val.height : `${val.height}px`;
+ }
+ return val;
+ });
+
+ const drawerStyle = computed(() => {
+ const { zIndex, wrapStyle, mask, style } = props;
+ const val = mask ? {} : offsetStyle.value;
return {
zIndex,
- transform: push ? this.getPushTransform(placement) : undefined,
+ transform: sPush.value ? pushTransform.value : undefined,
+ ...val,
...wrapStyle,
+ ...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 (
-