Переглянути джерело

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

- 修复了 AppendToBody API 不管用的问题.
- 修复了展开动画会出现滚动条的问题
- 新增了一个新的 API `withHeader` 来控制是否显示 Header 栏
- 动画流畅度的一个小改动
- 对应文档的改动
- 对应单元测试的改动
jeremywu 5 роки тому
батько
коміт
068b3ad1b0

+ 47 - 4
examples/docs/en-US/drawer.md

@@ -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
 
 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>
     <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>
     </div>
   </div>
@@ -132,20 +162,32 @@ export default {
         resource: '',
         desc: ''
       },
-      formLabelWidth: '80px'
+      formLabelWidth: '80px',
+      timer: null,
     };
   },
   methods: {
     handleClose(done) {
+      if (this.loading) {
+        return;
+      }
       this.$confirm('Do you want to submit?')
         .then(_ => {
           this.loading = true;
-          setTimeout(() => {
-            this.loading = false;
+          this.timer = setTimeout(() => {
             done();
+            // animation takes time
+            setTimeout(() => {
+              this.loading = false;
+            }, 400);
           }, 2000);
         })
         .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 | — | — |
 | 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 |
+| 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
 

+ 48 - 5
examples/docs/es/drawer.md

@@ -50,7 +50,37 @@ Llamada de un drawer temporal, desde varias direcciones
 ```
 :::
 
-### Personalizar el  contenido
+### 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
 
 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>
     <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>
     </div>
   </div>
@@ -132,20 +162,32 @@ export default {
         resource: '',
         desc: ''
       },
-      formLabelWidth: '80px'
+      formLabelWidth: '80px',
+      timer: null,
     };
   },
   methods: {
     handleClose(done) {
+      if (this.loading) {
+        return;
+      }
       this.$confirm('Do you want to submit?')
         .then(_ => {
           this.loading = true;
-          setTimeout(() => {
-            this.loading = false;
+          this.timer = setTimeout(() => {
             done();
+            // animation takes time
+            setTimeout(() => {
+              this.loading = false;
+            }, 400);
           }, 2000);
         })
         .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 | — | — |
 | 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 |
+| 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
 

+ 47 - 4
examples/docs/fr-FR/drawer.md

@@ -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
 
 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>
     <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>
     </div>
   </div>
@@ -132,20 +162,32 @@ export default {
         resource: '',
         desc: ''
       },
-      formLabelWidth: '80px'
+      formLabelWidth: '80px',
+      timer: null,
     };
   },
   methods: {
     handleClose(done) {
+      if (this.loading) {
+        return;
+      }
       this.$confirm('Do you want to submit?')
         .then(_ => {
           this.loading = true;
-          setTimeout(() => {
-            this.loading = false;
+          this.timer = setTimeout(() => {
             done();
+            // animation takes time
+            setTimeout(() => {
+              this.loading = false;
+            }, 400);
           }, 2000);
         })
         .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 | — | — |
 | 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 |
+| 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
 

+ 48 - 4
examples/docs/zh-CN/drawer.md

@@ -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` 同样可以在其内部嵌套各种丰富的操作
@@ -92,7 +123,7 @@
       </el-form-item>
     </el-form>
     <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>
     </div>
   </div>
@@ -132,20 +163,32 @@ export default {
         resource: '',
         desc: ''
       },
-      formLabelWidth: '80px'
+      formLabelWidth: '80px',
+      timer: null,
     };
   },
   methods: {
     handleClose(done) {
+      if (this.loading) {
+        return;
+      }
       this.$confirm('确定要提交表单吗?')
         .then(_ => {
           this.loading = true;
-          setTimeout(() => {
-            this.loading = false;
+          this.timer = setTimeout(() => {
             done();
+            // 动画关闭需要一定的时间
+            setTimeout(() => {
+              this.loading = false;
+            }, 400);
           }, 2000);
         })
         .catch(_ => {});
+    },
+    cancelForm() {
+      this.loading = false;
+      this.dialog = false;
+      clearTimeout(this.timer);
     }
   }
 }
@@ -239,6 +282,7 @@ Drawer 提供一个 `destroyOnClose` API, 用来在关闭 Drawer 时销毁子组
 | title     | Drawer 的标题,也可通过具名 slot (见下表)传入 | string    | — | — |
 | visible   | 是否显示 Drawer,支持 .sync 修饰符 | boolean | — | false |
 | wrapperClosable | 点击遮罩层是否可以关闭 Drawer | boolean | - | true |
+| withHeader | 控制是否显示 header 栏, 默认为 true, 当此项为 false 时, title attribute 和 title slot 均不生效 | boolean | - | true |
 
 ### Drawer Slot
 

+ 40 - 9
packages/drawer/src/main.vue

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

+ 14 - 4
packages/theme-chalk/src/drawer.scss

@@ -92,13 +92,13 @@
 
 @mixin animation-in($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) {
   &.#{$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);
   }
 
+  &__wrapper {
+    position: fixed;
+    top: 0;
+    right: 0;
+    bottom: 0;
+    left: 0;
+    overflow: hidden;
+    margin: 0;
+  }
+
   &__header {
     align-items: center;
     color: rgb(114, 118, 123);
@@ -199,9 +209,9 @@ $directions: rtl, ltr, ttb, btt;
 }
 
 .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 {
-  animation: el-drawer-fade-in 225ms cubic-bezier(0, 0, 0.2, 1) 0ms reverse;
+  animation: el-drawer-fade-in .3s reverse;
 }

+ 18 - 0
test/unit/specs/drawer.spec.js

@@ -218,6 +218,24 @@ describe('Drawer', () => {
     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', () => {
     const renderer = direction => {
       return createVue({