Skip to content

Commit 2992da0

Browse files
committed
refactor(modal): correct draggable implementation
1 parent 11ee8fc commit 2992da0

24 files changed

+414
-227
lines changed

CHANGELOG.en-US.md

+6
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@
66

77
- (**Vue 3.3+ required**) Add slot type for all components.
88

9+
### Features
10+
11+
- `n-modal` adds `draggable` prop, closes [#6525](https://github.com/tusen-ai/naive-ui/issues/6525), [#5792](https://github.com/tusen-ai/naive-ui/issues/5792), [#5711](https://github.com/tusen-ai/naive-ui/issues/5711), [#5501](https://github.com/tusen-ai/naive-ui/issues/5501) and [#2152](https://github.com/tusen-ai/naive-ui/issues/2152).
12+
- `useDialog` supports `draggable` option.
13+
- `useModal` supports `draggable` option.
14+
915
### Fixes
1016

1117
- Fix `n-data-table` may have multiple expand trigger with tree data.

CHANGELOG.zh-CN.md

+2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
### Features
1010

1111
- `n-modal` 新增 `draggable` 属性,关闭 [#6525](https://github.com/tusen-ai/naive-ui/issues/6525)[#5792](https://github.com/tusen-ai/naive-ui/issues/5792)[#5711](https://github.com/tusen-ai/naive-ui/issues/5711)[#5501](https://github.com/tusen-ai/naive-ui/issues/5501)[#2152](https://github.com/tusen-ai/naive-ui/issues/2152)
12+
- `useDialog` 支持 `draggable` 参数
13+
- `useModal` 支持 `draggable` 参数
1214

1315
### Fixes
1416

src/_utils/css/index.ts

-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
11
export { color2Class } from './color-to-class'
22
export { formatLength } from './format-length'
3-
export { mergeClass } from './merge-class'
43
export { rtlInset } from './rtl-inset'

src/_utils/css/merge-class.ts

-18
This file was deleted.

src/_utils/vue/get-first-slot-vnode.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,12 @@ export function getFirstSlotVNode(
2525

2626
export function getFirstSlotVNodeWithTypedProps<T>(
2727
slotName: string,
28-
slot: (props: T) => VNode[],
28+
slot: ((props: T) => VNode[]) | undefined,
2929
props: T
3030
): VNode | null {
31+
if (!slot) {
32+
return null
33+
}
3134
const slotContent = flatten(slot(props))
3235
// vue will normalize the slot, so slot must be an array
3336
if (slotContent.length === 1) {

src/dialog/demos/enUS/basic.demo.vue

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export default defineComponent({
1919
content: 'Are you sure?',
2020
positiveText: 'Sure',
2121
negativeText: 'Not Sure',
22+
draggable: true,
2223
onPositiveClick: () => {
2324
message.success('Sure')
2425
},

src/dialog/demos/enUS/index.demo-entry.md

+7-6
Original file line numberDiff line numberDiff line change
@@ -65,17 +65,18 @@ use-dialog-reactive-list.vue
6565
| Name | Type | Default | Description | Version |
6666
| --- | --- | --- | --- | --- |
6767
| action | `() => VNodeChild` | `undefined` | Content of the operation area, must be a render function. | |
68-
| actionClass | `string` | The class name of the action area. | 2.38.2 |
69-
| actionStyle | `Object \| string` | The style of the action area. | 2.38.2 |
68+
| actionClass | `string` | `undefined` | The class name of the action area. | 2.38.2 |
69+
| actionStyle | `Object \| string` | `undefined` | The style of the action area. | 2.38.2 |
7070
| autoFocus | `boolean` | `true` | Whether to focus the first focusable element inside modal. | 2.28.3 |
7171
| blockScroll | `boolean` | `true` | Whether to disabled body scrolling when it's active. | 2.28.3 |
7272
| bordered | `boolean` | `false` | Whether to show `border`. | |
7373
| class | `any` | `undefined` | Class name of the dialog. | 2.33.0 |
7474
| closable | `boolean` | `true` | Whether to show `close` icon. | |
7575
| closeOnEsc | `boolean` | `true` | Whether to close the dialog when the Esc key is pressed | 2.26.4 |
7676
| content | `string \| (() => VNodeChild)` | `undefined` | Content, can be a render function. | |
77-
| contentClass | `string` | The class name of the content. | 2.38.2 |
78-
| contentStyle | `Object \| string` | The style of the content. | 2.38.2 |
77+
| contentClass | `string` | `undefined` | The class name of the content. | 2.38.2 |
78+
| contentStyle | `Object \| string` | `undefined` | The style of the content. | 2.38.2 |
79+
| draggable | `boolean \| { bounds?: 'none' }` | `false` | Whether it is draggable. | NEXT_VERSION |
7980
| iconPlacement | `'left' \| 'top'` | `'left'` | Icon placement. | |
8081
| icon | `() => VNodeChild` | `undefined` | `Render` function of `icon`. | |
8182
| loading | `boolean` | `false` | Whether to display `loading` status. | |
@@ -87,8 +88,8 @@ use-dialog-reactive-list.vue
8788
| showIcon | `boolean` | `true` | Whether to show `icon`. | |
8889
| style | `string \| Object` | `undefined` | Style of the dialog. | |
8990
| title | `string \| (() => VNodeChild)` | `undefined` | Title, can be a render function. | |
90-
| titleClass | `string` | The class name of the content. | 2.38.2 |
91-
| titleStyle | `Object \| string` | The style of the content. | 2.38.2 |
91+
| titleClass | `string` | `undefined` | The class name of the content. | 2.38.2 |
92+
| titleStyle | `Object \| string` | `undefined` | The style of the content. | 2.38.2 |
9293
| transformOrigin | `'mouse' \| 'center'` | `'mouse'` | The transform origin of the dialog's display animation. | 2.34.0 |
9394
| type | `'error \| 'success' \| 'warning'` | `'warning'` | Dialog type. | |
9495
| onAfterEnter | `() => void` | `undefined` | Callback on enter animation ends. | 2.33.0 |

src/dialog/demos/zhCN/basic.demo.vue

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export default defineComponent({
1919
content: '你确定?',
2020
positiveText: '确定',
2121
negativeText: '不确定',
22+
draggable: true,
2223
onPositiveClick: () => {
2324
message.success('确定')
2425
},

src/dialog/demos/zhCN/index.demo-entry.md

+7-6
Original file line numberDiff line numberDiff line change
@@ -67,17 +67,18 @@ rtl-debug.vue
6767
| 名称 | 类型 | 默认值 | 说明 | 版本 |
6868
| --- | --- | --- | --- | --- |
6969
| action | `() => VNodeChild` | `undefined` | 操作区域的内容,需要是渲染函数 | |
70-
| actionClass | `string` | 操作区域的类名 | 2.38.2 |
71-
| actionStyle | `Object \| string` | 操作区域的样式 | 2.38.2 |
70+
| actionClass | `string` | `undefined` | 操作区域的类名 | 2.38.2 |
71+
| actionStyle | `Object \| string` | `undefined` | 操作区域的样式 | 2.38.2 |
7272
| autoFocus | `boolean` | `true` | 是否自动聚焦 Modal 第一个可聚焦的元素 | 2.28.3 |
7373
| blockScroll | `boolean` | `true` | 是否在打开时禁用 body 滚动 | 2.28.3 |
7474
| bordered | `boolean` | `false` | 是否显示 `border` | |
7575
| class | `any` | `undefined` | 类名 | 2.33.0 |
7676
| closable | `boolean` | `true` | 是否显示 `close` 图标 | |
7777
| closeOnEsc | `boolean` | `true` | 是否在摁下 Esc 键的时候关闭对话框 | 2.26.4 |
7878
| content | `string \| (() => VNodeChild)` | `undefined` | 对话框内容,可以是渲染函数 | |
79-
| contentClass | `string` | 内容的类名 | 2.38.2 |
80-
| contentStyle | `Object \| string` | 内容的样式 | 2.38.2 |
79+
| contentClass | `string` | `undefined` | 内容的类名 | 2.38.2 |
80+
| contentStyle | `Object \| string` | `undefined` | 内容的样式 | 2.38.2 |
81+
| draggable | `boolean \| { bounds?: 'none' }` | `false` | 是否可拖拽 | NEXT_VERSION |
8182
| iconPlacement | `'left' \| 'top'` | `'left'` | 图标的位置 | |
8283
| icon | `() => VNodeChild` | `undefined` | 对话框 `icon`, 需要是渲染函数 | |
8384
| loading | `boolean` | `false` | 是否显示 `loading` 状态 | |
@@ -89,8 +90,8 @@ rtl-debug.vue
8990
| showIcon | `boolean` | `true` | 是否显示 `icon` | |
9091
| style | `string \| Object` | `undefined` | 样式 | |
9192
| title | `string \| (() => VNodeChild)` | `undefined` | 标题,可以是渲染函数 | |
92-
| titleClass | `string` | 标题的类名 | 2.38.2 |
93-
| titleStyle | `Object \| string` | 标题的样式 | 2.38.2 |
93+
| titleClass | `string` | `undefined` | 标题的类名 | 2.38.2 |
94+
| titleStyle | `Object \| string` | `undefined` | 标题的样式 | 2.38.2 |
9495
| transformOrigin | `'mouse' \| 'center'` | `'mouse'` | 对话框动画出现的位置 | 2.34.0 |
9596
| type | `'error \| 'success' \| 'warning'` | `'warning'` | 对话框类型 | |
9697
| onAfterEnter | `() => void` | `undefined` | 出现动画完成执行的回调 | 2.33.0 |

src/dialog/src/DialogEnvironment.tsx

+15-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
1+
import type { ModalDraggableOptions } from '../../modal/src/interface'
12
// use absolute path to make sure no circular ref of style
23
// this -> modal-index -> modal-style
3-
import { type CSSProperties, defineComponent, h, type PropType, ref } from 'vue'
4+
import {
5+
type CSSProperties,
6+
defineComponent,
7+
h,
8+
normalizeClass,
9+
type PropType,
10+
ref
11+
} from 'vue'
412
import { keep } from '../../_utils'
513
import NModal from '../../modal/src/Modal'
614
import { NDialog } from './Dialog'
@@ -30,7 +38,8 @@ export const exposedDialogEnvProps = {
3038
(e: MouseEvent) => Promise<unknown> | unknown
3139
>,
3240
onClose: Function as PropType<() => Promise<unknown> | unknown>,
33-
onMaskClick: Function as PropType<(e: MouseEvent) => void>
41+
onMaskClick: Function as PropType<(e: MouseEvent) => void>,
42+
draggable: [Boolean, Object] as PropType<boolean | ModalDraggableOptions>
3443
} as const
3544

3645
export const NDialogEnvironment = defineComponent({
@@ -156,12 +165,15 @@ export const NDialogEnvironment = defineComponent({
156165
blockScroll={this.blockScroll}
157166
autoFocus={this.autoFocus}
158167
transformOrigin={this.transformOrigin}
168+
draggable={this.draggable}
159169
internalAppear
170+
internalDialog
160171
>
161172
{{
162-
default: () => (
173+
default: ({ draggableClass }: { draggableClass: string }) => (
163174
<NDialog
164175
{...keep(this.$props, dialogPropKeys)}
176+
titleClass={normalizeClass([this.titleClass, draggableClass])}
165177
style={this.internalStyle}
166178
onClose={handleCloseClick}
167179
onNegativeClick={handleNegativeClick}

src/dialog/src/DialogProvider.ts

+6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { ExtractPublicPropTypes, Mutable } from '../../_utils'
22
import { createId } from 'seemly'
3+
import { useClicked, useClickPosition } from 'vooks'
34
import {
45
type CSSProperties,
56
defineComponent,
@@ -15,6 +16,7 @@ import {
1516
import { omit } from '../../_utils'
1617
import {
1718
dialogApiInjectionKey,
19+
dialogProviderInjectionKey,
1820
dialogReactiveListInjectionKey
1921
} from './context'
2022
import {
@@ -125,6 +127,10 @@ export const NDialogProvider = defineComponent({
125127
error: typedApi[3]
126128
}
127129
provide(dialogApiInjectionKey, api)
130+
provide(dialogProviderInjectionKey, {
131+
clickedRef: useClicked(64),
132+
clickedPositionRef: useClickPosition()
133+
})
128134
provide(dialogReactiveListInjectionKey, dialogListRef)
129135
return {
130136
...api,

src/dialog/src/context.ts

+4
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
import type {
22
DialogApiInjection,
3+
DialogProviderInjection,
34
DialogReactiveListInjection
45
} from './DialogProvider'
56
import { createInjectionKey } from '../../_utils'
67

8+
export const dialogProviderInjectionKey
9+
= createInjectionKey<DialogProviderInjection>('n-dialog-provider')
10+
711
export const dialogApiInjectionKey
812
= createInjectionKey<DialogApiInjection>('n-dialog-api')
913

+129
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
<markdown>
2+
# Draggable
3+
4+
Set `draggable` to `true` to make modal draggable. If you want it to be dragged out of the window, you can set `draggable` to `{ bounds: 'none' }`.
5+
6+
If you want to completely customize the content of the modal, you can use the `draggableClass` in the `default` slot to set it on the element you want to trigger the drag.
7+
</markdown>
8+
9+
<script lang="ts">
10+
import { useModal } from 'naive-ui'
11+
import { defineComponent, ref } from 'vue'
12+
13+
export default defineComponent({
14+
data() {
15+
const modal = useModal()
16+
17+
function showDialogPreset() {
18+
modal.create({
19+
title: 'Dialog preset',
20+
draggable: true,
21+
preset: 'dialog',
22+
content: 'Placeholder....'
23+
})
24+
}
25+
26+
function showCardPreset() {
27+
modal.create({
28+
title: 'Card preset',
29+
draggable: true,
30+
preset: 'card',
31+
content: 'Placeholder....'
32+
})
33+
}
34+
35+
return {
36+
showModal1: ref(false),
37+
showModal2: ref(false),
38+
showModal3: ref(false),
39+
showModal4: ref(false),
40+
showCardPreset,
41+
showDialogPreset
42+
}
43+
}
44+
})
45+
</script>
46+
47+
<template>
48+
<n-flex>
49+
<n-button @click="showModal1 = !showModal1">
50+
Card preset
51+
</n-button>
52+
<n-button @click="showModal2 = !showModal2">
53+
Dialog preset
54+
</n-button>
55+
<n-button @click="showModal3 = !showModal3">
56+
No preset
57+
</n-button>
58+
<n-button @click="showDialogPreset">
59+
Imperative dialog preset
60+
</n-button>
61+
<n-button @click="showCardPreset">
62+
Imperative card preset
63+
</n-button>
64+
<n-button @click="showModal4 = !showModal4">
65+
Nested draggable
66+
</n-button>
67+
</n-flex>
68+
<n-modal
69+
v-model:show="showModal1"
70+
title="card 预设拖拽"
71+
preset="card"
72+
draggable
73+
:style="{ width: '800px' }"
74+
>
75+
<div>Placeholder...</div>
76+
<div>Placeholder...</div>
77+
<div>Placeholder...</div>
78+
<div>Placeholder...</div>
79+
<div>Placeholder...</div>
80+
<div>Placeholder...</div>
81+
<div>Placeholder...</div>
82+
<div>Placeholder...</div>
83+
<div>Placeholder...</div>
84+
<div>Placeholder...</div>
85+
</n-modal>
86+
<n-modal
87+
v-model:show="showModal2"
88+
title="Dialog preset draggable"
89+
preset="dialog"
90+
draggable
91+
:style="{ width: '800px' }"
92+
>
93+
<div>Placeholder...</div>
94+
<div>Placeholder...</div>
95+
<div>Placeholder...</div>
96+
<div>Placeholder...</div>
97+
<div>Placeholder...</div>
98+
<div>Placeholder...</div>
99+
<div>Placeholder...</div>
100+
<div>Placeholder...</div>
101+
<div>Placeholder...</div>
102+
<div>Placeholder...</div>
103+
</n-modal>
104+
<n-modal
105+
v-model:show="showModal3"
106+
title="No preset draggable"
107+
draggable
108+
:style="{ width: '800px' }"
109+
>
110+
<template #default="{ draggableClass }">
111+
<n-card>
112+
<div :class="draggableClass">
113+
Mouse down here to drag
114+
</div>
115+
</n-card>
116+
</template>
117+
</n-modal>
118+
<n-modal
119+
v-model:show="showModal4"
120+
title="Nested draggable"
121+
preset="card"
122+
:draggable="{ bounds: 'none' }"
123+
:style="{ width: '800px' }"
124+
>
125+
<n-button @click="showDialogPreset">
126+
Create a new modal
127+
</n-button>
128+
</n-modal>
129+
</template>

src/modal/demos/enUS/index.demo-entry.md

+6
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ preset-card.vue
2727
preset-confirm.vue
2828
preset-confirm-slot.vue
2929
transform-origin.vue
30+
draggable.vue
3031
```
3132

3233
## API
@@ -58,6 +59,7 @@ Provided since `2.38.0`.
5859
| block-scroll | `boolean` | `true` | Whether to disabled body scrolling when it's active. | 2.28.3 |
5960
| close-on-esc | `boolean` | `true` | Whether to close modal on Esc is pressed. | 2.24.2 |
6061
| display-directive | `'if' \| 'show'` | `'if'` | Use which directive to control the rendering of modal body. | |
62+
| draggable | `boolean \| { bounds?: 'window' }` | `false` | Whether the modal is draggable. Make its position not bound inside window using `bounds === 'none'`. | NEXT_VERSION |
6163
| mask-closable | `boolean` | `true` | Whether to emit `hide` event when click mask. | |
6264
| preset | `'dialog' \| 'card'` | `undefined` | The preset of `n-modal`. | |
6365
| show | `boolean` | `false` | Whether to show modal. | |
@@ -97,6 +99,10 @@ See [Dialog props](dialog#Dialog-Props)
9799

98100
See [Card slots](card#Card-Slots)
99101

102+
`default` slot's parameter is different, which is `(props: { draggableClass: string })`.
103+
100104
### Modal with Preset Dialog Slots
101105

102106
See [Dialog slots](dialog#Dialog-Slots)
107+
108+
`default` slot's parameter is different, which is `(props: { draggableClass: string })`.

0 commit comments

Comments
 (0)