UNPKG

react-navplus

Version:

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

206 lines 8.57 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.NavPlus = void 0; const jsx_runtime_1 = require("react/jsx-runtime"); /** * @file NavPlus.tsx * @description A clean, flexible navigation link component * @version 2.1.0 */ const react_1 = __importStar(require("react")); const react_router_dom_1 = require("react-router-dom"); const react_router_dom_2 = require("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 */ exports.NavPlus = react_1.default.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 = (0, react_router_dom_2.useLocation)(); const navigate = (0, react_router_dom_2.useNavigate)(); const timeoutRef = (0, react_1.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 = (0, react_1.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 = (0, react_1.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 = (0, react_1.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 = (0, react_1.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 = (0, react_1.useCallback)((e) => { onMouseLeave === null || onMouseLeave === void 0 ? void 0 : onMouseLeave(e); }, [onMouseLeave]); // Computed props const computedClassName = (0, react_1.useMemo)(() => buildClassName(className, linkIsActive, activeClassName, inActiveClassName), [className, linkIsActive, activeClassName, inActiveClassName]); const computedStyle = (0, react_1.useMemo)(() => linkIsActive ? activeStyle : inactiveStyle, [linkIsActive, activeStyle, inactiveStyle]); const commonProps = (0, react_1.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 = (0, react_1.useMemo)(() => { if (typeof children === 'function') { return children(linkIsActive); } return children; }, [children, linkIsActive]); // Cleanup timeout on unmount react_1.default.useEffect(() => { return () => { if (timeoutRef.current) { clearTimeout(timeoutRef.current); } }; }, []); // Render based on conditions if (Component) { return ((0, jsx_runtime_1.jsx)(Component, { ...commonProps, href: to, children: renderChildren })); } if (disabled) { return (0, jsx_runtime_1.jsx)("span", { ...commonProps, children: renderChildren }); } if (isExternal) { return ((0, jsx_runtime_1.jsx)("a", { ...commonProps, href: to, target: "_blank", rel: "noopener noreferrer", children: renderChildren })); } return ((0, jsx_runtime_1.jsx)(react_router_dom_1.Link, { to: to, replace: replace, ...commonProps, ...linkProps, children: renderChildren })); }); exports.NavPlus.displayName = 'NavPlus'; // Export everything exports.default = exports.NavPlus; //# sourceMappingURL=NavPlus.js.map