参照Vant中的Sticky实现,api与Vant中的保持一致。规避了直接使用position: fixed的各种限制问题。
基本说明
- 其中
useUpdateEffect和getScrollParent来源与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,
height: 0,
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]);
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);
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;