Skip to content

Commit

Permalink
fix: pass template refs to slots as functions (#19731)
Browse files Browse the repository at this point in the history
fixes #19713
fixes #19685

Co-authored-by: Kael <[email protected]>
  • Loading branch information
yuwu9145 and KaelWD authored May 7, 2024
1 parent 0db4297 commit f2c6050
Show file tree
Hide file tree
Showing 10 changed files with 74 additions and 52 deletions.
1 change: 1 addition & 0 deletions packages/api-generator/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,7 @@ const allowedRefs = [
'SelectStrategyFn',
'SortItem',
'SubmitEventPromise',
'TemplateRef',
'TouchHandlers',
'ValidationRule',
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> = Omit<
VDataTableSlotProps<T>,
Expand All @@ -46,7 +45,7 @@ export type VDataTableVirtualSlots<T> = VDataTableRowsSlots<T> & VDataTableHeade
'body.prepend': VDataTableVirtualSlotProps<T>
'body.append': VDataTableVirtualSlotProps<T>
item: {
itemRef: Ref<HTMLElement | undefined>
itemRef: TemplateRef
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -87,10 +87,11 @@ export const VDatePickerYears = genericComponent<VDatePickerYearsSlots>()({
model.value = model.value ?? adapter.getYear(adapter.date())
})

const yearRef = ref<VBtn>()
const yearRef = templateRef()

onMounted(async () => {
await nextTick()
yearRef.value?.$el.scrollIntoView({ block: 'center' })
yearRef.el?.scrollIntoView({ block: 'center' })
})

useRender(() => (
Expand Down
8 changes: 3 additions & 5 deletions packages/vuetify/src/components/VOverlay/VOverlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -77,7 +75,7 @@ function Scrim (props: ScrimProps) {

export type OverlaySlots = {
default: { isActive: Ref<boolean> }
activator: { isActive: boolean, props: Record<string, any>, targetRef: Ref<ComponentPublicInstance<any> | HTMLElement> }
activator: { isActive: boolean, props: Record<string, any>, targetRef: TemplateRef }
}

export const makeVOverlayProps = propsFactory({
Expand Down
10 changes: 5 additions & 5 deletions packages/vuetify/src/components/VOverlay/useActivator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {
IN_BROWSER,
matchesSelector,
propsFactory,
refElement,
templateRef,
unbindProps,
} from '@/util'

Expand Down Expand Up @@ -228,19 +228,19 @@ export function useActivator (
}
}, { flush: 'post' })

const activatorRef = ref<HTMLElement>()
const activatorRef = templateRef()
watchEffect(() => {
if (!activatorRef.value) return

nextTick(() => {
activatorEl.value = refElement(activatorRef.value)
activatorEl.value = activatorRef.el
})
})

const targetRef = ref<ComponentPublicInstance<any> | 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(() => {
Expand Down
50 changes: 25 additions & 25 deletions packages/vuetify/src/components/VSlideGroup/VSlideGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ export const VSlideGroup = genericComponent<new <T>(
const goTo = useGoTo()
const goToOptions = computed<Partial<GoToOptions>>(() => {
return {
container: containerRef.value,
container: containerRef.el,
duration: 200,
easing: 'easeOutQuart',
}
Expand Down Expand Up @@ -148,9 +148,9 @@ export const VSlideGroup = genericComponent<new <T>(
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)
}
Expand All @@ -165,13 +165,13 @@ export const VSlideGroup = genericComponent<new <T>(

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,
Expand All @@ -182,20 +182,20 @@ export const VSlideGroup = genericComponent<new <T>(
}

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 ||
// Prevent scrolling by only a couple of pixels, which doesn't look smooth
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
}
Expand All @@ -216,12 +216,12 @@ export const VSlideGroup = genericComponent<new <T>(
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
Expand All @@ -240,7 +240,7 @@ export const VSlideGroup = genericComponent<new <T>(
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
Expand All @@ -251,7 +251,7 @@ export const VSlideGroup = genericComponent<new <T>(
}

function onKeydown (e: KeyboardEvent) {
if (!contentRef.value) return
if (!contentRef.el) return

function toFocus (location: Parameters<typeof focus>[0]) {
e.preventDefault()
Expand Down Expand Up @@ -280,25 +280,25 @@ export const VSlideGroup = genericComponent<new <T>(
}

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) {
Expand All @@ -314,8 +314,8 @@ export const VSlideGroup = genericComponent<new <T>(
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
}
Expand Down Expand Up @@ -366,8 +366,8 @@ export const VSlideGroup = genericComponent<new <T>(
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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -22,7 +21,7 @@ export const VVirtualScrollItem = genericComponent<new <Renderless extends boole
},
slots: {
default: Renderless extends true ? {
itemRef: Ref<HTMLElement | undefined>
itemRef: TemplateRef
} : never
}
) => GenericProps<typeof props, typeof slots>>()({
Expand Down
13 changes: 7 additions & 6 deletions packages/vuetify/src/composables/resizeObserver.ts
Original file line number Diff line number Diff line change
@@ -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<HTMLElement | undefined>
resizeRef: TemplateRef
contentRect: DeepReadonly<Ref<DOMRectReadOnly | undefined>>
}

export function useResizeObserver (callback?: ResizeObserverCallback, box: 'content' | 'border' = 'content'): ResizeState {
const resizeRef = ref<HTMLElement>()
const resizeRef = templateRef()
const contentRect = ref<DOMRectReadOnly>()

if (IN_BROWSER) {
Expand All @@ -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',
})
Expand Down
23 changes: 23 additions & 0 deletions packages/vuetify/src/util/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLElement | ComponentPublicInstance | null>()
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
}

0 comments on commit f2c6050

Please sign in to comment.