UNPKG

next-active-link

Version:

✅ Active link component for Next.JS, wrapping next/link component

1 lines 6.71 kB
{"version":3,"sources":["../src/components/ActiveLink/ActiveLink.tsx","../src/components/ActiveLink/useAddActiveClassName.hook.ts","../src/components/ActiveLink/ActiveLink.helpers.ts"],"sourcesContent":["import Link from 'next/link'\nimport { Children, cloneElement } from 'react'\n\nimport { useAddActiveClassName } from './useAddActiveClassName.hook'\n\nimport type { LinkProps } from 'next/link'\n\n\n/**\n * A link that adds an active class name when the link is active.\n */\nexport type ActiveLinkProps = {\n /**\n * It must be a Single Component to be rendered (Child).\n *\n * NOTE: If you are using a Custom component instead of <a></a>,\n * remember to use 'passHref' prop to propagate href\n */\n children: JSX.Element\n\n\n /**\n * Active class name to be added.\n */\n activeClassName: string\n\n\n /**\n * A set of options which specify how to determine if a url is active\n */\n activeMatchOptions?: {\n exact: boolean\n } | {\n paths?: 'exact' | 'partial'\n queryParams?: 'exact' | 'partial'\n fragment?: 'exact'\n }\n\n\n /**\n * You can use the output onActiveChange to get notified\n * each time the link becomes active or inactive.\n */\n onActiveChange?: (isActive: boolean) => unknown\n} & LinkProps\n\n\n\n\n\n\n/**\n * A Link Component wrapper that adds an active class name when the link is active.\n */\nexport const ActiveLink = ({ children, activeClassName, ...props }: ActiveLinkProps): JSX.Element => {\n const child = Children.only(children)\n const childClassName = (child.props.className ?? '') as string\n\n /**\n * as => Dynamic routes and rewrites\n * href => Static routes\n */\n const { as, href, activeMatchOptions, onActiveChange } = props\n const url = (as || href) as URL\n\n const className = useAddActiveClassName({\n activeClassName,\n childClassName,\n linkUrl: url,\n activeMatchOptions,\n onActiveChange,\n })\n\n\n\n return (\n <Link {...props}>\n {cloneElement(child, {\n className: className || null,\n })}\n </Link>\n )\n}\n","import { useRouter } from 'next/router'\nimport { useEffect, useState } from 'react'\nimport { cn } from 'tiny-cn'\n\nimport { isLinkActive } from './ActiveLink.helpers'\n\nimport type { ActiveLinkProps } from './ActiveLink'\n\n\n\ninterface Params {\n activeClassName: string\n childClassName: string\n linkUrl: string | URL\n activeMatchOptions: ActiveLinkProps['activeMatchOptions']\n onActiveChange: ActiveLinkProps['onActiveChange']\n}\n\nexport const useAddActiveClassName = ({\n activeClassName,\n childClassName,\n linkUrl,\n activeMatchOptions,\n onActiveChange,\n}: Params): string => {\n const [className, setClassName] = useState(childClassName)\n const { asPath, isReady } = useRouter()\n\n useEffect(() => {\n /** Whether the router fields are updated client-side and ready for use */\n if (isReady) {\n const isActive = isLinkActive(linkUrl, asPath, activeMatchOptions)\n\n if (onActiveChange) onActiveChange(isActive)\n\n const newClassName = cn({\n [childClassName]: true,\n [activeClassName]: isActive,\n })\n\n const shouldUpdateLinkClassName = newClassName !== className\n\n if (shouldUpdateLinkClassName) setClassName(newClassName)\n }\n }, [\n asPath,\n isReady,\n linkUrl,\n childClassName,\n activeClassName,\n setClassName,\n className,\n ])\n\n\n return className\n}\n","import type { ActiveLinkProps } from './ActiveLink'\n\n\n\nconst ACTIVE_MATCH_OPTIONS_DEFAULT: ActiveLinkProps['activeMatchOptions'] = {\n fragment: undefined,\n paths: undefined,\n queryParams: undefined,\n}\n\nexport const isLinkActive = (\n linkUrl: string | URL,\n currentLink: string,\n activeMatchOptions: ActiveLinkProps['activeMatchOptions'] = {\n exact: true,\n },\n): boolean => {\n const currentLinkObj = new URL(currentLink, location.href)\n const linkUrlObj = new URL(linkUrl, location.href)\n\n if ('exact' in activeMatchOptions) {\n return activeMatchOptions.exact\n ? currentLinkObj.href === linkUrlObj.href\n : currentLinkObj.href.includes(linkUrlObj.pathname)\n } else {\n let isActive = false\n const { fragment, paths, queryParams } = { ...ACTIVE_MATCH_OPTIONS_DEFAULT, ...activeMatchOptions }\n\n if (fragment) {\n isActive = currentLinkObj.hash === linkUrlObj.hash\n }\n\n if (paths) {\n const pathList = currentLinkObj.pathname.split('/')\n const linkPathList = linkUrlObj.pathname.split('/')\n\n isActive = paths === 'exact'\n ? currentLinkObj.pathname === linkUrlObj.pathname\n : pathList.some((path) => linkPathList.includes(path))\n }\n\n if (queryParams) {\n const currentQueryParams = Object.fromEntries(currentLinkObj.searchParams.entries())\n const linkQueryParams = Object.fromEntries(linkUrlObj.searchParams.entries())\n\n isActive = queryParams === 'exact'\n ? currentLinkObj.search === linkUrlObj.search\n : Object.entries(currentQueryParams).some(([key, value]) => {\n return linkQueryParams[key] === value\n })\n }\n return isActive\n }\n}\n"],"mappings":"2fAAA,yBACA,mDCDA,wCACA,gDACA,6BCEA,GAAM,GAAsE,CAC1E,SAAU,OACV,MAAO,OACP,YAAa,MACf,EAEa,EAAe,CAC1B,EACA,EACA,EAA4D,CAC1D,MAAO,EACT,IACY,CACZ,GAAM,GAAiB,GAAI,KAAI,EAAa,SAAS,IAAI,EACnD,EAAa,GAAI,KAAI,EAAS,SAAS,IAAI,EAEjD,GAAI,SAAW,GACb,MAAO,GAAmB,MACtB,EAAe,OAAS,EAAW,KACnC,EAAe,KAAK,SAAS,EAAW,QAAQ,EAC/C,CACL,GAAI,GAAW,GACT,CAAE,WAAU,QAAO,eAAgB,OAAK,GAAiC,GAM/E,GAJI,GACF,GAAW,EAAe,OAAS,EAAW,MAG5C,EAAO,CACT,GAAM,GAAW,EAAe,SAAS,MAAM,GAAG,EAC5C,EAAe,EAAW,SAAS,MAAM,GAAG,EAElD,EAAW,IAAU,QACjB,EAAe,WAAa,EAAW,SACvC,EAAS,KAAK,AAAC,GAAS,EAAa,SAAS,CAAI,CAAC,CACzD,CAEA,GAAI,EAAa,CACf,GAAM,GAAqB,OAAO,YAAY,EAAe,aAAa,QAAQ,CAAC,EAC7E,EAAkB,OAAO,YAAY,EAAW,aAAa,QAAQ,CAAC,EAE5E,EAAW,IAAgB,QACvB,EAAe,SAAW,EAAW,OACrC,OAAO,QAAQ,CAAkB,EAAE,KAAK,CAAC,CAAC,EAAK,KACxC,EAAgB,KAAS,CACjC,CACL,CACA,MAAO,EACT,CACF,EDnCO,GAAM,GAAwB,CAAC,CACpC,kBACA,iBACA,UACA,qBACA,oBACoB,CACpB,GAAM,CAAC,EAAW,GAAgB,EAAS,CAAc,EACnD,CAAE,SAAQ,WAAY,EAAU,EAEtC,SAAU,IAAM,CAEd,GAAI,EAAS,CACX,GAAM,GAAW,EAAa,EAAS,EAAQ,CAAkB,EAEjE,AAAI,GAAgB,EAAe,CAAQ,EAE3C,GAAM,GAAe,EAAG,EACrB,GAAiB,IACjB,GAAkB,CACrB,CAAC,EAID,AAAI,AAF8B,IAAiB,GAEpB,EAAa,CAAY,CAC1D,CACF,EAAG,CACD,EACA,EACA,EACA,EACA,EACA,EACA,CACF,CAAC,EAGM,CACT,EDFO,GAAM,GAAa,AAAC,GAA0E,CAA1E,QAAE,YAAU,mBAAZ,EAAgC,IAAhC,EAAgC,CAA9B,WAAU,oBAtDvC,MAuDE,GAAM,GAAQ,EAAS,KAAK,CAAQ,EAC9B,EAAkB,KAAM,MAAM,YAAZ,OAAyB,GAM3C,CAAE,KAAI,OAAM,qBAAoB,kBAAmB,EAGnD,EAAY,EAAsB,CACtC,kBACA,iBACA,QALW,GAAM,EAMjB,qBACA,gBACF,CAAC,EAID,MACE,qBAAC,OAAS,GACP,EAAa,EAAO,CACnB,UAAW,GAAa,IAC1B,CAAC,CACH,CAEJ","names":[]}