client/components/shared/nav/DropdownNav.tsx (252 lines of code) (raw):

import { css } from '@emotion/react'; import { from, neutral, palette, space } from '@guardian/source/foundations'; import { useEffect, useRef, useState } from 'react'; import { Link } from 'react-router-dom'; import { gridItemPlacement } from '../../../styles/grid'; import { ProfileIcon } from '../../mma/shared/assets/ProfileIcon'; import { expanderButtonCss } from '../ExpanderButton'; import type { MenuSpecificNavItem } from './NavConfig'; import { NAV_LINKS } from './NavConfig'; const dropdownNavCss = (showMenu: boolean) => css({ display: `${showMenu ? 'block' : 'none'}`, background: palette.brand[400], borderTop: `1px solid ${palette.brand[600]}`, position: 'absolute', top: '50px', left: 0, width: 'calc(100% - 30px)', maxWidth: '350px', zIndex: 1071, listStyle: 'none', lineHeight: '1.375rem', boxShadow: '0 0 0 0.0625rem rgba(0,0,0,0.1)', margin: 0, padding: 0, ' li': { padding: 0, margin: 0, }, [from.desktop]: { width: 'auto', minWidth: '220px', maxWidth: 'none', top: `${space[9]}px`, left: 'auto', marginRight: '-32px', bottom: 'auto', borderTop: 'none', background: neutral['100'], 'li:not(:last-child)': { borderBottom: `1px solid ${neutral['86']}`, }, ':before': { content: "''", width: 0, height: 0, position: 'absolute', top: `-${space[2]}px`, right: '85px', borderLeft: `${space[2]}px solid transparent`, borderRight: `${space[2]}px solid transparent`, borderBottom: `${space[2]}px solid ${neutral['100']}`, }, }, }); const dropdownNavItemCss = css({ padding: `9px 30px ${space[2]}px 46px`, textDecoration: 'none', color: neutral['100'], whiteSpace: 'nowrap', position: 'relative', marginTop: '-1px', display: 'flex', alignItems: 'center', ':hover, :focus': { backgroundColor: palette.brand[300], textDecoration: 'none', }, ':focus': { outline: 0, }, ':after': { content: "''", display: 'block', zIndex: 1, position: 'absolute', bottom: 0, right: 0, width: 'calc(100% - 46px)', height: '1px', backgroundColor: `${palette.brand[600]}`, }, [from.desktop]: { padding: '18px 14px', color: neutral['20'], '.icon--fill': { fill: neutral['20'], }, ':after': { content: 'none', }, ':hover, :focus': { backgroundColor: neutral['97'], }, }, }); const DropdownNavItem = ({ navItem }: { navItem: MenuSpecificNavItem }) => ( <> {navItem.icon && ( <div css={{ ...(!navItem.isDropDownExclusive && { [from.desktop]: { display: 'none', }, }), position: 'absolute', left: `${space[3]}px`, }} > <navItem.icon overrideFillColor={neutral[100]} overrideWidthAtDesktop={12} /> </div> )} <span css={{ lineHeight: '33px', [from.desktop]: { lineHeight: 'normal', marginLeft: navItem.isDropDownExclusive && navItem.icon ? `${space[5]}px` : 0, }, }} > {navItem.title} </span> </> ); export const DropdownNav = (props: { isHelpCentrePage: boolean }) => { const [showMenu, setShowMenu] = useState(false); const wrapperRef = useRef<HTMLElement>(null); const buttonRef = useRef<HTMLButtonElement>(null); useEffect(() => { addListeners(); return () => { removeListeners(); }; }); const handleKeyDown = (event: KeyboardEvent) => { if (event.code === 'Escape' && showMenu) { setShowMenu(false); if (buttonRef.current) { buttonRef.current.focus(); } } }; const handleDismissiveClick = (event: MouseEvent): void => { if ( wrapperRef.current && event.target && !wrapperRef.current.contains(event.target as Node) ) { setShowMenu(false); } }; const addListeners = () => { document.addEventListener('keydown', handleKeyDown, false); document.addEventListener('click', handleDismissiveClick, false); }; const removeListeners = () => { document.removeEventListener('keydown', handleKeyDown, false); document.removeEventListener('click', handleDismissiveClick, false); }; return ( <nav ref={wrapperRef} css={{ ...gridItemPlacement(1, 2), whiteSpace: 'nowrap', maxHeight: '26px', margin: 'auto 0', [from.desktop]: { position: 'relative', left: '0.5rem', marginLeft: 'auto', }, ' button': { [from.tablet]: { marginLeft: 'auto', }, paddingTop: 0, paddingBottom: 0, }, }} > {/* TODO refactor to full use ExpanderButton */} <button css={{ ...expanderButtonCss( neutral['100'], neutral['100'], )(showMenu), }} type="button" aria-expanded={showMenu} onClick={() => setShowMenu(!showMenu)} ref={buttonRef} > { <i css={css` display: inline-block; width: 26px; height: 26px; margin-right: 0.5rem; border-radius: 50%; background-color: white; position: relative; `} > <ProfileIcon additionalCss={css` position: absolute; bottom: 0; left: 50%; transform: translateX(-50%); width: 65%; height: auto; `} /> </i> } My account </button> <ul css={dropdownNavCss(showMenu)}> {Object.values(NAV_LINKS).map( (navItem: MenuSpecificNavItem) => ( <li key={navItem.title}> {navItem.local && !props.isHelpCentrePage ? ( <Link to={navItem.link} css={dropdownNavItemCss} onClick={() => setShowMenu(false)} > <DropdownNavItem navItem={navItem} /> </Link> ) : ( <a href={navItem.link} css={dropdownNavItemCss}> <DropdownNavItem navItem={navItem} /> </a> )} </li> ), )} </ul> </nav> ); };