UNPKG

react-navplus

Version:

A flexible, performance-optimized navigation link component for React with multi-router support, prefetching, and advanced active state detection

170 lines 6.8 kB
import { jsx as _jsx } from "react/jsx-runtime"; /** * @file NavPlus.tsx * @description A clean, flexible navigation link component * @version 2.1.0 */ import React, { useMemo, useCallback, useRef } from 'react'; import { Link } from 'react-router-dom'; import { useLocation, useNavigate } from 'react-router-dom'; // Utility functions const matchers = { exact: (pathname, url) => pathname === url, startsWith: (pathname, url) => pathname.startsWith(url), includes: (pathname, url) => pathname.includes(url), pattern: (pathname, _url, pattern) => pattern ? pattern.test(pathname) : false }; /** * Determine if a link is active based on the current pathname, URL, and match mode. * @param {string} pathname - Current pathname * @param {string} url - URL to match against * @param {MatchMode} [matchMode='includes'] - How to match the URL * @param {RegExp} [matchPattern] - Optional regex pattern for matching * @returns {boolean} - Whether the link is active */ const isActive = (pathname, url, matchMode = 'includes', matchPattern) => { const matchFn = matchers[matchMode] || matchers.includes; return matchFn(pathname, url, matchPattern); }; const buildClassName = (baseClassName, isActive, activeClassName, inActiveClassName) => { const classes = [ baseClassName, isActive ? activeClassName : inActiveClassName, 'navplus-link' ].filter(Boolean); return classes.join(' ').trim(); }; // Simple prefetch implementation const prefetchCache = new Set(); const simplePrefetch = (url) => { if (prefetchCache.has(url) || typeof window === 'undefined') return; try { const link = document.createElement('link'); link.rel = 'prefetch'; link.href = url; document.head.appendChild(link); prefetchCache.add(url); } catch (error) { console.warn('Prefetch failed:', error); } }; /** * NavPlus Component - A flexible navigation link component */ export const NavPlus = React.memo(({ to, children, className = '', activeClassName = 'active', inActiveClassName = '', disabled = false, isExternal = false, matchMode = 'includes', matchPattern, customActiveUrl, activeStyle, inactiveStyle, prefetch = false, replace = false, triggerEvent = 'click', navigationDelay, onClick, onMouseEnter, onMouseLeave, as: Component, testId, linkProps = {}, ...restProps }) => { const location = useLocation(); const navigate = useNavigate(); const timeoutRef = useRef(); // Early validation if (!to) { if (process.env.NODE_ENV !== 'production') { console.warn('NavPlus: "to" prop is required'); } return null; } // Determine if link is active const linkIsActive = useMemo(() => { if (!(location === null || location === void 0 ? void 0 : location.pathname)) return false; const urlToMatch = customActiveUrl || to; return isActive(location.pathname, urlToMatch, matchMode, matchPattern); }, [location === null || location === void 0 ? void 0 : location.pathname, customActiveUrl, to, matchMode, matchPattern]); // Handle navigation with optional delay const handleNavigation = useCallback((e) => { if (disabled || isExternal) return; e.preventDefault(); if (timeoutRef.current) { clearTimeout(timeoutRef.current); } const navigateToUrl = () => navigate(to, { replace }); if (navigationDelay && navigationDelay > 0) { timeoutRef.current = setTimeout(navigateToUrl, navigationDelay); } else { navigateToUrl(); } }, [disabled, isExternal, navigate, to, replace, navigationDelay]); // Event handlers const handleClick = useCallback((e) => { if (disabled) { e.preventDefault(); return; } onClick === null || onClick === void 0 ? void 0 : onClick(e); if (triggerEvent === 'click' && !e.defaultPrevented) { handleNavigation(e); } }, [disabled, onClick, triggerEvent, handleNavigation]); const handleMouseEnterEvent = useCallback((e) => { // Handle prefetch if (prefetch && !isExternal && !disabled) { simplePrefetch(to); } // Handle hover navigation if (triggerEvent === 'hover') { handleNavigation(e); } onMouseEnter === null || onMouseEnter === void 0 ? void 0 : onMouseEnter(e); }, [prefetch, isExternal, disabled, to, triggerEvent, handleNavigation, onMouseEnter]); const handleMouseLeaveEvent = useCallback((e) => { onMouseLeave === null || onMouseLeave === void 0 ? void 0 : onMouseLeave(e); }, [onMouseLeave]); // Computed props const computedClassName = useMemo(() => buildClassName(className, linkIsActive, activeClassName, inActiveClassName), [className, linkIsActive, activeClassName, inActiveClassName]); const computedStyle = useMemo(() => linkIsActive ? activeStyle : inactiveStyle, [linkIsActive, activeStyle, inactiveStyle]); const commonProps = useMemo(() => ({ className: computedClassName, style: computedStyle, onClick: handleClick, onMouseEnter: handleMouseEnterEvent, onMouseLeave: handleMouseLeaveEvent, 'data-testid': testId, 'data-active': linkIsActive, 'aria-current': linkIsActive ? 'page' : undefined, 'aria-disabled': disabled, ...restProps }), [ computedClassName, computedStyle, handleClick, handleMouseEnterEvent, handleMouseLeaveEvent, testId, linkIsActive, disabled, restProps ]); // Render children const renderChildren = useMemo(() => { if (typeof children === 'function') { return children(linkIsActive); } return children; }, [children, linkIsActive]); // Cleanup timeout on unmount React.useEffect(() => { return () => { if (timeoutRef.current) { clearTimeout(timeoutRef.current); } }; }, []); // Render based on conditions if (Component) { return (_jsx(Component, { ...commonProps, href: to, children: renderChildren })); } if (disabled) { return _jsx("span", { ...commonProps, children: renderChildren }); } if (isExternal) { return (_jsx("a", { ...commonProps, href: to, target: "_blank", rel: "noopener noreferrer", children: renderChildren })); } return (_jsx(Link, { to: to, replace: replace, ...commonProps, ...linkProps, children: renderChildren })); }); NavPlus.displayName = 'NavPlus'; // Export everything export default NavPlus; //# sourceMappingURL=NavPlus.js.map