From 2992da02172b2f16fdd81e27d48e77543b9da646 Mon Sep 17 00:00:00 2001 From: 07akioni <07akioni2@gmail.com> Date: Sat, 4 Jan 2025 20:21:56 +0800 Subject: [PATCH] refactor(modal): correct draggable implementation --- CHANGELOG.en-US.md | 6 + CHANGELOG.zh-CN.md | 2 + src/_utils/css/index.ts | 1 - src/_utils/css/merge-class.ts | 18 -- src/_utils/vue/get-first-slot-vnode.ts | 5 +- src/dialog/demos/enUS/basic.demo.vue | 1 + src/dialog/demos/enUS/index.demo-entry.md | 13 +- src/dialog/demos/zhCN/basic.demo.vue | 1 + src/dialog/demos/zhCN/index.demo-entry.md | 13 +- src/dialog/src/DialogEnvironment.tsx | 18 +- src/dialog/src/DialogProvider.ts | 6 + src/dialog/src/context.ts | 4 + src/modal/demos/enUS/draggable.demo.vue | 129 +++++++++++ src/modal/demos/enUS/index.demo-entry.md | 6 + src/modal/demos/zhCN/draggable.demo.vue | 32 ++- src/modal/demos/zhCN/index.demo-entry.md | 6 +- src/modal/src/BodyWrapper.tsx | 252 ++++++++++++---------- src/modal/src/Modal.tsx | 37 +++- src/modal/src/ModalEnvironment.tsx | 1 + src/modal/src/ModalProvider.ts | 11 +- src/modal/src/composables.ts | 65 +++--- src/modal/src/context.ts | 4 + src/modal/src/interface.ts | 8 +- src/modal/src/styles/index.cssr.ts | 2 +- 24 files changed, 414 insertions(+), 227 deletions(-) delete mode 100644 src/_utils/css/merge-class.ts create mode 100644 src/modal/demos/enUS/draggable.demo.vue diff --git a/CHANGELOG.en-US.md b/CHANGELOG.en-US.md index 1773b8744..7ebce9d76 100644 --- a/CHANGELOG.en-US.md +++ b/CHANGELOG.en-US.md @@ -6,6 +6,12 @@ - (**Vue 3.3+ required**) Add slot type for all components. +### Features + +- `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). +- `useDialog` supports `draggable` option. +- `useModal` supports `draggable` option. + ### Fixes - Fix `n-data-table` may have multiple expand trigger with tree data. diff --git a/CHANGELOG.zh-CN.md b/CHANGELOG.zh-CN.md index 13433131a..01a5cbe34 100644 --- a/CHANGELOG.zh-CN.md +++ b/CHANGELOG.zh-CN.md @@ -9,6 +9,8 @@ ### Features - `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) +- `useDialog` 支持 `draggable` 参数 +- `useModal` 支持 `draggable` 参数 ### Fixes diff --git a/src/_utils/css/index.ts b/src/_utils/css/index.ts index 6657090f4..56cf19a87 100644 --- a/src/_utils/css/index.ts +++ b/src/_utils/css/index.ts @@ -1,4 +1,3 @@ export { color2Class } from './color-to-class' export { formatLength } from './format-length' -export { mergeClass } from './merge-class' export { rtlInset } from './rtl-inset' diff --git a/src/_utils/css/merge-class.ts b/src/_utils/css/merge-class.ts deleted file mode 100644 index 223bc245b..000000000 --- a/src/_utils/css/merge-class.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { isArray, isString } from 'lodash-es' - -export function mergeClass( - ...args: Array -) { - return args - .reduce>((p, c) => { - if (isString(c)) { - p.push(c) - } - if (isArray(c)) { - p.push(...c) - } - return p - }, []) - .filter(Boolean) - .join(' ') -} diff --git a/src/_utils/vue/get-first-slot-vnode.ts b/src/_utils/vue/get-first-slot-vnode.ts index 5be3cfb66..b9616d8ce 100644 --- a/src/_utils/vue/get-first-slot-vnode.ts +++ b/src/_utils/vue/get-first-slot-vnode.ts @@ -25,9 +25,12 @@ export function getFirstSlotVNode( export function getFirstSlotVNodeWithTypedProps( slotName: string, - slot: (props: T) => VNode[], + slot: ((props: T) => VNode[]) | undefined, props: T ): VNode | null { + if (!slot) { + return null + } const slotContent = flatten(slot(props)) // vue will normalize the slot, so slot must be an array if (slotContent.length === 1) { diff --git a/src/dialog/demos/enUS/basic.demo.vue b/src/dialog/demos/enUS/basic.demo.vue index fb66665b9..4f208b1d8 100644 --- a/src/dialog/demos/enUS/basic.demo.vue +++ b/src/dialog/demos/enUS/basic.demo.vue @@ -19,6 +19,7 @@ export default defineComponent({ content: 'Are you sure?', positiveText: 'Sure', negativeText: 'Not Sure', + draggable: true, onPositiveClick: () => { message.success('Sure') }, diff --git a/src/dialog/demos/enUS/index.demo-entry.md b/src/dialog/demos/enUS/index.demo-entry.md index dceacb2e9..43c07334a 100644 --- a/src/dialog/demos/enUS/index.demo-entry.md +++ b/src/dialog/demos/enUS/index.demo-entry.md @@ -65,8 +65,8 @@ use-dialog-reactive-list.vue | Name | Type | Default | Description | Version | | --- | --- | --- | --- | --- | | action | `() => VNodeChild` | `undefined` | Content of the operation area, must be a render function. | | -| actionClass | `string` | The class name of the action area. | 2.38.2 | -| actionStyle | `Object \| string` | The style of the action area. | 2.38.2 | +| actionClass | `string` | `undefined` | The class name of the action area. | 2.38.2 | +| actionStyle | `Object \| string` | `undefined` | The style of the action area. | 2.38.2 | | autoFocus | `boolean` | `true` | Whether to focus the first focusable element inside modal. | 2.28.3 | | blockScroll | `boolean` | `true` | Whether to disabled body scrolling when it's active. | 2.28.3 | | bordered | `boolean` | `false` | Whether to show `border`. | | @@ -74,8 +74,9 @@ use-dialog-reactive-list.vue | closable | `boolean` | `true` | Whether to show `close` icon. | | | closeOnEsc | `boolean` | `true` | Whether to close the dialog when the Esc key is pressed | 2.26.4 | | content | `string \| (() => VNodeChild)` | `undefined` | Content, can be a render function. | | -| contentClass | `string` | The class name of the content. | 2.38.2 | -| contentStyle | `Object \| string` | The style of the content. | 2.38.2 | +| contentClass | `string` | `undefined` | The class name of the content. | 2.38.2 | +| contentStyle | `Object \| string` | `undefined` | The style of the content. | 2.38.2 | +| draggable | `boolean \| { bounds?: 'none' }` | `false` | Whether it is draggable. | NEXT_VERSION | | iconPlacement | `'left' \| 'top'` | `'left'` | Icon placement. | | | icon | `() => VNodeChild` | `undefined` | `Render` function of `icon`. | | | loading | `boolean` | `false` | Whether to display `loading` status. | | @@ -87,8 +88,8 @@ use-dialog-reactive-list.vue | showIcon | `boolean` | `true` | Whether to show `icon`. | | | style | `string \| Object` | `undefined` | Style of the dialog. | | | title | `string \| (() => VNodeChild)` | `undefined` | Title, can be a render function. | | -| titleClass | `string` | The class name of the content. | 2.38.2 | -| titleStyle | `Object \| string` | The style of the content. | 2.38.2 | +| titleClass | `string` | `undefined` | The class name of the content. | 2.38.2 | +| titleStyle | `Object \| string` | `undefined` | The style of the content. | 2.38.2 | | transformOrigin | `'mouse' \| 'center'` | `'mouse'` | The transform origin of the dialog's display animation. | 2.34.0 | | type | `'error \| 'success' \| 'warning'` | `'warning'` | Dialog type. | | | onAfterEnter | `() => void` | `undefined` | Callback on enter animation ends. | 2.33.0 | diff --git a/src/dialog/demos/zhCN/basic.demo.vue b/src/dialog/demos/zhCN/basic.demo.vue index fed2eb3a4..0a7c1a4cf 100644 --- a/src/dialog/demos/zhCN/basic.demo.vue +++ b/src/dialog/demos/zhCN/basic.demo.vue @@ -19,6 +19,7 @@ export default defineComponent({ content: '你确定?', positiveText: '确定', negativeText: '不确定', + draggable: true, onPositiveClick: () => { message.success('确定') }, diff --git a/src/dialog/demos/zhCN/index.demo-entry.md b/src/dialog/demos/zhCN/index.demo-entry.md index a43733e0c..a37754a95 100644 --- a/src/dialog/demos/zhCN/index.demo-entry.md +++ b/src/dialog/demos/zhCN/index.demo-entry.md @@ -67,8 +67,8 @@ rtl-debug.vue | 名称 | 类型 | 默认值 | 说明 | 版本 | | --- | --- | --- | --- | --- | | action | `() => VNodeChild` | `undefined` | 操作区域的内容,需要是渲染函数 | | -| actionClass | `string` | 操作区域的类名 | 2.38.2 | -| actionStyle | `Object \| string` | 操作区域的样式 | 2.38.2 | +| actionClass | `string` | `undefined` | 操作区域的类名 | 2.38.2 | +| actionStyle | `Object \| string` | `undefined` | 操作区域的样式 | 2.38.2 | | autoFocus | `boolean` | `true` | 是否自动聚焦 Modal 第一个可聚焦的元素 | 2.28.3 | | blockScroll | `boolean` | `true` | 是否在打开时禁用 body 滚动 | 2.28.3 | | bordered | `boolean` | `false` | 是否显示 `border` | | @@ -76,8 +76,9 @@ rtl-debug.vue | closable | `boolean` | `true` | 是否显示 `close` 图标 | | | closeOnEsc | `boolean` | `true` | 是否在摁下 Esc 键的时候关闭对话框 | 2.26.4 | | content | `string \| (() => VNodeChild)` | `undefined` | 对话框内容,可以是渲染函数 | | -| contentClass | `string` | 内容的类名 | 2.38.2 | -| contentStyle | `Object \| string` | 内容的样式 | 2.38.2 | +| contentClass | `string` | `undefined` | 内容的类名 | 2.38.2 | +| contentStyle | `Object \| string` | `undefined` | 内容的样式 | 2.38.2 | +| draggable | `boolean \| { bounds?: 'none' }` | `false` | 是否可拖拽 | NEXT_VERSION | | iconPlacement | `'left' \| 'top'` | `'left'` | 图标的位置 | | | icon | `() => VNodeChild` | `undefined` | 对话框 `icon`, 需要是渲染函数 | | | loading | `boolean` | `false` | 是否显示 `loading` 状态 | | @@ -89,8 +90,8 @@ rtl-debug.vue | showIcon | `boolean` | `true` | 是否显示 `icon` | | | style | `string \| Object` | `undefined` | 样式 | | | title | `string \| (() => VNodeChild)` | `undefined` | 标题,可以是渲染函数 | | -| titleClass | `string` | 标题的类名 | 2.38.2 | -| titleStyle | `Object \| string` | 标题的样式 | 2.38.2 | +| titleClass | `string` | `undefined` | 标题的类名 | 2.38.2 | +| titleStyle | `Object \| string` | `undefined` | 标题的样式 | 2.38.2 | | transformOrigin | `'mouse' \| 'center'` | `'mouse'` | 对话框动画出现的位置 | 2.34.0 | | type | `'error \| 'success' \| 'warning'` | `'warning'` | 对话框类型 | | | onAfterEnter | `() => void` | `undefined` | 出现动画完成执行的回调 | 2.33.0 | diff --git a/src/dialog/src/DialogEnvironment.tsx b/src/dialog/src/DialogEnvironment.tsx index 0ab17c413..6d049fb27 100644 --- a/src/dialog/src/DialogEnvironment.tsx +++ b/src/dialog/src/DialogEnvironment.tsx @@ -1,6 +1,14 @@ +import type { ModalDraggableOptions } from '../../modal/src/interface' // use absolute path to make sure no circular ref of style // this -> modal-index -> modal-style -import { type CSSProperties, defineComponent, h, type PropType, ref } from 'vue' +import { + type CSSProperties, + defineComponent, + h, + normalizeClass, + type PropType, + ref +} from 'vue' import { keep } from '../../_utils' import NModal from '../../modal/src/Modal' import { NDialog } from './Dialog' @@ -30,7 +38,8 @@ export const exposedDialogEnvProps = { (e: MouseEvent) => Promise | unknown >, onClose: Function as PropType<() => Promise | unknown>, - onMaskClick: Function as PropType<(e: MouseEvent) => void> + onMaskClick: Function as PropType<(e: MouseEvent) => void>, + draggable: [Boolean, Object] as PropType } as const export const NDialogEnvironment = defineComponent({ @@ -156,12 +165,15 @@ export const NDialogEnvironment = defineComponent({ blockScroll={this.blockScroll} autoFocus={this.autoFocus} transformOrigin={this.transformOrigin} + draggable={this.draggable} internalAppear + internalDialog > {{ - default: () => ( + default: ({ draggableClass }: { draggableClass: string }) => ( ('n-dialog-provider') + export const dialogApiInjectionKey = createInjectionKey('n-dialog-api') diff --git a/src/modal/demos/enUS/draggable.demo.vue b/src/modal/demos/enUS/draggable.demo.vue new file mode 100644 index 000000000..eeeca56ab --- /dev/null +++ b/src/modal/demos/enUS/draggable.demo.vue @@ -0,0 +1,129 @@ + +# Draggable + +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' }`. + +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. + + + + + diff --git a/src/modal/demos/enUS/index.demo-entry.md b/src/modal/demos/enUS/index.demo-entry.md index cfd5ae6a9..baaebe3b3 100644 --- a/src/modal/demos/enUS/index.demo-entry.md +++ b/src/modal/demos/enUS/index.demo-entry.md @@ -27,6 +27,7 @@ preset-card.vue preset-confirm.vue preset-confirm-slot.vue transform-origin.vue +draggable.vue ``` ## API @@ -58,6 +59,7 @@ Provided since `2.38.0`. | block-scroll | `boolean` | `true` | Whether to disabled body scrolling when it's active. | 2.28.3 | | close-on-esc | `boolean` | `true` | Whether to close modal on Esc is pressed. | 2.24.2 | | display-directive | `'if' \| 'show'` | `'if'` | Use which directive to control the rendering of modal body. | | +| draggable | `boolean \| { bounds?: 'window' }` | `false` | Whether the modal is draggable. Make its position not bound inside window using `bounds === 'none'`. | NEXT_VERSION | | mask-closable | `boolean` | `true` | Whether to emit `hide` event when click mask. | | | preset | `'dialog' \| 'card'` | `undefined` | The preset of `n-modal`. | | | show | `boolean` | `false` | Whether to show modal. | | @@ -97,6 +99,10 @@ See [Dialog props](dialog#Dialog-Props) See [Card slots](card#Card-Slots) +`default` slot's parameter is different, which is `(props: { draggableClass: string })`. + ### Modal with Preset Dialog Slots See [Dialog slots](dialog#Dialog-Slots) + +`default` slot's parameter is different, which is `(props: { draggableClass: string })`. diff --git a/src/modal/demos/zhCN/draggable.demo.vue b/src/modal/demos/zhCN/draggable.demo.vue index 0e0ee5bb0..7e3877604 100644 --- a/src/modal/demos/zhCN/draggable.demo.vue +++ b/src/modal/demos/zhCN/draggable.demo.vue @@ -1,7 +1,9 @@ # 可拖拽 -有人需要,它便有了 +设定 `draggable` 属性为 `true`,弹窗即可拖拽。如果你希望弹窗可以被拖出 window 的范围,可以设置 `draggable` 为 `{ bounds: 'none' }`。 + +如果你希望拖拽完全自定义 modal 的内容,你可以使用 `default` 插槽内的 `draggableClass`,设定在你希望触发拖拽的元素上。