diff --git a/apps/website/pages/components/header/code.tsx b/apps/website/pages/components/header/code.tsx new file mode 100644 index 0000000000..5e5dc936e5 --- /dev/null +++ b/apps/website/pages/components/header/code.tsx @@ -0,0 +1,17 @@ +import Head from "next/head"; +import type { ReactElement } from "react"; +import HeaderPageLayout from "screens/components/header/HeaderPageLayout"; +import HeaderCodePage from "screens/components/header/code/HeaderCodePage"; + +const Code = () => ( + <> + + Header code — Halstack Design System + + + +); + +Code.getLayout = (page: ReactElement) => {page}; + +export default Code; diff --git a/apps/website/pages/components/header/index.tsx b/apps/website/pages/components/header/index.tsx index 9c7e40054f..a0e9565e2d 100644 --- a/apps/website/pages/components/header/index.tsx +++ b/apps/website/pages/components/header/index.tsx @@ -1,21 +1,17 @@ import Head from "next/head"; import type { ReactElement } from "react"; import HeaderPageLayout from "screens/components/header/HeaderPageLayout"; -import HeaderCodePage from "screens/components/header/code/HeaderCodePage"; +import HeaderOverviewPage from "screens/components/header/overview/HeaderOverviewPage"; -const Index = () => { - return ( - <> - - Header — Halstack Design System - - - - ); -}; +const Index = () => ( + <> + + Header — Halstack Design System + + + +); -Index.getLayout = function getLayout(page: ReactElement) { - return {page}; -}; +Index.getLayout = (page: ReactElement) => {page}; export default Index; diff --git a/apps/website/pages/components/header/specifications.tsx b/apps/website/pages/components/header/specifications.tsx deleted file mode 100644 index d703a956e9..0000000000 --- a/apps/website/pages/components/header/specifications.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import Head from "next/head"; -import type { ReactElement } from "react"; -import HeaderPageLayout from "screens/components/header/HeaderPageLayout"; -import HeaderSpecsPage from "screens/components/header/specs/HeaderSpecsPage"; - -const Specifications = () => { - return ( - <> - - Header Specs — Halstack Design System - - - - ); -}; - -Specifications.getLayout = function getLayout(page: ReactElement) { - return {page}; -}; - -export default Specifications; diff --git a/apps/website/pages/components/header/usage.tsx b/apps/website/pages/components/header/usage.tsx deleted file mode 100644 index 5d03475cab..0000000000 --- a/apps/website/pages/components/header/usage.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import Head from "next/head"; -import type { ReactElement } from "react"; -import HeaderPageLayout from "screens/components/header/HeaderPageLayout"; -import HeaderUsagePage from "screens/components/header/usage/HeaderUsagePage"; - -const Usage = () => { - return ( - <> - - Header Usage — Halstack Design System - - - - ); -}; - -Usage.getLayout = function getLayout(page: ReactElement) { - return {page}; -}; - -export default Usage; diff --git a/apps/website/screens/components/header/HeaderPageLayout.tsx b/apps/website/screens/components/header/HeaderPageLayout.tsx index 695b19614c..4f26a06796 100644 --- a/apps/website/screens/components/header/HeaderPageLayout.tsx +++ b/apps/website/screens/components/header/HeaderPageLayout.tsx @@ -7,9 +7,8 @@ import { ReactNode } from "react"; const HeaderPageHeading = ({ children }: { children: ReactNode }) => { const tabs = [ - { label: "Code", path: "/components/header" }, - { label: "Usage", path: "/components/header/usage" }, - { label: "Specifications", path: "/components/header/specifications" }, + { label: "Overview", path: "/components/header" }, + { label: "Code", path: "/components/header/code" }, ]; return ( @@ -18,9 +17,8 @@ const HeaderPageHeading = ({ children }: { children: ReactNode }) => { - The header is an important component in the interface, it is the area dedicated for the navigation across - the application and helps users understand what the content of the page is about. They appear at the top of - a page, above the main content. + A horizontal bar located at the top of the application, providing branding, primary navigation, and user + account controls. The header is part of the application layout, so it can only be used inside of it. Please check the{" "} @@ -29,7 +27,7 @@ const HeaderPageHeading = ({ children }: { children: ReactNode }) => { {" "} documentation. - + {children} diff --git a/apps/website/screens/components/header/code/HeaderCodePage.tsx b/apps/website/screens/components/header/code/HeaderCodePage.tsx index 71042044b7..d4debc4f24 100644 --- a/apps/website/screens/components/header/code/HeaderCodePage.tsx +++ b/apps/website/screens/components/header/code/HeaderCodePage.tsx @@ -3,7 +3,13 @@ import DocFooter from "@/common/DocFooter"; import QuickNavContainer from "@/common/QuickNavContainer"; import QuickNavContainerLayout from "@/common/QuickNavContainerLayout"; import Link from "next/link"; -import Code, { TableCode } from "@/common/Code"; +import Code, { ExtendedTableCode, TableCode } from "@/common/Code"; + +const logoTypeString = `{ + href?: string; + src: string; + title?: string; +}`; const sections = [ { @@ -19,16 +25,6 @@ const sections = [ - - underlined - - boolean - - Whether a contrast line should appear at the bottom of the header. - - false - - content @@ -42,14 +38,25 @@ const sections = [ - - responsiveContent + logo - {"(closeHandler: () => void) => React.ReactNode"} + + {"Logo"} +

+ being Logo an object with the following properties: +

+ {logoTypeString} + + Logo to be displayed inside the header. + - + + + margin - Content shown in responsive version. It receives the close menu handler that can be used to add that - functionality when a element is clicked. + 'xxsmall' | 'xsmall' | 'small' | 'medium' | 'large' | 'xlarge' | 'xxlarge' + Size of the bottom margin to be applied to the header. - @@ -61,11 +68,14 @@ const sections = [ - - margin + responsiveContent - 'xxsmall' | 'xsmall' | 'small' | 'medium' | 'large' | 'xlarge' | 'xxlarge' + {"(closeHandler: () => void) => React.ReactNode"} + + + Content shown in responsive version. It receives the close menu handler that can be used to add that + functionality when a element is clicked. - Size of the bottom margin to be applied to the header. - @@ -78,6 +88,16 @@ const sections = [ 0 + + underlined + + boolean + + Whether a contrast line should appear at the bottom of the header. + + false + + ), diff --git a/apps/website/screens/components/header/overview/HeaderOverviewPage.tsx b/apps/website/screens/components/header/overview/HeaderOverviewPage.tsx new file mode 100644 index 0000000000..4251a849e3 --- /dev/null +++ b/apps/website/screens/components/header/overview/HeaderOverviewPage.tsx @@ -0,0 +1,138 @@ +import { DxcBulletedList, DxcFlex, DxcParagraph } from "@dxc-technology/halstack-react"; +import DocFooter from "@/common/DocFooter"; +import Figure from "@/common/Figure"; +import Image from "@/common/Image"; +import QuickNavContainer from "@/common/QuickNavContainer"; +import QuickNavContainerLayout from "@/common/QuickNavContainerLayout"; +import anatomy from "./images/header_anatomy.png"; +import responsive from "./images/header_responsive.png"; +import variants from "./images/header_variants.png"; + +const sections = [ + { + title: "Introduction", + content: ( + + The Header serves as the primary navigation and identity element for an application. It includes branding, quick + access to key sections via navigation links, and a user account menu. Its consistent presence reinforces brand + recognition and improves usability by offering easy navigation and access to user-related actions. + + ), + }, + { + title: "Anatomy", + content: ( + <> + Header's anatomy + + + Container: a layout structure that wraps all Header elements, ensuring consistent + alignment, spacing, and maximum width limits. The container helps keep the header properly aligned across + different screen sizes. + + + Brand Image: a clear, balanced logo that fits well within the header without overpowering + other elements. + + + Application Name (Optional): a short, recognizable application name placed next to + the logo to reinforce brand identity. + + + Navigation Links (Optional): key links to main sections of the application. + + + Header Dropdown (Optional): a dropdown menu for user-specific actions such as + profile, settings, or logout, triggered by click or keyboard focus. + + + Divider (Optional): horizontal line that visually separates the Header from the + page content below, enhancing layout clarity. + + + + ), + }, + { + title: "Variants", + content: ( + <> + + To maintain consistency with the way variants are structured across components, the Header offers two primary + styles: default and underlined. + + + + The default variant features a clean header without a visual separation from the page + content, ideal for minimalistic or immersive layouts. + + + The underlined variant includes a subtle bottom divider, creating a clear visual boundary + between the header and the rest of the page content, enhancing structure and clarity. + + +
+ Header variants +
+ + ), + }, + { + title: "Responsive version for mobile and tablet", + content: ( + <> + + Since applications are accessed from a variety of devices, including laptops, tablets, and smartphones, it's + essential to design a Header that adapts fluidly to different screen sizes. The responsive Header should + maintain the core structure and visual hierarchy of the desktop version, ensuring a consistent and intuitive + user experience across all devices. + +
+ Header menu responsive version +
+ + ), + }, + { + title: "Best practices", + content: ( + + + Keep the Header minimal and functional: include only essential elements. + + + Select the correct variant according to visual needs: Use the default{" "} + variant for simple pages and underlined variant to visually separate the Header from the content when + necessary. + + + Use dropdowns correctly for complex navigation: Only use Header dropdowns when necessary to + organize multiple links logically without overwhelming the top navigation. + + + Avoid overcrowding the Header: Limit the number of top-level navigation links. Group + secondary links inside dropdowns if needed to maintain a clean and user-friendly interface. + + + Display the application name clearly and concisely: The application name should be readable, + short, and not overpower the brand logo. It reinforces brand identity and provides immediate context to users. + + + Design the Header to respond gracefully to smaller screens: When adapting the Header to + mobile or tablet layouts, restructure the content to preserve both visual clarity and functional hierarchy. + + + ), + }, +]; + +const HeaderOverviewPage = () => ( + + + + + + +); + +export default HeaderOverviewPage; diff --git a/apps/website/screens/components/header/overview/images/header_anatomy.png b/apps/website/screens/components/header/overview/images/header_anatomy.png new file mode 100644 index 0000000000..fc6fd66c30 Binary files /dev/null and b/apps/website/screens/components/header/overview/images/header_anatomy.png differ diff --git a/apps/website/screens/components/header/overview/images/header_responsive.png b/apps/website/screens/components/header/overview/images/header_responsive.png new file mode 100644 index 0000000000..ed3509d98d Binary files /dev/null and b/apps/website/screens/components/header/overview/images/header_responsive.png differ diff --git a/apps/website/screens/components/header/overview/images/header_variants.png b/apps/website/screens/components/header/overview/images/header_variants.png new file mode 100644 index 0000000000..8bc6f3d234 Binary files /dev/null and b/apps/website/screens/components/header/overview/images/header_variants.png differ diff --git a/apps/website/screens/components/header/specs/HeaderSpecsPage.tsx b/apps/website/screens/components/header/specs/HeaderSpecsPage.tsx deleted file mode 100644 index 9414807ee6..0000000000 --- a/apps/website/screens/components/header/specs/HeaderSpecsPage.tsx +++ /dev/null @@ -1,349 +0,0 @@ -import { DxcBulletedList, DxcFlex, DxcTable, DxcParagraph } from "@dxc-technology/halstack-react"; -import QuickNavContainer from "@/common/QuickNavContainer"; -import QuickNavContainerLayout from "@/common/QuickNavContainerLayout"; -import Code from "@/common/Code"; -import Figure from "@/common/Figure"; -import DocFooter from "@/common/DocFooter"; -import Image from "@/common/Image"; -import headerSpecs from "./images/header_specs.png"; -import headerAnatomy from "./images/header_anatomy.png"; - -const sections = [ - { - title: "Specifications", - content: ( -
- Header design specifications -
- ), - }, - { - title: "Anatomy", - content: ( - <> - Header anatomy - - Brand image - Application name - Navigation links - Header dropdown - Container - - - ), - }, - { - title: "Design tokens", - subSections: [ - { - title: "Color", - content: ( - - - - Component token - Element - Core token - Value - - - - - - backgroundColor - - Container - - color-white - - #ffffff - - - - contentColor - - Content - - color-black - - #000000 - - - - underlinedColor - - Container - - color-black - - #000000 - - - - hamburgerHoverColor - - Menu:hover - - color-grey-200 - - #e6e6e6 - - - - hamburgerFocusColor - - Menu:focus - - color-blue-600 - - #0095ff - - - - hamburgerFontColor - - Menu label - - color-black - - #000000 - - - - hamburgerIconColor - - Menu icon - - color-black - - #000000 - - - - menuBackgroundColor - - Menu - - color-white - - #ffffff - - - - overlayColor - - Overlay - - color-grey-800-a - - #000000b3 - - - - ), - }, - { - title: "Typography", - content: ( - - - - Component token - Element - Core token - Value - - - - - - hamburgerFontFamily - - Menu label - - font-family-sans - - 'Open Sans', sans-serif - - - - hamburgerFontStyle - - Menu label - - font-style-normal - - normal - - - - hamburgerFontSize - - Menu label - - font-scale-01 - - 0.75rem / 12px - - - - hamburgerFontWeight - - Menu label - - font-weight-semibold - - 600 - - - - ), - }, - { - title: "Border", - content: ( - - - - Property - Element - Core token - Value - - - - - - border-width - - Underline - - border-width-2 - - 2px - - - - border-style - - Underline - - border-style-solid - - solid - - - - ), - }, - { - title: "Size", - content: ( - - - - Poperty - Element - Core token - Value - - - - - - height - - Container - - - 64px - - - - width - - Container - - - 100% - - - - ), - }, - { - title: "Margin", - content: ( - <> - - The margin only applies to the margin-bottom of the header component. - - - - - Margin - Value - - - - - - xxsmall - - 6px - - - - xsmall - - 16px - - - - small - - 24px - - - - medium - - 36px - - - - large - - 48px - - - - xlarge - - 64px - - - - xxlarge - - 100px - - - - - ), - }, - ], - }, -]; - -const HeaderSpecsPage = () => { - return ( - - - - - - - ); -}; - -export default HeaderSpecsPage; diff --git a/apps/website/screens/components/header/specs/images/header_anatomy.png b/apps/website/screens/components/header/specs/images/header_anatomy.png deleted file mode 100644 index 503e7f420c..0000000000 Binary files a/apps/website/screens/components/header/specs/images/header_anatomy.png and /dev/null differ diff --git a/apps/website/screens/components/header/specs/images/header_specs.png b/apps/website/screens/components/header/specs/images/header_specs.png deleted file mode 100644 index 6540b894d9..0000000000 Binary files a/apps/website/screens/components/header/specs/images/header_specs.png and /dev/null differ diff --git a/apps/website/screens/components/header/usage/HeaderUsagePage.tsx b/apps/website/screens/components/header/usage/HeaderUsagePage.tsx deleted file mode 100644 index a095da12ea..0000000000 --- a/apps/website/screens/components/header/usage/HeaderUsagePage.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import { DxcBulletedList, DxcFlex, DxcParagraph } from "@dxc-technology/halstack-react"; -import DocFooter from "@/common/DocFooter"; -import Figure from "@/common/Figure"; -import Image from "@/common/Image"; -import QuickNavContainer from "@/common/QuickNavContainer"; -import QuickNavContainerLayout from "@/common/QuickNavContainerLayout"; -import headerResponsive from "./images/header_responsive.png"; - -const sections = [ - { - title: "Usage", - content: ( - - - Try to avoid to place a large number of items inside the content area. - - Avoid increasing the header height. - - Halstack components placed as a children should follow their respective guidelines. - - - ), - }, - { - title: "Variants", - content: ( - <> - {/* */} - - Following the convention of the variants that can be found in a component, two main variants are defined for - the header. Variants: default and underlined. - - - ), - }, - { - title: "Custom content", - content: ( - - - Application name: If the application has a specific name, can be placed following the brand - image. - - - Primary navigation: The links and dropdowns with a navigational purpose. - - - Site options: Language selector, settings, or other any option that applies. - - - Account: In the case that the application manages accounts, the element for the login and - register options should be positioned on the far right except in the tablet and mobile version that will be - covered in the following sections. - - - ), - }, - { - title: "Responsive version for mobile and tablet", - content: ( - <> - - Due to the applications are accessible from a laptop, tablet and mobile it is necessary to think and design a - header version for the corresponding device. The design for smaller devices tries to keep the consistency - respect to the other versions, allowing the user experiences a similar interaction although the space - available is less. - -
- Header menu responsive version -
- - ), - }, -]; - -const HeaderUsagePage = () => { - return ( - - - - - - - ); -}; - -export default HeaderUsagePage; diff --git a/apps/website/screens/components/header/usage/images/header_responsive.png b/apps/website/screens/components/header/usage/images/header_responsive.png deleted file mode 100644 index c0d5e3371c..0000000000 Binary files a/apps/website/screens/components/header/usage/images/header_responsive.png and /dev/null differ diff --git a/packages/lib/src/header/Header.stories.tsx b/packages/lib/src/header/Header.stories.tsx index 4c76a3b6d7..63c51cb6f0 100644 --- a/packages/lib/src/header/Header.stories.tsx +++ b/packages/lib/src/header/Header.stories.tsx @@ -6,7 +6,6 @@ import preview from "../../.storybook/preview"; import { disabledRules } from "../../test/accessibility/rules/specific/header/disabledRules"; import DxcButton from "../button/Button"; import DxcFlex from "../flex/Flex"; -import { HalstackProvider } from "../HalstackContext"; import DxcLink from "../link/Link"; import DxcHeader from "./Header"; import { Meta, StoryObj } from "@storybook/react"; @@ -51,21 +50,6 @@ const options2: any = [ }, ]; -const opinionatedTheme = { - header: { - baseColor: "#ffffff", - accentColor: "#000000", - fontColor: "#000000", - menuBaseColor: "#ffffff", - hamburgerColor: "#000000", - logo: "https://upload.wikimedia.org/wikipedia/commons/thumb/b/b8/2021_Facebook_icon.svg/2048px-2021_Facebook_icon.svg.png", - logoResponsive: - "https://upload.wikimedia.org/wikipedia/commons/thumb/b/b8/2021_Facebook_icon.svg/2048px-2021_Facebook_icon.svg.png", - contentColor: "#000000", - overlayColor: "#000000b3", - }, -}; - const Header = () => ( <> @@ -128,20 +112,21 @@ const Header = () => (

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras felis.

- + </> +); + +const HeaderCustomLogo = () => ( + <> <ExampleContainer> - <HalstackProvider theme={opinionatedTheme}> - <DxcHeader - underlined - content={<DxcButton label={"Custom Button"} />} - responsiveContent={(closeHandler) => ( - <> - <DxcButton label={"Custom Button"} onClick={closeHandler} /> - Custom content - </> - )} - /> - </HalstackProvider> + <Title title="Default with dropdown" theme="light" level={4} /> + <DxcHeader + content={<DxcHeader.Dropdown options={options} label="Default Dropdown" onSelectOption={() => {}} />} + logo={{ + src: "https://upload.wikimedia.org/wikipedia/commons/thumb/b/b8/2021_Facebook_icon.svg/2048px-2021_Facebook_icon.svg.png", + title: "Custom Logo", + href: "#test", + }} + /> </ExampleContainer> </> ); @@ -187,21 +172,16 @@ const RespHeaderMenuTablet = () => ( </ExampleContainer> ); -const RespHeaderMenuOpinionated = () => ( - <ExampleContainer> - <Title title="Responsive menu" theme="light" level={4} /> - <HalstackProvider theme={opinionatedTheme}> - <DxcHeader responsiveContent={(closeHandler) => <p>Lorem ipsum dolor sit amet.</p>} underlined /> - </HalstackProvider> - </ExampleContainer> -); - type Story = StoryObj<typeof DxcHeader>; export const Chromatic: Story = { render: Header, }; +export const CustomLogo: Story = { + render: HeaderCustomLogo, +}; + export const ResponsiveHeader: Story = { render: Responsive, parameters: { @@ -270,21 +250,6 @@ export const ResponsiveHeaderMenuTablet: Story = { }, }; -export const ResponsiveHeaderMenuOpinionated: Story = { - render: RespHeaderMenuOpinionated, - parameters: { - viewport: { - defaultViewport: "pixelxl", - }, - chromatic: { viewports: [720] }, - }, - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - await waitFor(() => canvas.findByText("Menu")); - await userEvent.click(canvas.getByText("Menu")); - }, -}; - export const ResponsiveHeaderTooltip: Story = { render: RespHeaderMenuMobile, parameters: { diff --git a/packages/lib/src/header/Header.tsx b/packages/lib/src/header/Header.tsx index 2dd9784283..326923a2e8 100644 --- a/packages/lib/src/header/Header.tsx +++ b/packages/lib/src/header/Header.tsx @@ -1,156 +1,14 @@ -import { ComponentProps, useEffect, useMemo, useRef, useState } from "react"; -import styled, { ThemeProvider } from "styled-components"; +import { ComponentProps, useEffect, useRef, useState } from "react"; +import styled from "styled-components"; import { responsiveSizes, spaces } from "../common/variables"; import DxcDropdown from "../dropdown/Dropdown"; import DxcIcon from "../icon/Icon"; -import HeaderPropsType from "./types"; -import { Tooltip } from "../tooltip/Tooltip"; +import HeaderPropsType, { Logo } from "./types"; import DxcFlex from "../flex/Flex"; import { useContext } from "react"; -import HalstackContext, { HalstackLanguageContext } from "../HalstackContext"; - -const Dropdown = (props: ComponentProps<typeof DxcDropdown>) => ( - <HeaderDropdown> - <DxcDropdown {...props} /> - </HeaderDropdown> -); - -const getLogoElement = (themeInput?: string, logoLabel?: string) => { - if (!themeInput) { - return ( - <svg xmlns="http://www.w3.org/2000/svg" width="73" height="40" viewBox="0 0 73 40"> - <title>DXC Logo - - - - - - - - ); - } else if (typeof themeInput === "string") return ; - else return themeInput; -}; - -type ContentProps = { - isResponsive: boolean; - responsiveContent: HeaderPropsType["responsiveContent"]; - handleMenu: () => void; - content: HeaderPropsType["content"]; -}; - -const Content = ({ isResponsive, responsiveContent, handleMenu, content }: ContentProps) => - isResponsive ? ( - {responsiveContent?.(handleMenu)} - ) : ( - {content} - ); - -const DxcHeader = ({ - underlined = false, - content, - responsiveContent, - onClick, - margin, - tabIndex = 0, -}: HeaderPropsType): JSX.Element => { - const [isResponsive, setIsResponsive] = useState(false); - const [isMenuVisible, setIsMenuVisible] = useState(false); - const colorsTheme = useContext(HalstackContext); - const translatedLabels = useContext(HalstackLanguageContext); - const ref = useRef(null); - - const handleMenu = () => { - if (isResponsive && !isMenuVisible) { - setIsMenuVisible(!isMenuVisible); - } else { - setIsMenuVisible(!isMenuVisible); - } - }; - - const headerLogo = useMemo( - () => getLogoElement(colorsTheme.header.logo, translatedLabels.formFields.logoAlternativeText), - [colorsTheme, translatedLabels] - ); - - const headerResponsiveLogo = useMemo( - () => getLogoElement(colorsTheme.header.logoResponsive, translatedLabels.formFields.logoAlternativeText), - [colorsTheme, translatedLabels] - ); - - useEffect(() => { - const handleResize = () => { - setIsResponsive(window.matchMedia(`(max-width: ${responsiveSizes.medium}rem)`).matches); - }; - - handleResize(); - window.addEventListener("resize", handleResize); - return () => { - window.removeEventListener("resize", handleResize); - }; - }, []); - - useEffect(() => { - if (!isResponsive) { - setIsMenuVisible(false); - } - }, [isResponsive]); - - return ( - - - - {headerLogo} - - {isResponsive && responsiveContent && ( - - - - - {translatedLabels.header.hamburgerTitle} - - - - - {headerResponsiveLogo} - - - - - - - - - - - )} - {!isResponsive && ( - - )} - - - ); -}; - -DxcHeader.Dropdown = Dropdown; +import { HalstackLanguageContext } from "../HalstackContext"; +import ActionIcon from "../action-icon/ActionIcon"; +import { dxcLogo } from "./Icons"; const HeaderDropdown = styled.div` display: flex; @@ -166,19 +24,17 @@ const HeaderContainer = styled.header<{ margin: HeaderPropsType["margin"]; underlined: HeaderPropsType["underlined"]; }>` + background-color: var(--color-bg-neutral-lightest); + border-bottom: ${(props) => + props.underlined && `var(--border-width-m) var(--border-style-default) var(--border-color-neutral-strongest)`}; + align-items: center; box-sizing: border-box; display: flex; flex-direction: row; - align-items: center; justify-content: space-between; - min-height: ${(props) => props.theme.minHeight}; margin-bottom: ${(props) => (props.margin ? spaces[props.margin] : "0px")}; - padding: ${(props) => - `${props.theme.paddingTop} ${props.theme.paddingRight} ${props.theme.paddingBottom} ${props.theme.paddingLeft}`}; - background-color: ${(props) => props.theme.backgroundColor}; - border-bottom: ${(props) => - props.underlined && - `${props.theme.underlinedThickness} ${props.theme.underlinedStyle} ${props.theme.underlinedColor}`}; + min-height: 64px; + padding: var(--spacing-padding-none) var(--spacing-padding-l); `; const LogoAnchor = styled.a<{ interactive: boolean }>` @@ -186,14 +42,14 @@ const LogoAnchor = styled.a<{ interactive: boolean }>` `; const LogoImg = styled.img` - max-height: ${(props) => props.theme.logoHeight}; - width: ${(props) => props.theme.logoWidth}; + max-height: var(--height-xl); + width: auto; `; const LogoContainer = styled.div` - max-height: ${(props) => props.theme.logoHeight}; - width: ${(props) => props.theme.logoWidth}; + max-height: var(--height-xl); vertical-align: middle; + width: auto; `; const ChildContainer = styled.div` @@ -210,60 +66,60 @@ const ContentContainer = styled.div` flex-grow: 1; justify-content: flex-end; width: calc(100% - 186px); - color: ${(props) => props.theme.contentColor}; + color: var(--color-fg-neutral-dark); `; const HamburgerTrigger = styled.button` + align-items: center; + background-color: transparent; + border-radius: var(--border-radius-xs); + border: var(--border-width-s) var(--border-style-default) transparent; + color: var(--color-fg-neutral-dark); + cursor: pointer; display: flex; flex-direction: column; + font-family: var(--typography-font-family); + font-size: var(--typography-label-s); + font-weight: var(--typography-label-semibold); + height: var(--height-xl); justify-content: center; - align-items: center; - width: 54px; - cursor: pointer; - border: 1px solid transparent; - border-radius: 2px; - background-color: transparent; + // TODO: Ask about padding (spacing-padding-m or spacing-padding-xs?) + padding: var(--spacing-padding-none) var(--spacing-padding-m); + text-transform: uppercase; :hover { - background-color: ${(props) => props.theme.hamburgerHoverColor}; + background-color: var(--color-bg-neutral-medium); } &:focus { - outline: ${(props) => props.theme.hamburgerFocusColor} auto 1px; + outline: var(--border-color-secondary-medium) var(--border-style-default) var(--border-width-m); } & > svg { - fill: ${(props) => props.theme.hamburgerIconColor}; + fill: var(--color-fg-neutral-dark); } & > span { - font-size: 24px; + font-size: var(--height-s); } - font-family: ${(props) => props.theme.hamburgerFontFamily}; - font-style: ${(props) => props.theme.hamburgerFontStyle}; - font-size: ${(props) => props.theme.hamburgerFontSize}; - text-transform: ${(props) => props.theme.hamburgerTextTransform}; - font-weight: ${(props) => props.theme.hamburgerFontWeight}; - color: ${(props) => props.theme.hamburgerFontColor}; `; const ResponsiveMenu = styled.div<{ hasVisibility: boolean }>` display: flex; flex-direction: column; - background-color: ${(props) => props.theme.menuBackgroundColor}; + background-color: var(--color-bg-neutral-lightest); position: fixed; top: 0; right: 0; - z-index: ${(props) => props.theme.menuZindex}; + z-index: 2000; @media (max-width: ${responsiveSizes.large}rem) and (min-width: ${responsiveSizes.small}rem) { - width: ${(props) => props.theme.menuTabletWidth}; + width: 60vw; } @media (not((max-width: ${responsiveSizes.large}rem) and (min-width: ${responsiveSizes.small}rem))) { - width: ${(props) => props.theme.menuMobileWidth}; + width: 100vw; } height: 100vh; padding: 20px; transform: ${(props) => (props.hasVisibility ? "translateX(0)" : "translateX(100vw)")}; - opacity: ${(props) => (props.hasVisibility ? "1" : "0.96")}; transition-property: transform, opacity; transition-duration: 0.6s; transition-timing-function: ease-in-out; @@ -271,30 +127,9 @@ const ResponsiveMenu = styled.div<{ hasVisibility: boolean }>` `; const ResponsiveLogoContainer = styled.div` - max-height: ${(props) => props.theme.logoHeight}; - width: ${(props) => props.theme.logoWidth}; - display: flex; -`; - -const CloseAction = styled.button` + max-height: var(--height-xl); + width: auto; display: flex; - justify-content: center; - align-content: center; - padding: 6px; - border: unset; - border-radius: 2px; - background-color: transparent; - cursor: pointer; - - :focus, - :focus-visible { - outline: ${(props) => props.theme.hamburgerFocusColor} auto 1px; - } - font-size: 24px; - svg { - height: 24px; - width: 24px; - } `; const MenuContent = styled.div` @@ -302,7 +137,7 @@ const MenuContent = styled.div` flex-direction: column; align-items: flex-start; height: 100%; - color: ${(props) => props.theme.contentColor}; + color: var(--color-fg-neutral-dark); `; const Overlay = styled.div<{ hasVisibility: boolean }>` @@ -311,17 +146,135 @@ const Overlay = styled.div<{ hasVisibility: boolean }>` left: 0; width: 100vw; height: 100vh; - background-color: ${(props) => props.theme.overlayColor}; - opacity: ${(props) => props.theme.overlayOpacity} !important; - visibility: ${(props) => (props.hasVisibility ? "visible" : "hidden")}; - opacity: ${(props) => (props.hasVisibility ? "1" : "0")}; + background-color: ${(props) => (props.hasVisibility ? "var(--color-bg-alpha-medium)" : "transparent")}; @media (max-width: ${responsiveSizes.small}rem) { - display: none; + ${(props) => !props.hasVisibility && "display: none"}; } - transition: opacity 0.2s 0.2s ease-in-out; - z-index: ${(props) => props.theme.overlayZindex}; + z-index: 1600; `; +const Dropdown = (props: ComponentProps) => ( + + + +); + +const getLogoElement = (logo?: Logo) => { + if (logo) { + return ; + } else { + return dxcLogo; + } +}; + +type ContentProps = { + isResponsive: boolean; + responsiveContent: HeaderPropsType["responsiveContent"]; + handleMenu: () => void; + content: HeaderPropsType["content"]; +}; + +const Content = ({ isResponsive, responsiveContent, handleMenu, content }: ContentProps) => + isResponsive ? ( + {responsiveContent?.(handleMenu)} + ) : ( + {content} + ); + +const DxcHeader = ({ + underlined = false, + content, + responsiveContent, + logo, + margin, + onClick, + tabIndex = 0, +}: HeaderPropsType): JSX.Element => { + const [isResponsive, setIsResponsive] = useState(false); + const [isMenuVisible, setIsMenuVisible] = useState(false); + const translatedLabels = useContext(HalstackLanguageContext); + const ref = useRef(null); + + const handleMenu = () => { + if (isResponsive && !isMenuVisible) { + setIsMenuVisible(!isMenuVisible); + } else { + setIsMenuVisible(!isMenuVisible); + } + }; + + const headerLogo = getLogoElement(logo); + + useEffect(() => { + const handleResize = () => { + setIsResponsive(window.matchMedia(`(max-width: ${responsiveSizes.medium}rem)`).matches); + }; + + handleResize(); + window.addEventListener("resize", handleResize); + return () => { + window.removeEventListener("resize", handleResize); + }; + }, []); + + useEffect(() => { + if (!isResponsive) { + setIsMenuVisible(false); + } + }, [isResponsive]); + + return ( + + + {headerLogo} + + {isResponsive && responsiveContent && ( + + + + + {translatedLabels.header.hamburgerTitle} + + + + + {headerLogo} + + + + + + + )} + {!isResponsive && ( + + )} + + ); +}; + +DxcHeader.Dropdown = Dropdown; + export default DxcHeader; diff --git a/packages/lib/src/header/Icons.tsx b/packages/lib/src/header/Icons.tsx new file mode 100644 index 0000000000..b674855295 --- /dev/null +++ b/packages/lib/src/header/Icons.tsx @@ -0,0 +1,19 @@ +export const dxcLogo = ( + + DXC Logo + + + + + + + +); diff --git a/packages/lib/src/header/types.ts b/packages/lib/src/header/types.ts index 6049310c0a..c175455d24 100644 --- a/packages/lib/src/header/types.ts +++ b/packages/lib/src/header/types.ts @@ -1,6 +1,21 @@ import { ReactNode } from "react"; import { Space } from "../common/utils"; +export type Logo = { + /** + * URL to navigate when the logo is clicked. + */ + href?: string; + /** + * Source of the logo image. + */ + src: string; + /** + * Alternative text for the logo image. + */ + title?: string; +}; + type Props = { /** * Whether a contrast line should appear at the bottom of the header. @@ -18,13 +33,17 @@ type Props = { */ responsiveContent?: (closeHandler: () => void) => ReactNode; /** - * This function will be called when the user clicks the header logo. + * Logo to be displayed inside the header */ - onClick?: () => void; + logo?: Logo; /** * Size of the bottom margin to be applied to the header. */ margin?: Space; + /** + * This function will be called when the user clicks the header logo. + */ + onClick?: () => void; /** * Value of the tabindex for all interactive elements, except those inside the * custom area.