Sticky组件的React版本

519 阅读1分钟

参照Vant中的Sticky实现,api与Vant中的保持一致。规避了直接使用position: fixed的各种限制问题。

基本说明

  • 其中useUpdateEffectgetScrollParent来源与ahooks和antd-mobile-v5
  • 其中ob-sticky--fixed的样式为position: fixed;

代码如下

import './index.less';
import { FC, useEffect, useMemo, useRef, useState } from 'react';
import { useUpdateEffect } from 'ahooks';
import { getScrollParent } from 'antd-mobile-v5/es/utils/get-scroll-parent';
interface IObStickyProps {
  position?: string;
  offsetTop?: number;
  offsetBottom?: number;
  zIndex?: number;
  container?: Element;
  scroll?: (scrollTop: number, isFixed: boolean) => void;
  change?: (isFixed: boolean) => void;
}
const ObSticky: FC<IObStickyProps> = ({
  position = 'top',
  offsetTop = 0,
  offsetBottom = 0,
  zIndex = 99,
  children,
  container,
  scroll,
  change,
}) => {
  const root = useRef<HTMLDivElement>(null);
  let [state, setState] = useState({
    fixed: false,
    width: 0, // root width
    height: 0, // root height
    transform: 0,
  });
  let offset = useMemo(() => {
    return position === 'top' ? offsetTop : offsetBottom;
  }, [position]);
  let rootStyle = useMemo(() => {
    const { fixed, height, width } = state;
    if (fixed) {
      return {
        width: `${width}px`,
        height: `${height}px`,
      };
    }
  }, [state]);
  const stickyStyle = useMemo(() => {
    if (!state.fixed) {
      return;
    }
    const style = {
      width: `${state.width}px`,
      height: `${state.height}px`,
      [position]: `${offset}px`,
      zIndex: zIndex,
    };
    if (state.transform) {
      style.transform = `translate3d(0, ${state.transform}px, 0)`;
    }
    return style;
  }, [state]);
  // 获取元素的scrollTop
  const getScrollTop = (el: Element | Window): number => {
    const top = 'scrollTop' in el ? el.scrollTop : el.pageYOffset;
    return Math.max(top, 0);
  };
  // 判断元素是否隐藏
  const isHidden = (element: HTMLElement | undefined) => {
    if (!element) {
      return false;
    }
    const style = window.getComputedStyle(element);
    const hidden = style.display === 'none';
    const parentHidden =
      element.offsetParent === null && style.position !== 'fixed';
    return hidden || parentHidden;
  };
  // 滚动事件
  const onScroll = () => {
    if (!root.current || isHidden(root.current)) {
      return;
    }
    const rootRect = root.current.getBoundingClientRect();
    const scrollTop = getScrollTop(window);
    // 此种写法是减少set次数
    let currentState = JSON.parse(JSON.stringify(state));
    currentState.width = rootRect.width;
    currentState.height = rootRect.height;
    if (position === 'top') {
      if (container) {
        const containerRect = container.getBoundingClientRect();
        const difference = containerRect.bottom - offset - currentState.height;
        currentState.fixed = offset > rootRect.top && containerRect.bottom > 0;
        currentState.transform = difference < 0 ? difference : 0;
      } else {
        currentState.fixed = offset > rootRect.top;
      }
    } else {
      const { clientHeight } = document.documentElement;
      if (container) {
        const containerRect = container.getBoundingClientRect();
        const difference =
          clientHeight - containerRect.top - offset - currentState.height;
        currentState.fixed =
          clientHeight - offset < rootRect.bottom &&
          clientHeight > containerRect.top;
        currentState.transform = difference < 0 ? -difference : 0;
      } else {
        currentState.fixed = clientHeight - offset < rootRect.bottom;
      }
    }
    setState(currentState);
    // 触发父级事件
    if (scroll) {
      scroll(scrollTop, currentState.fixed);
    }
  };
  useEffect(() => {
    // 监听父级的滚动事件
    const element = root.current;
    if (!element) return;
    const parent = getScrollParent(element);
    if (!parent) return;
    parent.addEventListener('scroll', onScroll);
    return () => {
      parent.removeEventListener('scroll', onScroll);
    };
  }, []);
  // 触发父级事件
  useUpdateEffect(() => {
    if (change) {
      change(state.fixed);
    }
  }, [state.fixed]);
  return (
    <div ref={root} style={rootStyle}>
      <div
        className={state.fixed ? 'ob-sticky--fixed' : ''}
        style={stickyStyle}
      >
        {children}
      </div>
    </div>
  );
};
export default ObSticky;

使用如下

import ObSticky from '@/components/obSticky';
import './demo.less';
function App() {
  let onChange = (isFixed:boolean) => {
    console.log(isFixed);
  };
  return (
    <div className="obPage">
      <div className="obMain">
        <div className="testBox1">测试盒子</div>
        <div className="testBox1">测试盒子</div>
        <div className="testBox1">测试盒子</div>
        <div className="testBox1">测试盒子</div>
        <div className="testBox1">测试盒子</div>
        <ObSticky offsetTop={20} change={onChange}>
          <div className="testBox">测试盒子</div>
        </ObSticky>
        <div className="testBox1">测试盒子</div>
      </div>
    </div>
  );
}
export default App;