移动端 antdMobile tabs + 锚点滚动定位功能

一、需求

antdMobile tabs 当锚点使用中,发现存在一个问题: 最后一个锚点的内容如果高度很小,则选不到最后一个节点。
增加功能如下:
1、如果已经滚动到最后了,直接选中最后一个节点(参考百度百科的锚点效果)
2、当需要滚动到第二个区域块才显示tabs,如果只有一个锚点或者在一个区域内是没有的。增加这种需求的标志位firstHide

二、组件

该组件基于react+antd-mobile,用于锚点滚动定位, 滚动部分是组件的children。
适用情况:滚动区域只有上下滚动没有左右滚动, 允许子组件滚动

  • 1、如何解决tabs的左右滚动和区域的上下滚动冲突的问题, 上下滚动引起tab的左右滚动, 导致原本的上下滚动事件被影响:
    解决方案:只监听上下滚动的区域,即保证这个区域没有左右滚动 overflow-x:hidden;。暂时没有第二种办法
  • 2、加{behavior: ‘smooth’} 有问题, 与scrollTo冲突,暂时没找到解决办法
import React, { useState, useEffect, ReactNode } from 'react';
import { Tabs } from 'antd-mobile';
import styles from './index.less';
import { useThrottleFn } from 'ahooks';

interface Item {
  key: string;
  title: string;
}
interface Params {
  items: Array<Item>;
  contentTop: number;
  offset: number;
  children: ReactNode;
  firstHide: boolean;
}
function Anchor(props: Params) {
  const { items, children, contentTop, offset, firstHide } = props;
  const [activeKey, setActiveKey] = useState('');
  const [showMenu, setShowMenu] = useState(false);

  // 下面antd tab的默认高度
  const tabHeight = 39; // 注意这里加了border的高度, 38 + 1
  const offsetH = contentTop + tabHeight + offset;
  const lastItem = items[items.length - 1];
  const goAssignBlock = (idName) => {
    // FIXME 加{behavior: 'smooth'} 有问题, 与下面的scrollTo冲突
    document.getElementById(idName)?.scrollIntoView();

    // 滚动到指定位置
    const node = document.getElementById('scrollContent');
    let top = node.scrollTop - tabHeight;
    // 兼容最后内容没有超过可显示区域的情况
    if (idName === lastItem.key) {
      const lastNode = document.getElementById(idName);
      if (lastNode.clientHeight < screen.height - contentTop - tabHeight) {
        top = node.scrollTop;
      }
    }
    node.scrollTo({ top });
  };

  // 设置tab是否显示
  const setMenuHide = (index: number) => {
    if (index === 0 && firstHide) {
      setShowMenu(false);
    } else {
      setShowMenu(true);
    }
  };

  useEffect(() => {
    if (items?.length > 0) {
      setMenuHide(0);
      setActiveKey(items[0].key);
    }
  }, [items]);

  // 滚动事件
  const handleScroll = (e) => {
    // 处理滚动内容内部的子组件有局部滚动的情况,不让它影响锚点
    if(e.target.id === 'scrollContent') return;
    
    const {
      scrollTop,
      clientHeight,
      scrollHeight,
    }: { scrollTop: number; clientHeight: number; scrollHeight: number } =
      e.target;

    // 滚动条到最底部了,直接选最后一个锚点
    // 23/5/31 补充修改: 手机端存在scrollTop有小数的情况,导致等号不成立
    // scrollTop + clientHeight === scrollHeight 修改为 如下
    if (scrollTop && scrollTop + 1 + clientHeight >= scrollHeight) {
      setMenuHide(items.length - 1);
      setActiveKey(lastItem.key);
      return;
    }

    // 滚动定位
    let currentKey = items[0].key;
    let index = 0;
    for (let idx = 0; idx < items.length; idx++) {
      const item = items[idx];
      const element = document.getElementById(`${item.key}`);
      if (!element) continue;
      const rect = element.getBoundingClientRect();
      if (rect.top <= offsetH) {
        currentKey = item.key;
        index = idx;
      } else {
        break;
      }
    }
    setMenuHide(index);
    setActiveKey(currentKey);
  };

  const { run: handleThrottleScroll } = useThrottleFn((e) => handleScroll(e), {
    leading: true,
    trailing: true,
    wait: 100,
  });

  useEffect(() => {
    const node = document.getElementById('scrollContent');
    if (node) {
      node.addEventListener('scroll', handleThrottleScroll, true);
      return () => {
        node.removeEventListener('scroll', handleThrottleScroll, true);
      };
    }
  }, [items]);

  return (
    <div className='h-full'>
      <div className='w-full absolute z-10' style={{ top: `${contentTop}px` }}>
        <Tabs
          activeLineMode='fixed'
          style={{
            '--fixed-active-line-width': '56px',
            visibility: showMenu ? 'visible' : 'hidden',
          }}
          activeKey={activeKey}
          onChange={(key) => goAssignBlock(key)}
          className={styles.active_tab}>
          {items.map(({ key, title }) => (
            <Tabs.Tab key={key} title={title}></Tabs.Tab>
          ))}
        </Tabs>
      </div>
      <div id='scrollContent' className='h-full w-full overflow-auto'>
        {children}
      </div>
    </div>
  );
}

Anchor.defaultProps = {
  items: [], // 列表
  contentTop: 45, // 菜单栏高度, 这里为滚动区域距离可视区域的高度
  offset: 0, // 偏移量,用于调整滚动过程中,滚动距离导致锚点的选中变化
  firstHide: true, // 第一项不显示
};

export default Anchor;

样式放在最后

三、使用

1、基本使用

// 定义8个锚点
const items = [{
     key: 'part-1',
     title: 'Part 1',
   },
   ....
   {
     key: 'part-8',
     title: 'Part 8',
   }]
   
  // 内容区- (菜单栏默认是整个系统固定头,不在内容区内, 高度为 45)
 <div className='h-full overflow-hidden'>
 	{/* 固定区域 高度为 72*/}
 	<div className='bg-yellow h-s72'>这里是一些不滚动的内容div>
 	
 	{/* anchor组件使用示例 */}
	 <div style={{ height: `calc(100% - 72px)` }}>
	   {/* 滚动区域距离顶部contentTop: 45 + 72 = 117*/}
	   <Anchor items={items} contentTop={117}>
	     <div className='bg-green h-s32'>滚动里面包括了非锚点内容块div>
	     <div className='h-full'>
	       {items.map((i) => (
	         <p className='h-96' key={i.key} id={i.key}>
	           {i.title}
	         p>
	       ))}
	     div>
	   Anchor>
	 div>
 div>

初始效果:
移动端 antdMobile tabs + 锚点滚动定位功能_第1张图片
滚动效果
移动端 antdMobile tabs + 锚点滚动定位功能_第2张图片

固定tabs

由于tabs是absolute脱离文档流的,如果在第一个区域的时候也要出现,那么需要把tabs的区域空出来,比如下面:第一个内容块设置margin-top:39px

<Anchor items={items} contentTop={117} firstHide={false}>
   <div className='bg-green h-s32 mt-s39'>滚动里面包括了非锚点内容块div>
   。。。其他锚点内容
<Anchor />      

四、样式文件

.active_tab{
  height: 38px;
  background-color: #fff;
  :global(.adm-tabs-tab-wrapper) {
    padding: 0;
  }
  :global(.adm-tabs-tab){
    padding:8px;
  }

  :global(.adm-tabs-tab-active) {
    font-weight: bold;
    color: #132240;
    font-size: 14px;
    line-height: 1.6;
    letter-spacing: 0px;
    text-align: left;
    
  }
  :global(.adm-tabs-tab-line) {
    bottom: 8px;
    height: 8px;
    border-radius: 5px;
    background: linear-gradient(91.16deg, #BBDCFF 3%, #739EFF 35%, #4E6AFF 72%, #AAA3FF 100%, #AAA3FF 100%);
    background-repeat: no-repeat;
    // background-position: 0 15px;
  }
}

你可能感兴趣的:(react,前端实践,javascript,react,锚点定位)