Skip to content

Commit

Permalink
feat: add catalog/explorer dropdown to anvil portal header (#2796)
Browse files Browse the repository at this point in the history
  • Loading branch information
Fran McDade authored and Fran McDade committed Nov 14, 2023
1 parent 20fcb12 commit 4f1a602
Show file tree
Hide file tree
Showing 22 changed files with 434 additions and 22 deletions.
2 changes: 1 addition & 1 deletion next/site-config/anvil-portal/prod/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ const config: SiteConfig = {
label: "Explorer",
}),
target: ANCHOR_TARGET.BLANK,
url: `${EXPLORER_URL}/datasets`,
url: `${EXPLORER_URL}/explore/datasets`,
},
],
url: "",
Expand Down
11 changes: 10 additions & 1 deletion src/components/header/common/entities.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { ReactNode } from "react";
import { Target } from "../../target/target.model";
import { Social } from "../components/socials/socials";

/**
Expand Down Expand Up @@ -27,6 +29,13 @@ export interface Logo {
* Model of nav link item to be use as props for the Header and Footer component.
*/
export interface NavLinkItem {
label: string;
featureFlag?: boolean;
label: ReactNode;
menuItems?: MenuItem[];
target?: Target;
url: string;
}

export interface MenuItem extends NavLinkItem {
description?: string;
}
43 changes: 43 additions & 0 deletions src/components/header/common/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { Header as HeaderProps, NavLinkItem } from "./entities";

/**
* Returns the header navigation links for the site config and feature flag.
* @param navLinks - Nav links.
* @param isFeatureFlag - Flag indicating if feature is available to user.
* @returns navigation links.
*/
function filterFeatureFlagNavigation(
navLinks: NavLinkItem[],
isFeatureFlag: boolean
): NavLinkItem[] {
return navLinks.filter(
({ featureFlag }) =>
featureFlag === undefined || featureFlag === isFeatureFlag
);
}

/**
* Returns the header properties for the site config and feature flag.
* @param header - Site config header.
* @param isFeatureFlag - Flag indicating if feature is available to user.
* @returns header properties.
*/
export function configureHeader(
header: HeaderProps,
isFeatureFlag: boolean
): HeaderProps {
const navLinks = filterFeatureFlagNavigation(header.navLinks, isFeatureFlag);
return {
...header,
navLinks,
};
}

/**
* Returns true if the given link is an internal link.
* @param link - Link.
* @returns true if the given link is an internal link.
*/
export function isClientSideNavigation(link: string): boolean {
return /^\/(?!\/)/.test(link);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { css } from "@emotion/react";
import styled from "@emotion/styled";
import { Button as MButton } from "@mui/material";

interface Props {
isActive: boolean;
}

export const NavLinkDropdownButton = styled(MButton, {
shouldForwardProp: (prop) => prop !== "isActive",
})<Props>`
background-color: ${({ theme }) => theme.palette.common.white};
color: inherit;
gap: 0;
&:active,
&:hover {
background-color: ${({ theme }) => theme.palette.smoke.light};
}
// Button is "active" i.e. menu is open.
${({ isActive, theme }) =>
isActive &&
css`
background-color: ${theme.palette.smoke.light};
`};
.MuiButton-endIcon {
margin-left: -3px;
margin-right: -6px;
}
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import ArrowDropDownRoundedIcon from "@mui/icons-material/ArrowDropDownRounded";
import { ButtonProps as MButtonProps } from "@mui/material";
import React from "react";
import { NavLinkDropdownButton as Button } from "./nav-link-dropdown-button.styles";

export interface NavLinkDropdownButtonProps extends MButtonProps {
isActive: boolean;
}

export const NavLinkDropdownButton = ({
children,
isActive,
...props /* Spread props to allow for Button specific props ButtonProps e.g. "onClick". */
}: NavLinkDropdownButtonProps): JSX.Element => {
return (
<Button
endIcon={<ArrowDropDownRoundedIcon />}
isActive={isActive}
sx={{ justifyContent: { desktop: "unset", mobile: "flex-start" } }}
variant="nav"
{...props}
>
{children}
</Button>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { SvgIcon, SvgIconProps } from "@mui/material";
import React from "react";

/**
* Custom open in new icon.
*/

export const OpenInNewIcon = ({
fontSize = "xsmall",
viewBox = "0 0 18 18",
...props /* Spread props to allow for Mui SvgIconProps specific prop overrides e.g. "htmlColor". */
}: SvgIconProps): JSX.Element => {
return (
<SvgIcon fontSize={fontSize} viewBox={viewBox} {...props}>
<path
d="M3.75 15.75C3.3375 15.75 2.98438 15.6031 2.69063 15.3094C2.39688 15.0156 2.25 14.6625 2.25 14.25V3.75C2.25 3.3375 2.39688 2.98438 2.69063 2.69063C2.98438 2.39688 3.3375 2.25 3.75 2.25H8.25C8.4625 2.25 8.64063 2.32187 8.78438 2.46562C8.92813 2.60937 9 2.7875 9 3C9 3.2125 8.92813 3.39062 8.78438 3.53437C8.64063 3.67812 8.4625 3.75 8.25 3.75H3.75V14.25H14.25V9.75C14.25 9.5375 14.3219 9.35938 14.4656 9.21563C14.6094 9.07188 14.7875 9 15 9C15.2125 9 15.3906 9.07188 15.5344 9.21563C15.6781 9.35938 15.75 9.5375 15.75 9.75V14.25C15.75 14.6625 15.6031 15.0156 15.3094 15.3094C15.0156 15.6031 14.6625 15.75 14.25 15.75H3.75ZM6.75 11.25C6.6125 11.1125 6.54375 10.9375 6.54375 10.725C6.54375 10.5125 6.6125 10.3375 6.75 10.2L13.2 3.75H11.25C11.0375 3.75 10.8594 3.67812 10.7156 3.53437C10.5719 3.39062 10.5 3.2125 10.5 3C10.5 2.7875 10.5719 2.60937 10.7156 2.46562C10.8594 2.32187 11.0375 2.25 11.25 2.25H15C15.2125 2.25 15.3906 2.32187 15.5344 2.46562C15.6781 2.60937 15.75 2.7875 15.75 3V6.75C15.75 6.9625 15.6781 7.14063 15.5344 7.28438C15.3906 7.42813 15.2125 7.5 15 7.5C14.7875 7.5 14.6094 7.42813 14.4656 7.28438C14.3219 7.14063 14.25 6.9625 14.25 6.75V4.8L7.78125 11.2688C7.64375 11.4063 7.475 11.475 7.275 11.475C7.075 11.475 6.9 11.4 6.75 11.25Z"
fill="currentColor"
/>
</SvgIcon>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import styled from "@emotion/styled";

export const Label = styled.div`
align-items: center;
display: flex;
gap: 4px;
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import React, { ElementType } from "react";
import { OpenInNewIcon } from "./components/open-in-new/open-in-new-icon/open-in-new-icon";
import { Label } from "./label-icon-menu-item.styles";

export interface LabelIconMenuItemProps {
Icon?: ElementType;
iconFontSize?: string;
label: string;
}

export const LabelIconMenuItem = ({
Icon = OpenInNewIcon,
iconFontSize = "xsmall",
label,
}: LabelIconMenuItemProps): JSX.Element => {
return (
<Label>
<div>{label}</div>
<Icon color="inkLight" fontSize={iconFontSize} />
</Label>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import styled from "@emotion/styled";
import { Menu } from "@mui/material";

export const NavLinkMenu = styled(Menu)`
.MuiPaper-menu {
margin: 4px 0;
min-width: 144px;
border-color: ${({ theme }) => theme.palette.smoke.main};
}
&& .MuiMenuItem-root {
margin: 0;
}
.MuiListItemText-root {
display: grid;
gap: 4px;
.MuiListItemText-secondary {
color: ${({ theme }) => theme.palette.ink.light};
max-width: 290px;
white-space: normal;
}
}
`;
76 changes: 76 additions & 0 deletions src/components/header/components/nav-link-menu/nav-link-menu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { ListItemText, MenuItem as MMenuItem } from "@mui/material";
import { navigate } from "gatsby";
import React, { MouseEvent, ReactNode, useState } from "react";
import { Target } from "../../../target/target.model";
import { MenuItem } from "../../common/entities";
import { isClientSideNavigation } from "../../common/utils";
import { NavLinkDropdownButton } from "../nav-link-dropdown-button/nav-link-dropdown-button";
import { NavLinkMenu as Menu } from "./nav-link-menu.styles";

export interface NavLinkMenuProps {
menuItems: MenuItem[];
menuLabel: ReactNode;
}

export const NavLinkMenu = ({
menuItems,
menuLabel,
}: NavLinkMenuProps): JSX.Element => {
const [anchorEl, setAnchorEl] = useState<null | HTMLButtonElement>(null);
const open = Boolean(anchorEl);

const onOpenMenu = (event: MouseEvent<HTMLButtonElement>): void => {
setAnchorEl(event.currentTarget);
};

const onCloseMenu = (): void => {
setAnchorEl(null);
};

return (
<>
<NavLinkDropdownButton isActive={open} onClick={onOpenMenu}>
{menuLabel}
</NavLinkDropdownButton>
<Menu
anchorEl={anchorEl}
anchorOrigin={{ horizontal: "left", vertical: "bottom" }}
autoFocus={false}
onClose={onCloseMenu}
open={open}
PaperProps={{ variant: "menu" }}
transformOrigin={{
horizontal: "left",
vertical: "top",
}}
>
{menuItems.map(
({ description, label, target = Target.SELF, url }, i) => (
<MMenuItem
key={i}
onClick={(): void => {
setAnchorEl(null);
if (isClientSideNavigation(url)) {
navigate(url);
} else {
window.open(url, target);
}
}}
>
<ListItemText
primary={label}
primaryTypographyProps={{
variant: description ? "text-body-500" : "text-body-400",
}}
secondary={description}
secondaryTypographyProps={{
variant: "text-body-small-400-2lines",
}}
/>
</MMenuItem>
)
)}
</Menu>
</>
);
};
37 changes: 24 additions & 13 deletions src/components/header/components/nav-links/nav-links.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { navigate } from "gatsby";
import { Box, Button } from "@mui/material";
import { navigate } from "gatsby";
import React from "react";
import { Target } from "../../../target/target.model";
import { NavLinkItem } from "../../common/entities";
import { isClientSideNavigation } from "../../common/utils";
import { NavLinkMenu } from "../nav-link-menu/nav-link-menu";

interface Props {
center?: boolean;
Expand All @@ -24,18 +27,26 @@ export default function NavLinks({
}}
marginLeft={{ desktop: center ? undefined : 6, mobile: undefined }}
>
{links.map(({ label, url }) => (
<Button
key={url}
onClick={() => navigate(url)}
sx={{
justifyContent: { desktop: "unset", mobile: "flex-start" },
}}
variant="nav"
>
{label}
</Button>
))}
{links.map(({ label, menuItems, target = Target.SELF, url }, i) =>
menuItems ? (
<NavLinkMenu key={i} menuItems={menuItems} menuLabel={label} />
) : (
<Button
key={`${url}${i}`}
onClick={() =>
isClientSideNavigation(url)
? navigate(url)
: window.open(url, target)
}
sx={{
justifyContent: { desktop: "unset", mobile: "flex-start" },
}}
variant="nav"
>
{label}
</Button>
)
)}
</Box>
);
}
12 changes: 10 additions & 2 deletions src/components/header/header.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import CloseRoundedIcon from "@mui/icons-material/CloseRounded";
import MenuRoundedIcon from "@mui/icons-material/MenuRounded";
import { Box, Divider, IconButton, Toolbar, Typography } from "@mui/material";
import React, { useEffect, useState } from "react";
import React, { useEffect, useMemo, useState } from "react";
import { FEATURES } from "../../hooks/useFeatureFlag/common/entities";
import { useFeatureFlag } from "../../hooks/useFeatureFlag/useFeatureFlag";
import { Header as HeaderProps } from "./common/entities";
import { configureHeader } from "./common/utils";
import Content from "./components/content/content";
import HeaderLogo from "./components/logo/header-logo";
import NavLinks from "./components/nav-links/nav-links";
Expand All @@ -25,14 +28,19 @@ interface Props {
}

export default function Header({ header, searchPath }: Props): JSX.Element {
const isFeatureFlag = useFeatureFlag(FEATURES.HEADER);
const configuredHeaderProps = useMemo(
() => configureHeader(header, isFeatureFlag),
[header, isFeatureFlag]
); // Configure header.
const {
authenticationEnabled,
logo,
navLinks,
searchEnabled,
slogan,
socials,
} = header;
} = configuredHeaderProps;
const [drawerOpen, setDrawerOpen] = useState(false);
const [searchOpen, setSearchOpen] = useState<boolean>(false);
const desktop = useBreakpointHelper(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ const SectionLatestUpdates: FC = (): JSX.Element => {
{/* Content */}
<>
<h3 className={sectionCardHeading}>News</h3>
{newsCards.map((card) => (
<NewsCard key={card.title} newsCard={card} />
{newsCards.map((card, i) => (
<NewsCard key={`${card.title}${i}`} newsCard={card} />
))}
</>
{/* CTAs */}
Expand All @@ -70,8 +70,8 @@ const SectionLatestUpdates: FC = (): JSX.Element => {
{/* Content */}
<>
<h3 className={sectionCardHeading}>Events</h3>
{eventCards.map((card) => (
<EventCard key={card.title} eventCard={card} />
{eventCards.map((card, i) => (
<EventCard key={`${card.title}${i}`} eventCard={card} />
))}
</>
{/* CTAs */}
Expand Down
Loading

0 comments on commit 4f1a602

Please sign in to comment.