UNPKG

@lobehub/ui

Version:

Lobe UI is an open-source UI component library for building AIGC web apps

141 lines (140 loc) • 4.99 kB
"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