Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: drawer and modal support destroy on close #5268

Merged
merged 4 commits into from
Dec 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 11 additions & 6 deletions docs/src/components/common-ui/vben-drawer.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ Drawer 内的内容一般业务中,会比较复杂,所以我们可以将 dra

- `VbenDrawer` 组件对与参数的处理优先级是 `slot` > `props` > `state`(通过api更新的状态以及useVbenDrawer参数)。如果你已经传入了 `slot` 或者 `props`,那么 `setState` 将不会生效,这种情况下你可以通过 `slot` 或者 `props` 来更新状态。
- 如果你使用到了 `connectedComponent` 参数,那么会存在 2 个`useVbenDrawer`, 此时,如果同时设置了相同的参数,那么以内部为准(也就是没有设置 connectedComponent 的代码)。比如 同时设置了 `onConfirm`,那么以内部的 `onConfirm` 为准。`onOpenChange`事件除外,内外都会触发。
- 使用了`connectedComponent`参数时,可以配置`destroyOnClose`属性来决定当关闭弹窗时,是否要销毁`connectedComponent`组件(重新创建`connectedComponent`组件,这将会把其内部所有的变量、状态、数据等恢复到初始状态。)。

:::

Expand All @@ -75,6 +76,8 @@ const [Drawer, drawerApi] = useVbenDrawer({
| 属性名 | 描述 | 类型 | 默认值 |
| --- | --- | --- | --- |
| appendToMain | 是否挂载到内容区域(默认挂载到body) | `boolean` | `false` |
| connectedComponent | 连接另一个Modal组件 | `Component` | - |
| destroyOnClose | 关闭时销毁`connectedComponent` | `boolean` | `false` |
| title | 标题 | `string\|slot` | - |
| titleTooltip | 标题提示信息 | `string\|slot` | - |
| description | 描述信息 | `string\|slot` | - |
Expand Down Expand Up @@ -108,12 +111,14 @@ const [Drawer, drawerApi] = useVbenDrawer({

以下事件,只有在 `useVbenDrawer({onCancel:()=>{}})` 中传入才会生效。

| 事件名 | 描述 | 类型 |
| --- | --- | --- |
| onBeforeClose | 关闭前触发,返回 `false`则禁止关闭 | `()=>boolean` |
| onCancel | 点击取消按钮触发 | `()=>void` |
| onConfirm | 点击确认按钮触发 | `()=>void` |
| onOpenChange | 关闭或者打开弹窗时触发 | `(isOpen:boolean)=>void` |
| 事件名 | 描述 | 类型 | 版本限制 |
| --- | --- | --- | --- |
| onBeforeClose | 关闭前触发,返回 `false`则禁止关闭 | `()=>boolean` | --- |
| onCancel | 点击取消按钮触发 | `()=>void` | --- |
| onClosed | 关闭动画播放完毕时触发 | `()=>void` | >5.5.2 |
| onConfirm | 点击确认按钮触发 | `()=>void` | --- |
| onOpenChange | 关闭或者打开弹窗时触发 | `(isOpen:boolean)=>void` | --- |
| onOpened | 打开动画播放完毕时触发 | `()=>void` | >5.5.2 |

### Slots

Expand Down
3 changes: 3 additions & 0 deletions docs/src/components/common-ui/vben-modal.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ Modal 内的内容一般业务中,会比较复杂,所以我们可以将 moda

- `VbenModal` 组件对与参数的处理优先级是 `slot` > `props` > `state`(通过api更新的状态以及useVbenModal参数)。如果你已经传入了 `slot` 或者 `props`,那么 `setState` 将不会生效,这种情况下你可以通过 `slot` 或者 `props` 来更新状态。
- 如果你使用到了 `connectedComponent` 参数,那么会存在 2 个`useVbenModal`, 此时,如果同时设置了相同的参数,那么以内部为准(也就是没有设置 connectedComponent 的代码)。比如 同时设置了 `onConfirm`,那么以内部的 `onConfirm` 为准。`onOpenChange`事件除外,内外都会触发。
- 使用了`connectedComponent`参数时,可以配置`destroyOnClose`属性来决定当关闭弹窗时,是否要销毁`connectedComponent`组件(重新创建`connectedComponent`组件,这将会把其内部所有的变量、状态、数据等恢复到初始状态。)。

:::

Expand All @@ -81,6 +82,8 @@ const [Modal, modalApi] = useVbenModal({
| 属性名 | 描述 | 类型 | 默认值 |
| --- | --- | --- | --- |
| appendToMain | 是否挂载到内容区域(默认挂载到body) | `boolean` | `false` |
| connectedComponent | 连接另一个Modal组件 | `Component` | - |
| destroyOnClose | 关闭时销毁`connectedComponent` | `boolean` | `false` |
| title | 标题 | `string\|slot` | - |
| titleTooltip | 标题提示信息 | `string\|slot` | - |
| description | 描述信息 | `string\|slot` | - |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,4 +110,19 @@ describe('drawerApi', () => {
expect(drawerApi.store.state.title).toBe('Batch Title');
expect(drawerApi.store.state.confirmText).toBe('Batch Confirm');
});

it('should call onClosed callback when provided', () => {
const onClosed = vi.fn();
const drawerApiWithHook = new DrawerApi({ onClosed });
drawerApiWithHook.onClosed();
expect(onClosed).toHaveBeenCalled();
});

it('should call onOpened callback when provided', () => {
const onOpened = vi.fn();
const drawerApiWithHook = new DrawerApi({ onOpened });
drawerApiWithHook.open();
drawerApiWithHook.onOpened();
expect(onOpened).toHaveBeenCalled();
});
});
29 changes: 28 additions & 1 deletion packages/@core/ui-kit/popup-ui/src/drawer/drawer-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@ import { bindMethods, isFunction } from '@vben-core/shared/utils';
export class DrawerApi {
private api: Pick<
DrawerApiOptions,
'onBeforeClose' | 'onCancel' | 'onConfirm' | 'onOpenChange'
| 'onBeforeClose'
| 'onCancel'
| 'onClosed'
| 'onConfirm'
| 'onOpenChange'
| 'onOpened'
>;
// private prevState!: DrawerState;
private state!: DrawerState;
Expand All @@ -23,8 +28,10 @@ export class DrawerApi {
connectedComponent: _,
onBeforeClose,
onCancel,
onClosed,
onConfirm,
onOpenChange,
onOpened,
...storeState
} = options;

Expand Down Expand Up @@ -68,8 +75,10 @@ export class DrawerApi {
this.api = {
onBeforeClose,
onCancel,
onClosed,
onConfirm,
onOpenChange,
onOpened,
};
bindMethods(this);
}
Expand Down Expand Up @@ -106,13 +115,31 @@ export class DrawerApi {
}
}

/**
* 弹窗关闭动画播放完毕后的回调
*/
onClosed() {
if (!this.state.isOpen) {
this.api.onClosed?.();
}
}

/**
* 确认操作
*/
onConfirm() {
this.api.onConfirm?.();
}

/**
* 弹窗打开动画播放完毕后的回调
*/
onOpened() {
if (this.state.isOpen) {
this.api.onOpened?.();
}
}

open() {
this.store.setState((prev) => ({ ...prev, isOpen: true }));
}
Expand Down
16 changes: 15 additions & 1 deletion packages/@core/ui-kit/popup-ui/src/drawer/drawer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,9 +126,13 @@ export type ExtendedDrawerApi = {

export interface DrawerApiOptions extends DrawerState {
/**
* 独立的弹窗组件
* 独立的抽屉组件
*/
connectedComponent?: Component;
/**
* 在关闭时销毁抽屉。仅在使用 connectedComponent 时有效
*/
destroyOnClose?: boolean;
/**
* 关闭前的回调,返回 false 可以阻止关闭
* @returns
Expand All @@ -138,6 +142,11 @@ export interface DrawerApiOptions extends DrawerState {
* 点击取消按钮的回调
*/
onCancel?: () => void;
/**
* 弹窗关闭动画结束的回调
* @returns
*/
onClosed?: () => void;
/**
* 点击确定按钮的回调
*/
Expand All @@ -148,4 +157,9 @@ export interface DrawerApiOptions extends DrawerState {
* @returns
*/
onOpenChange?: (isOpen: boolean) => void;
/**
* 弹窗打开动画结束的回调
* @returns
*/
onOpened?: () => void;
}
2 changes: 2 additions & 0 deletions packages/@core/ui-kit/popup-ui/src/drawer/drawer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -139,10 +139,12 @@ const getAppendTo = computed(() => {
:side="placement"
:z-index="zIndex"
@close-auto-focus="handleFocusOutside"
@closed="() => drawerApi?.onClosed()"
@escape-key-down="escapeKeyDown"
@focus-outside="handleFocusOutside"
@interact-outside="interactOutside"
@open-auto-focus="handerOpenAutoFocus"
@opened="() => drawerApi?.onOpened()"
@pointer-down-outside="pointerDownOutside"
>
<SheetHeader
Expand Down
31 changes: 29 additions & 2 deletions packages/@core/ui-kit/popup-ui/src/drawer/use-drawer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,15 @@ import type {
ExtendedDrawerApi,
} from './drawer';

import { defineComponent, h, inject, nextTick, provide, reactive } from 'vue';
import {
defineComponent,
h,
inject,
nextTick,
provide,
reactive,
ref,
} from 'vue';

import { useStore } from '@vben-core/shared/store';

Expand All @@ -22,6 +30,7 @@ export function useVbenDrawer<
const { connectedComponent } = options;
if (connectedComponent) {
const extendedApi = reactive({});
const isDrawerReady = ref(true);
const Drawer = defineComponent(
(props: TParentDrawerProps, { attrs, slots }) => {
provide(USER_DRAWER_INJECT_KEY, {
Expand All @@ -31,13 +40,23 @@ export function useVbenDrawer<
Object.setPrototypeOf(extendedApi, api);
},
options,
async reCreateDrawer() {
isDrawerReady.value = false;
await nextTick();
isDrawerReady.value = true;
},
});
checkProps(extendedApi as ExtendedDrawerApi, {
...props,
...attrs,
...slots,
});
return () => h(connectedComponent, { ...props, ...attrs }, slots);
return () =>
h(
isDrawerReady.value ? connectedComponent : 'div',
{ ...props, ...attrs },
slots,
);
},
{
inheritAttrs: false,
Expand All @@ -58,6 +77,14 @@ export function useVbenDrawer<
options.onOpenChange?.(isOpen);
injectData.options?.onOpenChange?.(isOpen);
};

const onClosed = mergedOptions.onClosed;
mergedOptions.onClosed = () => {
onClosed?.();
if (mergedOptions.destroyOnClose) {
injectData.reCreateDrawer?.();
}
};
const api = new DrawerApi(mergedOptions);

const extendedApi: ExtendedDrawerApi = api as never;
Expand Down
4 changes: 4 additions & 0 deletions packages/@core/ui-kit/popup-ui/src/modal/modal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,10 @@ export interface ModalApiOptions extends ModalState {
* 独立的弹窗组件
*/
connectedComponent?: Component;
/**
* 在关闭时销毁弹窗。仅在使用 connectedComponent 时有效
*/
destroyOnClose?: boolean;
/**
* 关闭前的回调,返回 false 可以阻止关闭
* @returns
Expand Down
27 changes: 25 additions & 2 deletions packages/@core/ui-kit/popup-ui/src/modal/use-modal.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
import type { ExtendedModalApi, ModalApiOptions, ModalProps } from './modal';

import { defineComponent, h, inject, nextTick, provide, reactive } from 'vue';
import {
defineComponent,
h,
inject,
nextTick,
provide,
reactive,
ref,
} from 'vue';

import { useStore } from '@vben-core/shared/store';

Expand All @@ -18,6 +26,7 @@ export function useVbenModal<TParentModalProps extends ModalProps = ModalProps>(
const { connectedComponent } = options;
if (connectedComponent) {
const extendedApi = reactive({});
const isModalReady = ref(true);
const Modal = defineComponent(
(props: TParentModalProps, { attrs, slots }) => {
provide(USER_MODAL_INJECT_KEY, {
Expand All @@ -27,6 +36,11 @@ export function useVbenModal<TParentModalProps extends ModalProps = ModalProps>(
Object.setPrototypeOf(extendedApi, api);
},
options,
async reCreateModal() {
isModalReady.value = false;
await nextTick();
isModalReady.value = true;
},
});
checkProps(extendedApi as ExtendedModalApi, {
...props,
Expand All @@ -35,7 +49,7 @@ export function useVbenModal<TParentModalProps extends ModalProps = ModalProps>(
});
return () =>
h(
connectedComponent,
isModalReady.value ? connectedComponent : 'div',
{
...props,
...attrs,
Expand All @@ -62,6 +76,15 @@ export function useVbenModal<TParentModalProps extends ModalProps = ModalProps>(
options.onOpenChange?.(isOpen);
injectData.options?.onOpenChange?.(isOpen);
};

const onClosed = mergedOptions.onClosed;

mergedOptions.onClosed = () => {
onClosed?.();
if (mergedOptions.destroyOnClose) {
injectData.reCreateModal?.();
}
};
const api = new ModalApi(mergedOptions);

const extendedApi: ExtendedModalApi = api as never;
Expand Down
19 changes: 17 additions & 2 deletions packages/@core/ui-kit/shadcn-ui/src/ui/sheet/SheetContent.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed } from 'vue';
import { computed, ref } from 'vue';

import { cn } from '@vben-core/shared/utils';

Expand Down Expand Up @@ -32,7 +32,9 @@ const props = withDefaults(defineProps<SheetContentProps>(), {
zIndex: 1000,
});

const emits = defineEmits<DialogContentEmits>();
const emits = defineEmits<
{ close: []; closed: []; opened: [] } & DialogContentEmits
>();

const delegatedProps = computed(() => {
const {
Expand All @@ -59,6 +61,17 @@ const position = computed(() => {
});

const forwarded = useForwardPropsEmits(delegatedProps, emits);
const contentRef = ref<InstanceType<typeof DialogContent> | null>(null);
function onAnimationEnd(event: AnimationEvent) {
// 只有在 contentRef 的动画结束时才触发 opened/closed 事件
if (event.target === contentRef.value?.$el) {
if (props.open) {
emits('opened');
} else {
emits('closed');
}
}
}
</script>

<template>
Expand All @@ -67,8 +80,10 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
<SheetOverlay v-if="open && modal" :style="{ zIndex, position }" />
</Transition>
<DialogContent
ref="contentRef"
:class="cn(sheetVariants({ side }), props.class)"
:style="{ zIndex, position }"
@animationend="onAnimationEnd"
v-bind="{ ...forwarded, ...$attrs }"
>
<slot></slot>
Expand Down
Loading