From 108d36fd8a4c7b325fcf85882f054bb5e784de57 Mon Sep 17 00:00:00 2001 From: Eugen Istoc Date: Thu, 27 Feb 2025 11:32:48 -0500 Subject: [PATCH] feat(useOverlay)!: handle programmatic modals and slideovers (#3279) Co-authored-by: Benjamin Canac --- .../content/examples/modal/ModalExample.vue | 14 +- .../modal/ModalProgrammaticExample.vue | 41 +++-- .../examples/slideover/SlideoverExample.vue | 14 +- .../SlideoverProgrammaticExample.vue | 41 +++-- .../2.installation/1.nuxt.md | 2 +- .../1.getting-started/2.installation/2.vue.md | 2 +- docs/content/2.composables/use-modal.md | 114 ------------ docs/content/2.composables/use-overlay.md | 166 ++++++++++++++++++ docs/content/2.composables/use-slideover.md | 114 ------------ docs/content/3.components/modal.md | 9 +- docs/content/3.components/slideover.md | 9 +- playground/app/components/ModalExample.vue | 6 +- .../app/components/SlideoverExample.vue | 6 +- playground/app/pages/components/modal.vue | 12 +- playground/app/pages/components/slideover.vue | 12 +- src/module.ts | 2 - src/runtime/components/App.vue | 6 +- src/runtime/components/Modal.vue | 6 +- src/runtime/components/ModalProvider.vue | 12 -- src/runtime/components/OverlayProvider.vue | 26 +++ src/runtime/components/Slideover.vue | 6 +- src/runtime/components/SlideoverProvider.vue | 12 -- src/runtime/composables/useModal.ts | 71 -------- src/runtime/composables/useOverlay.ts | 118 +++++++++++++ src/runtime/composables/useSlideover.ts | 71 -------- src/runtime/plugins/modal.ts | 13 -- src/runtime/plugins/slideover.ts | 14 -- 27 files changed, 422 insertions(+), 497 deletions(-) delete mode 100644 docs/content/2.composables/use-modal.md create mode 100644 docs/content/2.composables/use-overlay.md delete mode 100644 docs/content/2.composables/use-slideover.md delete mode 100644 src/runtime/components/ModalProvider.vue create mode 100644 src/runtime/components/OverlayProvider.vue delete mode 100644 src/runtime/components/SlideoverProvider.vue delete mode 100644 src/runtime/composables/useModal.ts create mode 100644 src/runtime/composables/useOverlay.ts delete mode 100644 src/runtime/composables/useSlideover.ts delete mode 100644 src/runtime/plugins/modal.ts delete mode 100644 src/runtime/plugins/slideover.ts diff --git a/docs/app/components/content/examples/modal/ModalExample.vue b/docs/app/components/content/examples/modal/ModalExample.vue index a409d417..5c0c862e 100644 --- a/docs/app/components/content/examples/modal/ModalExample.vue +++ b/docs/app/components/content/examples/modal/ModalExample.vue @@ -1,23 +1,17 @@ diff --git a/src/runtime/components/Modal.vue b/src/runtime/components/Modal.vue index c6ca5a3f..ada37dd3 100644 --- a/src/runtime/components/Modal.vue +++ b/src/runtime/components/Modal.vue @@ -56,7 +56,9 @@ export interface ModalProps extends DialogRootProps { ui?: Partial } -export interface ModalEmits extends DialogRootEmits {} +export interface ModalEmits extends DialogRootEmits { + 'after:leave': [] +} export interface ModalSlots { default(props: { open: boolean }): any @@ -126,7 +128,7 @@ const ui = computed(() => modal({ - + diff --git a/src/runtime/components/ModalProvider.vue b/src/runtime/components/ModalProvider.vue deleted file mode 100644 index 69799d52..00000000 --- a/src/runtime/components/ModalProvider.vue +++ /dev/null @@ -1,12 +0,0 @@ - - - diff --git a/src/runtime/components/OverlayProvider.vue b/src/runtime/components/OverlayProvider.vue new file mode 100644 index 00000000..868bc2d4 --- /dev/null +++ b/src/runtime/components/OverlayProvider.vue @@ -0,0 +1,26 @@ + + + diff --git a/src/runtime/components/Slideover.vue b/src/runtime/components/Slideover.vue index b06526aa..f935e1e8 100644 --- a/src/runtime/components/Slideover.vue +++ b/src/runtime/components/Slideover.vue @@ -55,7 +55,9 @@ export interface SlideoverProps extends DialogRootProps { ui?: Partial } -export interface SlideoverEmits extends DialogRootEmits {} +export interface SlideoverEmits extends DialogRootEmits { + 'after:leave': [] +} export interface SlideoverSlots { default(props: { open: boolean }): any @@ -126,7 +128,7 @@ const ui = computed(() => slideover({ - + diff --git a/src/runtime/components/SlideoverProvider.vue b/src/runtime/components/SlideoverProvider.vue deleted file mode 100644 index 8b0eafd5..00000000 --- a/src/runtime/components/SlideoverProvider.vue +++ /dev/null @@ -1,12 +0,0 @@ - - - diff --git a/src/runtime/composables/useModal.ts b/src/runtime/composables/useModal.ts deleted file mode 100644 index d0fd8ac8..00000000 --- a/src/runtime/composables/useModal.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { ref, inject } from 'vue' -import type { ShallowRef, Component, InjectionKey } from 'vue' -import type { ComponentProps } from 'vue-component-type-helpers' -import { createSharedComposable } from '@vueuse/core' -import type { ModalProps } from '../types' - -export interface ModalState { - component: Component | string - props: ModalProps -} - -export const modalInjectionKey: InjectionKey> = Symbol('nuxt-ui.modal') - -function _useModal() { - const modalState = inject(modalInjectionKey) - - const isOpen = ref(false) - - function open(component: T, props?: ModalProps & ComponentProps) { - if (!modalState) { - throw new Error('useModal() is called without provider') - } - - modalState.value = { - component, - props: props ?? {} - } - - isOpen.value = true - } - - async function close() { - if (!modalState) return - - isOpen.value = false - } - - function reset() { - if (!modalState) return - - modalState.value = { - component: 'div', - props: {} - } - } - - /** - * Allows updating the modal props - */ - function patch>(props: Partial>) { - if (!modalState) return - - modalState.value = { - ...modalState.value, - props: { - ...modalState.value.props, - ...props - } - } - } - - return { - open, - close, - reset, - patch, - isOpen - } -} - -export const useModal = createSharedComposable(_useModal) diff --git a/src/runtime/composables/useOverlay.ts b/src/runtime/composables/useOverlay.ts new file mode 100644 index 00000000..784e586f --- /dev/null +++ b/src/runtime/composables/useOverlay.ts @@ -0,0 +1,118 @@ +import type { Component } from 'vue' +import { createSharedComposable } from '@vueuse/core' +import type { ComponentProps } from 'vue-component-type-helpers' + +export type OverlayOptions> = { + defaultOpen?: boolean + props?: OverlayAttrs + destroyOnClose?: boolean +} + +type ManagedOverlayOptionsPrivate = { + component?: T + id: symbol + isMounted: boolean + modelValue: boolean + resolvePromise?: (value: unknown) => void +} +export type Overlay = OverlayOptions & ManagedOverlayOptionsPrivate + +interface OverlayInstance { + open: (props?: ComponentProps) => Promise + close: (value?: any) => void + patch: (props: Partial>) => void +} + +function _useOverlay() { + const overlays = shallowReactive([]) + + const create = (component: T, _options?: OverlayOptions>): OverlayInstance => { + const { props: props, defaultOpen, destroyOnClose } = _options || {} + + const options = reactive({ + id: Symbol(import.meta.dev ? 'useOverlay' : ''), + modelValue: !!defaultOpen, + component: markRaw(component!), + isMounted: !!defaultOpen, + destroyOnClose: !!destroyOnClose, + props: props || {} + }) + + overlays.push(options) + + return { + open: (props?: ComponentProps) => open(options.id, props), + close: value => close(options.id, value), + patch: (props: Partial>) => patch(options.id, props) + } + } + + const open = (id: symbol, props?: ComponentProps): Promise => { + const overlay = getOverlay(id) + + // If props are provided, update the overlay's props + if (props) { + patch(overlay.id, props) + } + + overlay.modelValue = true + overlay.isMounted = true + + // Return a new promise that will be resolved when close is called + return new Promise((resolve) => { + overlay.resolvePromise = resolve + }) + } + + const close = (id: symbol, value?: any): void => { + const overlay = getOverlay(id) + + overlay.modelValue = false + + // Resolve the promise if it exists + if (overlay.resolvePromise) { + overlay.resolvePromise(value) + overlay.resolvePromise = undefined + } + } + + const unMount = (id: symbol): void => { + const overlay = getOverlay(id) + + overlay.isMounted = false + + if (overlay.destroyOnClose) { + const index = overlays.findIndex(overlay => overlay.id === id) + overlays.splice(index, 1) + } + } + + const patch = (id: symbol, props: Partial>): void => { + const overlay = getOverlay(id) + + Object.entries(props!).forEach(([key, value]) => { + (overlay.props as any)[key] = value + }) + } + + const getOverlay = (id: symbol): Overlay => { + const overlay = overlays.find(overlay => overlay.id === id) + + if (!overlay) { + throw new Error('Overlay not found') + } + + return overlay + } + + return { + overlays, + open, + close, + create, + patch, + unMount + } +} + +export const useOverlay = createSharedComposable(_useOverlay) diff --git a/src/runtime/composables/useSlideover.ts b/src/runtime/composables/useSlideover.ts deleted file mode 100644 index 97122d98..00000000 --- a/src/runtime/composables/useSlideover.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { ref, inject } from 'vue' -import type { ShallowRef, Component, InjectionKey } from 'vue' -import type { ComponentProps } from 'vue-component-type-helpers' -import { createSharedComposable } from '@vueuse/core' -import type { SlideoverProps } from '../types' - -export interface SlideoverState { - component: Component | string - props: SlideoverProps -} - -export const slideoverInjectionKey: InjectionKey> = Symbol('nuxt-ui.slideover') - -function _useSlideover() { - const slideoverState = inject(slideoverInjectionKey) - - const isOpen = ref(false) - - function open(component: T, props?: SlideoverProps & ComponentProps) { - if (!slideoverState) { - throw new Error('useSlideover() is called without provider') - } - - slideoverState.value = { - component, - props: props ?? {} - } - - isOpen.value = true - } - - async function close() { - if (!slideoverState) return - - isOpen.value = false - } - - function reset() { - if (!slideoverState) return - - slideoverState.value = { - component: 'div', - props: {} - } - } - - /** - * Allows updating the slideover props - */ - function patch>(props: Partial>) { - if (!slideoverState) return - - slideoverState.value = { - ...slideoverState.value, - props: { - ...slideoverState.value.props, - ...props - } - } - } - - return { - open, - close, - reset, - patch, - isOpen - } -} - -export const useSlideover = createSharedComposable(_useSlideover) diff --git a/src/runtime/plugins/modal.ts b/src/runtime/plugins/modal.ts deleted file mode 100644 index 1cb3fdd2..00000000 --- a/src/runtime/plugins/modal.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { shallowRef } from 'vue' -import { defineNuxtPlugin } from '#imports' -// FIXME: https://github.com/nuxt/module-builder/issues/141#issuecomment-2078248248 -import type {} from '#app' -import { modalInjectionKey, type ModalState } from '../composables/useModal' - -export default defineNuxtPlugin((nuxtApp) => { - const modalState = shallowRef({ - component: 'div', - props: {} - }) - nuxtApp.vueApp.provide(modalInjectionKey, modalState) -}) diff --git a/src/runtime/plugins/slideover.ts b/src/runtime/plugins/slideover.ts deleted file mode 100644 index 41078f2f..00000000 --- a/src/runtime/plugins/slideover.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { shallowRef } from 'vue' -import { defineNuxtPlugin } from '#imports' -// FIXME: https://github.com/nuxt/module-builder/issues/141#issuecomment-2078248248 -import type {} from '#app' -import { slideoverInjectionKey, type SlideoverState } from '../composables/useSlideover' - -export default defineNuxtPlugin((nuxtApp) => { - const slideoverState = shallowRef({ - component: 'div', - props: {} - }) - - nuxtApp.vueApp.provide(slideoverInjectionKey, slideoverState) -})