feat(drawer): add a new prop(adjustable) so that the drawer can adjust its width/height (#3234)

* feat(drawer): add a new prop(adjustable) so that the drawer can adjust its width/height

* docs: optimize doc
This commit is contained in:
Jevon 2022-07-05 23:13:03 +08:00 committed by GitHub
parent 36be318d5a
commit 3d9f4c2f6f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 443 additions and 16 deletions

View File

@ -33,6 +33,7 @@
- `n-date-picker` would disable confirm button if end date is not selected, closes [#3226](https://github.com/TuSimple/naive-ui/issues/3226).
- `n-tree` adds `check-on-click` prop to control `checked` status, closes [#2968](https://github.com/TuSimple/naive-ui/issues/2968).
- `n-tree` adds `accrodion` prop, closes [#3129](https://github.com/TuSimple/naive-ui/issues/3129).
- `n-drawer` adds `adjustable` prop.
## 2.30.8

View File

@ -34,6 +34,7 @@
- `n-date-picker` 在选择结束日期过程中禁止点击确认按钮,关闭 [#3226](https://github.com/TuSimple/naive-ui/issues/3226)
- `n-tree` 新增 `check-on-click` 属性来控制可选状态下的选中交互方式,关闭 [#2968](https://github.com/TuSimple/naive-ui/issues/2968)
- `n-tree` 新增 `accrodion` 属性,关闭 [#3129](https://github.com/TuSimple/naive-ui/issues/3129)
- `n-drawer` 新增 `adjustable` 属性
## 2.30.8

View File

@ -189,6 +189,7 @@ const derived: ThemeCommonVars = {
buttonColor2: 'rgba(255, 255, 255, .08)',
buttonColor2Hover: 'rgba(255, 255, 255, .12)',
buttonColor2Pressed: 'rgba(255, 255, 255, .08)',
drawerBaseLineColor: 'rgba(255, 255, 255, 0.4)',
boxShadow1:
'0 1px 2px -2px rgba(0, 0, 0, .24), 0 3px 6px 0 rgba(0, 0, 0, .18), 0 5px 12px 4px rgba(0, 0, 0, .12)',

View File

@ -190,6 +190,7 @@ const derived = {
buttonColor2: 'rgba(46, 51, 56, .05)',
buttonColor2Hover: 'rgba(46, 51, 56, .09)',
buttonColor2Pressed: 'rgba(46, 51, 56, .13)',
drawerBaseLineColor: 'rgba(0, 0, 0, 0.5)',
boxShadow1:
'0 1px 2px -2px rgba(0, 0, 0, .08), 0 3px 6px 0 rgba(0, 0, 0, .06), 0 5px 12px 4px rgba(0, 0, 0, .04)',

View File

@ -0,0 +1,51 @@
<markdown>
# Adjustable drawer
</markdown>
<template>
<n-button-group>
<n-button @click="activate('top')">
Top
</n-button>
<n-button @click="activate('right')">
Right
</n-button>
<n-button @click="activate('bottom')">
Bottom
</n-button>
<n-button @click="activate('left')">
Left
</n-button>
</n-button-group>
<n-drawer
v-model:show="active"
:width="502"
:placement="placement"
:adjustable="true"
>
<n-drawer-content title="Stoner">
Stoner is a 1965 novel by the American writer John Williams.
</n-drawer-content>
</n-drawer>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue'
import type { DrawerPlacement } from 'naive-ui'
export default defineComponent({
setup () {
const active = ref(false)
const placement = ref<DrawerPlacement>('right')
const activate = (place: DrawerPlacement) => {
active.value = true
placement.value = place
}
return {
active,
placement,
activate
}
}
})
</script>

View File

@ -15,6 +15,7 @@ target.vue
closable.vue
slot.vue
scroll.vue
adjustable.vue
```
## API
@ -44,6 +45,7 @@ scroll.vue
| on-esc | `() => void` | `undefined` | Callback fired when the escape key is pressed and focus is within drawer. | 2.24.2 |
| on-mask-click | `(e: MouseEvent) => void` | `undefined` | Callback triggered on mask clicked. | |
| on-update:show | `(show: boolean) => void` | `undefined` | Callback triggered on drawer display status would change. | |
| adjustable | `boolean` | `false` | Whether to adjust the width/height of drawer. | |
### DrawerContent Props

View File

@ -0,0 +1,51 @@
<markdown>
# 可调整宽高的抽屉
</markdown>
<template>
<n-button-group>
<n-button @click="activate('top')">
</n-button>
<n-button @click="activate('right')">
</n-button>
<n-button @click="activate('bottom')">
</n-button>
<n-button @click="activate('left')">
</n-button>
</n-button-group>
<n-drawer
v-model:show="active"
:width="502"
:placement="placement"
:adjustable="true"
>
<n-drawer-content title="斯通纳">
斯通纳是美国作家约翰·威廉姆斯在 1965 年出版的小说
</n-drawer-content>
</n-drawer>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue'
import type { DrawerPlacement } from 'naive-ui'
export default defineComponent({
setup () {
const active = ref(false)
const placement = ref<DrawerPlacement>('right')
const activate = (place: DrawerPlacement) => {
active.value = true
placement.value = place
}
return {
active,
placement,
activate
}
}
})
</script>

View File

@ -21,6 +21,7 @@ dark-1-debug.vue
dark-2-debug.vue
dark-3-debug.vue
dark-4-debug.vue
adjustable.vue
```
## API
@ -50,6 +51,7 @@ dark-4-debug.vue
| on-esc | `() => void` | `undefined` | 焦点在 Drawer 内部时按下 Esc 键的回调 | 2.24.2 |
| on-mask-click | `(e: MouseEvent) => void` | `undefined` | 点击遮罩的回调 | |
| on-update:show | `(show: boolean) => void` | `undefined` | 抽屉显示状态改变时执行的回调函数 | |
| adjustable | `boolean` | `false` | 抽屉是否可以调整宽度/高度. | |
### DrawerContent Props

View File

@ -1,5 +1,6 @@
import {
h,
ref,
PropType,
defineComponent,
computed,
@ -82,6 +83,10 @@ export const drawerProps = {
type: Boolean,
default: true
},
adjustable: {
type: Boolean,
default: false
},
'onUpdate:show': [Function, Array] as PropType<
MaybeArray<(value: boolean) => void>
>,
@ -147,18 +152,34 @@ export default defineComponent({
props,
mergedClsPrefixRef
)
const styleWidthRef = computed(() => {
const { placement } = props
if (placement === 'top' || placement === 'bottom') return ''
const { width } = props
return formatLength(width)
const drawerWidth = ref('')
const drawerHeight = ref('')
const styleWidthRef = computed({
set (val: string) {
drawerWidth.value = val
},
get () {
const { placement } = props
if (placement === 'top' || placement === 'bottom') return ''
const { width } = props
return drawerWidth.value || formatLength(width)
}
})
const styleHeightRef = computed(() => {
const { placement } = props
if (placement === 'left' || placement === 'right') return ''
const { height } = props
return formatLength(height)
const styleHeightRef = computed({
set (val: string) {
drawerHeight.value = val
},
get () {
const { placement } = props
if (placement === 'left' || placement === 'right') return ''
const { height } = props
return drawerHeight.value || formatLength(height)
}
})
const mergedBodyStyleRef = computed<Array<CSSProperties | string>>(() => {
return [
{
@ -168,6 +189,7 @@ export default defineComponent({
props.drawerStyle || ''
]
})
function handleMaskClick (e: MouseEvent): void {
const { onMaskClick, maskClosable } = props
if (maskClosable) {
@ -195,7 +217,9 @@ export default defineComponent({
isMountedRef: isMountedRef,
mergedThemeRef: themeRef,
mergedClsPrefixRef,
doUpdateShow
doUpdateShow,
width: styleWidthRef,
height: styleHeightRef
})
const cssVarsRef = computed(() => {
const {
@ -220,7 +244,8 @@ export default defineComponent({
closeColorPressed,
closeIconSize,
closeSize,
closeBorderRadius
closeBorderRadius,
baseLineColor
}
} = themeRef.value
return {
@ -246,7 +271,8 @@ export default defineComponent({
'--n-close-color-hover': closeColorHover,
'--n-close-color-pressed': closeColorPressed,
'--n-close-icon-size': closeIconSize,
'--n-close-border-radius': closeBorderRadius
'--n-close-border-radius': closeBorderRadius,
'--n-base-line-color': baseLineColor
}
})
const themeClassHandle = inlineThemeDisabled
@ -315,6 +341,7 @@ export default defineComponent({
onAfterLeave={this.onAfterLeave}
trapFocus={this.trapFocus}
autoFocus={this.autoFocus}
adjustable={this.adjustable}
showMask={this.showMask}
onEsc={this.handleEsc}
onClickoutside={this.handleMaskClick}

View File

@ -60,6 +60,10 @@ export default defineComponent({
type: [Boolean, String] as PropType<boolean | 'transparent'>,
required: true
},
adjustable: {
type: Boolean,
default: true
},
onClickoutside: Function as PropType<(e: MouseEvent) => void>,
onAfterLeave: Function as PropType<() => void>,
onAfterEnter: Function as PropType<() => void>,
@ -70,6 +74,68 @@ export default defineComponent({
const bodyRef = ref<HTMLElement | null>(null) // used for detached content
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const NDrawer = inject(drawerInjectionKey)!
let startPos = 0
let cacheCusor = ''
let hoverTimer: ReturnType<typeof setTimeout>
const isHover = ref(false)
const isMousedown = ref(false)
const isVertical = computed<boolean>(() => {
return props.placement === 'top' || props.placement === 'bottom'
})
const removePx = (str: string): number => Number(str.replace('px', ''))
const mousedownHandler = (e: MouseEvent): void => {
isMousedown.value = true
startPos = isVertical.value ? e.clientY : e.clientX
cacheCusor = document.body.style.cursor
document.body.style.cursor = isVertical.value ? 'ns-resize' : 'ew-resize'
document.body.addEventListener('mousemove', mousemoveHandler)
document.body.addEventListener('mouseup', mouseupHandler)
}
const mouseenterHandler = (e: MouseEvent): void => {
if (isMousedown.value) return
hoverTimer = setTimeout(() => {
isHover.value = true
}, 300)
}
const mouseleaveHandler = (e: MouseEvent): void => {
if (hoverTimer) {
clearTimeout(hoverTimer)
}
isHover.value = false
}
const mousemoveHandler = (e: MouseEvent): void => {
if (isMousedown.value) {
if (isVertical.value) {
let height = removePx(NDrawer.height.value)
const increment = startPos - e.clientY
height += props.placement === 'bottom' ? increment : -increment
NDrawer.height.value = `${height}px`
startPos = e.clientY
} else {
let width = removePx(NDrawer.width.value)
const increment = startPos - e.clientX
width += props.placement === 'right' ? increment : -increment
NDrawer.width.value = `${width}px`
startPos = e.clientX
}
}
}
const mouseupHandler = (): void => {
startPos = 0
isMousedown.value = false
document.body.style.cursor = cacheCusor
document.body.removeEventListener('mousemove', mousemoveHandler)
document.body.removeEventListener('mouseup', mouseupHandler)
}
watchEffect(() => {
if (props.show) displayedRef.value = true
})
@ -109,7 +175,12 @@ export default defineComponent({
}[props.placement]
}),
handleAfterLeave,
bodyDirectives: bodyDirectivesRef
bodyDirectives: bodyDirectivesRef,
mousedownHandler,
mouseenterHandler,
mouseleaveHandler,
isMousedown,
isHover
}
},
render () {
@ -145,11 +216,31 @@ export default defineComponent({
class: [
`${mergedClsPrefix}-drawer`,
`${mergedClsPrefix}-drawer--${this.placement}-placement`,
/**
* When the mouse is pressed down to adjust the width/height,
* it is forbidden to select text to avoid bugs
*/
this.isMousedown &&
`${mergedClsPrefix}-no-select`,
this.nativeScrollbar &&
`${mergedClsPrefix}-drawer--native-scrollbar`
]
}),
[
this.adjustable ? (
<div
class={[
`${mergedClsPrefix}-drawer__adjustable-line`,
`${mergedClsPrefix}-drawer__adjustable-line--${this.placement}`,
this.isMousedown || this.isHover
? `${mergedClsPrefix}-drawer__adjustable-line--hover`
: ''
]}
onMouseenter={this.mouseenterHandler}
onMouseleave={this.mouseleaveHandler}
onMousedown={this.mousedownHandler}
></div>
) : null,
this.nativeScrollbar ? (
<div
class={`${mergedClsPrefix}-drawer-content-wrapper`}

View File

@ -12,6 +12,8 @@ export interface DrawerInjection {
mergedThemeRef: Ref<MergedTheme<DrawerTheme>>
mergedClsPrefixRef: Ref<string>
doUpdateShow: (show: boolean) => void
width: Ref<string>
height: Ref<string>
}
export const drawerInjectionKey =
createInjectionKey<DrawerInjection>('n-drawer')

View File

@ -30,6 +30,9 @@ import { fadeInTransition } from '../../../_styles/transitions/fade-in.cssr'
// --n-close-size
// --n-close-icon-size
export default c([
cB('no-select', `
user-select: none;
`),
cB('drawer', `
word-break: break-word;
line-height: var(--n-line-height);
@ -54,6 +57,48 @@ export default c([
height: 100%;
`)
]),
cE('adjustable-line', `
position: absolute;
background-color: transparent;
border-radius: 2px;
`, [
fadeInTransition({
name: 'fade-in',
leaveDuration: '0.4s',
enterDuration: '0.4s'
}),
cM('hover', `
background-color: var(--n-base-line-color);
`),
cM('top', `
width: 100%;
height: 6px;
bottom: 0;
left: 0;
cursor: ns-resize;
`),
cM('bottom', `
width: 100%;
height: 6px;
top: 0;
left: 0;
cursor: ns-resize;
`),
cM('left', `
width: 6px;
height: 100%;
top: 0;
right: 0;
cursor: ew-resize;
`),
cM('right', `
width: 6px;
height: 100%;
top: 0;
left: 0;
cursor: ew-resize;
`)
]),
cB('drawer-content-wrapper', `
box-sizing: border-box;
`),

View File

@ -17,7 +17,8 @@ export const self = (vars: ThemeCommonVars) => {
closeIconColor,
closeIconColorHover,
closeIconColorPressed,
borderRadius
borderRadius,
drawerBaseLineColor
} = vars
return {
bodyPadding: '16px 24px',
@ -39,7 +40,8 @@ export const self = (vars: ThemeCommonVars) => {
closeIconSize: '18px',
closeColorHover,
closeColorPressed,
closeBorderRadius: borderRadius
closeBorderRadius: borderRadius,
baseLineColor: drawerBaseLineColor
}
}

View File

@ -203,4 +203,154 @@ describe('n-drawer', () => {
wrapper.unmount()
})
it('should work with `adjustable` prop', async () => {
// placement top
let wrapper = await mountDrawer({
show: true,
drawerProps: { placement: 'top', adjustable: true, height: 251 }
})
expect(document.querySelector('.n-drawer')?.className).toContain(
'n-drawer--top-placement'
)
expect(
document.querySelector('.n-drawer__adjustable-line--top')
).not.toEqual(null)
let mousedownEvent = new MouseEvent('mousedown', {
bubbles: true,
clientX: 0,
clientY: 251
})
let mousemoveEvent = new MouseEvent('mousemove', {
bubbles: true,
clientX: 0,
clientY: 600
})
let mouseupEvent = new MouseEvent('mouseup', { bubbles: true })
document
.querySelector('.n-drawer__adjustable-line--top')
?.dispatchEvent(mousedownEvent)
document.body?.dispatchEvent(mousemoveEvent)
document.body?.dispatchEvent(mouseupEvent)
await nextTick()
expect(document.querySelector('.n-drawer')?.getAttribute('style')).toBe(
'height: 600px;'
)
wrapper.unmount()
// placement bottom
wrapper = await mountDrawer({
show: true,
drawerProps: { placement: 'bottom', adjustable: true, height: 251 }
})
expect(document.querySelector('.n-drawer')?.className).toContain(
'n-drawer--bottom-placement'
)
expect(
document.querySelector('.n-drawer__adjustable-line--bottom')
).not.toEqual(null)
mousedownEvent = new MouseEvent('mousedown', {
bubbles: true,
clientX: 0,
clientY: 600
})
mousemoveEvent = new MouseEvent('mousemove', {
bubbles: true,
clientX: 0,
clientY: 251
})
mouseupEvent = new MouseEvent('mouseup', { bubbles: true })
document
.querySelector('.n-drawer__adjustable-line--bottom')
?.dispatchEvent(mousedownEvent)
document.body?.dispatchEvent(mousemoveEvent)
document.body?.dispatchEvent(mouseupEvent)
await nextTick()
expect(document.querySelector('.n-drawer')?.getAttribute('style')).toBe(
'height: 600px;'
)
wrapper.unmount()
// placement left
wrapper = await mountDrawer({
show: true,
drawerProps: { placement: 'left', adjustable: true, width: 251 }
})
expect(document.querySelector('.n-drawer')?.className).toContain(
'n-drawer--left-placement'
)
expect(
document.querySelector('.n-drawer__adjustable-line--left')
).not.toEqual(null)
mousedownEvent = new MouseEvent('mousedown', {
bubbles: true,
clientX: 251,
clientY: 0
})
mousemoveEvent = new MouseEvent('mousemove', {
bubbles: true,
clientX: 600,
clientY: 0
})
mouseupEvent = new MouseEvent('mouseup', { bubbles: true })
document
.querySelector('.n-drawer__adjustable-line--left')
?.dispatchEvent(mousedownEvent)
document.body?.dispatchEvent(mousemoveEvent)
document.body?.dispatchEvent(mouseupEvent)
await nextTick()
expect(document.querySelector('.n-drawer')?.getAttribute('style')).toBe(
'width: 600px;'
)
wrapper.unmount()
// placement right
wrapper = await mountDrawer({
show: true,
drawerProps: { placement: 'right', adjustable: true, width: 251 }
})
expect(document.querySelector('.n-drawer')?.className).toContain(
'n-drawer--right-placement'
)
expect(
document.querySelector('.n-drawer__adjustable-line--right')
).not.toEqual(null)
mousedownEvent = new MouseEvent('mousedown', {
bubbles: true,
clientX: 600,
clientY: 0
})
mousemoveEvent = new MouseEvent('mousemove', {
bubbles: true,
clientX: 251,
clientY: 0
})
mouseupEvent = new MouseEvent('mouseup', { bubbles: true })
document
.querySelector('.n-drawer__adjustable-line--right')
?.dispatchEvent(mousedownEvent)
document.body?.dispatchEvent(mousemoveEvent)
document.body?.dispatchEvent(mouseupEvent)
await nextTick()
expect(document.querySelector('.n-drawer')?.getAttribute('style')).toBe(
'width: 600px;'
)
wrapper.unmount()
})
})