UNPKG

@lobehub/ui

Version:

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

1 lines • 9.31 kB
{"version":3,"file":"Anchor.mjs","names":["useControlledState"],"sources":["../../../src/base-ui/Anchor/Anchor.tsx"],"sourcesContent":["'use client';\n\nimport { cx } from 'antd-style';\nimport {\n type CSSProperties,\n memo,\n type MouseEvent,\n useEffect,\n useLayoutEffect,\n useRef,\n useState,\n} from 'react';\nimport { useMergeRefs } from 'react-merge-refs';\nimport useControlledState from 'use-merge-value';\n\nimport { useEventCallback } from '@/hooks/useEventCallback';\n\nimport { styles } from './style';\nimport type { AnchorItem, AnchorProps } from './type';\n\ntype ScrollContainer = HTMLElement | Window;\n\nconst SCROLL_LOCK_FALLBACK_MS = 1000;\n\nconst flatten = (items: AnchorItem[]): AnchorItem[] =>\n items.flatMap((item) => [item, ...flatten(item.children ?? [])]);\n\nconst getTarget = (href: string) => {\n const hash = href.split('#')[1];\n if (!hash) return null;\n try {\n return document.getElementById(decodeURIComponent(hash));\n } catch {\n return document.getElementById(hash);\n }\n};\n\nconst isWindow = (container: ScrollContainer): container is Window => container === window;\n\nconst getRelativeTop = (element: HTMLElement, container: ScrollContainer) =>\n element.getBoundingClientRect().top -\n (isWindow(container) ? 0 : container.getBoundingClientRect().top);\n\nconst getScrollTop = (container: ScrollContainer) =>\n isWindow(container) ? window.scrollY : container.scrollTop;\n\nconst getActiveKey = (\n items: AnchorItem[],\n container: ScrollContainer,\n offset: number,\n): string | null => {\n let activeKey: string | null = null;\n let activeTop = -Infinity;\n for (const item of flatten(items)) {\n const target = getTarget(item.href);\n if (!target) continue;\n const top = getRelativeTop(target, container);\n if (top <= offset + 1 && top >= activeTop) {\n activeKey = item.key;\n activeTop = top;\n }\n }\n return activeKey;\n};\n\nconst Anchor = memo<AnchorProps>(\n ({ activeKey, className, getContainer, items, offset = 0, onChange, onClick, ref, ...rest }) => {\n const [mergedActiveKey, setMergedActiveKey] = useControlledState<string | null>(null, {\n onChange,\n value: activeKey,\n });\n const scrollLockRef = useRef(false);\n const navRef = useRef<HTMLElement>(null);\n const mergedRef = useMergeRefs([ref, navRef]);\n const [marker, setMarker] = useState<CSSProperties>();\n const lockTimerRef = useRef<ReturnType<typeof setTimeout>>(undefined);\n\n const resolveContainer = useEventCallback((): ScrollContainer => getContainer?.() ?? window);\n const setActive = useEventCallback(setMergedActiveKey);\n const spy = useEventCallback(() => {\n if (scrollLockRef.current) return;\n setActive(getActiveKey(items, resolveContainer(), offset));\n });\n\n useEffect(() => {\n const container = resolveContainer();\n let frame = 0;\n const handleScroll = () => {\n cancelAnimationFrame(frame);\n frame = requestAnimationFrame(spy);\n };\n spy();\n container.addEventListener('scroll', handleScroll, { passive: true });\n return () => {\n cancelAnimationFrame(frame);\n container.removeEventListener('scroll', handleScroll);\n };\n }, [items, offset, resolveContainer, spy]);\n\n useEffect(() => () => clearTimeout(lockTimerRef.current), []);\n\n useLayoutEffect(() => {\n const nav = navRef.current;\n if (!nav) return;\n const place = () => {\n const link = [...nav.querySelectorAll<HTMLElement>('a[data-key]')].find(\n (element) => element.dataset.key === mergedActiveKey,\n );\n setMarker(\n link ? { height: link.offsetHeight, insetBlockStart: link.offsetTop } : undefined,\n );\n };\n place();\n if (typeof ResizeObserver === 'undefined') return;\n const observer = new ResizeObserver(place);\n observer.observe(nav);\n return () => observer.disconnect();\n }, [items, mergedActiveKey]);\n\n const handleClick = (event: MouseEvent<HTMLAnchorElement>, item: AnchorItem) => {\n onClick?.(event, item);\n if (event.defaultPrevented || event.button !== 0) return;\n if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;\n\n const target = getTarget(item.href);\n if (!target) return;\n event.preventDefault();\n\n const container = resolveContainer();\n const top = getRelativeTop(target, container) + getScrollTop(container) - offset;\n const reduceMotion = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;\n\n // Smooth scrolling may never bring a heading near the page end up to the offset line, so the\n // clicked item is pinned as active until the scroll settles.\n const release = () => {\n scrollLockRef.current = false;\n };\n scrollLockRef.current = true;\n setActive(item.key);\n clearTimeout(lockTimerRef.current);\n lockTimerRef.current = setTimeout(release, SCROLL_LOCK_FALLBACK_MS);\n container.addEventListener('scrollend', release, { once: true });\n container.scrollTo({ behavior: reduceMotion ? 'auto' : 'smooth', top });\n };\n\n const renderList = (list: AnchorItem[]) => (\n <ul className={styles.list}>\n {list.map((item) => (\n <li key={item.key}>\n <a\n aria-current={item.key === mergedActiveKey ? 'location' : undefined}\n className={styles.link}\n data-key={item.key}\n href={item.href}\n title={typeof item.title === 'string' ? item.title : undefined}\n onClick={(event) => handleClick(event, item)}\n >\n {item.title}\n </a>\n {item.children && item.children.length > 0 && renderList(item.children)}\n </li>\n ))}\n </ul>\n );\n\n return (\n <nav className={cx(styles.root, className)} ref={mergedRef} {...rest}>\n <span aria-hidden className={styles.marker} hidden={!marker} style={marker} />\n {renderList(items)}\n </nav>\n );\n },\n);\n\nAnchor.displayName = 'Anchor';\n\nexport default Anchor;\n"],"mappings":"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"}