From 8f15c0f28a0dfc9269424f7a0fb22832fdc0253e Mon Sep 17 00:00:00 2001 From: Blaine Landowski Date: Mon, 10 Nov 2025 13:50:50 -0600 Subject: [PATCH 1/2] feat(VDefaultProvider): Provides Defaults to component slots --- .../vuetify/src/components/VAlert/VAlert.tsx | 103 +++++++++------ packages/vuetify/src/composables/defaults.ts | 124 ++++++++++++++++-- 2 files changed, 179 insertions(+), 48 deletions(-) diff --git a/packages/vuetify/src/components/VAlert/VAlert.tsx b/packages/vuetify/src/components/VAlert/VAlert.tsx index 0f9818780bd..5b89edcd94d 100644 --- a/packages/vuetify/src/components/VAlert/VAlert.tsx +++ b/packages/vuetify/src/components/VAlert/VAlert.tsx @@ -10,6 +10,7 @@ import { VIcon } from '@/components/VIcon' // Composables import { useTextColor } from '@/composables/color' import { makeComponentProps } from '@/composables/component' +import { useDefaults, useSlotDefaults } from '@/composables/defaults' import { makeDensityProps, useDensity } from '@/composables/density' import { makeDimensionProps, useDimension } from '@/composables/dimensions' import { makeElevationProps, useElevation } from '@/composables/elevation' @@ -106,31 +107,33 @@ export const VAlert = genericComponent()({ }, setup (props, { emit, slots }) { - const isActive = useProxiedModel(props, 'modelValue') + const _props = useDefaults(props) + const { getSlotDefaultsInfo } = useSlotDefaults() + const isActive = useProxiedModel(_props, 'modelValue') const icon = toRef(() => { - if (props.icon === false) return undefined - if (!props.type) return props.icon + if (_props.icon === false) return undefined + if (!_props.type) return _props.icon - return props.icon ?? `$${props.type}` + return _props.icon ?? `$${_props.type}` }) - const { iconSize } = useIconSizes(props, () => props.prominent ? 44 : undefined) - const { themeClasses } = provideTheme(props) + const { iconSize } = useIconSizes(_props, () => _props.prominent ? 44 : undefined) + const { themeClasses } = provideTheme(_props) const { colorClasses, colorStyles, variantClasses } = useVariant(() => ({ - color: props.color ?? props.type, - variant: props.variant, + color: _props.color ?? _props.type, + variant: _props.variant, })) - const { densityClasses } = useDensity(props) - const { dimensionStyles } = useDimension(props) - const { elevationClasses } = useElevation(props) - const { locationStyles } = useLocation(props) - const { positionClasses } = usePosition(props) - const { roundedClasses } = useRounded(props) - const { textColorClasses, textColorStyles } = useTextColor(() => props.borderColor) + const { densityClasses } = useDensity(_props) + const { dimensionStyles } = useDimension(_props) + const { elevationClasses } = useElevation(_props) + const { locationStyles } = useLocation(_props) + const { positionClasses } = usePosition(_props) + const { roundedClasses } = useRounded(_props) + const { textColorClasses, textColorStyles } = useTextColor(() => _props.borderColor) const { t } = useLocale() const closeProps = toRef(() => ({ - 'aria-label': t(props.closeLabel), + 'aria-label': t(_props.closeLabel), onClick (e: MouseEvent) { isActive.value = false @@ -138,29 +141,52 @@ export const VAlert = genericComponent()({ }, })) + // Helper function to wrap slot content with defaults + function wrapSlot(slotName: string, slotFn: (() => any) | undefined, fallbackContent?: any) { + const slotDefaultsInfo = getSlotDefaultsInfo(slotName) + + if (!slotDefaultsInfo && !slotFn) { + return fallbackContent + } + + if (!slotDefaultsInfo) { + return slotFn?.() ?? fallbackContent + } + + const { componentDefaults, directProps } = slotDefaultsInfo + + return ( + +
+ {slotFn?.() ?? fallbackContent} +
+
+ ) + } + return () => { const hasPrepend = !!(slots.prepend || icon.value) - const hasTitle = !!(slots.title || props.title) - const hasClose = !!(slots.close || props.closable) + const hasTitle = !!(slots.title || _props.title) + const hasClose = !!(slots.close || _props.closable) const iconProps = { - density: props.density, + density: _props.density, icon: icon.value, - size: props.iconSize || props.prominent + size: _props.iconSize || _props.prominent ? iconSize.value : undefined, } return isActive.value && ( - ()({ positionClasses.value, roundedClasses.value, variantClasses.value, - props.class, + _props.class, ]} style={[ colorStyles.value, dimensionStyles.value, locationStyles.value, - props.style, + _props.style, ]} role="alert" > { genOverlays(false, 'v-alert') } - { props.border && ( + { _props.border && (
()({ key="prepend-defaults" disabled={ !icon.value } defaults={{ VIcon: { ...iconProps } }} - v-slots:default={ slots.prepend } - /> + > + { wrapSlot('prepend', slots.prepend) } + )}
)} @@ -210,18 +237,18 @@ export const VAlert = genericComponent()({
{ hasTitle && ( - { slots.title?.() ?? props.title } + { wrapSlot('title', slots.title, _props.title) } )} - { slots.text?.() ?? props.text } + { wrapSlot('text', slots.text, _props.text) } - { slots.default?.() } + { wrapSlot('default', slots.default) }
{ slots.append && (
- { slots.append() } + { wrapSlot('append', slots.append) }
)} @@ -230,7 +257,7 @@ export const VAlert = genericComponent()({ { !slots.close ? ( ()({ key="close-defaults" defaults={{ VBtn: { - icon: props.closeIcon, + icon: _props.closeIcon, size: 'x-small', variant: 'text', }, }} > - { slots.close?.({ props: closeProps.value }) } + { wrapSlot('close', () => slots.close?.({ props: closeProps.value })) } )} )} -
+ ) } }, diff --git a/packages/vuetify/src/composables/defaults.ts b/packages/vuetify/src/composables/defaults.ts index df7e48f7fcd..e9a9db7999b 100644 --- a/packages/vuetify/src/composables/defaults.ts +++ b/packages/vuetify/src/composables/defaults.ts @@ -8,9 +8,17 @@ import { injectSelf } from '@/util/injectSelf' import type { ComputedRef, InjectionKey, Ref, VNode } from 'vue' import type { MaybeRef } from '@/util' +export type SlotDefaults = { + [slotName: string]: Record +} + +export type ComponentDefaults = Record & { + [slotKey: `#${string}`]: Record +} + export type DefaultsInstance = undefined | { - [key: string]: undefined | Record - global?: Record + [key: string]: undefined | ComponentDefaults + global?: ComponentDefaults } export type DefaultsOptions = Partial @@ -89,6 +97,29 @@ function propIsDefined (vnode: VNode, prop: string) { typeof vnode.props[toKebabCase(prop)] !== 'undefined') } +function extractSlotDefaults (componentDefaults: ComponentDefaults | undefined): { + componentDefaults: Record + slotDefaults: SlotDefaults +} { + if (!componentDefaults) { + return { componentDefaults: {}, slotDefaults: {} } + } + + const slotDefaults: SlotDefaults = {} + const filteredComponentDefaults: Record = {} + + for (const [key, value] of Object.entries(componentDefaults)) { + if (key.startsWith('#')) { + const slotName = key.slice(1) // Remove the '#' prefix + slotDefaults[slotName] = value as Record + } else { + filteredComponentDefaults[key] = value + } + } + + return { componentDefaults: filteredComponentDefaults, slotDefaults } +} + export function internalUseDefaults ( props: Record = {}, name?: string, @@ -101,7 +132,12 @@ export function internalUseDefaults ( throw new Error('[Vuetify] Could not determine component name') } - const componentDefaults = computed(() => defaults.value?.[props._as ?? name]) + const rawComponentDefaults = computed(() => defaults.value?.[props._as ?? name] as ComponentDefaults | undefined) + const extractedDefaults = computed(() => + extractSlotDefaults(rawComponentDefaults.value) + ) + const componentDefaults = computed(() => extractedDefaults.value.componentDefaults) + const _props = new Proxy(props, { get (target, prop: string) { const propValue = Reflect.get(target, prop) @@ -118,14 +154,20 @@ export function internalUseDefaults ( }) const _subcomponentDefaults = shallowRef() + const _slotDefaults = shallowRef() + watchEffect(() => { - if (componentDefaults.value) { - const subComponents = Object.entries(componentDefaults.value) + const extracted = extractedDefaults.value + + if (extracted.componentDefaults) { + const subComponents = Object.entries(extracted.componentDefaults) .filter(([key]) => key.startsWith(key[0].toUpperCase())) _subcomponentDefaults.value = subComponents.length ? Object.fromEntries(subComponents) : undefined } else { _subcomponentDefaults.value = undefined } + + _slotDefaults.value = extracted.slotDefaults }) function provideSubDefaults () { @@ -138,16 +180,78 @@ export function internalUseDefaults ( })) } - return { props: _props, provideSubDefaults } + function getSlotDefaults (slotName: string): Record | undefined { + return _slotDefaults.value?.[slotName] + } + + return { props: _props, provideSubDefaults, getSlotDefaults } } -export function useDefaults> (props: T, name?: string): T -export function useDefaults (props?: undefined, name?: string): Record +export function useDefaults> (props: T, name?: string): T & { getSlotDefaults: (slotName: string) => Record | undefined } +export function useDefaults (props?: undefined, name?: string): Record & { getSlotDefaults: (slotName: string) => Record | undefined } export function useDefaults ( props: Record = {}, name?: string, ) { - const { props: _props, provideSubDefaults } = internalUseDefaults(props, name) + const { props: _props, provideSubDefaults, getSlotDefaults } = internalUseDefaults(props, name) provideSubDefaults() - return _props + + // Create a new proxy that includes getSlotDefaults + return new Proxy(_props, { + get(target, prop) { + if (prop === 'getSlotDefaults') { + return getSlotDefaults + } + return Reflect.get(target, prop) + }, + has(target, prop) { + if (prop === 'getSlotDefaults') { + return true + } + return Reflect.has(target, prop) + }, + ownKeys(target) { + return [...Reflect.ownKeys(target), 'getSlotDefaults'] + } + }) +} + +export function createSlotDefaults (slotDefaults: Record | undefined) { + if (!slotDefaults) return {} + + const componentDefaults: Record = {} + const directProps: Record = {} + + for (const [key, value] of Object.entries(slotDefaults)) { + if (key[0] === key[0].toUpperCase()) { + // Component defaults (e.g., VBtn: { size: 'md' }) + componentDefaults[key] = value + } else { + // Direct props (e.g., class: 'pa-0') + directProps[key] = value + } + } + + return { componentDefaults, directProps } +} + +// Helper function to get slot defaults info without rendering +export function useSlotDefaults() { + const defaults = injectDefaults() + const vm = getCurrentInstance('useSlotDefaults') + + function getSlotDefaultsInfo(slotName: string) { + const componentName = vm?.type.name ?? vm?.type.__name + if (!componentName) return null + + const componentDefaults = defaults.value?.[componentName] as ComponentDefaults | undefined + const { slotDefaults } = extractSlotDefaults(componentDefaults) + const slotDefaultsForSlot = slotDefaults[slotName] + + if (!slotDefaultsForSlot) return null + + return createSlotDefaults(slotDefaultsForSlot) + } + + return { getSlotDefaultsInfo } } From cfe77edd3b82a215c4cb51b899ae96fa4535f194 Mon Sep 17 00:00:00 2001 From: Blaine Landowski Date: Mon, 17 Nov 2025 11:03:21 -0600 Subject: [PATCH 2/2] feat(VDefaultProvider): Fix comments --- .../vuetify/src/components/VAlert/VAlert.tsx | 78 +++++++++---------- packages/vuetify/src/composables/defaults.ts | 4 +- 2 files changed, 38 insertions(+), 44 deletions(-) diff --git a/packages/vuetify/src/components/VAlert/VAlert.tsx b/packages/vuetify/src/components/VAlert/VAlert.tsx index 5b89edcd94d..d28ac5f0a79 100644 --- a/packages/vuetify/src/components/VAlert/VAlert.tsx +++ b/packages/vuetify/src/components/VAlert/VAlert.tsx @@ -10,7 +10,7 @@ import { VIcon } from '@/components/VIcon' // Composables import { useTextColor } from '@/composables/color' import { makeComponentProps } from '@/composables/component' -import { useDefaults, useSlotDefaults } from '@/composables/defaults' +import { useSlotDefaults } from '@/composables/defaults' import { makeDensityProps, useDensity } from '@/composables/density' import { makeDimensionProps, useDimension } from '@/composables/dimensions' import { makeElevationProps, useElevation } from '@/composables/elevation' @@ -107,33 +107,32 @@ export const VAlert = genericComponent()({ }, setup (props, { emit, slots }) { - const _props = useDefaults(props) const { getSlotDefaultsInfo } = useSlotDefaults() - const isActive = useProxiedModel(_props, 'modelValue') + const isActive = useProxiedModel(props, 'modelValue') const icon = toRef(() => { - if (_props.icon === false) return undefined - if (!_props.type) return _props.icon + if (props.icon === false) return undefined + if (!props.type) return props.icon - return _props.icon ?? `$${_props.type}` + return props.icon ?? `$${props.type}` }) - const { iconSize } = useIconSizes(_props, () => _props.prominent ? 44 : undefined) - const { themeClasses } = provideTheme(_props) + const { iconSize } = useIconSizes(props, () => props.prominent ? 44 : undefined) + const { themeClasses } = provideTheme(props) const { colorClasses, colorStyles, variantClasses } = useVariant(() => ({ - color: _props.color ?? _props.type, - variant: _props.variant, + color: props.color ?? props.type, + variant: props.variant, })) - const { densityClasses } = useDensity(_props) - const { dimensionStyles } = useDimension(_props) - const { elevationClasses } = useElevation(_props) - const { locationStyles } = useLocation(_props) - const { positionClasses } = usePosition(_props) - const { roundedClasses } = useRounded(_props) - const { textColorClasses, textColorStyles } = useTextColor(() => _props.borderColor) + const { densityClasses } = useDensity(props) + const { dimensionStyles } = useDimension(props) + const { elevationClasses } = useElevation(props) + const { locationStyles } = useLocation(props) + const { positionClasses } = usePosition(props) + const { roundedClasses } = useRounded(props) + const { textColorClasses, textColorStyles } = useTextColor(() => props.borderColor) const { t } = useLocale() const closeProps = toRef(() => ({ - 'aria-label': t(_props.closeLabel), + 'aria-label': t(props.closeLabel), onClick (e: MouseEvent) { isActive.value = false @@ -144,13 +143,10 @@ export const VAlert = genericComponent()({ // Helper function to wrap slot content with defaults function wrapSlot(slotName: string, slotFn: (() => any) | undefined, fallbackContent?: any) { const slotDefaultsInfo = getSlotDefaultsInfo(slotName) - - if (!slotDefaultsInfo && !slotFn) { - return fallbackContent - } + const content = slotFn ? slotFn() : fallbackContent if (!slotDefaultsInfo) { - return slotFn?.() ?? fallbackContent + return content } const { componentDefaults, directProps } = slotDefaultsInfo @@ -158,7 +154,7 @@ export const VAlert = genericComponent()({ return (
- {slotFn?.() ?? fallbackContent} + {content}
) @@ -166,27 +162,27 @@ export const VAlert = genericComponent()({ return () => { const hasPrepend = !!(slots.prepend || icon.value) - const hasTitle = !!(slots.title || _props.title) - const hasClose = !!(slots.close || _props.closable) + const hasTitle = !!(slots.title || props.title) + const hasClose = !!(slots.close || props.closable) const iconProps = { - density: _props.density, + density: props.density, icon: icon.value, - size: _props.iconSize || _props.prominent + size: props.iconSize || props.prominent ? iconSize.value : undefined, } return isActive.value && ( - <_props.tag + ()({ positionClasses.value, roundedClasses.value, variantClasses.value, - _props.class, + props.class, ]} style={[ colorStyles.value, dimensionStyles.value, locationStyles.value, - _props.style, + props.style, ]} role="alert" > { genOverlays(false, 'v-alert') } - { _props.border && ( + { props.border && (
()({
{ hasTitle && ( - { wrapSlot('title', slots.title, _props.title) } + { wrapSlot('title', slots.title, props.title) } )} - { wrapSlot('text', slots.text, _props.text) } + { wrapSlot('text', slots.text, props.text) } { wrapSlot('default', slots.default) }
@@ -257,7 +253,7 @@ export const VAlert = genericComponent()({ { !slots.close ? ( ()({ key="close-defaults" defaults={{ VBtn: { - icon: _props.closeIcon, + icon: props.closeIcon, size: 'x-small', variant: 'text', }, @@ -278,7 +274,7 @@ export const VAlert = genericComponent()({ )}
)} - +
) } }, diff --git a/packages/vuetify/src/composables/defaults.ts b/packages/vuetify/src/composables/defaults.ts index e9a9db7999b..a9c56283140 100644 --- a/packages/vuetify/src/composables/defaults.ts +++ b/packages/vuetify/src/composables/defaults.ts @@ -12,9 +12,7 @@ export type SlotDefaults = { [slotName: string]: Record } -export type ComponentDefaults = Record & { - [slotKey: `#${string}`]: Record -} +export type ComponentDefaults = Record export type DefaultsInstance = undefined | { [key: string]: undefined | ComponentDefaults