Skip to content

Commit 80133cf

Browse files
committed
feat(VDefaultProvider): Provides Defaults to component slots
1 parent ac35aa7 commit 80133cf

File tree

2 files changed

+179
-48
lines changed

2 files changed

+179
-48
lines changed

packages/vuetify/src/components/VAlert/VAlert.tsx

Lines changed: 65 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { VIcon } from '@/components/VIcon'
1010
// Composables
1111
import { useTextColor } from '@/composables/color'
1212
import { makeComponentProps } from '@/composables/component'
13+
import { useDefaults, useSlotDefaults } from '@/composables/defaults'
1314
import { makeDensityProps, useDensity } from '@/composables/density'
1415
import { makeDimensionProps, useDimension } from '@/composables/dimensions'
1516
import { makeElevationProps, useElevation } from '@/composables/elevation'
@@ -106,61 +107,86 @@ export const VAlert = genericComponent<VAlertSlots>()({
106107
},
107108

108109
setup (props, { emit, slots }) {
109-
const isActive = useProxiedModel(props, 'modelValue')
110+
const _props = useDefaults(props)
111+
const { getSlotDefaultsInfo } = useSlotDefaults()
112+
const isActive = useProxiedModel(_props, 'modelValue')
110113
const icon = toRef(() => {
111-
if (props.icon === false) return undefined
112-
if (!props.type) return props.icon
114+
if (_props.icon === false) return undefined
115+
if (!_props.type) return _props.icon
113116

114-
return props.icon ?? `$${props.type}`
117+
return _props.icon ?? `$${_props.type}`
115118
})
116119

117-
const { iconSize } = useIconSizes(props, () => props.prominent ? 44 : undefined)
118-
const { themeClasses } = provideTheme(props)
120+
const { iconSize } = useIconSizes(_props, () => _props.prominent ? 44 : undefined)
121+
const { themeClasses } = provideTheme(_props)
119122
const { colorClasses, colorStyles, variantClasses } = useVariant(() => ({
120-
color: props.color ?? props.type,
121-
variant: props.variant,
123+
color: _props.color ?? _props.type,
124+
variant: _props.variant,
122125
}))
123-
const { densityClasses } = useDensity(props)
124-
const { dimensionStyles } = useDimension(props)
125-
const { elevationClasses } = useElevation(props)
126-
const { locationStyles } = useLocation(props)
127-
const { positionClasses } = usePosition(props)
128-
const { roundedClasses } = useRounded(props)
129-
const { textColorClasses, textColorStyles } = useTextColor(() => props.borderColor)
126+
const { densityClasses } = useDensity(_props)
127+
const { dimensionStyles } = useDimension(_props)
128+
const { elevationClasses } = useElevation(_props)
129+
const { locationStyles } = useLocation(_props)
130+
const { positionClasses } = usePosition(_props)
131+
const { roundedClasses } = useRounded(_props)
132+
const { textColorClasses, textColorStyles } = useTextColor(() => _props.borderColor)
130133
const { t } = useLocale()
131134

132135
const closeProps = toRef(() => ({
133-
'aria-label': t(props.closeLabel),
136+
'aria-label': t(_props.closeLabel),
134137
onClick (e: MouseEvent) {
135138
isActive.value = false
136139

137140
emit('click:close', e)
138141
},
139142
}))
140143

144+
// Helper function to wrap slot content with defaults
145+
function wrapSlot(slotName: string, slotFn: (() => any) | undefined, fallbackContent?: any) {
146+
const slotDefaultsInfo = getSlotDefaultsInfo(slotName)
147+
148+
if (!slotDefaultsInfo && !slotFn) {
149+
return fallbackContent
150+
}
151+
152+
if (!slotDefaultsInfo) {
153+
return slotFn?.() ?? fallbackContent
154+
}
155+
156+
const { componentDefaults, directProps } = slotDefaultsInfo
157+
158+
return (
159+
<VDefaultsProvider defaults={componentDefaults as any}>
160+
<div {...directProps}>
161+
{slotFn?.() ?? fallbackContent}
162+
</div>
163+
</VDefaultsProvider>
164+
)
165+
}
166+
141167
return () => {
142168
const hasPrepend = !!(slots.prepend || icon.value)
143-
const hasTitle = !!(slots.title || props.title)
144-
const hasClose = !!(slots.close || props.closable)
169+
const hasTitle = !!(slots.title || _props.title)
170+
const hasClose = !!(slots.close || _props.closable)
145171

146172
const iconProps = {
147-
density: props.density,
173+
density: _props.density,
148174
icon: icon.value,
149-
size: props.iconSize || props.prominent
175+
size: _props.iconSize || _props.prominent
150176
? iconSize.value
151177
: undefined,
152178
}
153179

154180
return isActive.value && (
155-
<props.tag
181+
<_props.tag
156182
class={[
157183
'v-alert',
158-
props.border && {
159-
'v-alert--border': !!props.border,
160-
[`v-alert--border-${props.border === true ? 'start' : props.border}`]: true,
184+
_props.border && {
185+
'v-alert--border': !!_props.border,
186+
[`v-alert--border-${_props.border === true ? 'start' : _props.border}`]: true,
161187
},
162188
{
163-
'v-alert--prominent': props.prominent,
189+
'v-alert--prominent': _props.prominent,
164190
},
165191
themeClasses.value,
166192
colorClasses.value,
@@ -169,19 +195,19 @@ export const VAlert = genericComponent<VAlertSlots>()({
169195
positionClasses.value,
170196
roundedClasses.value,
171197
variantClasses.value,
172-
props.class,
198+
_props.class,
173199
]}
174200
style={[
175201
colorStyles.value,
176202
dimensionStyles.value,
177203
locationStyles.value,
178-
props.style,
204+
_props.style,
179205
]}
180206
role="alert"
181207
>
182208
{ genOverlays(false, 'v-alert') }
183209

184-
{ props.border && (
210+
{ _props.border && (
185211
<div
186212
key="border"
187213
class={[
@@ -201,27 +227,28 @@ export const VAlert = genericComponent<VAlertSlots>()({
201227
key="prepend-defaults"
202228
disabled={ !icon.value }
203229
defaults={{ VIcon: { ...iconProps } }}
204-
v-slots:default={ slots.prepend }
205-
/>
230+
>
231+
{ wrapSlot('prepend', slots.prepend) }
232+
</VDefaultsProvider>
206233
)}
207234
</div>
208235
)}
209236

210237
<div class="v-alert__content">
211238
{ hasTitle && (
212239
<VAlertTitle key="title">
213-
{ slots.title?.() ?? props.title }
240+
{ wrapSlot('title', slots.title, _props.title) }
214241
</VAlertTitle>
215242
)}
216243

217-
{ slots.text?.() ?? props.text }
244+
{ wrapSlot('text', slots.text, _props.text) }
218245

219-
{ slots.default?.() }
246+
{ wrapSlot('default', slots.default) }
220247
</div>
221248

222249
{ slots.append && (
223250
<div key="append" class="v-alert__append">
224-
{ slots.append() }
251+
{ wrapSlot('append', slots.append) }
225252
</div>
226253
)}
227254

@@ -230,7 +257,7 @@ export const VAlert = genericComponent<VAlertSlots>()({
230257
{ !slots.close ? (
231258
<VBtn
232259
key="close-btn"
233-
icon={ props.closeIcon }
260+
icon={ _props.closeIcon }
234261
size="x-small"
235262
variant="text"
236263
{ ...closeProps.value }
@@ -240,18 +267,18 @@ export const VAlert = genericComponent<VAlertSlots>()({
240267
key="close-defaults"
241268
defaults={{
242269
VBtn: {
243-
icon: props.closeIcon,
270+
icon: _props.closeIcon,
244271
size: 'x-small',
245272
variant: 'text',
246273
},
247274
}}
248275
>
249-
{ slots.close?.({ props: closeProps.value }) }
276+
{ wrapSlot('close', () => slots.close?.({ props: closeProps.value })) }
250277
</VDefaultsProvider>
251278
)}
252279
</div>
253280
)}
254-
</props.tag>
281+
</_props.tag>
255282
)
256283
}
257284
},

packages/vuetify/src/composables/defaults.ts

Lines changed: 114 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,17 @@ import { injectSelf } from '@/util/injectSelf'
88
import type { ComputedRef, InjectionKey, Ref, VNode } from 'vue'
99
import type { MaybeRef } from '@/util'
1010

11+
export type SlotDefaults = {
12+
[slotName: string]: Record<string, unknown>
13+
}
14+
15+
export type ComponentDefaults = Record<string, unknown> & {
16+
[slotKey: `#${string}`]: Record<string, unknown>
17+
}
18+
1119
export type DefaultsInstance = undefined | {
12-
[key: string]: undefined | Record<string, unknown>
13-
global?: Record<string, unknown>
20+
[key: string]: undefined | ComponentDefaults
21+
global?: ComponentDefaults
1422
}
1523

1624
export type DefaultsOptions = Partial<DefaultsInstance>
@@ -89,6 +97,29 @@ function propIsDefined (vnode: VNode, prop: string) {
8997
typeof vnode.props[toKebabCase(prop)] !== 'undefined')
9098
}
9199

100+
function extractSlotDefaults (componentDefaults: ComponentDefaults | undefined): {
101+
componentDefaults: Record<string, unknown>
102+
slotDefaults: SlotDefaults
103+
} {
104+
if (!componentDefaults) {
105+
return { componentDefaults: {}, slotDefaults: {} }
106+
}
107+
108+
const slotDefaults: SlotDefaults = {}
109+
const filteredComponentDefaults: Record<string, unknown> = {}
110+
111+
for (const [key, value] of Object.entries(componentDefaults)) {
112+
if (key.startsWith('#')) {
113+
const slotName = key.slice(1) // Remove the '#' prefix
114+
slotDefaults[slotName] = value as Record<string, unknown>
115+
} else {
116+
filteredComponentDefaults[key] = value
117+
}
118+
}
119+
120+
return { componentDefaults: filteredComponentDefaults, slotDefaults }
121+
}
122+
92123
export function internalUseDefaults (
93124
props: Record<string, any> = {},
94125
name?: string,
@@ -101,7 +132,12 @@ export function internalUseDefaults (
101132
throw new Error('[Vuetify] Could not determine component name')
102133
}
103134

104-
const componentDefaults = computed(() => defaults.value?.[props._as ?? name])
135+
const rawComponentDefaults = computed(() => defaults.value?.[props._as ?? name] as ComponentDefaults | undefined)
136+
const extractedDefaults = computed(() =>
137+
extractSlotDefaults(rawComponentDefaults.value)
138+
)
139+
const componentDefaults = computed(() => extractedDefaults.value.componentDefaults)
140+
105141
const _props = new Proxy(props, {
106142
get (target, prop: string) {
107143
const propValue = Reflect.get(target, prop)
@@ -118,14 +154,20 @@ export function internalUseDefaults (
118154
})
119155

120156
const _subcomponentDefaults = shallowRef()
157+
const _slotDefaults = shallowRef<SlotDefaults>()
158+
121159
watchEffect(() => {
122-
if (componentDefaults.value) {
123-
const subComponents = Object.entries(componentDefaults.value)
160+
const extracted = extractedDefaults.value
161+
162+
if (extracted.componentDefaults) {
163+
const subComponents = Object.entries(extracted.componentDefaults)
124164
.filter(([key]) => key.startsWith(key[0].toUpperCase()))
125165
_subcomponentDefaults.value = subComponents.length ? Object.fromEntries(subComponents) : undefined
126166
} else {
127167
_subcomponentDefaults.value = undefined
128168
}
169+
170+
_slotDefaults.value = extracted.slotDefaults
129171
})
130172

131173
function provideSubDefaults () {
@@ -138,16 +180,78 @@ export function internalUseDefaults (
138180
}))
139181
}
140182

141-
return { props: _props, provideSubDefaults }
183+
function getSlotDefaults (slotName: string): Record<string, unknown> | undefined {
184+
return _slotDefaults.value?.[slotName]
185+
}
186+
187+
return { props: _props, provideSubDefaults, getSlotDefaults }
142188
}
143189

144-
export function useDefaults<T extends Record<string, any>> (props: T, name?: string): T
145-
export function useDefaults (props?: undefined, name?: string): Record<string, any>
190+
export function useDefaults<T extends Record<string, any>> (props: T, name?: string): T & { getSlotDefaults: (slotName: string) => Record<string, unknown> | undefined }
191+
export function useDefaults (props?: undefined, name?: string): Record<string, any> & { getSlotDefaults: (slotName: string) => Record<string, unknown> | undefined }
146192
export function useDefaults (
147193
props: Record<string, any> = {},
148194
name?: string,
149195
) {
150-
const { props: _props, provideSubDefaults } = internalUseDefaults(props, name)
196+
const { props: _props, provideSubDefaults, getSlotDefaults } = internalUseDefaults(props, name)
151197
provideSubDefaults()
152-
return _props
198+
199+
// Create a new proxy that includes getSlotDefaults
200+
return new Proxy(_props, {
201+
get(target, prop) {
202+
if (prop === 'getSlotDefaults') {
203+
return getSlotDefaults
204+
}
205+
return Reflect.get(target, prop)
206+
},
207+
has(target, prop) {
208+
if (prop === 'getSlotDefaults') {
209+
return true
210+
}
211+
return Reflect.has(target, prop)
212+
},
213+
ownKeys(target) {
214+
return [...Reflect.ownKeys(target), 'getSlotDefaults']
215+
}
216+
})
217+
}
218+
219+
export function createSlotDefaults (slotDefaults: Record<string, unknown> | undefined) {
220+
if (!slotDefaults) return {}
221+
222+
const componentDefaults: Record<string, unknown> = {}
223+
const directProps: Record<string, unknown> = {}
224+
225+
for (const [key, value] of Object.entries(slotDefaults)) {
226+
if (key[0] === key[0].toUpperCase()) {
227+
// Component defaults (e.g., VBtn: { size: 'md' })
228+
componentDefaults[key] = value
229+
} else {
230+
// Direct props (e.g., class: 'pa-0')
231+
directProps[key] = value
232+
}
233+
}
234+
235+
return { componentDefaults, directProps }
236+
}
237+
238+
// Helper function to get slot defaults info without rendering
239+
export function useSlotDefaults() {
240+
const defaults = injectDefaults()
241+
const vm = getCurrentInstance('useSlotDefaults')
242+
243+
function getSlotDefaultsInfo(slotName: string) {
244+
const componentName = vm?.type.name ?? vm?.type.__name
245+
if (!componentName) return null
246+
247+
const componentDefaults = defaults.value?.[componentName] as ComponentDefaults | undefined
248+
const { slotDefaults } = extractSlotDefaults(componentDefaults)
249+
const slotDefaultsForSlot = slotDefaults[slotName]
250+
251+
if (!slotDefaultsForSlot) return null
252+
253+
return createSlotDefaults(slotDefaultsForSlot)
254+
}
255+
256+
return { getSlotDefaultsInfo }
153257
}

0 commit comments

Comments
 (0)