From de252869d56598d3c0d3b73fc56b1a1dc0eef36e Mon Sep 17 00:00:00 2001 From: Renato Massao Yonamine <1071799+massao@users.noreply.github.com> Date: Mon, 8 Jul 2024 06:55:24 -0300 Subject: [PATCH] feat: improve responsiveness for navigation [CFISO-1555] (#2805) * refactor: render navbar complete as fullscreen on storybook * feat: add mobile navigation to complete story * refactor: improve responsiveness on navbar * feat: add separate story for responsiveness * chore: update to latest version * feat: hide secondary nav children except search * docs: use only navbar compound elements * feat: prevent overflow of submenu on mobile * feat: prevent header overflow with long space names * feat: avoid overflow on small desktop screens * feat: increase icon size for mobile devices --------- Co-authored-by: Kathrin --- package-lock.json | 4 +- .../components/navbar/src/CompoundNavbar.ts | 3 + .../components/navbar/src/Navbar.styles.ts | 47 +++++- packages/components/navbar/src/Navbar.tsx | 57 +++++-- .../src/NavbarItem/NavbarItem.styles.ts | 16 +- .../navbar/src/NavbarItem/NavbarItem.tsx | 10 +- .../src/NavbarItemIcon/NavbarItemIcon.tsx | 11 +- .../src/NavbarMenu/NavbarMenu.styles.ts | 6 +- .../src/NavbarSubmenu/NavbarMenu.styles.ts | 21 +++ .../src/NavbarSubmenu/NavbarSubmenu.tsx | 41 +++++ .../src/NavbarSwitcher/NavbarEnvVariant.tsx | 9 +- .../NavbarSwitcher/NavbarSwitcher.styles.ts | 19 ++- .../src/NavbarSwitcher/NavbarSwitcher.tsx | 6 +- .../components/navbar/src/utils.styles.ts | 8 +- .../navbar/stories/Navbar.stories.tsx | 153 +++++++++++++++++- 15 files changed, 380 insertions(+), 31 deletions(-) create mode 100644 packages/components/navbar/src/NavbarSubmenu/NavbarMenu.styles.ts create mode 100644 packages/components/navbar/src/NavbarSubmenu/NavbarSubmenu.tsx diff --git a/package-lock.json b/package-lock.json index b493024afb..eca45caa49 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40320,10 +40320,11 @@ }, "packages/components/navbar": { "name": "@contentful/f36-navbar", - "version": "5.0.0-alpha.19", + "version": "5.0.0-alpha.23", "license": "MIT", "dependencies": { "@contentful/f36-avatar": "4.67.1", + "@contentful/f36-button": "4.67.0", "@contentful/f36-core": "^4.67.1", "@contentful/f36-icon": "5.0.0-alpha.20", "@contentful/f36-icons": "5.0.0-alpha.25", @@ -50004,6 +50005,7 @@ "version": "file:packages/components/navbar", "requires": { "@contentful/f36-avatar": "4.67.1", + "@contentful/f36-button": "4.67.0", "@contentful/f36-core": "^4.67.1", "@contentful/f36-icon": "5.0.0-alpha.20", "@contentful/f36-icons": "5.0.0-alpha.25", diff --git a/packages/components/navbar/src/CompoundNavbar.ts b/packages/components/navbar/src/CompoundNavbar.ts index 6b8330f7bb..3230a8644a 100644 --- a/packages/components/navbar/src/CompoundNavbar.ts +++ b/packages/components/navbar/src/CompoundNavbar.ts @@ -7,6 +7,7 @@ import { NavbarItem, NavbarItemSkeleton } from './NavbarItem'; import { NavbarMenuItem, NavbarMenuItemSkeleton } from './NavbarMenuItem'; import { NavbarSwitcher, NavbarSwitcherSkeleton } from './NavbarSwitcher'; import { NavbarBadge } from './NavbarBadge/NavbarBadge'; +import { NavbarSubmenu } from './NavbarSubmenu/NavbarSubmenu'; type CompoundNavbar = typeof OriginalNavbar & { Item: typeof NavbarItem; @@ -15,6 +16,7 @@ type CompoundNavbar = typeof OriginalNavbar & { MenuItemSkeleton: typeof NavbarMenuItemSkeleton; MenuDivider: typeof MenuDivider; MenuSectionTitle: typeof MenuSectionTitle; + Submenu: typeof NavbarSubmenu; Switcher: typeof NavbarSwitcher; SwitcherSkeleton: typeof NavbarSwitcherSkeleton; Account: typeof NavbarAccount; @@ -29,6 +31,7 @@ Navbar.MenuItem = NavbarMenuItem; Navbar.MenuItemSkeleton = NavbarMenuItemSkeleton; Navbar.MenuDivider = MenuDivider; Navbar.MenuSectionTitle = MenuSectionTitle; +Navbar.Submenu = NavbarSubmenu; Navbar.Switcher = NavbarSwitcher; Navbar.SwitcherSkeleton = NavbarSwitcherSkeleton; Navbar.Account = NavbarAccount; diff --git a/packages/components/navbar/src/Navbar.styles.ts b/packages/components/navbar/src/Navbar.styles.ts index 51fba3284c..d1adc24f6a 100644 --- a/packages/components/navbar/src/Navbar.styles.ts +++ b/packages/components/navbar/src/Navbar.styles.ts @@ -9,16 +9,57 @@ export const getNavbarStyles = (maxWidth: string, variant: string) => ({ width: '100%', }), logo: css({ - height: '28px', - width: '28px', + display: 'none', + [mqs.small]: { + display: 'block', + height: '28px', + width: '28px', + }, }), + navigation: css({ width: '100%', maxWidth: variant === 'wide' ? '1524px' : maxWidth, padding: `${tokens.spacingS} ${tokens.spacingM}`, minHeight: tokens.spacingL, - [mqs.medium]: { + [mqs.small]: { padding: `${tokens.spacingM} ${tokens.spacingL}`, }, }), + + mainNavigation: css({ + display: 'none', + [mqs.small]: { + display: 'flex', + }, + }), + + mobileNavigationButton: css({ + display: 'flex', + height: '36px', + borderRadius: '10px', + [mqs.small]: { + display: 'none', + }, + }), + mobileNavigationIcon: css({ + heigt: '20px', + width: '20px', + }), + + secondaryNavigationWrapper: css({ + '> *:not(:first-child)': { + display: 'none', + [mqs.xsmall]: { + display: 'flex', + }, + }, + }), + + account: css({ + display: 'none', + [mqs.xsmall]: { + display: 'flex', + }, + }), }); diff --git a/packages/components/navbar/src/Navbar.tsx b/packages/components/navbar/src/Navbar.tsx index eedad0463e..fad11e4ebb 100644 --- a/packages/components/navbar/src/Navbar.tsx +++ b/packages/components/navbar/src/Navbar.tsx @@ -3,6 +3,9 @@ import React from 'react'; import { getNavbarStyles } from './Navbar.styles'; import { ContentfulLogoIcon } from './icons'; import { cx } from 'emotion'; +import { Button } from '@contentful/f36-button'; +import { ListIcon } from '@contentful/f36-icons'; +import { NavbarMenu } from './NavbarMenu/NavbarMenu'; type NavbarOwnProps = CommonProps & { /** @@ -23,6 +26,9 @@ type NavbarOwnProps = CommonProps & { /** User Account Component */ account?: React.ReactNode; + /** Navigation displayed on mobile versions */ + mobileNavigation?: React.ReactNode; + /** * Defines the max-width of the content inside the navbar. * @default '100%' @@ -48,6 +54,7 @@ function _Navbar(props: ExpandProps, ref: React.Ref) { mainNavigation, secondaryNavigation, account, + mobileNavigation, className, contentMaxWidth = '100%', testId = 'cf-ui-navbar', @@ -68,25 +75,55 @@ function _Navbar(props: ExpandProps, ref: React.Ref) { as="nav" className={styles.navigation} justifyContent="space-between" + gap="spacingXs" > {logo || } + {mobileNavigation && ( + } + > + Menu + + } + > + {mobileNavigation} + + )} {mainNavigation && ( - + {mainNavigation} )} - {switcher} - {secondaryNavigation && ( - {secondaryNavigation} - )} - {account && ( - - {account} - - )} + {switcher} + + {secondaryNavigation && ( + + {secondaryNavigation} + + )} + {account && ( + + {account} + + )} + diff --git a/packages/components/navbar/src/NavbarItem/NavbarItem.styles.ts b/packages/components/navbar/src/NavbarItem/NavbarItem.styles.ts index f034f4d538..72fd5e592c 100644 --- a/packages/components/navbar/src/NavbarItem/NavbarItem.styles.ts +++ b/packages/components/navbar/src/NavbarItem/NavbarItem.styles.ts @@ -1,7 +1,7 @@ import { css } from 'emotion'; import tokens from '@contentful/f36-tokens'; import { hexToRGBA } from '@contentful/f36-utils'; -import { getGlowOnFocusStyles, increaseHitArea } from '../utils.styles'; +import { getGlowOnFocusStyles, increaseHitArea, mqs } from '../utils.styles'; export const getNavbarItemActiveStyles = () => css({ @@ -22,7 +22,7 @@ const commonItemStyles = { gap: tokens.spacing2Xs, }; -export const getNavbarItemStyles = () => ({ +export const getNavbarItemStyles = ({ title }) => ({ navbarItem: css( commonItemStyles, { @@ -76,6 +76,18 @@ export const getNavbarItemStyles = () => ({ paddingRight: tokens.spacingXs, }), isActive: getNavbarItemActiveStyles(), + icon: css({ + height: '20px', + width: '20px', + display: !title ? 'block' : 'none', + [mqs.small]: { + height: '16px', + width: '16px', + }, + [mqs.large]: { + display: 'block', + }, + }), }); export const getNavbarItemSkeletonStyles = () => ({ diff --git a/packages/components/navbar/src/NavbarItem/NavbarItem.tsx b/packages/components/navbar/src/NavbarItem/NavbarItem.tsx index 4b97c26367..9ba2cf2d7d 100644 --- a/packages/components/navbar/src/NavbarItem/NavbarItem.tsx +++ b/packages/components/navbar/src/NavbarItem/NavbarItem.tsx @@ -56,7 +56,7 @@ function _NavbarItem( onClose, ...otherProps } = props; - const styles = getNavbarItemStyles(); + const styles = getNavbarItemStyles({ title }); const isMenuTrigger = isNavbarItemHasMenu(props); const item = ( - {icon && } + {icon && ( + + )} {title && {title}} {title && isMenuTrigger && } diff --git a/packages/components/navbar/src/NavbarItemIcon/NavbarItemIcon.tsx b/packages/components/navbar/src/NavbarItemIcon/NavbarItemIcon.tsx index 5648891816..079de19fac 100644 --- a/packages/components/navbar/src/NavbarItemIcon/NavbarItemIcon.tsx +++ b/packages/components/navbar/src/NavbarItemIcon/NavbarItemIcon.tsx @@ -5,14 +5,19 @@ import { cx } from 'emotion'; export type NavbarItemIconProps = { icon: React.ReactElement; + className?: string; } & Partial>; -export const NavbarItemIcon = ({ icon, isActive }: NavbarItemIconProps) => { - const { className, size, ...rest } = icon.props; +export const NavbarItemIcon = ({ + icon, + isActive, + className, +}: NavbarItemIconProps) => { + const { className: iconClassName, size, ...rest } = icon.props; const styles = getNavbarItemIconStyles(); return React.cloneElement(icon, { - className: cx(className, styles.navbarItemIcon), + className: cx(iconClassName, styles.navbarItemIcon, className), size: size || 'small', isActive, ...rest, diff --git a/packages/components/navbar/src/NavbarMenu/NavbarMenu.styles.ts b/packages/components/navbar/src/NavbarMenu/NavbarMenu.styles.ts index d3a8249a34..be2240d26a 100644 --- a/packages/components/navbar/src/NavbarMenu/NavbarMenu.styles.ts +++ b/packages/components/navbar/src/NavbarMenu/NavbarMenu.styles.ts @@ -1,7 +1,11 @@ import { css } from 'emotion'; +import { mqs } from '../utils.styles'; export const getNavbarMenuStyles = () => ({ menuList: css({ - minWidth: '250px', + minWidth: 0, + [mqs.xsmall]: { + minWidth: '250px', + }, }), }); diff --git a/packages/components/navbar/src/NavbarSubmenu/NavbarMenu.styles.ts b/packages/components/navbar/src/NavbarSubmenu/NavbarMenu.styles.ts new file mode 100644 index 0000000000..04bd772eb3 --- /dev/null +++ b/packages/components/navbar/src/NavbarSubmenu/NavbarMenu.styles.ts @@ -0,0 +1,21 @@ +import tokens from '@contentful/f36-tokens'; +import { css } from 'emotion'; +import { mqs } from '../utils.styles'; + +export const getNavbarSubmenuStyles = () => ({ + navbarMenuItem: css({ + display: 'flex', + justifyContent: 'flex-start', + alignItems: 'center', + gap: tokens.spacingXs, + }), + menuList: css({ + minWidth: 0, + marginLeft: '-24px', + marginTop: '10px', + [mqs.xsmall]: { + minWidth: '250px', + margin: 0, + }, + }), +}); diff --git a/packages/components/navbar/src/NavbarSubmenu/NavbarSubmenu.tsx b/packages/components/navbar/src/NavbarSubmenu/NavbarSubmenu.tsx new file mode 100644 index 0000000000..bd26c5eba3 --- /dev/null +++ b/packages/components/navbar/src/NavbarSubmenu/NavbarSubmenu.tsx @@ -0,0 +1,41 @@ +import React from 'react'; +import { Menu, type MenuListProps, type MenuProps } from '@contentful/f36-menu'; +import { getNavbarSubmenuStyles } from './NavbarMenu.styles'; +import { + NavbarItemIcon, + type NavbarItemIconProps, +} from '../NavbarItemIcon/NavbarItemIcon'; +import { Flex } from '@contentful/f36-core'; + +export type NavbarSubmenuProps = { + title: string; + icon?: NavbarItemIconProps['icon']; + children?: React.ReactNode; +} & Pick & + Pick; + +export const NavbarSubmenu = (props: NavbarSubmenuProps) => { + const { + title, + icon, + children, + testId = 'cf-ui-navbar-submenu-list', + onOpen, + onClose, + } = props; + const styles = getNavbarSubmenuStyles(); + + return ( + + + + {icon && } + {title} + + + + {children} + + + ); +}; diff --git a/packages/components/navbar/src/NavbarSwitcher/NavbarEnvVariant.tsx b/packages/components/navbar/src/NavbarSwitcher/NavbarEnvVariant.tsx index 82e65da716..e6cf436cc6 100644 --- a/packages/components/navbar/src/NavbarSwitcher/NavbarEnvVariant.tsx +++ b/packages/components/navbar/src/NavbarSwitcher/NavbarEnvVariant.tsx @@ -6,17 +6,20 @@ import tokens from '@contentful/f36-tokens'; export type NavbarEnvVariantProps = Pick< NavbarSwitcherProps, 'isAlias' | 'envVariant' ->; +> & { + className?: string; +}; export function NavbarEnvVariant({ isAlias, envVariant, + className, }: NavbarEnvVariantProps) { const color = envVariant === 'master' ? tokens.green600 : tokens.orange500; return isAlias ? ( - + ) : ( - + ); } diff --git a/packages/components/navbar/src/NavbarSwitcher/NavbarSwitcher.styles.ts b/packages/components/navbar/src/NavbarSwitcher/NavbarSwitcher.styles.ts index 43f51bff8c..38420fb9dd 100644 --- a/packages/components/navbar/src/NavbarSwitcher/NavbarSwitcher.styles.ts +++ b/packages/components/navbar/src/NavbarSwitcher/NavbarSwitcher.styles.ts @@ -2,7 +2,7 @@ import { css } from 'emotion'; import tokens from '@contentful/f36-tokens'; import { hexToRGBA } from '@contentful/f36-utils'; -import { getGlowOnFocusStyles, increaseHitArea } from '../utils.styles'; +import { getGlowOnFocusStyles, increaseHitArea, mqs } from '../utils.styles'; export const getNavbarSwitcherStyles = () => ({ navbarSwitcher: css( @@ -26,6 +26,23 @@ export const getNavbarSwitcherStyles = () => ({ '&:has(> span:last-child:nth-child(3))': { minWidth: '12ch', }, + maxWidth: '15vw', + [mqs.xsmall]: { + maxWidth: '50vw', + }, + [mqs.small]: { + maxWidth: '10vw', + }, + [mqs.medium]: { + maxWidth: '50vw', + }, + }), + + switcherEnvIcon: css({ + [mqs.small]: { + width: '16px', + height: '16px', + }, }), switcherSpaceNameTruncation: css({ diff --git a/packages/components/navbar/src/NavbarSwitcher/NavbarSwitcher.tsx b/packages/components/navbar/src/NavbarSwitcher/NavbarSwitcher.tsx index fc579f5821..456c3d8d60 100644 --- a/packages/components/navbar/src/NavbarSwitcher/NavbarSwitcher.tsx +++ b/packages/components/navbar/src/NavbarSwitcher/NavbarSwitcher.tsx @@ -76,7 +76,11 @@ function _NavbarSwitcher( className={cx(styles.navbarSwitcher, className)} endIcon={ envVariant && ( - + ) } ref={ref} diff --git a/packages/components/navbar/src/utils.styles.ts b/packages/components/navbar/src/utils.styles.ts index 92e87cf2a9..eb130e67e5 100644 --- a/packages/components/navbar/src/utils.styles.ts +++ b/packages/components/navbar/src/utils.styles.ts @@ -1,11 +1,13 @@ import tokens from '@contentful/f36-tokens'; import type { CSSObject } from '@emotion/serialize'; -type screens = 'medium' | 'large' | 'xlarge'; +type screens = 'xsmall' | 'small' | 'medium' | 'large' | 'xlarge'; type mediaqueries = Record; export const mqs: mediaqueries = { - medium: '@media (min-width: 480px)', - large: '@media (min-width: 768px)', + xsmall: '@media (min-width: 576px)', + small: '@media (min-width: 867px)', + medium: '@media (min-width: 1024px)', + large: '@media (min-width: 1200px)', xlarge: '@media (min-width: 1920px)', }; diff --git a/packages/components/navbar/stories/Navbar.stories.tsx b/packages/components/navbar/stories/Navbar.stories.tsx index 9605d549df..9d62caa8a7 100644 --- a/packages/components/navbar/stories/Navbar.stories.tsx +++ b/packages/components/navbar/stories/Navbar.stories.tsx @@ -199,11 +199,89 @@ export const WithShortSpaceName = () => { ); }; +const MobileMenu = () => ( + <> + } /> + } /> + } /> + } /> + }> + + } + /> + + + + + + + + + + + General + + + Space + + + + + } /> + + + + + + + +); + export const Complete: Story<{ initials?: string; avatar?: string }> = ( args, ) => { return ( } switcher={} mainNavigation={} account={ @@ -271,6 +349,71 @@ Complete.args = { 'https://images.ctfassets.net/iq4lnigp6fgt/2EEEk92Kiz6KxREsjBLPAN/810d5a21650d91abad12e95da4cd3beb/2021-06_Everyone_is_Welcome_here_1_.png?fit=fill&f=top_left&w=100&h=100', }; +export const WithResponsiveness: Story<{ + initials?: string; + avatar?: string; +}> = (args) => { + return ( + } + switcher={} + mainNavigation={} + account={} + secondaryNavigation={ + <> + } /> + }> + + + + + + + }> + General + + + Space + + + + + } + /> + ); +}; +WithResponsiveness.parameters = { + layout: 'fullscreen', +}; + export const WithDifferentEnvironments: Story<{ initials?: string; avatar?: string; @@ -283,6 +426,7 @@ export const WithDifferentEnvironments: Story<{ } mainNavigation={} switcher={} account={} @@ -295,6 +439,7 @@ export const WithDifferentEnvironments: Story<{ } mainNavigation={} switcher={development} account={} @@ -307,6 +452,7 @@ export const WithDifferentEnvironments: Story<{ } mainNavigation={} switcher={} account={} @@ -319,6 +465,7 @@ export const WithDifferentEnvironments: Story<{ } mainNavigation={} switcher={} account={} @@ -346,6 +493,7 @@ export const WithAccountNotification: Story<{ } switcher={} account={} mainNavigation={} @@ -358,6 +506,7 @@ export const WithAccountNotification: Story<{ } switcher={} account={ @@ -372,6 +521,7 @@ export const WithAccountNotification: Story<{ } switcher={} account={ @@ -391,8 +541,9 @@ WithAccountNotification.args = { export const LoadingSkeleton: Story<{}> = () => { return ( -
+
} account={} switcher={