@@ -5,7 +5,6 @@ import type { AppConfig } from '@nuxt/schema'
5
5
import theme from ' #build/ui/modal'
6
6
import type { ButtonProps } from ' ../types'
7
7
import type { EmitsToProps , ComponentConfig } from ' ../types/utils'
8
- import ModalContext from ' ./ModalContext.vue'
9
8
10
9
type Modal = ComponentConfig <typeof theme , AppConfig , ' modal' >
11
10
@@ -59,6 +58,7 @@ export interface ModalEmits extends DialogRootEmits {
59
58
' after:leave' : []
60
59
' after:enter' : []
61
60
' close:prevent' : []
61
+ ' close' : []
62
62
}
63
63
64
64
export interface ModalSlots {
@@ -74,8 +74,8 @@ export interface ModalSlots {
74
74
</script >
75
75
76
76
<script setup lang="ts">
77
- import { computed , toRef } from ' vue'
78
- import { reactivePick } from ' @vueuse/core'
77
+ import { computed , toRef , watch } from ' vue'
78
+ import { reactivePick , useVModel } from ' @vueuse/core'
79
79
import { useAppConfig } from ' #imports'
80
80
import { useLocale } from ' ../composables/useLocale'
81
81
import { usePortal } from ' ../composables/usePortal'
@@ -96,7 +96,9 @@ const slots = defineSlots<ModalSlots>()
96
96
const { t } = useLocale ()
97
97
const appConfig = useAppConfig () as Modal [' AppConfig' ]
98
98
99
- 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 )
101
+
100
102
const portalProps = usePortal (toRef (() => props .portal ))
101
103
const contentProps = toRef (() => props .content )
102
104
const contentEvents = computed (() => {
@@ -123,77 +125,80 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.modal || {})
123
125
transition: props .transition ,
124
126
fullscreen: props .fullscreen
125
127
}))
128
+
129
+ function closeModal() {
130
+ open .value = false
131
+ emits (' close' )
132
+ }
126
133
</script >
127
134
128
135
<template >
129
- <DialogRoot v-slot =" { open }" v-bind =" rootProps" >
130
- <ModalContext v-slot =" { onOpenChange }" >
131
- <DialogTrigger v-if =" !!slots.default" as-child :class =" props.class" >
132
- <slot :open =" open" />
133
- </DialogTrigger >
134
-
135
- <DialogPortal v-bind =" portalProps" >
136
- <DialogOverlay v-if =" overlay" :class =" ui.overlay({ class: props.ui?.overlay })" />
137
-
138
- <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" >
139
- <VisuallyHidden v-if =" !!slots.content && ((title || !!slots.title) || (description || !!slots.description))" >
140
- <DialogTitle v-if =" title || !!slots.title" >
141
- <slot name =" title" :close =" () => onOpenChange(false)" >
142
- {{ title }}
143
- </slot >
144
- </DialogTitle >
145
-
146
- <DialogDescription v-if =" description || !!slots.description" >
147
- <slot name =" description" :close =" () => onOpenChange(false)" >
148
- {{ description }}
149
- </slot >
150
- </DialogDescription >
151
- </VisuallyHidden >
152
-
153
- <slot name =" content" :close =" () => onOpenChange(false)" >
154
- <div v-if =" !!slots.header || (title || !!slots.title) || (description || !!slots.description) || (close || !!slots.close)" :class =" ui.header({ class: props.ui?.header })" >
155
- <slot name =" header" :close =" () => onOpenChange(false)" >
156
- <div :class =" ui.wrapper({ class: props.ui?.wrapper })" >
157
- <DialogTitle v-if =" title || !!slots.title" :class =" ui.title({ class: props.ui?.title })" >
158
- <slot name =" title" :close =" () => onOpenChange(false)" >
159
- {{ title }}
160
- </slot >
161
- </DialogTitle >
162
-
163
- <DialogDescription v-if =" description || !!slots.description" :class =" ui.description({ class: props.ui?.description })" >
164
- <slot name =" description" :close =" () => onOpenChange(false)" >
165
- {{ description }}
166
- </slot >
167
- </DialogDescription >
168
- </div >
169
-
170
- <DialogClose v-if =" close || !!slots.close" as-child >
171
- <slot name =" close" :ui =" ui" >
172
- <UButton
173
- v-if =" close"
174
- :icon =" closeIcon || appConfig.ui.icons.close"
175
- size =" md"
176
- color =" neutral"
177
- variant =" ghost"
178
- :aria-label =" t('modal.close')"
179
- v-bind =" (typeof close === 'object' ? close as Partial<ButtonProps> : {})"
180
- :class =" ui.close({ class: props.ui?.close })"
181
- />
136
+ <DialogRoot v-model:open =" open" v-bind =" rootProps" >
137
+ <DialogTrigger v-if =" !!slots.default" as-child :class =" props.class" >
138
+ <slot :open =" open" />
139
+ </DialogTrigger >
140
+
141
+ <DialogPortal v-bind =" portalProps" >
142
+ <DialogOverlay v-if =" overlay" :class =" ui.overlay({ class: props.ui?.overlay })" />
143
+
144
+ <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" >
145
+ <VisuallyHidden v-if =" !!slots.content && ((title || !!slots.title) || (description || !!slots.description))" >
146
+ <DialogTitle v-if =" title || !!slots.title" >
147
+ <slot name =" title" :close =" closeModal" >
148
+ {{ title }}
149
+ </slot >
150
+ </DialogTitle >
151
+
152
+ <DialogDescription v-if =" description || !!slots.description" >
153
+ <slot name =" description" :close =" closeModal" >
154
+ {{ description }}
155
+ </slot >
156
+ </DialogDescription >
157
+ </VisuallyHidden >
158
+
159
+ <slot name =" content" :close =" closeModal" >
160
+ <div v-if =" !!slots.header || (title || !!slots.title) || (description || !!slots.description) || (close || !!slots.close)" :class =" ui.header({ class: props.ui?.header })" >
161
+ <slot name =" header" :close =" closeModal" >
162
+ <div :class =" ui.wrapper({ class: props.ui?.wrapper })" >
163
+ <DialogTitle v-if =" title || !!slots.title" :class =" ui.title({ class: props.ui?.title })" >
164
+ <slot name =" title" :close =" closeModal" >
165
+ {{ title }}
166
+ </slot >
167
+ </DialogTitle >
168
+
169
+ <DialogDescription v-if =" description || !!slots.description" :class =" ui.description({ class: props.ui?.description })" >
170
+ <slot name =" description" :close =" closeModal" >
171
+ {{ description }}
182
172
</slot >
183
- </DialogClose >
184
- </slot >
185
- </div >
186
-
187
- <div v-if =" !!slots.body" :class =" ui.body({ class: props.ui?.body })" >
188
- <slot name =" body" :close =" () => onOpenChange(false)" />
189
- </div >
190
-
191
- <div v-if =" !!slots.footer" :class =" ui.footer({ class: props.ui?.footer })" >
192
- <slot name =" footer" :close =" () => onOpenChange(false)" />
193
- </div >
194
- </slot >
195
- </DialogContent >
196
- </DialogPortal >
197
- </ModalContext >
173
+ </DialogDescription >
174
+ </div >
175
+
176
+ <DialogClose v-if =" close || !!slots.close" as-child >
177
+ <slot name =" close" :ui =" ui" >
178
+ <UButton
179
+ v-if =" close"
180
+ :icon =" closeIcon || appConfig.ui.icons.close"
181
+ size =" md"
182
+ color =" neutral"
183
+ variant =" ghost"
184
+ :aria-label =" t('modal.close')"
185
+ v-bind =" (typeof close === 'object' ? close as Partial<ButtonProps> : {})"
186
+ :class =" ui.close({ class: props.ui?.close })"
187
+ />
188
+ </slot >
189
+ </DialogClose >
190
+ </slot >
191
+ </div >
192
+
193
+ <div v-if =" !!slots.body" :class =" ui.body({ class: props.ui?.body })" >
194
+ <slot name =" body" :close =" closeModal" />
195
+ </div >
196
+
197
+ <div v-if =" !!slots.footer" :class =" ui.footer({ class: props.ui?.footer })" >
198
+ <slot name =" footer" :close =" closeModal" />
199
+ </div >
200
+ </slot >
201
+ </DialogContent >
202
+ </DialogPortal >
198
203
</DialogRoot >
199
204
</template >
0 commit comments