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
JavaScript
;
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