diff --git a/packages/api-generator/src/types.ts b/packages/api-generator/src/types.ts index 76c410b852a..818d8dc273f 100644 --- a/packages/api-generator/src/types.ts +++ b/packages/api-generator/src/types.ts @@ -325,6 +325,7 @@ const allowedRefs = [ 'SelectStrategyFn', 'SortItem', 'SubmitEventPromise', + 'TemplateRef', 'TouchHandlers', 'ValidationRule', ] diff --git a/packages/vuetify/src/components/VColorPicker/VColorPickerCanvas.tsx b/packages/vuetify/src/components/VColorPicker/VColorPickerCanvas.tsx index 5b76f1fb410..fa3f6e32a04 100644 --- a/packages/vuetify/src/components/VColorPicker/VColorPickerCanvas.tsx +++ b/packages/vuetify/src/components/VColorPicker/VColorPickerCanvas.tsx @@ -80,7 +80,7 @@ export const VColorPickerCanvas = defineComponent({ }) const { resizeRef } = useResizeObserver(entries => { - if (!resizeRef.value?.offsetParent) return + if (!resizeRef.el?.offsetParent) return const { width, height } = entries[0].contentRect diff --git a/packages/vuetify/src/components/VDataTable/VDataTableVirtual.tsx b/packages/vuetify/src/components/VDataTable/VDataTableVirtual.tsx index dc2c3b1941a..bee0fb23013 100644 --- a/packages/vuetify/src/components/VDataTable/VDataTableVirtual.tsx +++ b/packages/vuetify/src/components/VDataTable/VDataTableVirtual.tsx @@ -23,12 +23,11 @@ import { computed, shallowRef, toRef } from 'vue' import { convertToUnit, genericComponent, propsFactory, useRender } from '@/util' // Types -import type { Ref } from 'vue' import type { VDataTableSlotProps } from './VDataTable' import type { VDataTableHeadersSlots } from './VDataTableHeaders' import type { VDataTableRowsSlots } from './VDataTableRows' import type { CellProps, RowProps } from '@/components/VDataTable/types' -import type { GenericProps, SelectItemKey } from '@/util' +import type { GenericProps, SelectItemKey, TemplateRef } from '@/util' type VDataTableVirtualSlotProps = Omit< VDataTableSlotProps, @@ -46,7 +45,7 @@ export type VDataTableVirtualSlots = VDataTableRowsSlots & VDataTableHeade 'body.prepend': VDataTableVirtualSlotProps 'body.append': VDataTableVirtualSlotProps item: { - itemRef: Ref + itemRef: TemplateRef } } diff --git a/packages/vuetify/src/components/VDatePicker/VDatePickerYears.tsx b/packages/vuetify/src/components/VDatePicker/VDatePickerYears.tsx index d596f29ab92..4910d073cd2 100644 --- a/packages/vuetify/src/components/VDatePicker/VDatePickerYears.tsx +++ b/packages/vuetify/src/components/VDatePicker/VDatePickerYears.tsx @@ -9,8 +9,8 @@ import { useDate } from '@/composables/date' import { useProxiedModel } from '@/composables/proxiedModel' // Utilities -import { computed, nextTick, onMounted, ref, watchEffect } from 'vue' -import { convertToUnit, createRange, genericComponent, propsFactory, useRender } from '@/util' +import { computed, nextTick, onMounted, watchEffect } from 'vue' +import { convertToUnit, createRange, genericComponent, propsFactory, templateRef, useRender } from '@/util' // Types import type { PropType } from 'vue' @@ -87,10 +87,11 @@ export const VDatePickerYears = genericComponent()({ model.value = model.value ?? adapter.getYear(adapter.date()) }) - const yearRef = ref() + const yearRef = templateRef() + onMounted(async () => { await nextTick() - yearRef.value?.$el.scrollIntoView({ block: 'center' }) + yearRef.el?.scrollIntoView({ block: 'center' }) }) useRender(() => ( diff --git a/packages/vuetify/src/components/VOverlay/VOverlay.tsx b/packages/vuetify/src/components/VOverlay/VOverlay.tsx index c683fffa6b2..75da73ed33b 100644 --- a/packages/vuetify/src/components/VOverlay/VOverlay.tsx +++ b/packages/vuetify/src/components/VOverlay/VOverlay.tsx @@ -46,11 +46,9 @@ import { } from '@/util' // Types -import type { - ComponentPublicInstance, PropType, - Ref, -} from 'vue' +import type { PropType, Ref } from 'vue' import type { BackgroundColorData } from '@/composables/color' +import type { TemplateRef } from '@/util' interface ScrimProps { [key: string]: unknown @@ -77,7 +75,7 @@ function Scrim (props: ScrimProps) { export type OverlaySlots = { default: { isActive: Ref } - activator: { isActive: boolean, props: Record, targetRef: Ref | HTMLElement> } + activator: { isActive: boolean, props: Record, targetRef: TemplateRef } } export const makeVOverlayProps = propsFactory({ diff --git a/packages/vuetify/src/components/VOverlay/useActivator.tsx b/packages/vuetify/src/components/VOverlay/useActivator.tsx index b1555ee3c48..0bf11e5ac86 100644 --- a/packages/vuetify/src/components/VOverlay/useActivator.tsx +++ b/packages/vuetify/src/components/VOverlay/useActivator.tsx @@ -22,7 +22,7 @@ import { IN_BROWSER, matchesSelector, propsFactory, - refElement, + templateRef, unbindProps, } from '@/util' @@ -228,19 +228,19 @@ export function useActivator ( } }, { flush: 'post' }) - const activatorRef = ref() + const activatorRef = templateRef() watchEffect(() => { if (!activatorRef.value) return nextTick(() => { - activatorEl.value = refElement(activatorRef.value) + activatorEl.value = activatorRef.el }) }) - const targetRef = ref | HTMLElement>() + const targetRef = templateRef() const target = computed(() => { if (props.target === 'cursor' && cursorTarget.value) return cursorTarget.value - if (targetRef.value) return refElement(targetRef.value) + if (targetRef.value) return targetRef.el return getTarget(props.target, vm) || activatorEl.value }) const targetEl = computed(() => { diff --git a/packages/vuetify/src/components/VSlideGroup/VSlideGroup.tsx b/packages/vuetify/src/components/VSlideGroup/VSlideGroup.tsx index 0fa926635c9..dee0b516718 100644 --- a/packages/vuetify/src/components/VSlideGroup/VSlideGroup.tsx +++ b/packages/vuetify/src/components/VSlideGroup/VSlideGroup.tsx @@ -116,7 +116,7 @@ export const VSlideGroup = genericComponent( const goTo = useGoTo() const goToOptions = computed>(() => { return { - container: containerRef.value, + container: containerRef.el, duration: 200, easing: 'easeOutQuart', } @@ -148,9 +148,9 @@ export const VSlideGroup = genericComponent( isOverflowing.value = containerSize.value + 1 < contentSize.value } - if (firstSelectedIndex.value >= 0 && contentRef.value) { + if (firstSelectedIndex.value >= 0 && contentRef.el) { // TODO: Is this too naive? Should we store element references in group composable? - const selectedElement = contentRef.value.children[lastSelectedIndex.value] as HTMLElement + const selectedElement = contentRef.el.children[lastSelectedIndex.value] as HTMLElement scrollToChildren(selectedElement, props.centerActive) } @@ -165,13 +165,13 @@ export const VSlideGroup = genericComponent( if (center) { target = calculateCenteredTarget({ - containerElement: containerRef.value!, + containerElement: containerRef.el!, isHorizontal: isHorizontal.value, selectedElement: children, }) } else { target = calculateUpdatedTarget({ - containerElement: containerRef.value!, + containerElement: containerRef.el!, isHorizontal: isHorizontal.value, isRtl: isRtl.value, selectedElement: children, @@ -182,11 +182,11 @@ export const VSlideGroup = genericComponent( } function scrollToPosition (newPosition: number) { - if (!IN_BROWSER || !containerRef.value) return + if (!IN_BROWSER || !containerRef.el) return - const offsetSize = getOffsetSize(isHorizontal.value, containerRef.value) - const scrollPosition = getScrollPosition(isHorizontal.value, isRtl.value, containerRef.value) - const scrollSize = getScrollSize(isHorizontal.value, containerRef.value) + const offsetSize = getOffsetSize(isHorizontal.value, containerRef.el) + const scrollPosition = getScrollPosition(isHorizontal.value, isRtl.value, containerRef.el) + const scrollSize = getScrollSize(isHorizontal.value, containerRef.el) if ( scrollSize <= offsetSize || @@ -194,8 +194,8 @@ export const VSlideGroup = genericComponent( Math.abs(newPosition - scrollPosition) < 16 ) return - if (isHorizontal.value && isRtl.value && containerRef.value) { - const { scrollWidth, offsetWidth: containerWidth } = containerRef.value! + if (isHorizontal.value && isRtl.value && containerRef.el) { + const { scrollWidth, offsetWidth: containerWidth } = containerRef.el! newPosition = (scrollWidth - containerWidth) - newPosition } @@ -216,12 +216,12 @@ export const VSlideGroup = genericComponent( function onFocusin (e: FocusEvent) { isFocused.value = true - if (!isOverflowing.value || !contentRef.value) return + if (!isOverflowing.value || !contentRef.el) return // Focused element is likely to be the root of an item, so a // breadth-first search will probably find it in the first iteration for (const el of e.composedPath()) { - for (const item of contentRef.value.children) { + for (const item of contentRef.el.children) { if (item === el) { scrollToChildren(item as HTMLElement) return @@ -240,7 +240,7 @@ export const VSlideGroup = genericComponent( if ( !ignoreFocusEvent && !isFocused.value && - !(e.relatedTarget && contentRef.value?.contains(e.relatedTarget as Node)) + !(e.relatedTarget && contentRef.el?.contains(e.relatedTarget as Node)) ) focus() ignoreFocusEvent = false @@ -251,7 +251,7 @@ export const VSlideGroup = genericComponent( } function onKeydown (e: KeyboardEvent) { - if (!contentRef.value) return + if (!contentRef.el) return function toFocus (location: Parameters[0]) { e.preventDefault() @@ -280,25 +280,25 @@ export const VSlideGroup = genericComponent( } function focus (location?: 'next' | 'prev' | 'first' | 'last') { - if (!contentRef.value) return + if (!contentRef.el) return let el: HTMLElement | undefined if (!location) { - const focusable = focusableChildren(contentRef.value) + const focusable = focusableChildren(contentRef.el) el = focusable[0] } else if (location === 'next') { - el = contentRef.value.querySelector(':focus')?.nextElementSibling as HTMLElement | undefined + el = contentRef.el.querySelector(':focus')?.nextElementSibling as HTMLElement | undefined if (!el) return focus('first') } else if (location === 'prev') { - el = contentRef.value.querySelector(':focus')?.previousElementSibling as HTMLElement | undefined + el = contentRef.el.querySelector(':focus')?.previousElementSibling as HTMLElement | undefined if (!el) return focus('last') } else if (location === 'first') { - el = (contentRef.value.firstElementChild as HTMLElement) + el = (contentRef.el.firstElementChild as HTMLElement) } else if (location === 'last') { - el = (contentRef.value.lastElementChild as HTMLElement) + el = (contentRef.el.lastElementChild as HTMLElement) } if (el) { @@ -314,8 +314,8 @@ export const VSlideGroup = genericComponent( let newPosition = scrollOffset.value + offsetStep // TODO: improve it - if (isHorizontal.value && isRtl.value && containerRef.value) { - const { scrollWidth, offsetWidth: containerWidth } = containerRef.value! + if (isHorizontal.value && isRtl.value && containerRef.el) { + const { scrollWidth, offsetWidth: containerWidth } = containerRef.el! newPosition += scrollWidth - containerWidth } @@ -366,8 +366,8 @@ export const VSlideGroup = genericComponent( const hasNext = computed(() => { if (!containerRef.value) return false - const scrollSize = getScrollSize(isHorizontal.value, containerRef.value) - const clientSize = getClientSize(isHorizontal.value, containerRef.value) + const scrollSize = getScrollSize(isHorizontal.value, containerRef.el) + const clientSize = getClientSize(isHorizontal.value, containerRef.el) const scrollSizeMax = scrollSize - clientSize diff --git a/packages/vuetify/src/components/VVirtualScroll/VVirtualScrollItem.tsx b/packages/vuetify/src/components/VVirtualScroll/VVirtualScrollItem.tsx index 28cb31df9ca..a7327248cf0 100644 --- a/packages/vuetify/src/components/VVirtualScroll/VVirtualScrollItem.tsx +++ b/packages/vuetify/src/components/VVirtualScroll/VVirtualScrollItem.tsx @@ -7,8 +7,7 @@ import { watch } from 'vue' import { genericComponent, propsFactory, useRender } from '@/util' // Types -import type { Ref } from 'vue' -import type { GenericProps } from '@/util' +import type { GenericProps, TemplateRef } from '@/util' export const makeVVirtualScrollItemProps = propsFactory({ renderless: Boolean, @@ -22,7 +21,7 @@ export const VVirtualScrollItem = genericComponent + itemRef: TemplateRef } : never } ) => GenericProps>()({ diff --git a/packages/vuetify/src/composables/resizeObserver.ts b/packages/vuetify/src/composables/resizeObserver.ts index f9a6db1a0af..3652e258872 100644 --- a/packages/vuetify/src/composables/resizeObserver.ts +++ b/packages/vuetify/src/composables/resizeObserver.ts @@ -1,18 +1,19 @@ // Utilities import { onBeforeUnmount, readonly, ref, watch } from 'vue' -import { refElement } from '@/util' +import { templateRef } from '@/util' import { IN_BROWSER } from '@/util/globals' // Types import type { DeepReadonly, Ref } from 'vue' +import type { TemplateRef } from '@/util' interface ResizeState { - resizeRef: Ref + resizeRef: TemplateRef contentRect: DeepReadonly> } export function useResizeObserver (callback?: ResizeObserverCallback, box: 'content' | 'border' = 'content'): ResizeState { - const resizeRef = ref() + const resizeRef = templateRef() const contentRect = ref() if (IN_BROWSER) { @@ -32,13 +33,13 @@ export function useResizeObserver (callback?: ResizeObserverCallback, box: 'cont observer.disconnect() }) - watch(resizeRef, (newValue, oldValue) => { + watch(() => resizeRef.el, (newValue, oldValue) => { if (oldValue) { - observer.unobserve(refElement(oldValue) as Element) + observer.unobserve(oldValue) contentRect.value = undefined } - if (newValue) observer.observe(refElement(newValue) as Element) + if (newValue) observer.observe(newValue) }, { flush: 'post', }) diff --git a/packages/vuetify/src/util/helpers.ts b/packages/vuetify/src/util/helpers.ts index cb60134d49a..cfff79aa004 100644 --- a/packages/vuetify/src/util/helpers.ts +++ b/packages/vuetify/src/util/helpers.ts @@ -735,3 +735,26 @@ export function isClickInsideElement (event: MouseEvent, targetDiv: HTMLElement) return mouseX >= divLeft && mouseX <= divRight && mouseY >= divTop && mouseY <= divBottom } + +export type TemplateRef = { + (target: Element | ComponentPublicInstance | null): void + value: HTMLElement | ComponentPublicInstance | null | undefined + readonly el: HTMLElement | undefined +} +export function templateRef () { + const el = shallowRef() + const fn = (target: HTMLElement | ComponentPublicInstance | null) => { + el.value = target + } + Object.defineProperty(fn, 'value', { + enumerable: true, + get: () => el.value, + set: val => el.value = val, + }) + Object.defineProperty(fn, 'el', { + enumerable: true, + get: () => refElement(el.value), + }) + + return fn as TemplateRef +}