@lobehub/ui
Version:
Lobe UI is an open-source UI component library for building AIGC web apps
141 lines (140 loc) • 4.99 kB
JavaScript
"use client";
import { useEventCallback } from "../../hooks/useEventCallback.mjs";
import { styles } from "./style.mjs";
import { memo, useEffect, useLayoutEffect, useRef, useState } from "react";
import { jsx, jsxs } from "react/jsx-runtime";
import { cx } from "antd-style";
import useMergeState from "use-merge-value";
import { useMergeRefs } from "react-merge-refs";
//#region src/base-ui/Anchor/Anchor.tsx
const SCROLL_LOCK_FALLBACK_MS = 1e3;
const flatten = (items) => items.flatMap((item) => [item, ...flatten(item.children ?? [])]);
const getTarget = (href) => {
const hash = href.split("#")[1];
if (!hash) return null;
try {
return document.getElementById(decodeURIComponent(hash));
} catch {
return document.getElementById(hash);
}
};
const isWindow = (container) => container === window;
const getRelativeTop = (element, container) => element.getBoundingClientRect().top - (isWindow(container) ? 0 : container.getBoundingClientRect().top);
const getScrollTop = (container) => isWindow(container) ? window.scrollY : container.scrollTop;
const getActiveKey = (items, container, offset) => {
let activeKey = null;
let activeTop = -Infinity;
for (const item of flatten(items)) {
const target = getTarget(item.href);
if (!target) continue;
const top = getRelativeTop(target, container);
if (top <= offset + 1 && top >= activeTop) {
activeKey = item.key;
activeTop = top;
}
}
return activeKey;
};
const Anchor = memo(({ activeKey, className, getContainer, items, offset = 0, onChange, onClick, ref, ...rest }) => {
const [mergedActiveKey, setMergedActiveKey] = useMergeState(null, {
onChange,
value: activeKey
});
const scrollLockRef = useRef(false);
const navRef = useRef(null);
const mergedRef = useMergeRefs([ref, navRef]);
const [marker, setMarker] = useState();
const lockTimerRef = useRef(void 0);
const resolveContainer = useEventCallback(() => getContainer?.() ?? window);
const setActive = useEventCallback(setMergedActiveKey);
const spy = useEventCallback(() => {
if (scrollLockRef.current) return;
setActive(getActiveKey(items, resolveContainer(), offset));
});
useEffect(() => {
const container = resolveContainer();
let frame = 0;
const handleScroll = () => {
cancelAnimationFrame(frame);
frame = requestAnimationFrame(spy);
};
spy();
container.addEventListener("scroll", handleScroll, { passive: true });
return () => {
cancelAnimationFrame(frame);
container.removeEventListener("scroll", handleScroll);
};
}, [
items,
offset,
resolveContainer,
spy
]);
useEffect(() => () => clearTimeout(lockTimerRef.current), []);
useLayoutEffect(() => {
const nav = navRef.current;
if (!nav) return;
const place = () => {
const link = [...nav.querySelectorAll("a[data-key]")].find((element) => element.dataset.key === mergedActiveKey);
setMarker(link ? {
height: link.offsetHeight,
insetBlockStart: link.offsetTop
} : void 0);
};
place();
if (typeof ResizeObserver === "undefined") return;
const observer = new ResizeObserver(place);
observer.observe(nav);
return () => observer.disconnect();
}, [items, mergedActiveKey]);
const handleClick = (event, item) => {
onClick?.(event, item);
if (event.defaultPrevented || event.button !== 0) return;
if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
const target = getTarget(item.href);
if (!target) return;
event.preventDefault();
const container = resolveContainer();
const top = getRelativeTop(target, container) + getScrollTop(container) - offset;
const reduceMotion = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
const release = () => {
scrollLockRef.current = false;
};
scrollLockRef.current = true;
setActive(item.key);
clearTimeout(lockTimerRef.current);
lockTimerRef.current = setTimeout(release, SCROLL_LOCK_FALLBACK_MS);
container.addEventListener("scrollend", release, { once: true });
container.scrollTo({
behavior: reduceMotion ? "auto" : "smooth",
top
});
};
const renderList = (list) => /* @__PURE__ */ jsx("ul", {
className: styles.list,
children: list.map((item) => /* @__PURE__ */ jsxs("li", { children: [/* @__PURE__ */ jsx("a", {
"aria-current": item.key === mergedActiveKey ? "location" : void 0,
className: styles.link,
"data-key": item.key,
href: item.href,
title: typeof item.title === "string" ? item.title : void 0,
onClick: (event) => handleClick(event, item),
children: item.title
}), item.children && item.children.length > 0 && renderList(item.children)] }, item.key))
});
return /* @__PURE__ */ jsxs("nav", {
className: cx(styles.root, className),
ref: mergedRef,
...rest,
children: [/* @__PURE__ */ jsx("span", {
"aria-hidden": true,
className: styles.marker,
hidden: !marker,
style: marker
}), renderList(items)]
});
});
Anchor.displayName = "Anchor";
//#endregion
export { Anchor as default };
//# sourceMappingURL=Anchor.mjs.map