Skip to content

Commit

Permalink
fix: fix header navigation active nested menu highlight (#167) (#169)
Browse files Browse the repository at this point in the history
* fix: fix header navigation active nested menu highlight (#167)

* feat: update selected match util (#167)

---------

Co-authored-by: Fran McDade <franmcdade@Frans-MacBook-Pro.local>
  • Loading branch information
frano-m and Fran McDade authored Aug 20, 2024
1 parent acd1526 commit 818b31f
Show file tree
Hide file tree
Showing 15 changed files with 223 additions and 93 deletions.
7 changes: 5 additions & 2 deletions src/components/Layout/components/Header/common/entities.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ReactNode } from "react";
import { BreakpointKey } from "../../../../../hooks/useBreakpointHelper";
import { Social } from "../../../../common/Socials/socials";
import { NavLinkItem } from "../components/Content/components/Navigation/navigation";

Expand All @@ -8,12 +8,15 @@ export type Navigation = [
NavLinkItem[] | undefined
]; // [LEFT, CENTER, RIGHT]

export type SelectedMatch =
| SELECTED_MATCH
| Partial<Record<BreakpointKey, boolean | SELECTED_MATCH>>;

export enum SELECTED_MATCH {
EQUALS = "EQUALS",
STARTS_WITH = "STARTS_WITH", // Default value.
}

export interface SocialMedia {
label: ReactNode;
socials: Social[];
}
121 changes: 107 additions & 14 deletions src/components/Layout/components/Header/common/utils.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,45 @@
import { Breakpoint } from "@mui/material";
import { isClientSideNavigation } from "../../../../Links/common/utils";
import { NavLinkItem } from "../components/Content/components/Navigation/navigation";
import { Navigation } from "./entities";
import { Navigation, SelectedMatch, SELECTED_MATCH } from "./entities";

/**
* Returns the configured menu navigation links, for the current breakpoint.
* Adds to the set of selected patterns, for the navigation link, at the current breakpoint.
* @param setOfPatterns - Set of selected patterns.
* @param navLinkItem - Navigation link.
* @param breakpoint - Breakpoint.
*/
function addSelectedPattern(
setOfPatterns: Set<string>,
navLinkItem: NavLinkItem,
breakpoint?: Breakpoint
): void {
if (!navLinkItem.url) return;
// Exclude external links.
if (!isClientSideNavigation(navLinkItem.url)) return;
// Get the configured selected match for the current breakpoint.
const selectedMatch = getSelectedMatch(navLinkItem.selectedMatch, breakpoint);
if (!selectedMatch) return;
// Add the selected pattern for the navigation link.
if (selectedMatch === SELECTED_MATCH.EQUALS) {
setOfPatterns.add(getPatternEquals(navLinkItem.url));
return;
}
setOfPatterns.add(getPatternStartsWith(navLinkItem.url));
}

/**
* Returns the configured menu navigation links.
* @param navigation - Navigation links.
* @param breakpoint - Current breakpoint.
* @returns navigation links.
*/
export function getMenuNavigationLinks(
navigation?: Navigation,
breakpoint?: Breakpoint
): NavLinkItem[] {
export function getMenuNavigationLinks(navigation?: Navigation): NavLinkItem[] {
if (!navigation) return [];
const navLinkItems = navigation.reduce((acc: NavLinkItem[], navLinkItems) => {
return navigation.reduce((acc: NavLinkItem[], navLinkItems) => {
if (!navLinkItems) return acc;
acc.push(...navLinkItems);
return acc;
}, []);
return getNavigationLinks(navLinkItems, breakpoint);
}

/**
Expand All @@ -32,16 +53,62 @@ export function getNavigationLinks(
breakpoint?: Breakpoint
): NavLinkItem[] {
if (!navigationLinks) return [];
return navigationLinks.reduce(
(acc: NavLinkItem[], navLinkItem: NavLinkItem) => {
return navigationLinks
.map((navigationLink) => mapSelectedMatches(navigationLink, breakpoint))
.reduce((acc: NavLinkItem[], navLinkItem: NavLinkItem) => {
const processedNavLink = processNavLinkItem(navLinkItem, breakpoint);
if (processedNavLink) {
acc.push(...processedNavLink);
}
return acc;
},
[]
);
}, []);
}

/**
* Returns the pattern for an exact match, for the given URL e.g. "^/about$".
* @param url - URL.
* @returns pattern for an exact match.
*/
function getPatternEquals(url: string): string {
return `^${url}$`;
}

/**
* Returns the pattern for a match that starts with the given URL e.g. "^/about".
* @param url - URL.
* @returns pattern for a match that starts with the given URL.
*/
function getPatternStartsWith(url: string): string {
return `^${url}`;
}

/**
* Returns the configured selected match.
* @param selectedMatch - Selected match.
* @param breakpoint - Breakpoint.
* @returns selected match.
*/
function getSelectedMatch(
selectedMatch?: SelectedMatch,
breakpoint?: Breakpoint
): SELECTED_MATCH | undefined {
if (!selectedMatch) return SELECTED_MATCH.STARTS_WITH;
if (typeof selectedMatch === "string") return selectedMatch;
if (!breakpoint) return;
return getSelectMatchValue(selectedMatch[breakpoint]);
}

/**
* Returns the selected match value, for the current breakpoint.
* @param selectedMatchValue - Selected match value.
* @returns selected match.
*/
function getSelectMatchValue(
selectedMatchValue?: boolean | SELECTED_MATCH
): SELECTED_MATCH | undefined {
if (selectedMatchValue === false) return undefined;
if (selectedMatchValue === true) return SELECTED_MATCH.STARTS_WITH;
return selectedMatchValue || SELECTED_MATCH.STARTS_WITH;
}

/**
Expand Down Expand Up @@ -74,6 +141,32 @@ function isLinkVisible(
return navLinkItem.visible[breakpoint] !== false;
}

/**
* Returns the navigation link with the selected matches, for the current breakpoint.
* @param navLinkItem - Navigation link.
* @param breakpoint - Breakpoint.
* @returns navigation link with the selected matches.
*/
function mapSelectedMatches(
navLinkItem: NavLinkItem,
breakpoint?: Breakpoint
): NavLinkItem {
const setOfPatterns = new Set<string>();
// Add selected pattern for the current navigation link.
addSelectedPattern(setOfPatterns, navLinkItem, breakpoint);
const cloneLink = { ...navLinkItem };
if (cloneLink.menuItems) {
cloneLink.menuItems = [...cloneLink.menuItems].map((menuItem) =>
mapSelectedMatches(menuItem, breakpoint)
);
for (const { selectedPatterns = [] } of cloneLink.menuItems) {
selectedPatterns.forEach((pattern) => setOfPatterns.add(pattern));
}
}
cloneLink.selectedPatterns = [...setOfPatterns];
return cloneLink;
}

/**
* Returns the processed navigation link item.
* Flattens menu items, and removes items that are not visible for the current breakpoint.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export const Menu = forwardRef<HTMLButtonElement, MenuProps>(
ref
): JSX.Element | null {
const { navigation, slogan, socialMedia } = headerProps;
const { breakpoint, smDown } = useBreakpoint();
const { smDown } = useBreakpoint();

// Set drawer open state to false on change of media breakpoint from small desktop "md" and up.
useEffect(() => {
Expand Down Expand Up @@ -61,7 +61,7 @@ export const Menu = forwardRef<HTMLButtonElement, MenuProps>(
<Navigation
closeAncestor={closeMenu}
headerProps={headerProps}
links={getMenuNavigationLinks(navigation, breakpoint)}
links={getMenuNavigationLinks(navigation)}
pathname={pathname}
/>
{socialMedia && (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,19 @@
import { SELECTED_MATCH } from "../../../../../common/entities";

/**
* Returns true if the navigation link is selected.
* @param url - The URL of the navigation link.
* The pathname is matched against the selected patterns.
* @param pathname - The current pathname.
* @param selectedMatch - The selected match type.
* @param selectedPatterns - Selected match patterns.
* @returns true if the navigation link is selected.
*/
export function isNavigationLinkSelected(
url: string,
pathname?: string,
selectedMatch: SELECTED_MATCH = SELECTED_MATCH.STARTS_WITH
selectedPatterns?: string[]
): boolean {
if (!pathname) return false;
if (isSelectedMatchEqual(selectedMatch)) return url === pathname;
return pathname.startsWith(url);
}

/**
* Returns true if the selected match type is "EQUAL".
* @param selectedMatch - The selected match type.
* @returns True if the selected match type is "EQUAL".
*/
export function isSelectedMatchEqual(selectedMatch: SELECTED_MATCH): boolean {
return selectedMatch === SELECTED_MATCH.EQUALS;
for (const selectedPattern of selectedPatterns ?? []) {
if (new RegExp(selectedPattern).test(pathname)) {
return true;
}
}
return false;
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import ArrowDropDownRoundedIcon from "@mui/icons-material/ArrowDropDownRounded";
import React, { ReactNode, useState } from "react";
import React, { ReactNode, useCallback } from "react";
import { Button } from "../../../../../../../../../common/Button/button";
import { BackArrowIcon } from "../../../../../../../../../common/CustomIcon/components/BackArrowIcon/backArrowIcon";
import { useDialog } from "../../../../../../../../../common/Dialog/hooks/useDialog";
import { HeaderProps } from "../../../../../../header";
import { AppBar } from "../../../../../../header.styles";
import { DrawerNavigation as Navigation } from "../../../Actions/components/Menu/components/Content/components/Navigation/navigation.styles";
Expand All @@ -17,6 +18,7 @@ import {
export interface NavigationDrawerProps {
closeAncestor?: () => void;
headerProps?: HeaderProps;
isSelected?: boolean;
menuItems: MenuItem[];
menuLabel: ReactNode;
pathname?: string;
Expand All @@ -25,27 +27,22 @@ export interface NavigationDrawerProps {
export const NavigationDrawer = ({
closeAncestor,
headerProps,
isSelected = false,
menuItems,
menuLabel,
pathname,
}: NavigationDrawerProps): JSX.Element => {
const [drawerOpen, setDrawerOpen] = useState<boolean>(false);
const openDrawer = (): void => {
setDrawerOpen(true);
};
const closeDrawer = (): void => {
setDrawerOpen(false);
};
const closeDrawers = (): void => {
setDrawerOpen(false);
const { onClose, onOpen, open } = useDialog();
const closeDrawers = useCallback((): void => {
onClose();
closeAncestor?.();
};
}, [closeAncestor, onClose]);
return (
<>
<Button
EndIcon={ArrowDropDownRoundedIcon}
onClick={openDrawer}
variant="nav"
onClick={onOpen}
variant={isSelected ? "activeNav" : "nav"}
>
{menuLabel}
</Button>
Expand All @@ -55,7 +52,7 @@ export const NavigationDrawer = ({
hideBackdrop
keepMounted={false}
onClose={closeDrawers}
open={drawerOpen}
open={open}
PaperProps={{ elevation: 0 }}
TransitionComponent={Slide}
transitionDuration={300}
Expand All @@ -66,7 +63,7 @@ export const NavigationDrawer = ({
<Content>
<BackButton
fullWidth
onClick={closeDrawer}
onClick={onClose}
StartIcon={BackArrowIcon}
variant="backNav"
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import ArrowDropDownRoundedIcon from "@mui/icons-material/ArrowDropDownRounded";
import { MenuProps as MMenuProps } from "@mui/material";
import React, { Fragment, ReactNode } from "react";
import { useBreakpoint } from "../../../../../../../../../../hooks/useBreakpoint";
import { useMenuWithPosition } from "../../../../../../../../../../hooks/useMenuWithPosition";
import { useMenu } from "../../../../../../../../../common/Menu/hooks/useMenu";
import { NavigationButtonLabel } from "../NavigationButtonLabel/navigationButtonLabel";
import {
MenuItem,
Expand All @@ -15,6 +15,7 @@ export interface NavLinkMenuProps {
anchorOrigin?: MMenuProps["anchorOrigin"];
closeAncestor?: () => void;
disablePortal?: boolean;
isSelected?: boolean;
menuItems: MenuItem[];
menuLabel: ReactNode;
pathname?: string;
Expand All @@ -24,12 +25,13 @@ export const NavigationMenu = ({
anchorOrigin = MENU_ANCHOR_ORIGIN_LEFT_BOTTOM,
closeAncestor,
disablePortal,
isSelected = false,
menuItems,
menuLabel,
pathname,
}: NavLinkMenuProps): JSX.Element => {
const { mdUp } = useBreakpoint();
const { anchorEl, onClose, onToggleOpen, open } = useMenuWithPosition();
const { anchorEl, onClose, onToggleOpen, open } = useMenu();
const MenuItem = disablePortal ? StyledMenuItem : Fragment;
const menuItemProps = disablePortal ? { onMouseLeave: onClose } : {};
return (
Expand All @@ -38,7 +40,7 @@ export const NavigationMenu = ({
EndIcon={ArrowDropDownRoundedIcon}
isActive={open}
onClick={onToggleOpen}
variant="nav"
variant={isSelected ? "activeNav" : "nav"}
>
<NavigationButtonLabel label={menuLabel} />
</Button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export const NavigationMenuItems = ({
icon,
label,
menuItems: nestedMenuItems,
selectedMatch,
selectedPatterns,
target = ANCHOR_TARGET.SELF,
url,
},
Expand Down Expand Up @@ -79,11 +79,7 @@ export const NavigationMenuItems = ({
REL_ATTRIBUTE.NO_OPENER_NO_REFERRER
);
}}
selected={isNavigationLinkSelected(
url,
pathname,
selectedMatch
)}
selected={isNavigationLinkSelected(pathname, selectedPatterns)}
>
{icon && <ListItemIcon>{icon}</ListItemIcon>}
<ListItemText
Expand Down
Loading

0 comments on commit 818b31f

Please sign in to comment.