diff --git a/packages/core/admin/admin/src/components/LeftMenu.tsx b/packages/core/admin/admin/src/components/LeftMenu.tsx index 6e61da6e89b..5242a398304 100644 --- a/packages/core/admin/admin/src/components/LeftMenu.tsx +++ b/packages/core/admin/admin/src/components/LeftMenu.tsx @@ -1,82 +1,38 @@ import * as React from 'react'; -import { - Box, - Divider, - Flex, - FocusTrap, - Typography, - MainNav, - NavBrand, - NavCondense, - NavFooter, - NavLink, - NavSection, - NavSections, - NavUser, -} from '@strapi/design-system'; -import { SignOut, Feather, Lock, House } from '@strapi/icons'; +import { Divider, Flex } from '@strapi/design-system'; +import { Feather, Lock, House } from '@strapi/icons'; import { useIntl } from 'react-intl'; -import { NavLink as RouterNavLink, useLocation } from 'react-router-dom'; +import { useLocation } from 'react-router-dom'; import styled from 'styled-components'; import { useAuth } from '../features/Auth'; -import { useConfiguration } from '../features/Configuration'; import { useTracking } from '../features/Tracking'; import { Menu } from '../hooks/useMenu'; -import { usePersistentState } from '../hooks/usePersistentState'; import { getDisplayName } from '../utils/users'; -import { NavBrand as NewNavBrand } from './MainNav/NavBrand'; -import { NavLink as NewNavLink } from './MainNav/NavLink'; +import { MainNav } from './MainNav/MainNav'; +import { NavBrand } from './MainNav/NavBrand'; +import { NavLink } from './MainNav/NavLink'; +import { NavUser } from './MainNav/NavUser'; -const LinkUserWrapper = styled(Box)` - width: 15rem; - position: absolute; - bottom: ${({ theme }) => theme.spaces[9]}; - left: ${({ theme }) => theme.spaces[5]}; -`; - -const LinkUser = styled(RouterNavLink)<{ logout?: boolean }>` - display: flex; - justify-content: space-between; - align-items: center; - text-decoration: none; - padding: ${({ theme }) => `${theme.spaces[2]} ${theme.spaces[4]}`}; - border-radius: ${({ theme }) => theme.spaces[1]}; - - &:hover { - background: ${({ theme, logout }) => - logout ? theme.colors.danger100 : theme.colors.primary100}; - text-decoration: none; - } - - svg { - fill: ${({ theme }) => theme.colors.danger600}; +const NewNavLinkBadge = styled(NavLink.Badge)` + span { + color: ${({ theme }) => theme.colors.neutral0}; } `; -const NavLinkWrapper = styled(Box)` - div:nth-child(2) { - /* remove badge background color */ - background: transparent; - } +const NavListWrapper = styled(Flex)` + overflow-y: auto; `; interface LeftMenuProps extends Pick {} const LeftMenu = ({ generalSectionLinks, pluginsSectionLinks }: LeftMenuProps) => { - const navUserRef = React.useRef(null!); - const [userLinksVisible, setUserLinksVisible] = React.useState(false); - const { - logos: { menu }, - } = useConfiguration('LeftMenu'); - const [condensed, setCondensed] = usePersistentState('navbar-condensed', false); const user = useAuth('AuthenticatedApp', (state) => state.user); const { formatMessage } = useIntl(); const { trackUsage } = useTracking(); const { pathname } = useLocation(); - const logout = useAuth('Logout', (state) => state.logout); const userDisplayName = getDisplayName(user); const initials = userDisplayName @@ -85,198 +41,122 @@ const LeftMenu = ({ generalSectionLinks, pluginsSectionLinks }: LeftMenuProps) = .join('') .substring(0, 2); - const handleToggleUserLinks = () => setUserLinksVisible((prev) => !prev); - - const handleBlur: React.FocusEventHandler = (e) => { - if ( - !e.currentTarget.contains(e.relatedTarget) && - /** - * TODO: can we replace this by just using the navUserRef? - */ - e.relatedTarget?.parentElement?.id !== 'main-nav-user-button' - ) { - setUserLinksVisible(false); - } - }; - const handleClickOnLink = (destination: string) => { trackUsage('willNavigate', { from: pathname, to: destination }); }; - const menuTitle = formatMessage({ - id: 'app.components.LeftMenu.navbrand.title', - defaultMessage: 'Strapi Dashboard', - }); - return ( - - {condensed ? ( - /** - * TODO: remove the conditional rendering once the new Main nav is fully implemented - */ - - ) : ( - - } - /> - )} + + - - {condensed && ( - handleClickOnLink('/')}> - - + + + handleClickOnLink('/')} + aria-label={formatMessage({ id: 'global.home', defaultMessage: 'Home' })} + > + + - - - - )} - } - onClick={() => handleClickOnLink('/content-manager')} - > - {formatMessage({ id: 'global.content-manager', defaultMessage: 'Content manager' })} - - - {pluginsSectionLinks.length > 0 ? ( - + + + + + handleClickOnLink('/content-manager')} + aria-label={formatMessage({ + id: 'global.content-manager', + defaultMessage: 'Content Manager', })} > - {pluginsSectionLinks.map((link) => { + + + + + + + + {pluginsSectionLinks.length > 0 + ? pluginsSectionLinks.map((link) => { if (link.to === 'content-manager') { return null; } const LinkIcon = link.icon; + const badgeContent = link?.lockIcon ? : undefined; + + const labelValue = formatMessage(link.intlLabel); return ( - - + } onClick={() => handleClickOnLink(link.to)} - // @ts-expect-error: badgeContent in the DS accept only strings - badgeContent={ - link?.lockIcon ? : undefined - } + aria-label={labelValue} > - {formatMessage(link.intlLabel)} - - + + + + + {badgeContent && ( + + {badgeContent} + + )} + + + ); - })} - - ) : null} - - {generalSectionLinks.length > 0 ? ( - - {generalSectionLinks.map((link) => { + }) + : null} + {generalSectionLinks.length > 0 + ? generalSectionLinks.map((link) => { const LinkIcon = link.icon; - return ( - 0 - ? link.notificationsCount.toString() - : undefined - } - // @ts-expect-error the props from the passed as prop are not inferred // joined together - to={link.to} - key={link.to} - icon={} - onClick={() => handleClickOnLink(link.to)} - > - {formatMessage(link.intlLabel)} - - ); - })} - - ) : null} - + const badgeContent = + link.notificationsCount && link.notificationsCount > 0 + ? link.notificationsCount.toString() + : undefined; - - - {userDisplayName} - - {userLinksVisible && ( - - - - - - {formatMessage({ - id: 'global.profile', - defaultMessage: 'Profile', - })} - - - - - {formatMessage({ - id: 'app.components.LeftMenu.logout', - defaultMessage: 'Logout', - })} - - - - - - - )} + const labelValue = formatMessage(link.intlLabel); - setCondensed((s) => !s)}> - {condensed - ? formatMessage({ - id: 'app.components.LeftMenu.expand', - defaultMessage: 'Expand the navbar', - }) - : formatMessage({ - id: 'app.components.LeftMenu.collapse', - defaultMessage: 'Collapse the navbar', - })} - - + return ( + + handleClickOnLink(link.to)} + > + + + + + {badgeContent && ( + + {badgeContent} + + )} + + + + ); + }) + : null} + + {userDisplayName} ); }; diff --git a/packages/core/admin/admin/src/components/MainNav/MainNav.tsx b/packages/core/admin/admin/src/components/MainNav/MainNav.tsx new file mode 100644 index 00000000000..86275905b2c --- /dev/null +++ b/packages/core/admin/admin/src/components/MainNav/MainNav.tsx @@ -0,0 +1,23 @@ +import { Flex, FlexProps } from '@strapi/design-system'; +import styled from 'styled-components'; + +const MainNavWrapper = styled(Flex)` + border-right: 1px solid ${({ theme }) => theme.colors.neutral150}; +`; + +const MainNav = (props: FlexProps<'nav'>) => ( + +); + +export { MainNav }; diff --git a/packages/core/admin/admin/src/components/MainNav/NavLink.tsx b/packages/core/admin/admin/src/components/MainNav/NavLink.tsx index 3c3d19f471a..61127897421 100644 --- a/packages/core/admin/admin/src/components/MainNav/NavLink.tsx +++ b/packages/core/admin/admin/src/components/MainNav/NavLink.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; -import { Tooltip, Flex, Badge } from '@strapi/design-system'; +import { Tooltip, Flex, Badge, BadgeProps } from '@strapi/design-system'; import { NavLink as RouterLink, LinkProps } from 'react-router-dom'; import styled from 'styled-components'; @@ -12,8 +12,9 @@ const MainNavLinkWrapper = styled(RouterLink)` display: block; border-radius: ${({ theme }) => theme.borderRadius}; background: ${({ theme }) => theme.colors.neutral0}; - color: ${({ theme }) => theme.colors.neutral600}; + color: ${({ theme }) => theme.colors.neutral500}; position: relative; + width: fit-content; &:hover, &.active { @@ -74,14 +75,23 @@ const IconImpl = ({ children }: { children: React.ReactNode }) => { const CustomBadge = styled(Badge)` /* override default badge styles to change the border radius of the Base element in the Design System */ border-radius: ${({ theme }) => theme.spaces[10]}; + height: 2rem; `; -const BadgeImpl = ({ children, label, ...props }: NavLink.BadgeProps) => { +const BadgeImpl = ({ children, label, ...props }: NavLink.NavBadgeProps) => { if (!children) { return null; } return ( - + {children} ); @@ -100,7 +110,7 @@ const NavLink = { // eslint-disable-next-line @typescript-eslint/no-namespace namespace NavLink { - export interface BadgeProps { + export interface NavBadgeProps extends BadgeProps { children: React.ReactNode; label: string; } diff --git a/packages/core/admin/admin/src/components/MainNav/NavUser.tsx b/packages/core/admin/admin/src/components/MainNav/NavUser.tsx new file mode 100644 index 00000000000..3220b2cb8ee --- /dev/null +++ b/packages/core/admin/admin/src/components/MainNav/NavUser.tsx @@ -0,0 +1,72 @@ +import React from 'react'; + +import { Initials, Flex, Menu, ButtonProps, VisuallyHidden } from '@strapi/design-system'; +import { SignOut } from '@strapi/icons'; +import { useIntl } from 'react-intl'; +import { useNavigate } from 'react-router-dom'; +import styled from 'styled-components'; + +import { useAuth } from '../../features/Auth'; + +export interface NavUserProps extends ButtonProps { + initials: string; + children?: React.ReactNode; +} + +/** + * TODO: this needs to be solved in the Design-System + */ +const MenuTrigger = styled(Menu.Trigger)` + height: 100%; +`; + +const MenuContent = styled(Menu.Content)` + left: ${({ theme }) => theme.spaces[5]}; +`; + +const MenuItem = styled(Menu.Item)` + span { + width: 100%; + display: flex; + justify-content: space-between; + } +`; + +export const NavUser = ({ children, initials, ...props }: NavUserProps) => { + const { formatMessage } = useIntl(); + const navigate = useNavigate(); + const logout = useAuth('Logout', (state) => state.logout); + const handleProfile = () => { + navigate('/me'); + }; + const handleLogout = () => { + logout(); + navigate('/auth/login'); + }; + return ( + + + + {initials} + {children} + + + + {formatMessage({ + id: 'global.profile', + defaultMessage: 'Profile', + })} + + + + {formatMessage({ + id: 'app.components.LeftMenu.logout', + defaultMessage: 'Logout', + })} + + + + + + ); +}; diff --git a/packages/core/admin/admin/src/components/MainNav/tests/NavUser.test.tsx b/packages/core/admin/admin/src/components/MainNav/tests/NavUser.test.tsx new file mode 100644 index 00000000000..bfeb1236775 --- /dev/null +++ b/packages/core/admin/admin/src/components/MainNav/tests/NavUser.test.tsx @@ -0,0 +1,40 @@ +import { screen, render } from '@tests/utils'; + +import { NavUser } from '../NavUser'; + +describe('NavUser', () => { + it('shows the initials of the user', async () => { + render(John Doe); + expect(screen.getByText('JD')).toBeInTheDocument(); + }); + + it('contains the user name', async () => { + render(John Doe); + const userName = screen.getByText('John Doe'); + expect(userName).toBeInTheDocument(); + }); + + it('shows the user menu when clicked', async () => { + const { user } = render(John Doe); + const buttonMenu = screen.getByRole('button'); + await user.click(buttonMenu); + const userMenu = screen.getByRole('menu'); + expect(userMenu).toBeInTheDocument(); + }); + + it('shows the profile link in the user menu when clicked', async () => { + const { user } = render(John Doe); + const buttonMenu = screen.getByRole('button'); + await user.click(buttonMenu); + const profileLink = screen.getByText('Profile'); + expect(profileLink).toBeInTheDocument(); + }); + + it('shows the logout link in the user menu when clicked', async () => { + const { user } = render(John Doe); + const buttonMenu = screen.getByRole('button'); + await user.click(buttonMenu); + const logoutLink = screen.getByText('Logout'); + expect(logoutLink).toBeInTheDocument(); + }); +});