Drawer: bugfix/drawer-append-to-body-not-working (#16953)

- 修复了 AppendToBody API 不管用的问题.
- 修复了展开动画会出现滚动条的问题
- 新增了一个新的 API `withHeader` 来控制是否显示 Header 栏
- 动画流畅度的一个小改动
- 对应文档的改动
- 对应单元测试的改动
pull/17264/head^2
jeremywu 2019-11-01 15:06:56 +08:00 committed by Zhi Cun
parent 7bf3924b29
commit 068b3ad1b0
7 changed files with 262 additions and 30 deletions

View File

@ -50,6 +50,36 @@ Callout a temporary drawer, from multiple direction
``` ```
::: :::
### No Title
When you no longer need a title, you can remove title from drawer.
:::demo Set the `withHeader` attribute to **false**, you can remove the title from drawer, thus your drawer can have more space on screen. If you want to be accessible, make sure to set the `title` attribute.
```html
<el-button @click="drawer = true" type="primary" style="margin-left: 16px;">
open
</el-button>
<el-drawer
title="I am the title"
:visible.sync="drawer"
:with-header="false">
<span>Hi there!</span>
</el-drawer>
<script>
export default {
data() {
return {
drawer: false,
};
}
};
</script>
```
:::
### Customization Content ### Customization Content
Like `Dialog`, `Drawer` can do many diverse interaction as you wanted. Like `Dialog`, `Drawer` can do many diverse interaction as you wanted.
@ -92,7 +122,7 @@ Like `Dialog`, `Drawer` can do many diverse interaction as you wanted.
</el-form-item> </el-form-item>
</el-form> </el-form>
<div class="demo-drawer__footer"> <div class="demo-drawer__footer">
<el-button @click="dialog = false">Cancel</el-button> <el-button @click="cancelForm">Cancel</el-button>
<el-button type="primary" @click="$refs.drawer.closeDrawer()" :loading="loading">{{ loading ? 'Submitting ...' : 'Submit' }}</el-button> <el-button type="primary" @click="$refs.drawer.closeDrawer()" :loading="loading">{{ loading ? 'Submitting ...' : 'Submit' }}</el-button>
</div> </div>
</div> </div>
@ -132,20 +162,32 @@ export default {
resource: '', resource: '',
desc: '' desc: ''
}, },
formLabelWidth: '80px' formLabelWidth: '80px',
timer: null,
}; };
}, },
methods: { methods: {
handleClose(done) { handleClose(done) {
if (this.loading) {
return;
}
this.$confirm('Do you want to submit?') this.$confirm('Do you want to submit?')
.then(_ => { .then(_ => {
this.loading = true; this.loading = true;
this.timer = setTimeout(() => {
done();
// animation takes time
setTimeout(() => { setTimeout(() => {
this.loading = false; this.loading = false;
done(); }, 400);
}, 2000); }, 2000);
}) })
.catch(_ => {}); .catch(_ => {});
},
cancelForm() {
this.loading = false;
this.dialog = false;
clearTimeout(this.timer);
} }
} }
} }
@ -238,6 +280,7 @@ If the variable bound to `visible` is managed in Vuex store, the `.sync` can not
| title | Drawer's title, can also be set by named slot, detailed descriptions can be found in the slot form | string | — | — | | title | Drawer's title, can also be set by named slot, detailed descriptions can be found in the slot form | string | — | — |
| visible | Should Drawer be displayed, also support the `.sync` notation | boolean | — | false | | visible | Should Drawer be displayed, also support the `.sync` notation | boolean | — | false |
| wrapperClosable | Indicates whether user can close Drawer by clicking the shadowing layer. | boolean | - | true | | wrapperClosable | Indicates whether user can close Drawer by clicking the shadowing layer. | boolean | - | true |
| withHeader | Flag that controls the header section's existance, default to true, when withHeader set to false, both `title attribute` and `title slot` won't work | boolean | - | true |
### Drawer Slot ### Drawer Slot

View File

@ -50,6 +50,36 @@ Llamada de un drawer temporal, desde varias direcciones
``` ```
::: :::
### No Title
When you no longer need a title, you can remove title from drawer.
:::demo Set the `withHeader` attribute to **false**, you can remove the title from drawer, thus your drawer can have more space on screen. If you want to be accessible, make sure to set the `title` attribute.
```html
<el-button @click="drawer = true" type="primary" style="margin-left: 16px;">
open
</el-button>
<el-drawer
title="I am the title"
:visible.sync="drawer"
:with-header="false">
<span>Hi there!</span>
</el-drawer>
<script>
export default {
data() {
return {
drawer: false,
};
}
};
</script>
```
:::
### Personalizar el contenido ### Personalizar el contenido
Al igual que `Dialog`, `Drawer` puede hacer muchas interacciones diversas. Al igual que `Dialog`, `Drawer` puede hacer muchas interacciones diversas.
@ -92,7 +122,7 @@ Al igual que `Dialog`, `Drawer` puede hacer muchas interacciones diversas.
</el-form-item> </el-form-item>
</el-form> </el-form>
<div class="demo-drawer__footer"> <div class="demo-drawer__footer">
<el-button @click="dialog = false">Cancel</el-button> <el-button @click="cancelForm">Cancel</el-button>
<el-button type="primary" @click="$refs.drawer.closeDrawer()" :loading="loading">{{ loading ? 'Submitting ...' : 'Submit' }}</el-button> <el-button type="primary" @click="$refs.drawer.closeDrawer()" :loading="loading">{{ loading ? 'Submitting ...' : 'Submit' }}</el-button>
</div> </div>
</div> </div>
@ -132,20 +162,32 @@ export default {
resource: '', resource: '',
desc: '' desc: ''
}, },
formLabelWidth: '80px' formLabelWidth: '80px',
timer: null,
}; };
}, },
methods: { methods: {
handleClose(done) { handleClose(done) {
if (this.loading) {
return;
}
this.$confirm('Do you want to submit?') this.$confirm('Do you want to submit?')
.then(_ => { .then(_ => {
this.loading = true; this.loading = true;
this.timer = setTimeout(() => {
done();
// animation takes time
setTimeout(() => { setTimeout(() => {
this.loading = false; this.loading = false;
done(); }, 400);
}, 2000); }, 2000);
}) })
.catch(_ => {}); .catch(_ => {});
},
cancelForm() {
this.loading = false;
this.dialog = false;
clearTimeout(this.timer);
} }
} }
} }
@ -238,6 +280,7 @@ Si la variable `visible` se gestiona en el almacén de Vuex, el `.sync` no puede
| title | El título del Drawer, también se puede establecer por slot con nombre, las descripciones detalladas se pueden encontrar en el formulario de slot. | string | — | — | | title | El título del Drawer, también se puede establecer por slot con nombre, las descripciones detalladas se pueden encontrar en el formulario de slot. | string | — | — |
| visible | Si se muestra el Drawer, también soporta la notación `.sync` | boolean | — | false | | visible | Si se muestra el Drawer, también soporta la notación `.sync` | boolean | — | false |
| wrapperClosable | Indica si el usuario puede cerrar el Drawer haciendo clic en la capa de sombreado. | boolean | - | true | | wrapperClosable | Indica si el usuario puede cerrar el Drawer haciendo clic en la capa de sombreado. | boolean | - | true |
| withHeader | Flag that controls the header section's existance, default to true, when withHeader set to false, both `title attribute` and `title slot` won't work | boolean | - | true |
### Drawer Slot's ### Drawer Slot's

View File

@ -50,6 +50,36 @@ Callout a temporary drawer, from multiple direction
``` ```
::: :::
### No Title
When you no longer need a title, you can remove title from drawer.
:::demo Set the `withHeader` attribute to **false**, you can remove the title from drawer, thus your drawer can have more space on screen. If you want to be accessible, make sure to set the `title` attribute.
```html
<el-button @click="drawer = true" type="primary" style="margin-left: 16px;">
open
</el-button>
<el-drawer
title="I am the title"
:visible.sync="drawer"
:with-header="false">
<span>Hi there!</span>
</el-drawer>
<script>
export default {
data() {
return {
drawer: false,
};
}
};
</script>
```
:::
### Customization Content ### Customization Content
Like `Dialog`, `Drawer` can do many diverse interaction as you wanted. Like `Dialog`, `Drawer` can do many diverse interaction as you wanted.
@ -92,7 +122,7 @@ Like `Dialog`, `Drawer` can do many diverse interaction as you wanted.
</el-form-item> </el-form-item>
</el-form> </el-form>
<div class="demo-drawer__footer"> <div class="demo-drawer__footer">
<el-button @click="dialog = false">Cancel</el-button> <el-button @click="cancelForm">Cancel</el-button>
<el-button type="primary" @click="$refs.drawer.closeDrawer()" :loading="loading">{{ loading ? 'Submitting ...' : 'Submit' }}</el-button> <el-button type="primary" @click="$refs.drawer.closeDrawer()" :loading="loading">{{ loading ? 'Submitting ...' : 'Submit' }}</el-button>
</div> </div>
</div> </div>
@ -132,20 +162,32 @@ export default {
resource: '', resource: '',
desc: '' desc: ''
}, },
formLabelWidth: '80px' formLabelWidth: '80px',
timer: null,
}; };
}, },
methods: { methods: {
handleClose(done) { handleClose(done) {
if (this.loading) {
return;
}
this.$confirm('Do you want to submit?') this.$confirm('Do you want to submit?')
.then(_ => { .then(_ => {
this.loading = true; this.loading = true;
this.timer = setTimeout(() => {
done();
// animation takes time
setTimeout(() => { setTimeout(() => {
this.loading = false; this.loading = false;
done(); }, 400);
}, 2000); }, 2000);
}) })
.catch(_ => {}); .catch(_ => {});
},
cancelForm() {
this.loading = false;
this.dialog = false;
clearTimeout(this.timer);
} }
} }
} }
@ -238,6 +280,7 @@ If the variable bound to `visible` is managed in Vuex store, the `.sync` can not
| title | Drawer's title, can also be set by named slot, detailed descriptions can be found in the slot form | string | — | — | | title | Drawer's title, can also be set by named slot, detailed descriptions can be found in the slot form | string | — | — |
| visible | Should Drawer be displayed, also support the `.sync` notation | boolean | — | false | | visible | Should Drawer be displayed, also support the `.sync` notation | boolean | — | false |
| wrapperClosable | Indicates whether user can close Drawer by clicking the shadowing layer. | boolean | - | true | | wrapperClosable | Indicates whether user can close Drawer by clicking the shadowing layer. | boolean | - | true |
| withHeader | Flag that controls the header section's existance, default to true, when withHeader set to false, both `title attribute` and `title slot` won't work | boolean | - | true |
### Drawer Slot ### Drawer Slot

View File

@ -50,6 +50,37 @@
``` ```
::: :::
### 不添加 Title
当你不需要标题到时候, 你还可以去掉标题
:::demo 当遇到不需要 title 的场景时, 可以通过 `withHeader` 这个属性来关闭掉 title 的显示, 这样可以留出更大的空间给到用户, 为了用户的可访问性, 请务必设定 `title` 的值
```html
<el-button @click="drawer = true" type="primary" style="margin-left: 16px;">
点我打开
</el-button>
<el-drawer
title="我是标题"
:visible.sync="drawer"
:with-header="false">
<span>我来啦!</span>
</el-drawer>
<script>
export default {
data() {
return {
drawer: false,
};
}
};
</script>
```
:::
### 自定义内容 ### 自定义内容
`Dialog` 组件一样, `Drawer` 同样可以在其内部嵌套各种丰富的操作 `Dialog` 组件一样, `Drawer` 同样可以在其内部嵌套各种丰富的操作
@ -92,7 +123,7 @@
</el-form-item> </el-form-item>
</el-form> </el-form>
<div class="demo-drawer__footer"> <div class="demo-drawer__footer">
<el-button @click="dialog = false">取 消</el-button> <el-button @click="cancelForm">取 消</el-button>
<el-button type="primary" @click="$refs.drawer.closeDrawer()" :loading="loading">{{ loading ? '提交中 ...' : '确 定' }}</el-button> <el-button type="primary" @click="$refs.drawer.closeDrawer()" :loading="loading">{{ loading ? '提交中 ...' : '确 定' }}</el-button>
</div> </div>
</div> </div>
@ -132,20 +163,32 @@ export default {
resource: '', resource: '',
desc: '' desc: ''
}, },
formLabelWidth: '80px' formLabelWidth: '80px',
timer: null,
}; };
}, },
methods: { methods: {
handleClose(done) { handleClose(done) {
if (this.loading) {
return;
}
this.$confirm('确定要提交表单吗?') this.$confirm('确定要提交表单吗?')
.then(_ => { .then(_ => {
this.loading = true; this.loading = true;
this.timer = setTimeout(() => {
done();
// 动画关闭需要一定的时间
setTimeout(() => { setTimeout(() => {
this.loading = false; this.loading = false;
done(); }, 400);
}, 2000); }, 2000);
}) })
.catch(_ => {}); .catch(_ => {});
},
cancelForm() {
this.loading = false;
this.dialog = false;
clearTimeout(this.timer);
} }
} }
} }
@ -239,6 +282,7 @@ Drawer 提供一个 `destroyOnClose` API, 用来在关闭 Drawer 时销毁子组
| title | Drawer 的标题,也可通过具名 slot (见下表)传入 | string | — | — | | title | Drawer 的标题,也可通过具名 slot (见下表)传入 | string | — | — |
| visible | 是否显示 Drawer支持 .sync 修饰符 | boolean | — | false | | visible | 是否显示 Drawer支持 .sync 修饰符 | boolean | — | false |
| wrapperClosable | 点击遮罩层是否可以关闭 Drawer | boolean | - | true | | wrapperClosable | 点击遮罩层是否可以关闭 Drawer | boolean | - | true |
| withHeader | 控制是否显示 header 栏, 默认为 true, 当此项为 false 时, title attribute 和 title slot 均不生效 | boolean | - | true |
### Drawer Slot ### Drawer Slot

View File

@ -4,8 +4,8 @@
@after-enter="afterEnter" @after-enter="afterEnter"
@after-leave="afterLeave"> @after-leave="afterLeave">
<div <div
class="el-dialog__wrapper" class="el-drawer__wrapper"
role="presentation" tabindex="-1"
v-show="visible"> v-show="visible">
<div <div
class="el-drawer__container" class="el-drawer__container"
@ -16,14 +16,17 @@
<div <div
aria-modal="true" aria-modal="true"
aria-labelledby="el-drawer__title" aria-labelledby="el-drawer__title"
:aria-label="title"
class="el-drawer" class="el-drawer"
:class="[direction, customClass]" :class="[direction, customClass]"
:style="isHorizontal ? `width: ${size}` : `height: ${size}`" :style="isHorizontal ? `width: ${size}` : `height: ${size}`"
ref="drawer" ref="drawer"
role="presentation"> role="dialog"
<header class="el-drawer__header" id="el-drawer__title"> tabindex="-1"
>
<header class="el-drawer__header" id="el-drawer__title" v-if="withHeader">
<slot name="title"> <slot name="title">
<span role="heading">{{ title }}</span> <span role="heading" tabindex="0" :title="title">{{ title }}</span>
</slot> </slot>
<button <button
:aria-label="`close ${title || 'drawer'}`" :aria-label="`close ${title || 'drawer'}`"
@ -45,16 +48,16 @@
<script> <script>
import Popup from 'element-ui/src/utils/popup'; import Popup from 'element-ui/src/utils/popup';
import Migrating from 'element-ui/src/mixins/migrating';
import emitter from 'element-ui/src/mixins/emitter'; import emitter from 'element-ui/src/mixins/emitter';
import Utils from 'element-ui/src/utils/aria-utils';
export default { export default {
name: 'ElDrawer', name: 'ElDrawer',
mixins: [Popup, emitter, Migrating], mixins: [Popup, emitter],
props: { props: {
appendToBody: { appendToBody: {
type: Boolean, type: Boolean,
default: true default: false
}, },
beforeClose: { beforeClose: {
type: Function type: Function
@ -63,6 +66,10 @@ export default {
type: String, type: String,
default: '' default: ''
}, },
closeOnPressEscape: {
type: Boolean,
default: true
},
destroyOnClose: { destroyOnClose: {
type: Boolean, type: Boolean,
default: false default: false
@ -78,6 +85,10 @@ export default {
return ['ltr', 'rtl', 'ttb', 'btt'].indexOf(val) !== -1; return ['ltr', 'rtl', 'ttb', 'btt'].indexOf(val) !== -1;
} }
}, },
modalAppendToBody: {
type: Boolean,
default: true
},
showClose: { showClose: {
type: Boolean, type: Boolean,
default: true default: true
@ -96,6 +107,10 @@ export default {
wrapperClosable: { wrapperClosable: {
type: Boolean, type: Boolean,
default: true default: true
},
withHeader: {
type: Boolean,
default: true
} }
}, },
computed: { computed: {
@ -105,7 +120,8 @@ export default {
}, },
data() { data() {
return { return {
closed: false closed: false,
prevActiveElement: null
}; };
}, },
watch: { watch: {
@ -116,8 +132,17 @@ export default {
if (this.appendToBody) { if (this.appendToBody) {
document.body.appendChild(this.$el); document.body.appendChild(this.$el);
} }
this.prevActiveElement = document.activeElement;
this.$nextTick(() => {
Utils.focusFirstDescendant(this.$refs.drawer);
});
} else { } else {
if (!this.closed) this.$emit('close'); if (!this.closed) this.$emit('close');
this.$nextTick(() => {
if (this.prevActiveElement) {
this.prevActiveElement.focus();
}
});
} }
} }
}, },
@ -149,6 +174,12 @@ export default {
} else { } else {
this.hide(); this.hide();
} }
},
handleClose() {
// This method here will be called by PopupManger, when the `closeOnPressEscape` was set to true
// pressing `ESC` will call this method, and also close the drawer.
// This method also calls `beforeClose` if there was one.
this.closeDrawer();
} }
}, },
mounted() { mounted() {

View File

@ -92,13 +92,13 @@
@mixin animation-in($direction) { @mixin animation-in($direction) {
.el-drawer__open &.#{$direction} { .el-drawer__open &.#{$direction} {
animation: #{$direction}-drawer-in 225ms cubic-bezier(0, 0, .2, 1) 0ms; animation: #{$direction}-drawer-in .3s 1ms;
} }
} }
@mixin animation-out($direction) { @mixin animation-out($direction) {
&.#{$direction} { &.#{$direction} {
animation: #{$direction}-drawer-out 225ms cubic-bezier(0, 0, .2, 1) 0ms; animation: #{$direction}-drawer-out .3s;
} }
} }
@ -125,6 +125,16 @@ $directions: rtl, ltr, ttb, btt;
@include animation-in($direction); @include animation-in($direction);
} }
&__wrapper {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
overflow: hidden;
margin: 0;
}
&__header { &__header {
align-items: center; align-items: center;
color: rgb(114, 118, 123); color: rgb(114, 118, 123);
@ -199,9 +209,9 @@ $directions: rtl, ltr, ttb, btt;
} }
.el-drawer-fade-enter-active { .el-drawer-fade-enter-active {
animation: el-drawer-fade-in 225ms cubic-bezier(0, 0, 0.2, 1) 0ms; animation: el-drawer-fade-in .3s;
} }
.el-drawer-fade-leave-active { .el-drawer-fade-leave-active {
animation: el-drawer-fade-in 225ms cubic-bezier(0, 0, 0.2, 1) 0ms reverse; animation: el-drawer-fade-in .3s reverse;
} }

View File

@ -218,6 +218,24 @@ describe('Drawer', () => {
expect(vm.$el.querySelector(`.${classes}`)).to.exist; expect(vm.$el.querySelector(`.${classes}`)).to.exist;
}); });
it('should not render header when withHeader attribute is false', () => {
vm = createVue({
template: `
<el-drawer :title='title' :visible='visible' ref='drawer' :with-header='false'>
<span>${content}</span>
</el-drawer>
`,
data() {
return {
title,
visible: true
};
}
});
expect(vm.$el.querySelector('.el-drawer__header')).to.not.exist;
});
describe('directions', () => { describe('directions', () => {
const renderer = direction => { const renderer = direction => {
return createVue({ return createVue({