Skip to content

Commit 6b6dadf

Browse files
committed
feat(Modal): implement programmatic close method and update documentation
refactor: keep local state for open value, so it can be modified internally
1 parent f95abf8 commit 6b6dadf

File tree

4 files changed

+68
-17
lines changed

4 files changed

+68
-17
lines changed
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<template>
2+
<UModal
3+
title="Modal with close method"
4+
description="This is useful when you want to close the modal from within the content without creating a new ref."
5+
>
6+
<UButton label="Open" color="neutral" variant="subtle" />
7+
8+
<template #body="{ close }">
9+
<div class="space-y-4">
10+
<p>You can close this modal using the close method exposed in the slots.</p>
11+
<UButton label="Close Modal" color="primary" @click="close" />
12+
</div>
13+
</template>
14+
15+
<template #footer="{ close }">
16+
<UButton label="Cancel" color="neutral" variant="outline" @click="close" />
17+
<UButton label="Submit" color="neutral" />
18+
</template>
19+
</UModal>
20+
</template>

docs/content/3.components/modal.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,22 @@ name: 'modal-nested-example'
347347
---
348348
::
349349

350+
### Using the close method
351+
352+
All slots in the Modal component receive a `close` method that can be used to programmatically close the modal from within the content.
353+
354+
::component-example
355+
---
356+
name: 'modal-programatic-close'
357+
props:
358+
class: 'px-4'
359+
---
360+
::
361+
362+
::tip
363+
The `close` method is available in all slots: `default`, `content`, `header`, `title`, `description`, `body`, and `footer`.
364+
::
365+
350366
### With footer slot
351367

352368
Use the `#footer` slot to add content after the Modal's body.

playground/app/pages/components/modal.vue

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,5 +69,13 @@ function openModal() {
6969
</UModal>
7070

7171
<UButton label="Open programmatically" color="neutral" variant="outline" @click="openModal" />
72+
73+
<UModal title="First modal">
74+
<UButton color="neutral" variant="outline" label="Close with scoped close" />
75+
76+
<template #footer="{ close }">
77+
<UButton label="Close with scoped close" @click="close" />
78+
</template>
79+
</UModal>
7280
</div>
7381
</template>

src/runtime/components/Modal.vue

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -57,24 +57,25 @@ export interface ModalEmits extends DialogRootEmits {
5757
'after:leave': []
5858
'after:enter': []
5959
'close:prevent': []
60+
'close': []
6061
}
6162
6263
export interface ModalSlots {
6364
default(props: { open: boolean }): any
64-
content(props?: {}): any
65-
header(props?: {}): any
66-
title(props?: {}): any
67-
description(props?: {}): any
65+
content(props: { close: () => void }): any
66+
header(props: { close: () => void }): any
67+
title(props: { close: () => void }): any
68+
description(props: { close: () => void }): any
6869
close(props: { ui: { [K in keyof Required<Modal['slots']>]: (props?: Record<string, any>) => string } }): any
69-
body(props?: {}): any
70-
footer(props?: {}): any
70+
body(props: { close: () => void }): any
71+
footer(props: { close: () => void }): any
7172
}
7273
</script>
7374

7475
<script setup lang="ts">
7576
import { computed, toRef } from 'vue'
7677
import { DialogRoot, DialogTrigger, DialogPortal, DialogOverlay, DialogContent, DialogTitle, DialogDescription, DialogClose, VisuallyHidden, useForwardPropsEmits } from 'reka-ui'
77-
import { reactivePick } from '@vueuse/core'
78+
import { reactivePick, useVModel } from '@vueuse/core'
7879
import { useAppConfig } from '#imports'
7980
import { useLocale } from '../composables/useLocale'
8081
import { usePortal } from '../composables/usePortal'
@@ -95,7 +96,8 @@ const slots = defineSlots<ModalSlots>()
9596
const { t } = useLocale()
9697
const appConfig = useAppConfig() as Modal['AppConfig']
9798
98-
const rootProps = useForwardPropsEmits(reactivePick(props, 'open', 'defaultOpen', 'modal'), emits)
99+
const open = useVModel(props, 'open', emits, { passive: true })
100+
const rootProps = useForwardPropsEmits(reactivePick(props, 'defaultOpen', 'modal'), emits)
99101
const portalProps = usePortal(toRef(() => props.portal))
100102
const contentProps = toRef(() => props.content)
101103
const contentEvents = computed(() => {
@@ -122,10 +124,15 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.modal || {})
122124
transition: props.transition,
123125
fullscreen: props.fullscreen
124126
}))
127+
128+
function closeModal() {
129+
open.value = false
130+
emits('close')
131+
}
125132
</script>
126133

127134
<template>
128-
<DialogRoot v-slot="{ open }" v-bind="rootProps">
135+
<DialogRoot v-model:open="open" v-bind="rootProps">
129136
<DialogTrigger v-if="!!slots.default" as-child :class="props.class">
130137
<slot :open="open" />
131138
</DialogTrigger>
@@ -136,30 +143,30 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.modal || {})
136143
<DialogContent :class="ui.content({ class: [!slots.default && props.class, props.ui?.content] })" v-bind="contentProps" @after-enter="emits('after:enter')" @after-leave="emits('after:leave')" v-on="contentEvents">
137144
<VisuallyHidden v-if="!!slots.content && ((title || !!slots.title) || (description || !!slots.description))">
138145
<DialogTitle v-if="title || !!slots.title">
139-
<slot name="title">
146+
<slot name="title" :close="closeModal">
140147
{{ title }}
141148
</slot>
142149
</DialogTitle>
143150

144151
<DialogDescription v-if="description || !!slots.description">
145-
<slot name="description">
152+
<slot name="description" :close="closeModal">
146153
{{ description }}
147154
</slot>
148155
</DialogDescription>
149156
</VisuallyHidden>
150157

151-
<slot name="content">
158+
<slot name="content" :close="closeModal">
152159
<div v-if="!!slots.header || (title || !!slots.title) || (description || !!slots.description) || (close || !!slots.close)" :class="ui.header({ class: props.ui?.header })">
153-
<slot name="header">
160+
<slot name="header" :close="closeModal">
154161
<div :class="ui.wrapper({ class: props.ui?.wrapper })">
155162
<DialogTitle v-if="title || !!slots.title" :class="ui.title({ class: props.ui?.title })">
156-
<slot name="title">
163+
<slot name="title" :close="closeModal">
157164
{{ title }}
158165
</slot>
159166
</DialogTitle>
160167

161168
<DialogDescription v-if="description || !!slots.description" :class="ui.description({ class: props.ui?.description })">
162-
<slot name="description">
169+
<slot name="description" :close="closeModal">
163170
{{ description }}
164171
</slot>
165172
</DialogDescription>
@@ -183,11 +190,11 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.modal || {})
183190
</div>
184191

185192
<div v-if="!!slots.body" :class="ui.body({ class: props.ui?.body })">
186-
<slot name="body" />
193+
<slot name="body" :close="closeModal" />
187194
</div>
188195

189196
<div v-if="!!slots.footer" :class="ui.footer({ class: props.ui?.footer })">
190-
<slot name="footer" />
197+
<slot name="footer" :close="closeModal" />
191198
</div>
192199
</slot>
193200
</DialogContent>

0 commit comments

Comments
 (0)