antdMobile tabs 当锚点使用中,发现存在一个问题: 最后一个锚点的内容如果高度很小,则选不到最后一个节点。
增加功能如下:
1、如果已经滚动到最后了,直接选中最后一个节点(参考百度百科的锚点效果)
2、当需要滚动到第二个区域块才显示tabs,如果只有一个锚点或者在一个区域内是没有的。增加这种需求的标志位firstHide
该组件基于react+antd-mobile,用于锚点滚动定位, 滚动部分是组件的children。
适用情况:滚动区域只有上下滚动没有左右滚动, 允许子组件滚动
overflow-x:hidden;
。暂时没有第二种办法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;
样式放在最后
// 定义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>
由于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;
}
}