Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: pass template refs to slots as functions #19731

Merged
merged 9 commits into from
May 7, 2024
1 change: 1 addition & 0 deletions packages/api-generator/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,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
}
Loading