feat(components): [drawer] add resizable prop (#21608)

* feat(components): [drawer] add `resizable` prop

* style(theme-chalk): [drawer] update based on review feeddback

* chore: adjust style & example

* docs: update example

* docs: remove default direction

---------

Co-authored-by: btea <2356281422@qq.com>
This commit is contained in:
Zhong
2025-08-17 14:46:27 +08:00
committed by GitHub
parent 348a855890
commit ca976e3620
8 changed files with 153 additions and 84 deletions

View File

@@ -53,6 +53,16 @@ drawer/customization-header
:::
## Resizable Drawer ^(2.11.0)
Try to drag the edge part.
:::demo Set `resizable` to `true` to resize.
drawer/resizable-drawer
:::
## Nested Drawer
You can also have multiple layer of `Drawer` just like `Dialog`.
@@ -93,6 +103,7 @@ Drawer provides an API called `destroyOnClose`, which is a flag variable that in
| destroy-on-close | Indicates whether children should be destroyed after Drawer closed | ^[boolean] | false |
| modal | Should show shadowing layer | ^[boolean] | true |
| direction | Drawer's opening direction | ^[enum]`'rtl' \| 'ltr' \| 'ttb' \| 'btt'` | rtl |
| resizable ^(2.11.0) | enable resizable feature for Drawer | ^[boolean] | false |
| show-close | Should show close button at the top right of Drawer | ^[boolean] | true |
| size | Drawer's size, if Drawer is horizontal mode, it effects the width property, otherwise it effects the height property, when size is `number` type, it describes the size by unit of pixels; when size is `string` type, it should be used with `x%` notation, other wise it will be interpreted to pixel unit | ^[number] / ^[string] | 30% |
| title | Drawer's title, can also be set by named slot, detailed descriptions can be found in the slot form | ^[string] | — |

View File

@@ -0,0 +1,31 @@
<template>
<el-segmented
v-model="direction"
:options="options"
size="large"
@change="visible = true"
/>
<el-drawer v-model="visible" :direction="direction" resizable>
<template #header="{ titleId, titleClass }">
<h4 :id="titleId" :class="titleClass">This is a custom header!</h4>
</template>
This is drawer content.
</el-drawer>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import type { DrawerProps } from 'element-plus'
const visible = ref(false)
const direction = ref<DrawerProps['direction']>()
const options = [
{ label: 'Top', value: 'ttb' },
{ label: 'Right', value: 'rtl' },
{ label: 'Bottom', value: 'btt' },
{ label: 'Left', value: 'ltr' },
]
</script>

View File

@@ -406,15 +406,15 @@ describe('Drawer', () => {
)
test('should effect height when drawer is vertical', async () => {
const drawerEl = renderer('50%', true).find('.el-drawer')
.element as HTMLDivElement
expect(drawerEl.style.width).toEqual('50%')
const wrapper = renderer('50%', true)
const drawerEl = wrapper.findAllComponents({ name: 'ElSplitterPanel' })[0]
expect(drawerEl.vm.size).toContain('50%')
})
test('should effect width when drawer is horizontal', async () => {
const drawerEl = renderer('50%', false).find('.el-drawer')
.element as HTMLDivElement
expect(drawerEl.style.height).toEqual('50%')
const wrapper = renderer('50%', false)
const drawerEl = wrapper.findAllComponents({ name: 'ElSplitterPanel' })[0]
expect(drawerEl.vm.size).toContain('50%')
})
})

View File

@@ -10,6 +10,7 @@ export const drawerProps = buildProps({
default: 'rtl',
values: ['ltr', 'rtl', 'ttb', 'btt'],
},
resizable: Boolean,
size: {
type: [String, Number],
default: '30%',

View File

@@ -26,63 +26,85 @@
@focusout-prevented="onFocusoutPrevented"
@release-requested="onCloseRequested"
>
<div
ref="drawerRef"
aria-modal="true"
:aria-label="title || undefined"
:aria-labelledby="!title ? titleId : undefined"
:aria-describedby="bodyId"
v-bind="$attrs"
:class="[ns.b(), direction, visible && 'open']"
:style="
isHorizontal ? 'width: ' + drawerSize : 'height: ' + drawerSize
"
role="dialog"
@click.stop
<el-splitter
:class="ns.b('splitter')"
:layout="isHorizontal ? 'horizontal' : 'vertical'"
>
<span ref="focusStartRef" :class="ns.e('sr-focus')" tabindex="-1" />
<header v-if="withHeader" :class="[ns.e('header'), headerClass]">
<slot
v-if="!$slots.title"
name="header"
:close="handleClose"
:title-id="titleId"
:title-class="ns.e('title')"
<el-splitter-panel
v-if="['rtl', 'btt'].includes(direction)"
@click="onModalClick"
/>
<el-splitter-panel :resizable="resizable" :size="drawerSize">
<div
ref="drawerRef"
aria-modal="true"
:aria-label="title || undefined"
:aria-labelledby="!title ? titleId : undefined"
:aria-describedby="bodyId"
v-bind="$attrs"
:class="[ns.b(), direction, visible && 'open']"
role="dialog"
@click.stop
>
<span
v-if="!$slots.title"
:id="titleId"
role="heading"
:aria-level="headerAriaLevel"
:class="ns.e('title')"
ref="focusStartRef"
:class="ns.e('sr-focus')"
tabindex="-1"
/>
<header
v-if="withHeader"
:class="[ns.e('header'), headerClass]"
>
{{ title }}
</span>
</slot>
<slot v-else name="title">
<!-- DEPRECATED SLOT -->
</slot>
<button
v-if="showClose"
:aria-label="t('el.drawer.close')"
:class="ns.e('close-btn')"
type="button"
@click="handleClose"
>
<el-icon :class="ns.e('close')">
<close />
</el-icon>
</button>
</header>
<template v-if="rendered">
<div :id="bodyId" :class="[ns.e('body'), bodyClass]">
<slot />
<slot
v-if="!$slots.title"
name="header"
:close="handleClose"
:title-id="titleId"
:title-class="ns.e('title')"
>
<span
v-if="!$slots.title"
:id="titleId"
role="heading"
:aria-level="headerAriaLevel"
:class="ns.e('title')"
>
{{ title }}
</span>
</slot>
<slot v-else name="title">
<!-- DEPRECATED SLOT -->
</slot>
<button
v-if="showClose"
:aria-label="t('el.drawer.close')"
:class="ns.e('close-btn')"
type="button"
@click="handleClose"
>
<el-icon :class="ns.e('close')">
<close />
</el-icon>
</button>
</header>
<template v-if="rendered">
<div :id="bodyId" :class="[ns.e('body'), bodyClass]">
<slot />
</div>
</template>
<div
v-if="$slots.footer"
:class="[ns.e('footer'), footerClass]"
>
<slot name="footer" />
</div>
</div>
</template>
<div v-if="$slots.footer" :class="[ns.e('footer'), footerClass]">
<slot name="footer" />
</div>
</div>
</el-splitter-panel>
<el-splitter-panel
v-if="['ltr', 'ttb'].includes(direction)"
@click="onModalClick"
/>
</el-splitter>
</el-focus-trap>
</el-overlay>
</transition>
@@ -95,11 +117,12 @@ import { Close } from '@element-plus/icons-vue'
import { ElOverlay } from '@element-plus/components/overlay'
import ElFocusTrap from '@element-plus/components/focus-trap'
import ElTeleport from '@element-plus/components/teleport'
import ElSplitter, { ElSplitterPanel } from '@element-plus/components/splitter'
import { useDialog } from '@element-plus/components/dialog'
import { addUnit } from '@element-plus/utils'
import ElIcon from '@element-plus/components/icon'
import { useDeprecated, useLocale, useNamespace } from '@element-plus/hooks'
import { drawerEmits, drawerProps } from './drawer'
import { addUnit } from '@element-plus/utils'
defineOptions({
name: 'ElDrawer',

View File

@@ -1,3 +1,5 @@
import '@element-plus/components/base/style/css'
import '@element-plus/theme-chalk/el-drawer.css'
import '@element-plus/components/overlay/style/css'
import '@element-plus/components/splitter/style/css'
import '@element-plus/components/splitter-panel/style/css'

View File

@@ -1,3 +1,5 @@
import '@element-plus/components/base/style'
import '@element-plus/theme-chalk/src/drawer.scss'
import '@element-plus/components/overlay/style'
import '@element-plus/components/splitter/style'
import '@element-plus/components/splitter-panel/style'

View File

@@ -9,7 +9,8 @@ $directions: rtl, ltr, ttb, btt;
}
@include b(drawer) {
position: absolute;
width: 100%;
height: 100%;
box-sizing: border-box;
background-color: getCssVar('drawer', 'bg-color');
display: flex;
@@ -78,35 +79,29 @@ $directions: rtl, ltr, ttb, btt;
box-sizing: border-box;
}
}
}
&.ltr,
&.rtl {
height: 100%;
top: 0;
bottom: 0;
}
.#{$namespace}-drawer-splitter {
$bar: #{$namespace}-splitter-bar;
&.ttb,
&.btt {
width: 100%;
left: 0;
right: 0;
}
> .#{$bar} {
.#{$bar}__disable {
display: none;
}
&.ltr {
left: 0;
}
.#{$bar}__dragger {
&-horizontal::before {
width: 1px;
}
&.rtl {
right: 0;
}
&-vertical::before {
height: 1px;
}
&.ttb {
top: 0;
}
&.btt {
bottom: 0;
&:not(&-active, &:hover)::before {
background-color: transparent;
}
}
}
}
@@ -114,6 +109,10 @@ $directions: rtl, ltr, ttb, btt;
&-enter-active,
&-leave-active {
transition: all getCssVar('transition-duration');
.#{$namespace}-splitter-bar__dragger {
display: none;
}
}
&-enter-from,