Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: fix header navigation active nested menu highlight (#167) #169

Merged
merged 2 commits into from
Aug 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading