feat(Modal/Slideover): add close method in slots (#4219)

Co-authored-by: Benjamin Canac <canacb1@gmail.com>
This commit is contained in:
Joseph Anson
2025-05-28 14:57:12 +01:00
committed by GitHub
parent ca507c6a0d
commit 5835eb5f0f
8 changed files with 64 additions and 38 deletions

View File

@@ -10,8 +10,8 @@ const open = ref(false)
<Placeholder class="h-48" /> <Placeholder class="h-48" />
</template> </template>
<template #footer> <template #footer="{ close }">
<UButton label="Cancel" color="neutral" variant="outline" @click="open = false" /> <UButton label="Cancel" color="neutral" variant="outline" @click="close" />
<UButton label="Submit" color="neutral" /> <UButton label="Submit" color="neutral" />
</template> </template>
</UModal> </UModal>

View File

@@ -10,8 +10,8 @@ const open = ref(false)
<Placeholder class="h-full" /> <Placeholder class="h-full" />
</template> </template>
<template #footer> <template #footer="{ close }">
<UButton label="Cancel" color="neutral" variant="outline" @click="open = false" /> <UButton label="Cancel" color="neutral" variant="outline" @click="close" />
<UButton label="Submit" color="neutral" /> <UButton label="Submit" color="neutral" />
</template> </template>
</USlideover> </USlideover>

View File

@@ -305,13 +305,13 @@ slots:
### Programmatic usage ### Programmatic usage
You can use the [`useOverlay`](/composables/use-overlay) composable to open a Modal programatically. You can use the [`useOverlay`](/composables/use-overlay) composable to open a Modal programmatically.
::warning ::warning
Make sure to wrap your app with the [`App`](/components/app) component which uses the [`OverlayProvider`](https://github.com/nuxt/ui/blob/v3/src/runtime/components/OverlayProvider.vue) component. Make sure to wrap your app with the [`App`](/components/app) component which uses the [`OverlayProvider`](https://github.com/nuxt/ui/blob/v3/src/runtime/components/OverlayProvider.vue) component.
:: ::
First, create a modal component that will be opened programatically: First, create a modal component that will be opened programmatically:
::component-example ::component-example
--- ---

View File

@@ -304,13 +304,13 @@ slots:
### Programmatic usage ### Programmatic usage
You can use the [`useOverlay`](/composables/use-overlay) composable to open a Slideover programatically. You can use the [`useOverlay`](/composables/use-overlay) composable to open a Slideover programmatically.
::warning ::warning
Make sure to wrap your app with the [`App`](/components/app) component which uses the [`OverlayProvider`](https://github.com/nuxt/ui/blob/v3/src/runtime/components/OverlayProvider.vue) component. Make sure to wrap your app with the [`App`](/components/app) component which uses the [`OverlayProvider`](https://github.com/nuxt/ui/blob/v3/src/runtime/components/OverlayProvider.vue) component.
:: ::
First, create a slideover component that will be opened programatically: First, create a slideover component that will be opened programmatically:
::component-example ::component-example
--- ---

View File

@@ -69,5 +69,13 @@ function openModal() {
</UModal> </UModal>
<UButton label="Open programmatically" color="neutral" variant="outline" @click="openModal" /> <UButton label="Open programmatically" color="neutral" variant="outline" @click="openModal" />
<UModal title="First modal">
<UButton color="neutral" variant="outline" label="Close with scoped slot close" />
<template #footer="{ close }">
<UButton label="Close with scoped slot close" @click="close" />
</template>
</UModal>
</div> </div>
</template> </template>

View File

@@ -125,5 +125,21 @@ function openSlideover() {
</USlideover> </USlideover>
<UButton label="Open programmatically" color="neutral" variant="outline" @click="openSlideover" /> <UButton label="Open programmatically" color="neutral" variant="outline" @click="openSlideover" />
<USlideover title="Slideover with scoped slot close" description="This slideover has a scoped slot close that can be used to close the slideover from within the content.">
<UButton color="neutral" variant="subtle" label="Open with scoped slot close" />
<template #header="{ close }">
<UButton label="Close with scoped slot close" @click="close" />
</template>
<template #body="{ close }">
<UButton label="Close with scoped slot close" @click="close" />
</template>
<template #footer="{ close }">
<UButton label="Close with scoped slot close" @click="close" />
</template>
</USlideover>
</div> </div>
</template> </template>

View File

@@ -61,13 +61,13 @@ export interface ModalEmits extends DialogRootEmits {
export interface ModalSlots { export interface ModalSlots {
default(props: { open: boolean }): any default(props: { open: boolean }): any
content(props?: {}): any content(props: { close: () => void }): any
header(props?: {}): any header(props: { close: () => void }): any
title(props?: {}): any title(props?: {}): any
description(props?: {}): any description(props?: {}): any
close(props: { ui: { [K in keyof Required<Modal['slots']>]: (props?: Record<string, any>) => string } }): any close(props: { close: () => void, ui: { [K in keyof Required<Modal['slots']>]: (props?: Record<string, any>) => string } }): any
body(props?: {}): any body(props: { close: () => void }): any
footer(props?: {}): any footer(props: { close: () => void }): any
} }
</script> </script>
@@ -124,8 +124,9 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.modal || {})
})) }))
</script> </script>
<!-- eslint-disable vue/no-template-shadow -->
<template> <template>
<DialogRoot v-slot="{ open }" v-bind="rootProps"> <DialogRoot v-slot="{ open, close }" v-bind="rootProps">
<DialogTrigger v-if="!!slots.default" as-child :class="props.class"> <DialogTrigger v-if="!!slots.default" as-child :class="props.class">
<slot :open="open" /> <slot :open="open" />
</DialogTrigger> </DialogTrigger>
@@ -148,9 +149,9 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.modal || {})
</DialogDescription> </DialogDescription>
</VisuallyHidden> </VisuallyHidden>
<slot name="content"> <slot name="content" :close="close">
<div v-if="!!slots.header || (title || !!slots.title) || (description || !!slots.description) || (close || !!slots.close)" :class="ui.header({ class: props.ui?.header })"> <div v-if="!!slots.header || (title || !!slots.title) || (description || !!slots.description) || (props.close || !!slots.close)" :class="ui.header({ class: props.ui?.header })">
<slot name="header"> <slot name="header" :close="close">
<div :class="ui.wrapper({ class: props.ui?.wrapper })"> <div :class="ui.wrapper({ class: props.ui?.wrapper })">
<DialogTitle v-if="title || !!slots.title" :class="ui.title({ class: props.ui?.title })"> <DialogTitle v-if="title || !!slots.title" :class="ui.title({ class: props.ui?.title })">
<slot name="title"> <slot name="title">
@@ -165,16 +166,16 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.modal || {})
</DialogDescription> </DialogDescription>
</div> </div>
<DialogClose v-if="close || !!slots.close" as-child> <DialogClose v-if="props.close || !!slots.close" as-child>
<slot name="close" :ui="ui"> <slot name="close" :close="close" :ui="ui">
<UButton <UButton
v-if="close" v-if="props.close"
:icon="closeIcon || appConfig.ui.icons.close" :icon="closeIcon || appConfig.ui.icons.close"
size="md" size="md"
color="neutral" color="neutral"
variant="ghost" variant="ghost"
:aria-label="t('modal.close')" :aria-label="t('modal.close')"
v-bind="(typeof close === 'object' ? close as Partial<ButtonProps> : {})" v-bind="(typeof props.close === 'object' ? props.close as Partial<ButtonProps> : {})"
:class="ui.close({ class: props.ui?.close })" :class="ui.close({ class: props.ui?.close })"
/> />
</slot> </slot>
@@ -183,11 +184,11 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.modal || {})
</div> </div>
<div v-if="!!slots.body" :class="ui.body({ class: props.ui?.body })"> <div v-if="!!slots.body" :class="ui.body({ class: props.ui?.body })">
<slot name="body" /> <slot name="body" :close="close" />
</div> </div>
<div v-if="!!slots.footer" :class="ui.footer({ class: props.ui?.footer })"> <div v-if="!!slots.footer" :class="ui.footer({ class: props.ui?.footer })">
<slot name="footer" /> <slot name="footer" :close="close" />
</div> </div>
</slot> </slot>
</DialogContent> </DialogContent>

View File

@@ -61,13 +61,13 @@ export interface SlideoverEmits extends DialogRootEmits {
export interface SlideoverSlots { export interface SlideoverSlots {
default(props: { open: boolean }): any default(props: { open: boolean }): any
content(props?: {}): any content(props: { close: () => void }): any
header(props?: {}): any header(props: { close: () => void }): any
title(props?: {}): any title(props?: {}): any
description(props?: {}): any description(props?: {}): any
close(props: { ui: { [K in keyof Required<Slideover['slots']>]: (props?: Record<string, any>) => string } }): any close(props: { close: () => void, ui: { [K in keyof Required<Slideover['slots']>]: (props?: Record<string, any>) => string } }): any
body(props?: {}): any body(props: { close: () => void }): any
footer(props?: {}): any footer(props: { close: () => void }): any
} }
</script> </script>
@@ -124,8 +124,9 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.slideover ||
})) }))
</script> </script>
<!-- eslint-disable vue/no-template-shadow -->
<template> <template>
<DialogRoot v-slot="{ open }" v-bind="rootProps"> <DialogRoot v-slot="{ open, close }" v-bind="rootProps">
<DialogTrigger v-if="!!slots.default" as-child :class="props.class"> <DialogTrigger v-if="!!slots.default" as-child :class="props.class">
<slot :open="open" /> <slot :open="open" />
</DialogTrigger> </DialogTrigger>
@@ -155,9 +156,9 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.slideover ||
</DialogDescription> </DialogDescription>
</VisuallyHidden> </VisuallyHidden>
<slot name="content"> <slot name="content" :close="close">
<div v-if="!!slots.header || (title || !!slots.title) || (description || !!slots.description) || (close || !!slots.close)" :class="ui.header({ class: props.ui?.header })"> <div v-if="!!slots.header || (title || !!slots.title) || (description || !!slots.description) || (props.close || !!slots.close)" :class="ui.header({ class: props.ui?.header })">
<slot name="header"> <slot name="header" :close="close">
<div :class="ui.wrapper({ class: props.ui?.wrapper })"> <div :class="ui.wrapper({ class: props.ui?.wrapper })">
<DialogTitle v-if="title || !!slots.title" :class="ui.title({ class: props.ui?.title })"> <DialogTitle v-if="title || !!slots.title" :class="ui.title({ class: props.ui?.title })">
<slot name="title"> <slot name="title">
@@ -172,16 +173,16 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.slideover ||
</DialogDescription> </DialogDescription>
</div> </div>
<DialogClose v-if="close || !!slots.close" as-child> <DialogClose v-if="props.close || !!slots.close" as-child>
<slot name="close" :ui="ui"> <slot name="close" :close="close" :ui="ui">
<UButton <UButton
v-if="close" v-if="props.close"
:icon="closeIcon || appConfig.ui.icons.close" :icon="closeIcon || appConfig.ui.icons.close"
size="md" size="md"
color="neutral" color="neutral"
variant="ghost" variant="ghost"
:aria-label="t('slideover.close')" :aria-label="t('slideover.close')"
v-bind="(typeof close === 'object' ? close as Partial<ButtonProps> : {})" v-bind="(typeof props.close === 'object' ? props.close as Partial<ButtonProps> : {})"
:class="ui.close({ class: props.ui?.close })" :class="ui.close({ class: props.ui?.close })"
/> />
</slot> </slot>
@@ -190,11 +191,11 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.slideover ||
</div> </div>
<div :class="ui.body({ class: props.ui?.body })"> <div :class="ui.body({ class: props.ui?.body })">
<slot name="body" /> <slot name="body" :close="close" />
</div> </div>
<div v-if="!!slots.footer" :class="ui.footer({ class: props.ui?.footer })"> <div v-if="!!slots.footer" :class="ui.footer({ class: props.ui?.footer })">
<slot name="footer" /> <slot name="footer" :close="close" />
</div> </div>
</slot> </slot>
</DialogContent> </DialogContent>