121 lines
3.4 KiB
TypeScript
121 lines
3.4 KiB
TypeScript
import { useMemoizedFn } from 'ahooks';
|
|
import { useCallback, useRef, useState } from 'react';
|
|
|
|
interface ScrollOptions {
|
|
wait?: number;
|
|
behavior?: 'smooth' | 'auto';
|
|
block?: 'start' | 'end' | 'center';
|
|
offsetTop?: number;
|
|
}
|
|
|
|
export default function useScrollAfterExpand({
|
|
form,
|
|
activeKey,
|
|
setActiveKey,
|
|
segmentOptions,
|
|
defaultWait = 300,
|
|
segmentedTop = { top: 0, offsetTop: 96 },
|
|
getScrollElementScrollableHeight
|
|
}: {
|
|
form: any;
|
|
activeKey: string[];
|
|
setActiveKey: React.Dispatch<React.SetStateAction<string[]>>;
|
|
segmentOptions: { value: string; field: string }[];
|
|
getScrollElementScrollableHeight?: () => {
|
|
scrollHeight: number;
|
|
scrollTop: number;
|
|
};
|
|
defaultWait?: number;
|
|
segmentedTop: {
|
|
top: number;
|
|
offsetTop: number;
|
|
};
|
|
}) {
|
|
const [holderHeight, setHolderHeight] = useState<number>(0);
|
|
const boxHeightRef = useRef<number>(0);
|
|
|
|
const scrollToElement = useCallback(
|
|
(
|
|
el: HTMLElement,
|
|
{ behavior = 'smooth', offsetTop = 0 }: ScrollOptions = {}
|
|
) => {
|
|
// find the nearest scrollable parent
|
|
const scrollParent = (() => {
|
|
let node: HTMLElement | null = el;
|
|
while (node) {
|
|
const { overflowY } = getComputedStyle(node);
|
|
if (overflowY === 'auto' || overflowY === 'scroll') return node;
|
|
node = node.parentElement;
|
|
}
|
|
return document.scrollingElement || document.documentElement;
|
|
})();
|
|
|
|
const parentRect = scrollParent.getBoundingClientRect();
|
|
const elRect = el.getBoundingClientRect();
|
|
const top =
|
|
elRect.top - parentRect.top + scrollParent.scrollTop - offsetTop;
|
|
|
|
scrollParent.scrollTo({ top, behavior });
|
|
},
|
|
[]
|
|
);
|
|
|
|
/**
|
|
* due to the scrollheight changes after expanding the segment and including the holder height.
|
|
*
|
|
*/
|
|
const scrollToSegment = useMemoizedFn(
|
|
async (val: string, options?: ScrollOptions) => {
|
|
if (!activeKey.includes(val)) {
|
|
setActiveKey((prev) => [...prev, val]);
|
|
await new Promise((r) => {
|
|
setTimeout(r, options?.wait ?? defaultWait);
|
|
});
|
|
}
|
|
|
|
const current = segmentOptions.find((item) => item.value === val);
|
|
if (!current?.field) return;
|
|
|
|
await new Promise(requestAnimationFrame);
|
|
|
|
const el: HTMLElement | null = document.querySelector(
|
|
`[data-field="${current.field}"]`
|
|
) as HTMLElement | null;
|
|
|
|
const targetRectTop = el?.getBoundingClientRect().top || 0;
|
|
|
|
const scroller = getScrollElementScrollableHeight?.() || {
|
|
scrollHeight: 0,
|
|
scrollTop: 0
|
|
};
|
|
|
|
// remaining scroll height
|
|
const remainingScrollHeight = scroller.scrollHeight - scroller.scrollTop;
|
|
|
|
// total distance from the top of the scroller to the target element
|
|
const offsetDistance =
|
|
targetRectTop - segmentedTop.offsetTop - segmentedTop.top;
|
|
|
|
let boxHeight = 0;
|
|
|
|
// verify boxHeight is correct, if setting the boxHeight causes the element to be hidden, use the previous boxHeight
|
|
if (offsetDistance <= 0) {
|
|
boxHeight = boxHeightRef.current;
|
|
} else {
|
|
boxHeight =
|
|
offsetDistance - remainingScrollHeight + boxHeightRef.current;
|
|
}
|
|
|
|
// update boxHeightRef
|
|
boxHeightRef.current = boxHeight;
|
|
|
|
setHolderHeight(boxHeight);
|
|
await new Promise(requestAnimationFrame);
|
|
|
|
if (el) scrollToElement(el, options);
|
|
}
|
|
);
|
|
|
|
return { scrollToSegment, holderHeight };
|
|
}
|