UNPKG

antd

Version:

An enterprise-class UI design language and React components implementation

204 lines (203 loc) 7.18 kB
"use client"; import React from 'react'; import ResizeObserver from '@rc-component/resize-observer'; import { clsx } from 'clsx'; import throttleByAnimationFrame from '../_util/throttleByAnimationFrame'; import { ConfigContext, useComponentConfig } from '../config-provider/context'; import useStyle from './style'; import { getFixedBottom, getFixedTop, getTargetRect } from './utils'; const TRIGGER_EVENTS = ['resize', 'scroll', 'touchstart', 'touchmove', 'touchend', 'pageshow', 'load']; function getDefaultTarget() { return typeof window !== 'undefined' ? window : null; } const AFFIX_STATUS_NONE = 0; const AFFIX_STATUS_PREPARE = 1; const Affix = /*#__PURE__*/React.forwardRef((props, ref) => { const { style, offsetTop, offsetBottom, prefixCls, className, rootClassName, children, target, onChange, onTestUpdatePosition, ...restProps } = props; const { getPrefixCls, className: contextClassName, style: contextStyle } = useComponentConfig('affix'); const { getTargetContainer } = React.useContext(ConfigContext); const affixPrefixCls = getPrefixCls('affix', prefixCls); const [lastAffix, setLastAffix] = React.useState(false); const [affixStyle, setAffixStyle] = React.useState(); const [placeholderStyle, setPlaceholderStyle] = React.useState(); const status = React.useRef(AFFIX_STATUS_NONE); const prevTarget = React.useRef(null); const prevListener = React.useRef(null); const placeholderNodeRef = React.useRef(null); const fixedNodeRef = React.useRef(null); const timer = React.useRef(null); const targetFunc = target ?? getTargetContainer ?? getDefaultTarget; const internalOffsetTop = offsetBottom === undefined && offsetTop === undefined ? 0 : offsetTop; // =================== Measure =================== const measure = () => { if (status.current !== AFFIX_STATUS_PREPARE || !fixedNodeRef.current || !placeholderNodeRef.current || !targetFunc) { return; } const targetNode = targetFunc(); if (targetNode) { const newState = { status: AFFIX_STATUS_NONE }; const placeholderRect = getTargetRect(placeholderNodeRef.current); if (placeholderRect.top === 0 && placeholderRect.left === 0 && placeholderRect.width === 0 && placeholderRect.height === 0) { return; } const targetRect = getTargetRect(targetNode); const fixedTop = getFixedTop(placeholderRect, targetRect, internalOffsetTop); const fixedBottom = getFixedBottom(placeholderRect, targetRect, offsetBottom); if (fixedTop !== undefined) { newState.affixStyle = { position: 'fixed', top: fixedTop, width: placeholderRect.width, height: placeholderRect.height }; newState.placeholderStyle = { width: placeholderRect.width, height: placeholderRect.height }; } else if (fixedBottom !== undefined) { newState.affixStyle = { position: 'fixed', bottom: fixedBottom, width: placeholderRect.width, height: placeholderRect.height }; newState.placeholderStyle = { width: placeholderRect.width, height: placeholderRect.height }; } newState.lastAffix = !!newState.affixStyle; if (lastAffix !== newState.lastAffix) { onChange?.(newState.lastAffix); } status.current = newState.status; setAffixStyle(newState.affixStyle); setPlaceholderStyle(newState.placeholderStyle); setLastAffix(newState.lastAffix); } }; const prepareMeasure = () => { status.current = AFFIX_STATUS_PREPARE; measure(); if (process.env.NODE_ENV === 'test') { onTestUpdatePosition?.(); } }; const updatePosition = throttleByAnimationFrame(() => { prepareMeasure(); }); const lazyUpdatePosition = throttleByAnimationFrame(() => { // Check position change before measure to make Safari smooth if (targetFunc && affixStyle) { const targetNode = targetFunc(); if (targetNode && placeholderNodeRef.current) { const targetRect = getTargetRect(targetNode); const placeholderRect = getTargetRect(placeholderNodeRef.current); const fixedTop = getFixedTop(placeholderRect, targetRect, internalOffsetTop); const fixedBottom = getFixedBottom(placeholderRect, targetRect, offsetBottom); if (fixedTop !== undefined && affixStyle.top === fixedTop || fixedBottom !== undefined && affixStyle.bottom === fixedBottom) { return; } } } // Directly call prepare measure since it's already throttled. prepareMeasure(); }); const addListeners = () => { const listenerTarget = targetFunc?.(); if (!listenerTarget) { return; } TRIGGER_EVENTS.forEach(eventName => { if (prevListener.current) { prevTarget.current?.removeEventListener(eventName, prevListener.current); } listenerTarget?.addEventListener(eventName, lazyUpdatePosition); }); prevTarget.current = listenerTarget; prevListener.current = lazyUpdatePosition; }; const removeListeners = () => { const newTarget = targetFunc?.(); TRIGGER_EVENTS.forEach(eventName => { newTarget?.removeEventListener(eventName, lazyUpdatePosition); if (prevListener.current) { prevTarget.current?.removeEventListener(eventName, prevListener.current); } }); updatePosition.cancel(); lazyUpdatePosition.cancel(); }; React.useImperativeHandle(ref, () => ({ updatePosition })); // mount & unmount React.useEffect(() => { // [Legacy] Wait for parent component ref has its value. // We should use target as directly element instead of function which makes element check hard. timer.current = setTimeout(addListeners); return () => { if (timer.current) { clearTimeout(timer.current); timer.current = null; } removeListeners(); }; }, []); React.useEffect(() => { addListeners(); return () => removeListeners(); }, [target, affixStyle, lastAffix, offsetTop, offsetBottom]); React.useEffect(() => { updatePosition(); }, [target, offsetTop, offsetBottom]); const [hashId, cssVarCls] = useStyle(affixPrefixCls); const rootCls = clsx(rootClassName, hashId, affixPrefixCls, cssVarCls); const mergedCls = clsx({ [rootCls]: affixStyle }); return /*#__PURE__*/React.createElement(ResizeObserver, { onResize: updatePosition }, /*#__PURE__*/React.createElement("div", { style: { ...contextStyle, ...style }, className: clsx(className, contextClassName), ref: placeholderNodeRef, ...restProps }, affixStyle && /*#__PURE__*/React.createElement("div", { style: placeholderStyle, "aria-hidden": "true" }), /*#__PURE__*/React.createElement("div", { className: mergedCls, ref: fixedNodeRef, style: affixStyle }, /*#__PURE__*/React.createElement(ResizeObserver, { onResize: updatePosition }, children)))); }); if (process.env.NODE_ENV !== 'production') { Affix.displayName = 'Affix'; } export default Affix;