/** * Which sidebar row is highlighted. * * `nav-active.ts` answers "does this href match the URL, given every other * href". This module answers the harder question the sidebar actually asks: * given a parent row and one of its children, which single row should carry * `data-active`. That needs the sibling set, because a hub URL repeated across * several children would otherwise light all of them at once. * * The predicates are built per URL corpus rather than read from module state, so * two shells in one process cannot overwrite each other's matching. The corpus * changes when tenant products hydrate, which is why it is a factory argument * and not a constant. */ import { buildNavHashClaims, isNavHrefActive, navUrlPath, normalizedLocationHash, } from "./nav-active" /** Minimum a row needs for an active-state decision. Structural so the shell's render types satisfy it. */ export interface NavActiveRow { key: string url: string children?: readonly NavActiveRow[] primaryHubChildKey?: string } /** * App refinement for a child row after hash and URL eligibility are established. * * Some rows are active on a route the generic rules cannot derive: a list hub * that stays selected across every filter in its query string, for instance, * where the shell would compare the child against its parent's hub path and * miss. Return `undefined` to defer to the remaining child-specific rules. * Returning `false` can suppress an eligible row; it cannot activate a row * whose URL or hash does not match. */ export type NavRowActiveOverride = ( row: { parentKey: string; childKey: string }, pathname: string, ) => boolean | undefined export interface NavActiveStateOptions { /** Every href the sidebar can expose in any product; longest match wins. */ urls: readonly string[] rowActive?: NavRowActiveOverride } export interface NavActiveState { /** A flat row (no children) against the whole corpus. */ isRowActive: (pathname: string, url: string, locationHash?: string) => boolean /** A child row, disambiguated against its siblings. */ isChildActive: ( pathname: string, parent: NavActiveRow, child: NavActiveRow, locationHash: string, ) => boolean /** * A collapsible parent in the **expanded** sidebar. Neutral whenever a child * is active, because the child carries the highlight; lit only when the * parent's own URL matches and no child does. The collapsed icon rail wants * the opposite and uses `isChildActive` across the children instead, since * the parent icon is the only affordance there. */ isParentActive: (pathname: string, parent: NavActiveRow, locationHash: string) => boolean } export function createNavActiveState({ urls, rowActive, }: NavActiveStateOptions): NavActiveState { const hashClaimsByPath = buildNavHashClaims(urls) function isRowActive(pathname: string, url: string, locationHash = ""): boolean { return isNavHrefActive(pathname, url, urls, { locationHash, hashClaimsByPath }) } function isChildActive( pathname: string, parent: NavActiveRow, child: NavActiveRow, locationHash: string, ): boolean { if (child.children?.length) { const anyNestedActive = child.children.some(grandchild => isChildActive(pathname, child, grandchild, locationHash), ) if (anyNestedActive) return true return isRowActive(pathname, child.url, locationHash) } const children = parent.children if (!children?.length) return isRowActive(pathname, child.url, locationHash) // Siblings distinguished by fragment: the URL's hash decides, and the hub // child owns the no-fragment case. const hasHashChild = children.some(c => c.url.includes("#")) if (hasHashChild) { const h = normalizedLocationHash(locationHash) const childHash = child.url.includes("#") ? child.url.split("#")[1] : "" if (parent.primaryHubChildKey && child.key === parent.primaryHubChildKey) { return h === "" } if (childHash) { return h === childHash } return false } if (!isRowActive(pathname, child.url, locationHash)) return false const override = rowActive?.( { parentKey: parent.key, childKey: child.key }, pathname, ) if (override !== undefined) return override // The hub entry must not stay selected on routes below it. if (parent.primaryHubChildKey && child.key === parent.primaryHubChildKey) { const hubPath = navUrlPath(parent.url) if (hubPath) { const normalized = pathname.length > 1 && pathname.endsWith("/") ? pathname.slice(0, -1) : pathname if (normalized !== hubPath) return false } } // Every child points at the same href, so href matching cannot separate // them. Only a declared hub child wins; otherwise none do. const hrefs = children.map(c => c.url) const allSameUrl = hrefs.length > 1 && hrefs.every(u => u === hrefs[0]) if (allSameUrl) { if (parent.primaryHubChildKey) { return child.key === parent.primaryHubChildKey } return false } return true } function isParentActive( pathname: string, parent: NavActiveRow, locationHash: string, ): boolean { const children = parent.children if (!children?.length) return isRowActive(pathname, parent.url, locationHash) const anyChildActive = children.some(c => isChildActive(pathname, parent, c, locationHash), ) if (anyChildActive) return false return isRowActive(pathname, parent.url, locationHash) } return { isRowActive, isChildActive, isParentActive } }