diff --git a/tailwind.common.js b/tailwind.common.js index f5a48d98eb..5414a017d4 100644 --- a/tailwind.common.js +++ b/tailwind.common.js @@ -408,6 +408,36 @@ module.exports = { }, }, }, + keyframes: { + menuOpen: { + '0%': { + opacity: '0', + transform: 'scale(0.60)', + transformOrigin: 'top right', + }, + '100%': { + opacity: '1', + transform: 'scale(1)', + transformOrigin: 'top right', + }, + }, + menuClose: { + '0%': { + opacity: '1', + transform: 'scale(1)', + transformOrigin: 'top right', + }, + '100%': { + opacity: '0', + transform: 'scale(0.60)', + transformOrigin: 'top right', + }, + }, + }, + animation: { + menuOpen: 'menuOpen 300ms ease-in-out', + menuClose: 'menuClose 225ms ease-in-out', + }, }, }, plugins: [], diff --git a/web-components/src/components/header/Header.styles.ts b/web-components/src/components/header/Header.styles.ts index e00d7668da..5fb37f3ea9 100644 --- a/web-components/src/components/header/Header.styles.ts +++ b/web-components/src/components/header/Header.styles.ts @@ -102,14 +102,6 @@ export const useHeaderStyles = makeStyles()( // BEGIN HACK setting jss styles (duplicated from mui components built-in emotion styles) // so it's initially rendered on gatsby build // Remove once migrations from mui jss to emotion and to latest gatsby done - desktop: { - [theme.breakpoints.down('md')]: { - display: 'none', - }, - [theme.breakpoints.up('md')]: { - display: 'block', - }, - }, mobile: { [theme.breakpoints.down('md')]: { display: 'block', diff --git a/web-components/src/components/header/components/HeaderDropdown/HeaderDropdown.Item.tsx b/web-components/src/components/header/components/HeaderDropdown/HeaderDropdown.Item.tsx index cc31099e7f..82c1e17ab4 100644 --- a/web-components/src/components/header/components/HeaderDropdown/HeaderDropdown.Item.tsx +++ b/web-components/src/components/header/components/HeaderDropdown/HeaderDropdown.Item.tsx @@ -56,6 +56,7 @@ export const HeaderDropdownItem: React.FC< alignItems="center" className={cn(styles.item, className)} onMouseEnter={onHover} + component="li" > {SVG && ( diff --git a/web-components/src/components/header/components/HeaderDropdown/HeaderDropdown.tsx b/web-components/src/components/header/components/HeaderDropdown/HeaderDropdown.tsx index b06bbd9dcb..d1cfd24eca 100644 --- a/web-components/src/components/header/components/HeaderDropdown/HeaderDropdown.tsx +++ b/web-components/src/components/header/components/HeaderDropdown/HeaderDropdown.tsx @@ -16,11 +16,16 @@ const HeaderDropdown: React.FC< items: HeaderDropdownItemProps[]; linkComponent: React.FC>; title?: string; + isUserMenu?: boolean; }> > = props => { const { classes: styles } = useStyles(); return ( - + {props.title && ( @@ -28,13 +33,15 @@ const HeaderDropdown: React.FC< )} - {props.items.map((link, i) => ( - - ))} + + {props.items.map((link, i) => ( + + ))} + ); }; diff --git a/web-components/src/components/header/components/HeaderMenuItem/HeaderMenuItem.Content.tsx b/web-components/src/components/header/components/HeaderMenuItem/HeaderMenuItem.Content.tsx index 9613a72603..83895d8554 100644 --- a/web-components/src/components/header/components/HeaderMenuItem/HeaderMenuItem.Content.tsx +++ b/web-components/src/components/header/components/HeaderMenuItem/HeaderMenuItem.Content.tsx @@ -13,6 +13,7 @@ type Props = { classes?: { paper?: string; }; + isUserMenu?: boolean; }; export const HeaderMenuItemContent = ({ @@ -20,6 +21,7 @@ export const HeaderMenuItemContent = ({ linkComponent: LinkComponent, pathname, classes, + isUserMenu, }: Props): JSX.Element => { const theme = useTheme(); const { classes: styles } = useHeaderMenuHoverStyles(); @@ -41,12 +43,14 @@ export const HeaderMenuItemContent = ({ title={item.title} renderTitle={item.renderTitle} classes={{ title: styles.title, paper: classes?.paper }} + isUserMenu={isUserMenu} > {/* `render` overrides default dropdown */} {item.dropdownItems && !item.renderDropdownItems && ( )} {item.renderDropdownItems && item.renderDropdownItems()} diff --git a/web-components/src/components/header/components/HeaderMenuItem/HeaderMenuItem.Hover.styles.ts b/web-components/src/components/header/components/HeaderMenuItem/HeaderMenuItem.Hover.styles.ts index e0dab8598a..452b8ec43b 100644 --- a/web-components/src/components/header/components/HeaderMenuItem/HeaderMenuItem.Hover.styles.ts +++ b/web-components/src/components/header/components/HeaderMenuItem/HeaderMenuItem.Hover.styles.ts @@ -2,18 +2,11 @@ import { Theme } from '@mui/material/styles'; import { makeStyles } from 'tss-react/mui'; export const useMenuHoverStyles = makeStyles()((theme: Theme) => ({ - popover: { - pointerEvents: 'none', - }, - popoverContent: { - pointerEvents: 'auto', - marginTop: theme.spacing(4), - }, text: { '& li.MuiMenuItem-root:hover': { backgroundColor: 'transparent', }, - '& li > a': { + '& ul > li > a': { fontFamily: 'lato', color: '#000', textDecoration: 'none', @@ -29,11 +22,6 @@ export const useMenuHoverStyles = makeStyles()((theme: Theme) => ({ outline: 'none', }, }, - paper: { - borderRadius: '2px', - border: `1px solid ${theme.palette.grey[400]}`, - padding: theme.spacing(6.25), - }, icon: { marginLeft: theme.spacing(1), }, diff --git a/web-components/src/components/header/components/HeaderMenuItem/HeaderMenuItem.Hover.tsx b/web-components/src/components/header/components/HeaderMenuItem/HeaderMenuItem.Hover.tsx index f285e8f3d2..7862040d07 100644 --- a/web-components/src/components/header/components/HeaderMenuItem/HeaderMenuItem.Hover.tsx +++ b/web-components/src/components/header/components/HeaderMenuItem/HeaderMenuItem.Hover.tsx @@ -1,9 +1,8 @@ -import React, { useRef } from 'react'; -import { MenuList, Paper, Popover } from '@mui/material'; -import cx from 'clsx'; +import { MenuList } from '@mui/material'; import DropdownIcon from '../../../icons/DropdownIcon'; import { useMenuHoverStyles } from './HeaderMenuItem.Hover.styles'; +import { useMenuState } from './hooks/useMenuState'; export interface MenuTitle { title?: string; @@ -18,6 +17,7 @@ interface Props extends MenuTitle { children: React.ReactNode; textColor?: string; dropdownColor?: string; + isUserMenu?: boolean; } /** @@ -30,31 +30,30 @@ const HeaderMenuItemHover = ({ classes, dropdownColor, children, + isUserMenu, }: Props): JSX.Element => { const { classes: styles } = useMenuHoverStyles(); - const popoverAnchor = useRef(null); - const [anchorEl, setAnchorEl] = React.useState(null); - - const handlePopoverOpen = () => { - setAnchorEl(popoverAnchor.current); - }; - - const handlePopoverClose = () => { - setAnchorEl(null); - }; - - const open = Boolean(anchorEl); + const { + isMenuOpen, + hasInteracted, + isTouchScreen, + openMenu, + closeMenu, + toggleMenu, + } = useMenuState(); return ( -
+
{title && ( @@ -64,40 +63,29 @@ const HeaderMenuItemHover = ({ )} {renderTitle && renderTitle()} - - - - {children} - - - + + {children} + +
); }; diff --git a/web-components/src/components/header/components/HeaderMenuItem/HeaderMenuItem.styles.ts b/web-components/src/components/header/components/HeaderMenuItem/HeaderMenuItem.styles.ts index 67db0e62ad..297ad6766d 100644 --- a/web-components/src/components/header/components/HeaderMenuItem/HeaderMenuItem.styles.ts +++ b/web-components/src/components/header/components/HeaderMenuItem/HeaderMenuItem.styles.ts @@ -5,8 +5,6 @@ export const useHeaderMenuHoverStyles = makeStyles()(theme => ({ boxSizing: 'border-box', height: '100%', lineHeight: theme.spacing(6), - paddingRight: theme.spacing(7.375), - paddingLeft: theme.spacing(7.375), backgroundColor: 'inherit', '& > a': { borderBottom: '2px solid transparent', diff --git a/web-components/src/components/header/components/HeaderMenuItem/HeaderMenuItem.tsx b/web-components/src/components/header/components/HeaderMenuItem/HeaderMenuItem.tsx index 5b8061a8af..0c9edd90b2 100644 --- a/web-components/src/components/header/components/HeaderMenuItem/HeaderMenuItem.tsx +++ b/web-components/src/components/header/components/HeaderMenuItem/HeaderMenuItem.tsx @@ -1,5 +1,10 @@ -import React from 'react'; -import { BoxProps, MenuItem, SxProps } from '@mui/material'; +import { + BoxProps, + MenuItem, + SxProps, + useMediaQuery, + useTheme, +} from '@mui/material'; import cx from 'clsx'; import { Theme } from '../../../../theme/muiTheme'; @@ -30,6 +35,7 @@ export interface HeaderMenuItemBase { export interface MenuItemProps extends HeaderMenuItemBase { item: Item; + isUserMenu?: boolean; } const HeaderMenuItem: React.FC = ({ @@ -39,15 +45,18 @@ const HeaderMenuItem: React.FC = ({ classes, sx, component = 'li', + isUserMenu, }) => { const { classes: styles } = useHeaderMenuHoverStyles(); - + const theme = useTheme(); + const isTablet = useMediaQuery(theme.breakpoints.down('md')); return ( = ({ linkComponent={linkComponent} pathname={pathname} classes={{ paper: classes?.paper }} + isUserMenu={isUserMenu} /> ); diff --git a/web-components/src/components/header/components/HeaderMenuItem/hooks/useMenuState.tsx b/web-components/src/components/header/components/HeaderMenuItem/hooks/useMenuState.tsx new file mode 100644 index 0000000000..c1eb313a37 --- /dev/null +++ b/web-components/src/components/header/components/HeaderMenuItem/hooks/useMenuState.tsx @@ -0,0 +1,48 @@ +import { useEffect, useState } from 'react'; +import { useLocation } from 'react-router-dom'; +import { useMediaQuery } from '@mui/material'; + +/** + * Manages dropdown menu interactions for both mouse and touch devices. + * Provides smooth transitions by coordinating animations with state changes. + */ +export const useMenuState = () => { + const location = useLocation(); + // eslint-disable-next-line lingui/no-unlocalized-strings + const isTouchScreen = useMediaQuery('(pointer: coarse)'); + const [isMenuOpen, setIsMenuOpen] = useState(false); + // This state prevents animation flicker on initial page load + const [hasInteracted, setHasInteracted] = useState(false); + + const openMenu = () => { + // Track first interaction to enable animations only after user engagement + if (!hasInteracted) setHasInteracted(true); + setIsMenuOpen(true); + }; + + const closeMenu = () => { + setIsMenuOpen(false); + }; + + // Toggle needed for touch devices where hover isn't available + const toggleMenu = () => { + requestAnimationFrame(() => { + setIsMenuOpen(prev => !prev); + }); + }; + + // Reset on route change prevents ghost menus when navigating + useEffect(() => { + setIsMenuOpen(false); + setHasInteracted(false); + }, [location.pathname]); + + return { + isMenuOpen, + hasInteracted, + isTouchScreen, + openMenu, + closeMenu, + toggleMenu, + }; +}; diff --git a/web-components/src/components/header/components/UserMenuItem.Profile.tsx b/web-components/src/components/header/components/UserMenuItem.Profile.tsx index 79bd40d831..f6415b3aa0 100644 --- a/web-components/src/components/header/components/UserMenuItem.Profile.tsx +++ b/web-components/src/components/header/components/UserMenuItem.Profile.tsx @@ -36,7 +36,7 @@ const UserMenuItemProfile: React.FC = ({ className="w-full" onClick={() => onProfileClick && onProfileClick(id, selected)} > - + {selected && ( diff --git a/web-components/src/components/header/components/UserMenuItem.styles.ts b/web-components/src/components/header/components/UserMenuItem.styles.ts index 4614b044bb..3f74ade86b 100644 --- a/web-components/src/components/header/components/UserMenuItem.styles.ts +++ b/web-components/src/components/header/components/UserMenuItem.styles.ts @@ -2,13 +2,13 @@ import { makeStyles } from '@mui/styles'; export const useUserMenuItemStyles = makeStyles(theme => ({ userMenuItem: { - padding: theme.spacing(2.5), - paddingRight: 0, + padding: 0, borderRadius: '2px', border: `1px solid ${theme.palette.grey[100]}`, position: 'relative', backgroundColor: `${theme.palette.primary.main} !important`, color: `${theme.palette.primary.contrastText} !important`, + height: 'auto', '&:hover': { '&:after': { background: theme.palette.secondary.main, diff --git a/web-components/src/components/header/components/UserMenuItem.tsx b/web-components/src/components/header/components/UserMenuItem.tsx index fd0178c1e9..04a9db6c38 100644 --- a/web-components/src/components/header/components/UserMenuItem.tsx +++ b/web-components/src/components/header/components/UserMenuItem.tsx @@ -13,6 +13,7 @@ const UserMenuItem: React.FC> = ({ ); diff --git a/web-components/src/components/header/components/UserMenuItems.tsx b/web-components/src/components/header/components/UserMenuItems.tsx index d749562395..ddad762808 100644 --- a/web-components/src/components/header/components/UserMenuItems.tsx +++ b/web-components/src/components/header/components/UserMenuItems.tsx @@ -51,7 +51,7 @@ const UserMenuItems: React.FC> = ({
), extras: ( -
+
- + {menuItems?.map((item, index) => { return ( @@ -97,20 +94,15 @@ export default function Header({ {isRegistry && extras} {websiteExtras} - - - + + + diff --git a/web-components/src/components/mobile-menu/MobileMenu.styles.ts b/web-components/src/components/mobile-menu/MobileMenu.styles.ts index c732ea0643..82cbe7d4a7 100644 --- a/web-components/src/components/mobile-menu/MobileMenu.styles.ts +++ b/web-components/src/components/mobile-menu/MobileMenu.styles.ts @@ -12,9 +12,11 @@ export const useMobileMenuStyles = makeStyles()((theme: Theme) => ({ backgroundColor: theme.palette.primary.light, width: '85%', maxWidth: '350px', + height: `calc(100% - 75px)`, }, '& .MuiBackdrop-root, & .MuiDrawer-paper': { - top: theme.spacing(15), + top: theme.spacing(16), + right: '0', }, }, menuList: { diff --git a/web-components/src/components/mobile-menu/index.tsx b/web-components/src/components/mobile-menu/index.tsx index 69f0c73858..294668bea6 100644 --- a/web-components/src/components/mobile-menu/index.tsx +++ b/web-components/src/components/mobile-menu/index.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useState } from 'react'; -import { useTheme } from '@mui/material'; +import { useMediaQuery, useTheme } from '@mui/material'; import Box from '@mui/material/Box'; import Drawer from '@mui/material/Drawer'; import MenuItem from '@mui/material/MenuItem'; @@ -15,9 +15,7 @@ import { useMobileMenuStyles } from './MobileMenu.styles'; type Props = { menuItems?: Item[]; - isRegistry?: boolean; pathname: string; - extras?: JSX.Element; linkComponent: React.FC>; websiteExtras?: JSX.Element; isUserLoggedIn?: boolean; @@ -26,17 +24,19 @@ type Props = { const MobileMenu: React.FC> = ({ menuItems, pathname, - isRegistry, - extras, websiteExtras, linkComponent: Link, isUserLoggedIn, }) => { const { classes: styles, cx } = useMobileMenuStyles(); const theme = useTheme(); + // eslint-disable-next-line lingui/no-unlocalized-strings + const isTouchScreen = useMediaQuery('(pointer: coarse)'); const [open, setOpen] = useState(false); - const handleOpen = (): void => setOpen(true); + const handleToggle = () => { + setOpen(open => !open); + }; const handleClose = (): void => setOpen(false); // close drawer if route changes @@ -47,10 +47,10 @@ const MobileMenu: React.FC> = ({ return (
- {isRegistry && extras}