Skip to content

滚动感知 useScrollParent

查询并监听最近可滚动父元素,响应视口 resize 的组合式函数

该组合式函数被 SelectAutoCompleteTooltip 等弹出类组件内部使用,用于在可滚动容器内正确跟随滚动并维护定位;同时也可独立复用。

Show Source Code
ts
import { ref, toValue, computed, watch, onBeforeUnmount, onMounted } from 'vue'
import type { Ref } from 'vue'
import { useOptionsSupported, useEventListener, useMutationObserver, getScrollParent } from 'vue-amazing-ui'
export interface ScrollParentOptions {
  passive?: boolean // 是否使用 passive 滚动监听,默认跟随浏览器支持情况
  onCleanup?: () => void // 附加清理:组件自身需在 cleanup 时执行的逻辑(如 Tooltip 的 cancelRaf)
}
export function useScrollParent(
  contentRef: Ref<HTMLElement | null>,
  onScroll: () => void,
  options: ScrollParentOptions = {}
): {
  scrollTarget: Ref<HTMLElement | null>
  viewportWidth: Ref<number>
  viewportHeight: Ref<number>
  observeScroll: () => void
  cleanup: () => void
} {
  const scrollTarget = ref<HTMLElement | null>(null) // 最近的可滚动父元素
  const scrollTop = ref<number>(0) // scrollTarget 的滚动位置
  const viewportWidth = ref(document.documentElement.clientWidth)
  const viewportHeight = ref(document.documentElement.clientHeight)
  const { isSupported: passiveSupported } = useOptionsSupported('passive')
  const usePassive = options.passive !== false && passiveSupported.value

  // vitepress 文档页滚动监听(scrollTarget 为 documentElement 时启用)
  const mutationObserver = useMutationObserver(
    scrollTarget,
    () => {
      if (scrollTop.value !== scrollTarget.value?.scrollTop) {
        scrollTop.value = scrollTarget.value?.scrollTop ?? 0
        onScroll()
      }
    },
    { subtree: true, attributes: true }
  )

  // 更新视口尺寸,重新查询滚动父元素并触发重排
  function getViewportSize() {
    viewportWidth.value = document.documentElement.clientWidth
    viewportHeight.value = document.documentElement.clientHeight
    observeScroll()
    onScroll()
  }

  // 查询并监听最近可滚动父元素
  function observeScroll() {
    cleanup()
    scrollTarget.value = getScrollParent(contentRef.value)
    scrollTarget.value?.addEventListener('scroll', onScroll, usePassive ? { passive: true } : undefined)
    if (scrollTarget.value === document.documentElement) {
      mutationObserver.start()
    } else {
      mutationObserver.stop()
    }
  }

  // 清理滚动监听并重置滚动目标(含组件注入的附加清理)
  function cleanup() {
    scrollTarget.value?.removeEventListener('scroll', onScroll)
    scrollTarget.value = null
    options.onCleanup?.()
  }

  useEventListener(window, 'resize', getViewportSize)
  onMounted(observeScroll)
  onBeforeUnmount(cleanup)

  return { scrollTarget, viewportWidth, viewportHeight, observeScroll, cleanup }
}

基本使用

实现弹出面板在可滚动容器内跟随滚动

vue
<script setup lang="ts">
import { ref } from 'vue'
import { useScrollParent } from 'vue-amazing-ui'
const contentRef = ref<HTMLElement | null>(null)
const { scrollTarget, viewportWidth, viewportHeight } = useScrollParent(contentRef, () => {
  console.log('scrolled')
})
</script>
<template>
  <div class="scroll-container">
    <div ref="contentRef">Content</div>
  </div>
</template>

Params

参数说明类型默认值
contentRef触发器内容元素,用于向上查找可滚动父元素Ref<HTMLElement | null>undefined
onScroll滚动/resize 触发的回调函数() => voidundefined
options配置项ScrollParentOptions{}

ScrollParentOptions

参数说明类型默认值
passive是否使用 passive 滚动监听,默认跟随浏览器支持情况booleanfalse
onCleanup附加清理,组件自身需在 cleanup 时执行的逻辑() => voidundefined

Return

名称说明类型
scrollTarget最近的可滚动父元素Ref<HTMLElement | null>
viewportWidth视口宽度Ref<number>
viewportHeight视口高度Ref<number>
observeScroll查询并监听最近可滚动父元素() => void
cleanup清理滚动监听并重置滚动目标() => void

Released under the MIT License.