diff --git a/apps/website/pages/_app.tsx b/apps/website/pages/_app.tsx index 7227ba1b3..34fbd44b0 100644 --- a/apps/website/pages/_app.tsx +++ b/apps/website/pages/_app.tsx @@ -101,9 +101,7 @@ export default function App({ Component, pageProps, emotionCache = clientSideEmo + } sidenav={ void; - }; - appTitle?: string; +const logoTypeString = `{ + src: string | SVG; + alt: string; + href?: string; + onClick?: () => void; }`; const navItemsTypeString = `(GroupItem | Item)[]`; @@ -45,17 +42,25 @@ const sections = [ + + appTitle + + string + + Object used to configure the header application title. + - + - branding + logo - {brandingTypeString} + {logoTypeString} - Object used to configure the header branding, including logo and application title. + Object used to configure the header logo. - diff --git a/apps/website/screens/components/header/overview/HeaderOverviewPage.tsx b/apps/website/screens/components/header/overview/HeaderOverviewPage.tsx index 94253f02b..443815f60 100644 --- a/apps/website/screens/components/header/overview/HeaderOverviewPage.tsx +++ b/apps/website/screens/components/header/overview/HeaderOverviewPage.tsx @@ -140,6 +140,33 @@ const sections = [ }, ], }, + { + title: "Responsiveness", + content: ( + <> + + On smaller viewports, the header adapts to its responsive version. This layout is designed to accommodate the + component's elements clearly and consistently, so the experience remains coherent across all screen sizes. The + responsive header is organized into three sections: the top bar, the body, and the bottom. + + + + Top bar: this section displays the product or application branding, along with the most + relevant utilities from the right slot of the standard header. + + + Body: this is where the main navigation items are placed. Visually, these items follow a + structure similar to the ones in our sidenav component, and their behavior remains consistent with how they + function in the default sidenav. + + + Bottom: this area is custom, but it's reserved for the header's call-to-action elements, + when present. + + + + ), + }, { title: "Best practices", subSections: [ @@ -236,6 +263,31 @@ const sections = [ ), }, + { + title: "Responsive mode", + content: ( + + + Even though the responsive header is composed of custom sections,{" "} + each area should remain aligned with the structure described above to preserve clarity + and consistency across smaller viewports. + + + Avoid overcrowding the top bar in the responsive layout. Only the most essential and + frequently used actions should remain visible there. Additional actions or CTAs should be placed in the + bottom section, while navigation items should be housed in the body. + + + Keep visual hierarchy as simple as possible. In reduced spaces, users scan content + faster, so each section should present its elements in a clear and predictable order. + + + Ensure that labels, icons, and interactive elements resize or reorganize in a way that maintains + readability and touch friendliness, especially on mobile devices. + + + ), + }, ], }, ]; diff --git a/packages/lib/src/header/Header.accessibility.test.tsx b/packages/lib/src/header/Header.accessibility.test.tsx index 7ee54278a..5dd429bb0 100644 --- a/packages/lib/src/header/Header.accessibility.test.tsx +++ b/packages/lib/src/header/Header.accessibility.test.tsx @@ -25,15 +25,13 @@ const iconSVG = ( const iconUrl = "https://iconape.com/wp-content/files/yd/367773/svg/logo-linkedin-logo-icon-png-svg.png"; -const branding = { - logo: { - src: iconSVG, - alt: "DXC Logo", - href: iconUrl, - }, - appTitle: - "Application Title with a very long name that exceeds the normal length to test how the header manages overflow situations", +const logo = { + src: iconSVG, + alt: "DXC Logo", + href: iconUrl, }; +const appTitle = + "Application Title with a very long name that exceeds the normal length to test how the header manages overflow situations"; const items = [ { @@ -73,7 +71,8 @@ describe("Header component accessibility tests", () => { it("Should not have basic accessibility issues", async () => { const { container } = render( {}} /> diff --git a/packages/lib/src/header/Header.stories.tsx b/packages/lib/src/header/Header.stories.tsx index ff4c847ee..36039c9f2 100644 --- a/packages/lib/src/header/Header.stories.tsx +++ b/packages/lib/src/header/Header.stories.tsx @@ -6,7 +6,6 @@ import DxcFlex from "../flex/Flex"; import Title from "../../.storybook/components/Title"; import DxcApplicationLayout from "../layout/ApplicationLayout"; import DxcParagraph from "../paragraph/Paragraph"; -import { dxcLogo } from "./Icons"; import DxcButton from "../button/Button"; import { userEvent, within } from "storybook/internal/test"; import preview from "../../.storybook/preview"; @@ -41,38 +40,43 @@ export default { }, } satisfies Meta; -const branding = { - logo: { - src: "https://picsum.photos/id/1000/104/34", - alt: "DXC Logo", - href: "https://www.dxc.com", - }, - appTitle: "Application Title", -}; +const dxcLogo = ( + + DXC Logo + + + + + + + +); -const brandingWithoutTitle = { - logo: { - src: "https://picsum.photos/id/1000/104/34", - alt: "DXC Logo", - href: "https://www.dxc.com", - }, +const logo = { + src: "https://picsum.photos/id/1000/104/34", + alt: "DXC Logo", + href: "https://www.dxc.com", }; const dxcBrandedLogo = { - logo: { - src: dxcLogo, - alt: "DXC Logo", - }, + src: dxcLogo, + alt: "DXC Logo", }; -const longBranding = { - logo: { - src: "https://picsum.photos/id/1000/104/34", - alt: "DXC Logo", - href: "https://www.dxc.com", - }, - appTitle: - "Application Title with a very long name that exceeds the normal length to test how the header manages overflow situations", +const longAppTitle = "This is a very long application title to test the header component behavior with long titles"; + +const longLogo = { + src: "https://picsum.photos/id/1000/104/34", + alt: "DXC Logo", + href: "https://www.dxc.com", }; const longSideContent = `Long side content that is intended to test how the header component handles situations where the side content exceeds the typical length. This content should ideally wrap or be truncated based on the design specifications of the header component within the application.`; @@ -118,17 +122,22 @@ const longItems = [ const Header = () => ( - <DxcHeader branding={brandingWithoutTitle} /> - <DxcHeader branding={branding} /> - <DxcHeader branding={branding} sideContent={<div>Side Content</div>} /> - <DxcHeader branding={branding} navItems={items} sideContent={<div>Side Content</div>} /> + <DxcHeader logo={logo} /> + <DxcHeader logo={logo} /> + <DxcHeader logo={logo} sideContent={<div>Side Content</div>} /> + <DxcHeader logo={logo} navItems={items} sideContent={<div>Side Content</div>} /> <Title title="Header with long content" theme="light" level={3} /> - <DxcHeader branding={branding} navItems={items} sideContent={<div>{longSideContent}</div>} /> - <DxcHeader branding={longBranding} navItems={items} /> - <DxcHeader branding={longBranding} navItems={items} sideContent={<div>{longSideContent}</div>} /> - <DxcHeader branding={longBranding} sideContent={<div>{longSideContent}</div>} /> - <DxcHeader branding={longBranding} navItems={longItems} /> - <DxcHeader branding={longBranding} navItems={longItems} sideContent={<div>{longSideContent}</div>} /> + <DxcHeader logo={logo} navItems={items} sideContent={<div>{longSideContent}</div>} /> + <DxcHeader logo={longLogo} appTitle={longAppTitle} navItems={items} /> + <DxcHeader logo={longLogo} appTitle={longAppTitle} navItems={items} sideContent={<div>{longSideContent}</div>} /> + <DxcHeader logo={longLogo} appTitle={longAppTitle} sideContent={<div>{longSideContent}</div>} /> + <DxcHeader logo={longLogo} appTitle={longAppTitle} navItems={longItems} /> + <DxcHeader + logo={longLogo} + appTitle={longAppTitle} + navItems={longItems} + sideContent={<div>{longSideContent}</div>} + /> </DxcFlex> ); @@ -136,7 +145,7 @@ const HeaderInLayout = () => ( <DxcApplicationLayout header={ <DxcHeader - branding={dxcBrandedLogo} + logo={dxcBrandedLogo} navItems={items} sideContent={(isResponsive) => isResponsive ? ( diff --git a/packages/lib/src/header/Header.test.tsx b/packages/lib/src/header/Header.test.tsx index c9fbbf5fe..74727b755 100644 --- a/packages/lib/src/header/Header.test.tsx +++ b/packages/lib/src/header/Header.test.tsx @@ -2,10 +2,8 @@ import { render } from "@testing-library/react"; import DxcHeader from "./Header"; const defaultHeaderBranding = { - logo: { - src: "url-to-dxc-logo", - alt: "DXC Logo", - }, + src: "url-to-dxc-logo", + alt: "DXC Logo", }; describe("Header component tests", () => { @@ -18,7 +16,7 @@ describe("Header component tests", () => { }); }); test("Header renders with default logo", () => { - const { getByAltText } = render(<DxcHeader branding={defaultHeaderBranding} />); + const { getByAltText } = render(<DxcHeader logo={defaultHeaderBranding} />); expect(getByAltText("DXC Logo")).toBeTruthy(); }); }); diff --git a/packages/lib/src/header/Header.tsx b/packages/lib/src/header/Header.tsx index 43e201c87..8cac6a1cc 100644 --- a/packages/lib/src/header/Header.tsx +++ b/packages/lib/src/header/Header.tsx @@ -10,6 +10,7 @@ import { useEffect, useMemo, useState } from "react"; import DxcNavigationTree from "../navigation-tree/NavigationTree"; import { responsiveSizes } from "../common/variables"; import DxcButton from "../button/Button"; +import scrollbarStyles from "../styles/scroll"; const MAX_MAIN_NAV_SIZE = "60%"; const LEVEL_LIMIT = 1; @@ -79,6 +80,9 @@ const HamburguerButton = ({ onClick }: { onClick: () => void }) => { const ResponsiveMenuContainer = styled.div` display: grid; grid-template-rows: auto 1fr; + max-height: 100%; + overflow: auto; + ${scrollbarStyles} `; const ResponsiveMenu = styled.div` @@ -126,7 +130,7 @@ const sanitizeNavItems = (navItems: HeaderProps["navItems"], level?: number): (G return sanitizedItems; }; -const DxcHeader = ({ branding, navItems, sideContent, responsiveBottomContent }: HeaderProps): JSX.Element => { +const DxcHeader = ({ logo, appTitle, navItems, sideContent, responsiveBottomContent }: HeaderProps): JSX.Element => { const [isResponsive, setIsResponsive] = useState(false); const [isMenuVisible, setIsMenuVisible] = useState(false); @@ -168,25 +172,17 @@ const DxcHeader = ({ branding, navItems, sideContent, responsiveBottomContent }: placeItems="center" > <BrandingContainer> - <LogoContainer - role={branding.logo.onClick ? "button" : undefined} - as={branding.logo.href ? "a" : undefined} - > - {typeof branding.logo.src === "string" ? ( - <DxcImage - src={branding.logo.src} - alt={branding.logo.alt} - height="var(--height-m)" - objectFit="contain" - /> + <LogoContainer role={logo.onClick ? "button" : undefined} as={logo.href ? "a" : undefined}> + {typeof logo.src === "string" ? ( + <DxcImage src={logo.src} alt={logo.alt} height="var(--height-m)" objectFit="contain" /> ) : ( - branding.logo.src + logo.src )} </LogoContainer> - {branding.appTitle && !isResponsive && ( + {appTitle && !isResponsive && ( <> <DxcDivider orientation="vertical" /> - <DxcHeading text={branding.appTitle} as="h1" level={5} /> + <DxcHeading text={appTitle} as="h1" level={5} /> </> )} </BrandingContainer> @@ -213,7 +209,7 @@ const DxcHeader = ({ branding, navItems, sideContent, responsiveBottomContent }: {isResponsive && isMenuVisible && ( <ResponsiveMenuContainer> <ResponsiveMenu> - {branding.appTitle && <DxcHeading text={branding.appTitle} as="h1" level={5} />} + {appTitle && <DxcHeading text={appTitle} as="h1" level={5} />} <DxcNavigationTree items={sanitizedNavItems} displayGroupLines={false} diff --git a/packages/lib/src/header/Icons.tsx b/packages/lib/src/header/Icons.tsx deleted file mode 100644 index b67485529..000000000 --- a/packages/lib/src/header/Icons.tsx +++ /dev/null @@ -1,19 +0,0 @@ -export const dxcLogo = ( - <svg xmlns="http://www.w3.org/2000/svg" width="73" height="40" viewBox="0 0 73 40"> - <title>DXC Logo - - - - - - - -); diff --git a/packages/lib/src/header/types.ts b/packages/lib/src/header/types.ts index 10f9da349..3c053a81d 100644 --- a/packages/lib/src/header/types.ts +++ b/packages/lib/src/header/types.ts @@ -9,11 +9,6 @@ type LogoPropsType = { onClick?: () => void; }; -type BrandingPropsType = { - logo: LogoPropsType; - appTitle?: string; -}; - type GroupItem = CommonItemProps & { items: Item[]; }; @@ -21,7 +16,8 @@ type GroupItem = CommonItemProps & { type MainNavPropsType = (GroupItem | Item)[]; type Props = { - branding: BrandingPropsType; + logo: LogoPropsType; + appTitle?: string; navItems?: MainNavPropsType; responsiveBottomContent?: ReactNode; sideContent?: ReactNode | ((isResponsive: boolean) => ReactNode); diff --git a/packages/lib/src/layout/ApplicationLayout.tsx b/packages/lib/src/layout/ApplicationLayout.tsx index 890c5b5c7..e65c8aa74 100644 --- a/packages/lib/src/layout/ApplicationLayout.tsx +++ b/packages/lib/src/layout/ApplicationLayout.tsx @@ -24,16 +24,15 @@ const HeaderContainer = styled.div` z-index: var(--z-app-layout-header); `; -const BodyContainer = styled.div` - display: flex; - width: 100%; - height: 100%; +const BodyContainer = styled.div<{ hasSidenav?: boolean }>` + display: grid; + grid-template-columns: ${({ hasSidenav }) => (hasSidenav ? "auto 1fr" : "1fr")}; + grid-template-rows: 1fr; overflow: hidden; `; const SidenavContainer = styled.div` width: fit-content; - min-width: 280px; height: 100%; z-index: var(--z-app-layout-sidenav); position: sticky; @@ -69,7 +68,7 @@ const DxcApplicationLayout = ({ header, sidenav, footer, children }: Application return ( {header && {header}} - + {sidenav && {sidenav}} diff --git a/packages/lib/src/sidenav/Sidenav.tsx b/packages/lib/src/sidenav/Sidenav.tsx index 20f3705eb..5ecdf675b 100644 --- a/packages/lib/src/sidenav/Sidenav.tsx +++ b/packages/lib/src/sidenav/Sidenav.tsx @@ -7,6 +7,7 @@ import DxcButton from "../button/Button"; import DxcImage from "../image/Image"; import { useState } from "react"; import DxcNavigationTree from "../navigation-tree/NavigationTree"; +import DxcInset from "../inset/Inset"; const SidenavContainer = styled.div<{ expanded: boolean }>` box-sizing: border-box; @@ -19,9 +20,16 @@ const SidenavContainer = styled.div<{ expanded: boolean }>` @media (max-width: ${responsiveSizes.large}rem) { width: 100vw; } - padding: var(--spacing-padding-m) var(--spacing-padding-xs); + padding-top: var(--spacing-padding-m); + padding-bottom: var(--spacing-padding-m); gap: var(--spacing-gap-l); background-color: var(--color-bg-neutral-lightest); + border-right: var(--border-width-s) var(--border-style-default) var(--border-color-neutral-lighter); + & > div { + box-sizing: border-box; + padding-left: var(--spacing-padding-xs); + padding-right: var(--spacing-padding-xs); + } `; const SidenavTitle = styled.div` @@ -103,7 +111,11 @@ const DxcSidenav = ({ branding )} - {topContent} + {topContent && ( + + {topContent} + + )} {navItems && ( )} - - {bottomContent} + {bottomContent && ( + <> + + + + + {bottomContent} + + + )} ); };