UNPKG

@ant-design/x

Version:

Craft AI-driven interfaces effortlessly

171 lines (166 loc) 6.11 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.useCompatibleScroll = useCompatibleScroll; var _react = require("react"); function isReverse(scrollDom) { return getComputedStyle(scrollDom).flexDirection === 'column-reverse'; } /** * Safari 兼容的倒序滚动视窗锁定与 scrollTo 方法适配 * @param {HTMLElement} scrollDom - 倒序滚动元素 * @param {HTMLElement} contentDom - 滚动内容容器元素 */ function useCompatibleScroll(scrollDom, contentDom) { // 底部哨兵 const sentinelRef = (0, _react.useRef)(null); const sentinelHeight = 10; const isAtBottom = (0, _react.useRef)(true); const shouldLock = (0, _react.useRef)(false); const lockedScrollBottomPos = (0, _react.useRef)(0); const scrolling = (0, _react.useRef)(undefined); const callOnScrollNotNative = (0, _react.useRef)(false); const isScrollToBottom = (0, _react.useRef)(false); // 初始化哨兵元素 (0, _react.useLayoutEffect)(() => { if (!scrollDom || !contentDom) return; if (!sentinelRef.current) { const sentinel = document.createElement('div'); // sentinel.style.position = 'absolute'; sentinel.style.bottom = '0'; sentinel.style.flexShrink = '0'; sentinel.style.pointerEvents = 'none'; sentinel.style.height = `${sentinelHeight}px`; sentinel.style.visibility = 'hidden'; scrollDom.insertBefore(sentinel, scrollDom.firstChild); sentinelRef.current = sentinel; } const intersectionObserver = new IntersectionObserver(([entry]) => { isAtBottom.current = entry.isIntersecting; shouldLock.current = !entry.isIntersecting; }, { root: scrollDom, threshold: 0.0 }); intersectionObserver.observe(sentinelRef.current); // 监听 DOM 内容变化,锁定视窗 const resizeObserver = new ResizeObserver(() => { if (!scrollDom) return; // 内容变化时正在滚动,交互优先,不锁定视窗 if (scrolling.current) { // 动态处理滚动到底 isScrollToBottom.current && requestAnimationFrame(() => scrollDom.scrollTo({ top: isReverse(scrollDom) ? 0 : scrollDom.scrollHeight, behavior: 'instant' })); return; } isReverse(scrollDom) && shouldLock.current && enforceScrollLock(); }); resizeObserver.observe(contentDom); return () => { intersectionObserver.disconnect(); resizeObserver.disconnect(); clearTimeout(scrolling.current); if (sentinelRef.current?.parentNode) { sentinelRef.current.parentNode.removeChild(sentinelRef.current); sentinelRef.current = null; } }; }, [scrollDom, contentDom]); const setTimer = (0, _react.useCallback)(() => { scrolling.current = setTimeout(() => { clearTimeout(scrolling.current); scrolling.current = undefined; isScrollToBottom.current = false; }, 50); }, []); const handleScroll = (0, _react.useCallback)(e => { const target = e.target; if (!isReverse(target)) return; const { scrollTop, scrollHeight } = target; // 倒序, top 在变化,但 bottom 固定 lockedScrollBottomPos.current = scrollHeight + scrollTop; // 检测并恢复自然触发状态 if (callOnScrollNotNative.current) { callOnScrollNotNative.current = false; return; } if (scrolling.current) { clearTimeout(scrolling.current); } setTimer(); }, [setTimer]); (0, _react.useLayoutEffect)(() => { if (!scrollDom) return; scrollDom.addEventListener('scroll', handleScroll, { capture: true }); return () => scrollDom?.removeEventListener('scroll', handleScroll, { capture: true }); }, [scrollDom, handleScroll]); // 强制锁定滚动位置 const enforceScrollLock = (0, _react.useCallback)(() => { /** * 同时发生滚动+内容变化,在 safari 内有两种可选行为: * 1、强制锁定视窗,可视内容不变,但会造成滚动抖动。 * 2、不锁定视窗,内容会变化。 * 出于鲁棒性考虑,选择行为2,在滚动结束后再锁视窗 * 最终效果: * 1、滚动+内容变化同时发生,表现为浏览器默认行为 * 2、仅内容变化,表现为 chrome 行为(视窗锁定)(无论是否贴底) **/ const targetScroll = lockedScrollBottomPos.current - scrollDom.scrollHeight; scrollDom.scrollTop = targetScroll; // 赋值 scrollTop 会立即触发 onScroll callOnScrollNotNative.current = true; }, [scrollDom]); const reset = (0, _react.useCallback)(() => { isAtBottom.current = true; shouldLock.current = false; lockedScrollBottomPos.current = scrollDom?.scrollHeight || 0; }, [scrollDom]); const scrollTo = (0, _react.useCallback)(option => { if (!scrollDom || !contentDom) return; const { top, intoView, intoViewDom } = option || {}; if (isReverse(scrollDom)) { if (top !== undefined && top >= -sentinelHeight) { isScrollToBottom.current = true; } else if (intoViewDom && intoView?.block === 'end') { isScrollToBottom.current = contentDom.lastElementChild === intoViewDom; } else { isScrollToBottom.current = false; } } else { if (top !== undefined && top >= scrollDom.scrollHeight - scrollDom.clientHeight - sentinelHeight) { isScrollToBottom.current = true; } else if (intoViewDom && intoView?.block === 'end') { isScrollToBottom.current = contentDom.lastElementChild === intoViewDom; } else { isScrollToBottom.current = false; } } // 立即进入滚动状态,提升 api 滚动行为的优先级,避免在同时存在内容增长的情况下,api 滚动行为被强制锁定视窗的行为覆盖 if (!scrolling.current) { setTimer(); } if (intoViewDom) { intoViewDom.scrollIntoView(intoView); } else { scrollDom.scrollTo(option); } }, [scrollDom, contentDom]); return { reset, scrollTo }; }