diff --git a/apps/website/pages/_app.tsx b/apps/website/pages/_app.tsx
index b685f49697..7227ba1b3f 100644
--- a/apps/website/pages/_app.tsx
+++ b/apps/website/pages/_app.tsx
@@ -14,6 +14,7 @@ import Link from "next/link";
import { GroupItem, Item, Section } from "../../../packages/lib/src/base-menu/types";
import { isGroupItem } from "../../../packages/lib/src/base-menu/utils";
import SidenavLogo from "@/common/sidenav/SidenavLogo";
+import { dxcLogo } from "@/common/images/dxc_logo";
type NextPageWithLayout = NextPage & {
getLayout?: (_page: ReactElement) => ReactNode;
@@ -99,6 +100,11 @@ export default function App({ Component, pageProps, emotionCache = clientSideEmo
+ }
sidenav={
+ DXC Logo
+
+
+
+
+
+
+
+);
diff --git a/apps/website/screens/components/header/code/HeaderCodePage.tsx b/apps/website/screens/components/header/code/HeaderCodePage.tsx
index 30eddc65c8..2f1b3d4841 100644
--- a/apps/website/screens/components/header/code/HeaderCodePage.tsx
+++ b/apps/website/screens/components/header/code/HeaderCodePage.tsx
@@ -1,14 +1,34 @@
-import { DxcFlex, DxcTable, DxcParagraph, DxcLink } from "@dxc-technology/halstack-react";
+import { DxcFlex, DxcTable } from "@dxc-technology/halstack-react";
import DocFooter from "@/common/DocFooter";
import QuickNavContainer from "@/common/QuickNavContainer";
-import Link from "next/link";
import Code, { ExtendedTableCode, TableCode } from "@/common/Code";
import StatusBadge from "@/common/StatusBadge";
-const logoTypeString = `{
- href?: string;
- src: string;
- title?: string;
+const brandingTypeString = `{
+ logo : {
+ src: string | SVG;
+ alt: string;
+ href?: string;
+ onClick?: () => void;
+ };
+ appTitle?: string;
+}`;
+
+const navItemsTypeString = `(GroupItem | Item)[]`;
+
+const itemTypeString = `{
+ badge?: ReactElement;
+ icon?: string | SVG;
+ label: string;
+ onSelect?: () => void;
+ selected?: boolean;
+}`;
+
+const groupItemTypeString = `{
+ badge?: ReactElement;
+ icon?: string | SVG;
+ label: string;
+ items: (Item)[];
}`;
const sections = [
@@ -25,124 +45,90 @@ const sections = [
-
- content
-
- React.ReactNode
-
-
- Content shown in the header. Take into account that the component applies styles for the first child in
- the content, so we recommend the use of React.Fragment to be applied correctly. Otherwise,
- the styles can be modified.
-
- -
-
-
- logo
+
+ branding
-
- {"Logo"}
-
- being Logo an object with the following properties:
-
- {logoTypeString}
-
+ {brandingTypeString}
- Logo to be displayed inside the header.
+ Object used to configure the header branding, including logo and application title.
-
- margin
+ navItems
- 'xxsmall' | 'xsmall' | 'small' | 'medium' | 'large' | 'xlarge' | 'xxlarge'
+ {navItemsTypeString}
- Size of the bottom margin to be applied to the header.
- -
-
-
- onClick
- {"() => void"}
+ Array of navigation items to be displayed in the header navigation menu. Each item can be a single/simple
+ item or a group item.
+
+ Being Item an object with the following properties:
+
+ {itemTypeString}
+
+ and GroupItem an object with the following properties:
+
+ {groupItemTypeString}
+ Group items will ignore any nested group items to maintain a maximum of two levels in the navigation menu.
+ When responsive, navigation items will be displayed in a vertical menu below the header in a vertical
+ layout.
- This function will be called when the user clicks the header logo.
-
- responsiveContent
+ responsiveBottomContent
- {"(closeHandler: () => void) => React.ReactNode"}
+ 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.
+ The content rendered in the bottom part of the header menu under the navigation items when in responsive
+ mode.
-
- tabIndex
-
- number
-
- Value of the tabindex for all interactive elements, except those inside the custom area.
-
- 0
-
-
-
- underlined
+ sideContent
- boolean
+ {"React.ReactNode | (isResponsive: boolean) => React.ReactNode"}
- Whether a contrast line should appear at the bottom of the header.
- false
+ Content to be displayed on the right side of the header. It can be a ReactNode or a function that receives
+ a boolean indicating if the header is in responsive mode and returns a ReactNode.
),
},
- {
- title: "DxcHeader.Dropdown",
- content: (
-
- Everything between the tags will be displayed as a dropdown. If you want to show a{" "}
-
- DxcDropdown
-
- , as a shortcut, you can also use it as a direct child of the DxcHeader without the tags, but we recommend to
- use it with the tags since some styles will be applied for a better fit in the header.
-
- ),
- },
- {
- title: "Examples",
- subSections: [
- {
- title: "Header in application layout",
- content: (
-
- ),
- },
- ],
- },
+ // UPDATE to new sandbox link when available
+ // {
+ // title: "Examples",
+ // subSections: [
+ // {
+ // title: "Header in application layout",
+ // content: (
+ //
+ // ),
+ // },
+ // ],
+ // },
];
const HeaderCodePage = () => {
diff --git a/apps/website/screens/components/header/overview/HeaderOverviewPage.tsx b/apps/website/screens/components/header/overview/HeaderOverviewPage.tsx
index 1558e31f01..aa30d8e96b 100644
--- a/apps/website/screens/components/header/overview/HeaderOverviewPage.tsx
+++ b/apps/website/screens/components/header/overview/HeaderOverviewPage.tsx
@@ -1,10 +1,6 @@
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 anatomy from "./images/header_anatomy.png";
-import variants from "./images/header_variants.png";
const sections = [
{
@@ -12,8 +8,8 @@ const sections = [
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.
+ access to key sections via navigation links, and a customizable side content. Its consistent presence reinforces
+ brand recognition and improves usability by offering easy navigation and access to quick actions.
),
},
@@ -21,7 +17,7 @@ const sections = [
title: "Anatomy",
content: (
<>
-
+ {/* */}
Container: a layout structure that wraps all Header elements, ensuring consistent
@@ -40,41 +36,13 @@ const sections = [
Navigation Links (Optional) : key links to main sections of the application.
- Header Dropdown (Optional) : a dropdown menu for user-specific actions such as
+ Side Content (Optional) : a customizable area 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.
-
-
-
-
-
- >
- ),
- },
{
title: "Responsive version",
content: (
@@ -87,7 +55,7 @@ const sections = [
"On smaller screens, the header content is replaced by a button. Triggering this button opens a menu that
- displays custom content."
+ displays navigation links and a bottom section."
>
),
@@ -100,17 +68,12 @@ const sections = [
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.
+ Use navigation links correctly: Only use navigation groups 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.
+ secondary links inside navigation groups if needed to maintain a clean and user-friendly interface.
Display the application name clearly and concisely: The application name should be readable,
@@ -118,7 +81,8 @@ const sections = [
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.
+ mobile or tablet layouts, restructure the side content to preserve both visual clarity and functional
+ hierarchy.
),
diff --git a/packages/lib/src/base-menu/GroupItem.tsx b/packages/lib/src/base-menu/GroupItem.tsx
index db9c6b328b..e72c73bf4f 100644
--- a/packages/lib/src/base-menu/GroupItem.tsx
+++ b/packages/lib/src/base-menu/GroupItem.tsx
@@ -13,10 +13,10 @@ const GroupItem = ({ items, ...props }: GroupItemProps) => {
const NavigationTreeId = `sidenav-${useId()}`;
const contextValue = useContext(BaseMenuContext) ?? {};
- const { groupSelected, isOpen, toggleOpen, responsiveView } = useGroupItem(items, contextValue);
+ const { groupSelected, isOpen, toggleOpen, hasPopOver, isHorizontal } = useGroupItem(items, contextValue);
// TODO: SET A FIXED WIDTH TO PREVENT MOVING CONTENT WHEN EXPANDING/COLLAPSING IN RESPONSIVEVIEW
- return responsiveView ? (
+ return hasPopOver ? (
<>
{
/>
-
+
{
@@ -47,12 +47,18 @@ const GroupItem = ({ items, ...props }: GroupItemProps) => {
event.preventDefault();
}}
align="start"
- side="right"
+ side={isHorizontal ? "bottom" : "right"}
style={{ zIndex: "var(--z-contextualmenu)" }}
+ sideOffset={isHorizontal ? 16 : 0}
+ onInteractOutside={isHorizontal ? () => toggleOpen() : undefined}
>
-
diff --git a/packages/lib/src/base-menu/ItemAction.tsx b/packages/lib/src/base-menu/ItemAction.tsx
index c402126de9..68ecb91909 100644
--- a/packages/lib/src/base-menu/ItemAction.tsx
+++ b/packages/lib/src/base-menu/ItemAction.tsx
@@ -9,19 +9,20 @@ const Action = styled.a<{
depthLevel: ItemActionProps["depthLevel"];
selected: ItemActionProps["selected"];
displayGroupLines: boolean;
- responsiveView?: boolean;
+ hasPopOver?: boolean;
+ isHorizontal?: boolean;
}>`
box-sizing: content-box;
border: none;
border-radius: var(--border-radius-s);
- ${({ displayGroupLines, depthLevel, responsiveView }) => `
- ${!responsiveView ? `padding: var(--spacing-padding-xxs) var(--spacing-padding-xxs) var(--spacing-padding-xxs) calc(var(--spacing-padding-xs) + ${!displayGroupLines ? depthLevel : 0} * var(--spacing-padding-l))` : "padding: var(--spacing-padding-xxs) var(--spacing-padding-none)"};
+ ${({ displayGroupLines, depthLevel, hasPopOver, isHorizontal }) => `
+ ${!hasPopOver || isHorizontal ? `padding: var(--spacing-padding-xxs) var(--spacing-padding-xxs) var(--spacing-padding-xxs) calc(var(--spacing-padding-xs) + ${!displayGroupLines ? depthLevel : 0} * var(--spacing-padding-l))` : "padding: var(--spacing-padding-xxs) var(--spacing-padding-none)"};
${displayGroupLines && depthLevel > 0 ? "margin-left: var(--spacing-padding-xs);" : ""}
`}
display: flex;
align-items: center;
gap: var(--spacing-gap-m);
- justify-content: ${({ responsiveView }) => (responsiveView ? "center" : "space-between")};
+ justify-content: ${({ hasPopOver }) => (hasPopOver ? "center" : "space-between")};
background-color: ${({ selected }) => (selected ? "var(--color-bg-primary-lighter)" : "transparent")};
height: var(--height-s);
cursor: pointer;
@@ -84,8 +85,9 @@ const ItemAction = memo(
hasTooltip,
modifiedBadge,
displayControlsAfter,
- responsiveView,
+ hasPopOver,
displayGroupLines,
+ isHorizontal,
handleTextMouseEnter,
getWrapper,
} = useItemAction(props);
@@ -100,31 +102,32 @@ const ItemAction = memo(
depthLevel={depthLevel}
selected={selected}
displayGroupLines={!!displayGroupLines}
- responsiveView={responsiveView}
+ hasPopOver={hasPopOver}
+ isHorizontal={isHorizontal}
{...(href && { href })}
{...rest}
aria-pressed={!href ? ariaPressed : undefined}
>
-
+
{!displayControlsAfter && collapseIcon && (
{collapseIcon}
)}
- {(icon || responsiveView) && (
-
+ {(icon || hasPopOver) && (
+
{typeof icon === "string" ? : icon ? icon : }
)}
- {!responsiveView && (
+ {(!hasPopOver || isHorizontal) && (
{label}
)}
- {!responsiveView && (modifiedBadge || (displayControlsAfter && collapseIcon)) && (
+ {(!hasPopOver || isHorizontal) && (modifiedBadge || (displayControlsAfter && collapseIcon)) && (
{modifiedBadge}
{displayControlsAfter && collapseIcon && {collapseIcon} }
diff --git a/packages/lib/src/base-menu/Section.tsx b/packages/lib/src/base-menu/Section.tsx
index 5981b34906..dab8e8007b 100644
--- a/packages/lib/src/base-menu/Section.tsx
+++ b/packages/lib/src/base-menu/Section.tsx
@@ -23,10 +23,10 @@ const Title = styled.h2`
export default function Section({ index, length, section }: SectionProps) {
const id = `section-${useId()}`;
- const { responsiveView } = useContext(BaseMenuContext) ?? {};
+ const { hasPopOver } = useContext(BaseMenuContext) ?? {};
return (
- {!responsiveView && section.title && {section.title} }
+ {!hasPopOver && section.title && {section.title} }
{section.items.map((item, i) => (
diff --git a/packages/lib/src/base-menu/SubMenu.tsx b/packages/lib/src/base-menu/SubMenu.tsx
index a0414a3d22..f7114b96c0 100644
--- a/packages/lib/src/base-menu/SubMenu.tsx
+++ b/packages/lib/src/base-menu/SubMenu.tsx
@@ -3,26 +3,54 @@ import { SubMenuProps } from "./types";
import BaseMenuContext from "./BaseMenuContext";
import { useContext } from "react";
-const SubMenuContainer = styled.ul<{ depthLevel: number; displayGroupLines?: boolean }>`
+const SubMenuContainer = styled.ul<{
+ depthLevel: number;
+ displayGroupLines?: boolean;
+ isHorizontal?: boolean;
+ isPopOver?: boolean;
+}>`
margin: 0;
padding: 0;
- display: grid;
- gap: var(--spacing-gap-xs);
+ display: flex;
+ flex-direction: ${({ isHorizontal }) => (isHorizontal ? "row" : "column")};
+ gap: ${({ isHorizontal }) => (isHorizontal ? "var(--spacing-gap-s)" : "var(--spacing-gap-xs)")};
list-style: none;
-
+ ${({ isPopOver }) =>
+ isPopOver &&
+ `
+ min-width: 200px;
+ max-width: 320px;
+ padding: var(--spacing-padding-xs);
+ background-color: var(--color-bg-neutral-lightest);
+ border-radius: var(--border-radius-m);
+ box-shadow: var(--shadow-100);
+ `}
${({ depthLevel, displayGroupLines }) =>
displayGroupLines &&
depthLevel >= 0 &&
`
margin-left: calc(var(--spacing-padding-m) + ${depthLevel} * var(--spacing-padding-xs));
border-left: var(--border-width-s) solid var(--border-color-neutral-lighter);
- `}
+ `};
`;
-export default function SubMenu({ children, id, depthLevel = 0 }: SubMenuProps) {
+export default function SubMenu({
+ children,
+ id,
+ depthLevel = 0,
+ isHorizontal = false,
+ isPopOver = false,
+}: SubMenuProps) {
const { displayGroupLines } = useContext(BaseMenuContext) ?? {};
return (
-
+
{children}
);
diff --git a/packages/lib/src/base-menu/types.ts b/packages/lib/src/base-menu/types.ts
index 9024dad6a0..86d70c0f94 100644
--- a/packages/lib/src/base-menu/types.ts
+++ b/packages/lib/src/base-menu/types.ts
@@ -39,7 +39,11 @@ type Props = {
/**
* If true the menu will be icons only and display a popover on click.
*/
- responsiveView?: boolean;
+ hasPopOver?: boolean;
+ /**
+ * If true the menu will be displayed horizontally.
+ */
+ isHorizontal?: boolean;
};
type ItemWithId = Item & { id: number };
@@ -76,16 +80,24 @@ type SectionProps = {
index: number;
length: number;
};
-type SubMenuProps = { children: ReactNode; id?: string; depthLevel?: number };
+type SubMenuProps = {
+ children: ReactNode;
+ id?: string;
+ depthLevel?: number;
+ isHorizontal?: boolean;
+ isPopOver?: boolean;
+};
type BaseMenuContextProps = {
selectedItemId?: number;
setSelectedItemId?: Dispatch>;
displayGroupLines?: boolean;
displayControlsAfter?: boolean;
- responsiveView?: boolean;
+ hasPopOver?: boolean;
+ isHorizontal?: boolean;
};
export type {
+ CommonItemProps,
BaseMenuContextProps,
GroupItem,
GroupItemProps,
diff --git a/packages/lib/src/base-menu/useGroupItem.ts b/packages/lib/src/base-menu/useGroupItem.ts
index c8997ec3f4..1bb2096f35 100644
--- a/packages/lib/src/base-menu/useGroupItem.ts
+++ b/packages/lib/src/base-menu/useGroupItem.ts
@@ -10,9 +10,9 @@ const isGroupSelected = (items: GroupItemProps["items"], selectedItemId?: number
export const useGroupItem = (items: GroupItemProps["items"], context: BaseMenuContextProps) => {
const groupMenuId = `group-menu-${useId()}`;
- const { selectedItemId } = context ?? {};
+ const { selectedItemId, hasPopOver } = context ?? {};
const groupSelected = useMemo(() => isGroupSelected(items, selectedItemId), [items, selectedItemId]);
- const [isOpen, setIsOpen] = useState(groupSelected && selectedItemId === -1);
+ const [isOpen, setIsOpen] = useState(hasPopOver ? false : groupSelected && selectedItemId === -1);
const toggleOpen = () => setIsOpen((prev) => !prev);
@@ -21,6 +21,7 @@ export const useGroupItem = (items: GroupItemProps["items"], context: BaseMenuCo
groupSelected,
isOpen,
toggleOpen,
- responsiveView: context.responsiveView,
+ hasPopOver: context.hasPopOver,
+ isHorizontal: context.isHorizontal,
};
};
diff --git a/packages/lib/src/base-menu/useItemAction.ts b/packages/lib/src/base-menu/useItemAction.ts
index dda97fa109..b1a2e3eb84 100644
--- a/packages/lib/src/base-menu/useItemAction.ts
+++ b/packages/lib/src/base-menu/useItemAction.ts
@@ -5,7 +5,7 @@ import { ItemActionProps } from "./types";
export function useItemAction({ badge, renderItem }: ItemActionProps) {
const [hasTooltip, setHasTooltip] = useState(false);
const modifiedBadge = badge && cloneElement(badge, { size: "small" });
- const { displayControlsAfter, responsiveView, displayGroupLines } = useContext(BaseMenuContext) ?? {};
+ const { displayControlsAfter, hasPopOver, displayGroupLines, isHorizontal } = useContext(BaseMenuContext) ?? {};
const handleTextMouseEnter = (event: React.MouseEvent) => {
const text = event.currentTarget;
@@ -17,8 +17,9 @@ export function useItemAction({ badge, renderItem }: ItemActionProps) {
hasTooltip,
modifiedBadge,
displayControlsAfter,
- responsiveView,
+ hasPopOver,
displayGroupLines,
+ isHorizontal,
handleTextMouseEnter,
getWrapper,
};
diff --git a/packages/lib/src/contextual-menu/types.ts b/packages/lib/src/contextual-menu/types.ts
index 83d6b0a53f..194c79aaed 100644
--- a/packages/lib/src/contextual-menu/types.ts
+++ b/packages/lib/src/contextual-menu/types.ts
@@ -15,9 +15,9 @@ import BaseProps, {
} from "../base-menu/types";
type Item = Omit;
-type Props = Omit;
+type Props = Omit;
type ItemActionProps = Omit;
-type ContextualMenuContextProps = Omit;
+type ContextualMenuContextProps = Omit;
export type {
ContextualMenuContextProps,
diff --git a/packages/lib/src/header/Header.accessibility.test.tsx b/packages/lib/src/header/Header.accessibility.test.tsx
index ce2d4ce149..7ee54278ad 100644
--- a/packages/lib/src/header/Header.accessibility.test.tsx
+++ b/packages/lib/src/header/Header.accessibility.test.tsx
@@ -1,10 +1,10 @@
import { render } from "@testing-library/react";
import { axe, formatRules } from "../../test/accessibility/axe-helper";
import DxcHeader from "./Header";
-import DxcFlex from "../flex/Flex";
-import DxcLink from "../link/Link";
import rules from "../../test/accessibility/rules/specific/header/disabledRules";
import { vi } from "vitest";
+import DxcBadge from "../badge/Badge";
+import DxcButton from "../button/Button";
global.ResizeObserver = vi.fn().mockImplementation(() => ({
observe: vi.fn(),
@@ -25,27 +25,40 @@ const iconSVG = (
const iconUrl = "https://iconape.com/wp-content/files/yd/367773/svg/logo-linkedin-logo-icon-png-svg.png";
-const options = [
- {
- value: "1",
- label: "Amazon",
- icon: iconUrl,
- },
- {
- value: "2",
- label: "Ebay",
- icon: iconUrl,
- },
- {
- value: "3",
- label: "Wallapop",
- icon: iconSVG,
+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 items = [
{
- value: "4",
- label: "Aliexpress",
- icon: iconSVG,
+ label: "Grouped Item 1",
+ icon: "favorite",
+ items: [
+ { label: "Item 1", icon: "person", selected: true },
+ {
+ label: "Grouped Item 2",
+ items: [
+ {
+ label: "Item 2",
+ icon: "bookmark",
+ badge: ,
+ },
+ { label: "Selected Item 3" },
+ ],
+ },
+ ],
+ badge: ,
},
+ { label: "Item 4", icon: "key" },
+ { label: "Item 5", icon: "person" },
+ { label: "Grouped Item 6", items: [{ label: "Item 7", icon: "person" }, { label: "Item 8" }] },
+ { label: "Item 9" },
];
describe("Header component accessibility tests", () => {
@@ -60,25 +73,11 @@ describe("Header component accessibility tests", () => {
it("Should not have basic accessibility issues", async () => {
const { container } = render(
- Link 1
- Link 2
- Link 3
- {}}
- />
-
+ branding={branding}
+ navItems={items}
+ sideContent={
+ {}} />
}
- margin="medium"
- underlined
/>
);
const results = await axe(container, disabledRules);
diff --git a/packages/lib/src/header/Header.stories.tsx b/packages/lib/src/header/Header.stories.tsx
index dbc69cae44..ff4c847ee4 100644
--- a/packages/lib/src/header/Header.stories.tsx
+++ b/packages/lib/src/header/Header.stories.tsx
@@ -1,16 +1,34 @@
+import { Meta, StoryObj } from "@storybook/react-vite";
+import DxcHeader from "./Header";
+import DxcBadge from "../badge/Badge";
+import { useEffect } from "react";
+import DxcFlex from "../flex/Flex";
import Title from "../../.storybook/components/Title";
-import ExampleContainer from "../../.storybook/components/ExampleContainer";
-import disabledRules from "../../test/accessibility/rules/specific/header/disabledRules";
+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";
-import DxcFlex from "../flex/Flex";
-import DxcLink from "../link/Link";
-import DxcHeader from "./Header";
-import { Meta, StoryObj } from "@storybook/react-vite";
-import { userEvent, waitFor, within } from "storybook/internal/test";
+import disabledRules from "../../test/accessibility/rules/specific/header/disabledRules";
export default {
title: "Header",
component: DxcHeader,
+ decorators: [
+ (Story) => {
+ useEffect(() => {
+ const prev = document.body.style.cssText;
+ document.body.style.backgroundColor = "var(--color-bg-neutral-light)";
+ document.body.style.padding = "0";
+ return () => {
+ document.body.style.cssText = prev;
+ };
+ }, []);
+
+ return ;
+ },
+ ],
parameters: {
a11y: {
config: {
@@ -23,146 +41,172 @@ export default {
},
} satisfies Meta;
-const options = [
- {
- value: "1",
- label: "Amazon",
+const branding = {
+ logo: {
+ src: "https://picsum.photos/id/1000/104/34",
+ alt: "DXC Logo",
+ href: "https://www.dxc.com",
},
-];
+ appTitle: "Application Title",
+};
-const options2 = [
- {
- value: "1",
- label: "Home",
+const brandingWithoutTitle = {
+ logo: {
+ src: "https://picsum.photos/id/1000/104/34",
+ alt: "DXC Logo",
+ href: "https://www.dxc.com",
},
- {
- value: "2",
- label: "Release notes",
+};
+
+const dxcBrandedLogo = {
+ 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 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.`;
+
+const items = [
{
- value: "3",
- label: "Sign out",
+ label: "Grouped Item 1",
+ icon: "favorite",
+ items: [
+ { label: "Item 1", icon: "person", selected: true },
+ {
+ label: "Grouped Item 2",
+ items: [
+ {
+ label: "Item 2",
+ icon: "bookmark",
+ badge: ,
+ },
+ { label: "Selected Item 3" },
+ ],
+ },
+ ],
+ badge: ,
},
+ { label: "Item 4", icon: "key" },
+ { label: "Item 5", icon: "person" },
+ { label: "Grouped Item 6", items: [{ label: "Item 7", icon: "person" }, { label: "Item 8" }] },
+ { label: "Item 9" },
];
-const responsiveContentFunction = () => Lorem ipsum dolor sit amet.
;
+const longItems = [
+ ...items,
+ { label: "Item 10" },
+ { label: "Item 11" },
+ { label: "Item 12" },
+ { label: "Item 13" },
+ { label: "Item 14" },
+ { label: "Item 15" },
+ { label: "Item 16" },
+ { label: "Item 17" },
+];
const Header = () => (
- <>
-
-
- {}} />}
- />
-
-
-
- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras felis.
} />
-
-
-
-
- Link 1
- Link 2
- Link 3
- {}} />
-
- }
- underlined
- />
-
-
-
-
-
- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras felis.
-
-
-
-
- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras felis.
-
-
-
-
- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras felis.
-
-
-
-
- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras felis.
-
-
-
-
- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras felis.
-
-
-
-
- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras felis.
-
-
-
-
- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras felis.
-
- >
+
+
+
+
+ Side Content} />
+ Side Content} />
+
+ {longSideContent}} />
+
+ {longSideContent}} />
+ {longSideContent}} />
+
+ {longSideContent}} />
+
);
-const HeaderCustomLogo = () => (
- <>
-
-
+const HeaderInLayout = () => (
+ {}} />}
- 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",
- }}
+ branding={dxcBrandedLogo}
+ navItems={items}
+ sideContent={(isResponsive) =>
+ isResponsive ? (
+ <>
+
+ >
+ ) : (
+ <>
+
+
+
+ >
+ )
+ }
+ responsiveBottomContent={
+ <>
+
+
+ >
+ }
/>
-
- >
-);
-
-const Responsive = () => (
-
-
- {}} />}
- responsiveContent={responsiveContentFunction}
- underlined
- />
-
-);
-
-const RespHeaderFocus = () => (
-
-
-
-
-);
-const RespHeaderHover = () => (
-
-
-
-
-);
-const RespHeaderMenuMobile = () => (
-
-
-
-
-);
-
-const RespHeaderMenuTablet = () => (
-
-
-
-
+ }
+ >
+
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec ultrices fermentum ante et pharetra. Integer
+ ullamcorper ante non laoreet suscipit. Integer pharetra viverra nunc, quis fermentum urna eleifend eget.
+ Maecenas dolor justo, ullamcorper ac posuere tincidunt, dictum id urna. Suspendisse est metus, euismod et felis
+ eget, condimentum elementum eros. Curabitur ut lorem ut odio volutpat lacinia. Interdum et malesuada fames ac
+ ante ipsum primis in faucibus. Sed leo quam, lobortis in ultricies ac, interdum in sem. Suspendisse magna enim,
+ rhoncus eget lectus vitae, rutrum interdum ligula. Nunc efficitur neque ac orci pretium lacinia. Proin sagittis
+ condimentum mi, eu dapibus quam faucibus eget. Aenean fermentum nisl ut mauris convallis, in imperdiet neque
+ porttitor. Aliquam erat volutpat. Fusce tincidunt arcu id arcu dignissim viverra. Sed imperdiet vitae odio eget
+ consequat. Vivamus eu dictum orci.
+
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec ultrices fermentum ante et pharetra. Integer
+ ullamcorper ante non laoreet suscipit. Integer pharetra viverra nunc, quis fermentum urna eleifend eget.
+ Maecenas dolor justo, ullamcorper ac posuere tincidunt, dictum id urna. Suspendisse est metus, euismod et felis
+ eget, condimentum elementum eros. Curabitur ut lorem ut odio volutpat lacinia. Interdum et malesuada fames ac
+ ante ipsum primis in faucibus. Sed leo quam, lobortis in ultricies ac, interdum in sem. Suspendisse magna enim,
+ rhoncus eget lectus vitae, rutrum interdum ligula. Nunc efficitur neque ac orci pretium lacinia. Proin sagittis
+ condimentum mi, eu dapibus quam faucibus eget. Aenean fermentum nisl ut mauris convallis, in imperdiet neque
+ porttitor. Aliquam erat volutpat. Fusce tincidunt arcu id arcu dignissim viverra. Sed imperdiet vitae odio eget
+ consequat. Vivamus eu dictum orci.
+
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec ultrices fermentum ante et pharetra. Integer
+ ullamcorper ante non laoreet suscipit. Integer pharetra viverra nunc, quis fermentum urna eleifend eget.
+ Maecenas dolor justo, ullamcorper ac posuere tincidunt, dictum id urna. Suspendisse est metus, euismod et felis
+ eget, condimentum elementum eros. Curabitur ut lorem ut odio volutpat lacinia. Interdum et malesuada fames ac
+ ante ipsum primis in faucibus. Sed leo quam, lobortis in ultricies ac, interdum in sem. Suspendisse magna enim,
+ rhoncus eget lectus vitae, rutrum interdum ligula. Nunc efficitur neque ac orci pretium lacinia. Proin sagittis
+ condimentum mi, eu dapibus quam faucibus eget. Aenean fermentum nisl ut mauris convallis, in imperdiet neque
+ porttitor. Aliquam erat volutpat. Fusce tincidunt arcu id arcu dignissim viverra. Sed imperdiet vitae odio eget
+ consequat. Vivamus eu dictum orci.
+
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec ultrices fermentum ante et pharetra. Integer
+ ullamcorper ante non laoreet suscipit. Integer pharetra viverra nunc, quis fermentum urna eleifend eget.
+ Maecenas dolor justo, ullamcorper ac posuere tincidunt, dictum id urna. Suspendisse est metus, euismod et felis
+ eget, condimentum elementum eros. Curabitur ut lorem ut odio volutpat lacinia. Interdum et malesuada fames ac
+ ante ipsum primis in faucibus. Sed leo quam, lobortis in ultricies ac, interdum in sem. Suspendisse magna enim,
+ rhoncus eget lectus vitae, rutrum interdum ligula. Nunc efficitur neque ac orci pretium lacinia. Proin sagittis
+ condimentum mi, eu dapibus quam faucibus eget. Aenean fermentum nisl ut mauris convallis, in imperdiet neque
+ porttitor. Aliquam erat volutpat. Fusce tincidunt arcu id arcu dignissim viverra. Sed imperdiet vitae odio eget
+ consequat. Vivamus eu dictum orci.
+
+
+
);
type Story = StoryObj;
@@ -171,80 +215,12 @@ export const Chromatic: Story = {
render: Header,
};
-export const CustomLogo: Story = {
- render: HeaderCustomLogo,
-};
-
-export const ResponsiveHeader: Story = {
- render: Responsive,
- parameters: {
- chromatic: { viewports: [375] },
- },
- globals: {
- viewport: { value: "iphonex", isRotated: false },
- },
-};
-
-export const ResponsiveHeaderFocus: Story = {
- render: RespHeaderFocus,
- parameters: {
- chromatic: { viewports: [375] },
- },
- globals: {
- viewport: { value: "iphonex", isRotated: false },
- },
- play: async ({ canvasElement }) => {
- const canvas = within(canvasElement);
- await waitFor(() => canvas.findByText("Menu"));
- },
-};
-
-export const ResponsiveHeaderHover: Story = {
- render: RespHeaderHover,
- parameters: {
- chromatic: { viewports: [375] },
- },
- globals: {
- viewport: { value: "iphonex", isRotated: false },
- },
- play: async ({ canvasElement }) => {
- const canvas = within(canvasElement);
- await waitFor(() => canvas.findByText("Menu"));
- },
-};
-
-export const ResponsiveHeaderMenuMobile: Story = {
- render: RespHeaderMenuMobile,
- parameters: {
- chromatic: { viewports: [375] },
- },
- globals: {
- viewport: { value: "iphonex", isRotated: false },
- },
- play: async ({ canvasElement }) => {
- const canvas = within(canvasElement);
- await waitFor(() => canvas.findByText("Menu"));
- await userEvent.click(await canvas.findByText("Menu"));
- },
+export const InLayout: Story = {
+ render: HeaderInLayout,
};
-export const ResponsiveHeaderMenuTablet: Story = {
- render: RespHeaderMenuTablet,
- parameters: {
- chromatic: { viewports: [720] },
- },
- globals: {
- viewport: { value: "pixelxl", isRotated: false },
- },
- play: async ({ canvasElement }) => {
- const canvas = within(canvasElement);
- await waitFor(() => canvas.findByText("Menu"));
- await userEvent.click(await canvas.findByText("Menu"));
- },
-};
-
-export const ResponsiveHeaderTooltip: Story = {
- render: RespHeaderMenuMobile,
+export const Responsive: Story = {
+ render: HeaderInLayout,
parameters: {
chromatic: { viewports: [375] },
},
@@ -253,11 +229,11 @@ export const ResponsiveHeaderTooltip: Story = {
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
- await waitFor(() => canvas.findByText("Menu"));
- await userEvent.click(await canvas.findByText("Menu"));
- const closeButton = (await canvas.findAllByRole("button"))[1];
- if (closeButton != null) {
- await userEvent.hover(closeButton);
- }
+ await new Promise((resolve) => setTimeout(resolve, 100));
+ await userEvent.tab();
+ await new Promise((resolve) => setTimeout(resolve, 100));
+ await userEvent.tab();
+ await userEvent.keyboard("{Enter}");
+ await canvas.findByText("Bottom content button");
},
};
diff --git a/packages/lib/src/header/Header.test.tsx b/packages/lib/src/header/Header.test.tsx
index c280117d09..c9fbbf5fec 100644
--- a/packages/lib/src/header/Header.test.tsx
+++ b/packages/lib/src/header/Header.test.tsx
@@ -1,6 +1,13 @@
-import { fireEvent, render } from "@testing-library/react";
+import { render } from "@testing-library/react";
import DxcHeader from "./Header";
+const defaultHeaderBranding = {
+ logo: {
+ src: "url-to-dxc-logo",
+ alt: "DXC Logo",
+ },
+};
+
describe("Header component tests", () => {
beforeAll(() => {
Object.defineProperty(window, "matchMedia", {
@@ -11,38 +18,7 @@ describe("Header component tests", () => {
});
});
test("Header renders with default logo", () => {
- const { getByTitle } = render( );
- expect(getByTitle("DXC Logo")).toBeTruthy();
- });
- test("Call correct function on logo click", () => {
- const onClick = jest.fn();
- const { getByTitle } = render( );
- const logo = getByTitle("DXC Logo");
- fireEvent.click(logo);
- expect(onClick).toHaveBeenCalled();
- });
- test("Header renders with correct children", () => {
- // We need to force the offsetWidth value
- Object.defineProperty(HTMLElement.prototype, "offsetWidth", {
- configurable: true,
- value: 1024,
- });
-
- const { getByText } = render(header-child-text} />);
- expect(getByText("header-child-text")).toBeTruthy();
- });
- test("Header renders menu button in mobile", () => {
- Object.defineProperty(HTMLElement.prototype, "offsetWidth", {
- configurable: true,
- value: 425,
- });
- Object.defineProperty(window, "matchMedia", {
- writable: true,
- value: jest.fn().mockImplementation(() => ({
- matches: true,
- })),
- });
- const { getByText } = render( header-child-text
} />);
- expect(getByText("Menu")).toBeTruthy();
+ const { getByAltText } = render( );
+ expect(getByAltText("DXC Logo")).toBeTruthy();
});
});
diff --git a/packages/lib/src/header/Header.tsx b/packages/lib/src/header/Header.tsx
index 97b9c18bae..43e201c879 100644
--- a/packages/lib/src/header/Header.tsx
+++ b/packages/lib/src/header/Header.tsx
@@ -1,215 +1,139 @@
-import { ComponentProps, useContext, useEffect, useRef, useState } from "react";
-import { responsiveSizes, spaces } from "../common/variables";
-import DxcDropdown from "../dropdown/Dropdown";
-import DxcIcon from "../icon/Icon";
-import HeaderPropsType, { Logo } from "./types";
-import DxcFlex from "../flex/Flex";
-import { HalstackLanguageContext } from "../HalstackContext";
-import DxcActionIcon from "../action-icon/ActionIcon";
-import { dxcLogo } from "./Icons";
import styled from "@emotion/styled";
-
-const HeaderDropdown = styled.div`
- display: flex;
- button {
- background-color: transparent;
- :hover {
- background-color: transparent;
- }
- }
+import DxcGrid from "../grid/Grid";
+import HeaderProps from "./types";
+import DxcImage from "../image/Image";
+import DxcDivider from "../divider/Divider";
+import DxcHeading from "../heading/Heading";
+import { isGroupItem } from "../base-menu/utils";
+import { GroupItem, Item } from "../base-menu/types";
+import { useEffect, useMemo, useState } from "react";
+import DxcNavigationTree from "../navigation-tree/NavigationTree";
+import { responsiveSizes } from "../common/variables";
+import DxcButton from "../button/Button";
+
+const MAX_MAIN_NAV_SIZE = "60%";
+const LEVEL_LIMIT = 1;
+
+const MainContainer = styled.div<{ isResponsive: boolean; isMenuVisible: boolean }>`
+ display: grid;
+ width: 100%;
+ grid-template-rows: ${(props) =>
+ props.isResponsive && props.isMenuVisible
+ ? "var(--height-xxxl) calc(100vh - var(--height-xxxl))"
+ : "var(--height-xxxl)"};
+ ${(props) => (props.isResponsive && props.isMenuVisible ? "position: fixed;" : "")}
`;
-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;
+const HeaderContainer = styled.header`
+ width: 100%;
+ height: var(--height-xxxl);
box-sizing: border-box;
- display: flex;
- flex-direction: row;
- justify-content: space-between;
- margin-bottom: ${(props) => (props.margin ? spaces[props.margin] : "0px")};
- min-height: 64px;
- padding: var(--spacing-padding-none) var(--spacing-padding-l);
-`;
-
-const LogoAnchor = styled.a<{ interactive: boolean }>`
- ${(props) => (props.interactive ? "cursor: pointer" : "cursor: default; outline:none;")};
+ background: var(--color-bg-neutral-lightest);
+ box-shadow: var(--shadow-100);
+ padding: 0 var(--spacing-gap-l);
`;
-const LogoImg = styled.img`
- max-height: var(--height-xl);
- width: auto;
+const SideContainer = styled.div`
+ display: flex;
+ align-items: center;
+ height: 100%;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ width: 100%;
+ gap: var(--spacing-gap-m);
`;
-const LogoContainer = styled.div`
- max-height: var(--height-xl);
- vertical-align: middle;
- width: auto;
+const BrandingContainer = styled(SideContainer)`
+ justify-content: flex-start;
+ height: var(--height-m);
`;
-const ChildContainer = styled.div`
- display: flex;
- align-items: center;
+const RightSideContainer = styled(SideContainer)`
justify-content: flex-end;
- flex-grow: 1;
- width: calc(100% - 186px);
`;
-const ContentContainer = styled.div`
+const LogoContainer = styled.div`
display: flex;
align-items: center;
- flex-grow: 1;
- justify-content: flex-end;
- width: calc(100% - 186px);
- color: var(--color-fg-neutral-dark);
+ svg {
+ height: var(--height-m);
+ width: auto;
+ }
`;
-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;
+const MainNavContainer = styled.div`
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;
- padding: var(--spacing-padding-none) var(--spacing-padding-m);
- text-transform: uppercase;
- :hover {
- background-color: var(--color-bg-neutral-medium);
- }
- &:focus {
- outline: var(--border-color-secondary-medium) var(--border-style-default) var(--border-width-m);
- }
- & > svg {
- fill: var(--color-fg-neutral-dark);
- }
- & > span {
- font-size: var(--height-s);
- }
+ align-items: center;
+ width: 100%;
+ overflow: hidden;
+ white-space: nowrap;
+ gap: var(--spacing-gap-s);
`;
-const ResponsiveMenu = styled.div<{ hasVisibility: boolean }>`
- display: flex;
- flex-direction: column;
- background-color: var(--color-bg-neutral-lightest);
- position: fixed;
- top: 0;
- right: 0;
- z-index: var(--z-header-menu);
-
- @media (max-width: ${responsiveSizes.large}rem) and (min-width: ${responsiveSizes.small}rem) {
- width: 60vw;
- }
-
- @media (not((max-width: ${responsiveSizes.large}rem) and (min-width: ${responsiveSizes.small}rem))) {
- width: 100vw;
- }
-
- height: 100vh;
- padding: 20px;
- transform: ${(props) => (props.hasVisibility ? "translateX(0)" : "translateX(100vw)")};
- transition-property: transform, opacity;
- transition-duration: 0.6s;
- transition-timing-function: ease-in-out;
- box-sizing: border-box;
-`;
+const HamburguerButton = ({ onClick }: { onClick: () => void }) => {
+ return ;
+};
-const ResponsiveLogoContainer = styled.div`
- max-height: var(--height-xl);
- width: auto;
- display: flex;
+const ResponsiveMenuContainer = styled.div`
+ display: grid;
+ grid-template-rows: auto 1fr;
`;
-const MenuContent = styled.div`
+const ResponsiveMenu = styled.div`
+ width: 100%;
+ background-color: var(--color-bg-neutral-lightest);
display: flex;
+ padding: var(--spacing-padding-m);
+ box-sizing: border-box;
flex-direction: column;
- align-items: flex-start;
- height: 100%;
- color: var(--color-fg-neutral-dark);
+ gap: var(--spacing-gap-m);
`;
-const Overlay = styled.div<{ hasVisibility: boolean }>`
- position: fixed;
- top: 0;
- left: 0;
- width: 100vw;
- height: 100vh;
- background-color: ${(props) => (props.hasVisibility ? "var(--color-bg-alpha-medium)" : "transparent")};
-
- @media (max-width: ${responsiveSizes.small}rem) {
- ${(props) => !props.hasVisibility && "display: none"};
- }
-
+const Overlay = styled.div`
+ width: 100%;
+ height: 100%;
+ background-color: var(--color-bg-alpha-medium);
z-index: var(--z-header-overlay);
`;
-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"];
+/**
+ * Prepares the navigation items to be rendered in the header.
+ * Even though the typing does not allow this, the navigation tree does.
+ * So this function limits the levels of navigation to the limit by ignoring any nested group items over that limit.
+ * @param navItems prop with the navigation items.
+ * @param level current level of recursion.
+ * @return Processed navigation items with limited levels.
+ */
+const sanitizeNavItems = (navItems: HeaderProps["navItems"], level?: number): (GroupItem | Item)[] => {
+ if (!navItems) return [];
+ if (!level) level = 0;
+ const sanitizedItems = navItems.reduce<(GroupItem | Item)[]>((acc, item) => {
+ if (isGroupItem(item)) {
+ if (level < LEVEL_LIMIT) {
+ const processedGroup: GroupItem = {
+ ...item,
+ items: sanitizeNavItems(item.items, level + 1),
+ };
+ return [...acc, processedGroup];
+ }
+ } else {
+ return [...acc, item];
+ }
+ return acc;
+ }, []);
+ return sanitizedItems;
};
-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 DxcHeader = ({ branding, navItems, sideContent, responsiveBottomContent }: HeaderProps): 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 () => {
@@ -217,63 +141,97 @@ const DxcHeader = ({
};
}, []);
- useEffect(() => {
- if (!isResponsive) {
- setIsMenuVisible(false);
+ const toggleMenu = () => {
+ if (isResponsive && !isMenuVisible) {
+ setIsMenuVisible(!isMenuVisible);
+ } else {
+ setIsMenuVisible(!isMenuVisible);
}
- }, [isResponsive]);
+ };
+ const sanitizedNavItems = useMemo(() => (navItems ? sanitizeNavItems(navItems) : []), [navItems]);
return (
-
-
- {headerLogo}
-
- {isResponsive && responsiveContent && (
-
-
-
-
- {translatedLabels.header.hamburgerTitle}
-
-
-
-
- {headerLogo}
-
+
+ 0
+ ? [
+ `minmax(auto, calc((100% - ${MAX_MAIN_NAV_SIZE}) / 2))`,
+ `minmax(auto, ${MAX_MAIN_NAV_SIZE})`,
+ `minmax(auto, calc((100% - ${MAX_MAIN_NAV_SIZE}) / 2))`,
+ ]
+ : ["auto", "auto"]
+ }
+ templateRows={["var(--height-xxxl)"]}
+ gap="var(--spacing-gap-ml)"
+ placeItems="center"
+ >
+
+
+ {typeof branding.logo.src === "string" ? (
+
+ ) : (
+ branding.logo.src
+ )}
+
+ {branding.appTitle && !isResponsive && (
+ <>
+
+
+ >
+ )}
+
+ {!isResponsive && sanitizedNavItems && sanitizedNavItems.length > 0 && (
+
+
-
-
+ )}
+ {sideContent && (
+
+ {typeof sideContent === "function" ? sideContent(isResponsive) : sideContent}{" "}
+ {isResponsive && }
+
+ )}
+
+
+ {isResponsive && isMenuVisible && (
+
+
+ {branding.appTitle && }
+
+ {responsiveBottomContent && (
+ <>
+
+ {responsiveBottomContent}
+ >
+ )}
-
-
+
+
)}
- {!isResponsive && (
-
- )}
-
+
);
};
-DxcHeader.Dropdown = Dropdown;
-
export default DxcHeader;
diff --git a/packages/lib/src/header/types.ts b/packages/lib/src/header/types.ts
index 05857fa7f7..10f9da3497 100644
--- a/packages/lib/src/header/types.ts
+++ b/packages/lib/src/header/types.ts
@@ -1,54 +1,30 @@
import { ReactNode } from "react";
-import { Space } from "../common/utils";
+import { CommonItemProps, Item } from "../base-menu/types";
+import { SVG } from "../common/utils";
-export type Logo = {
- /**
- * URL to navigate when the logo is clicked.
- */
+type LogoPropsType = {
+ src: string | SVG;
+ alt: string;
href?: string;
- /**
- * Source of the logo image.
- */
- src: string;
- /**
- * Alternative text for the logo image.
- */
- title?: string;
+ onClick?: () => void;
+};
+
+type BrandingPropsType = {
+ logo: LogoPropsType;
+ appTitle?: string;
};
+type GroupItem = CommonItemProps & {
+ items: Item[];
+};
+
+type MainNavPropsType = (GroupItem | Item)[];
+
type Props = {
- /**
- * Whether a contrast line should appear at the bottom of the header.
- */
- underlined?: boolean;
- /**
- * Content shown in the header. Take into account that the component applies styles
- * for the first child in the content, so we recommend the use of Fragment
- * to be applied correctly. Otherwise, the styles can be modified.
- */
- content?: 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.
- */
- responsiveContent?: (closeHandler: () => void) => ReactNode;
- /**
- * Logo to be displayed inside the header
- */
- 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.
- */
- tabIndex?: number;
+ branding: BrandingPropsType;
+ navItems?: MainNavPropsType;
+ responsiveBottomContent?: ReactNode;
+ sideContent?: ReactNode | ((isResponsive: boolean) => ReactNode);
};
export default Props;
diff --git a/packages/lib/src/layout/ApplicationLayout.tsx b/packages/lib/src/layout/ApplicationLayout.tsx
index 72808a6085..890c5b5c7a 100644
--- a/packages/lib/src/layout/ApplicationLayout.tsx
+++ b/packages/lib/src/layout/ApplicationLayout.tsx
@@ -6,11 +6,11 @@ import DxcSidenav from "../sidenav/Sidenav";
import ApplicationLayoutPropsType, { AppLayoutMainPropsType } from "./types";
import { bottomLinks, findChildType, socialLinks, year } from "./utils";
-const ApplicationLayoutContainer = styled.div`
+const ApplicationLayoutContainer = styled.div<{ header?: React.ReactNode }>`
top: 0;
left: 0;
display: grid;
- grid-template-rows: auto 1fr;
+ grid-template-rows: ${({ header }) => (header ? "auto 1fr" : "1fr")};
height: 100vh;
width: 100vw;
position: absolute;
@@ -19,6 +19,7 @@ const ApplicationLayoutContainer = styled.div`
const HeaderContainer = styled.div`
width: 100%;
+ min-height: var(--height-xxxl);
height: fit-content;
z-index: var(--z-app-layout-header);
`;
@@ -66,8 +67,8 @@ const DxcApplicationLayout = ({ header, sidenav, footer, children }: Application
const ref = useRef(null);
return (
-
- {header ?? }
+
+ {header && {header} }
{sidenav && {sidenav} }
diff --git a/packages/lib/src/layout/Icons.tsx b/packages/lib/src/layout/Icons.tsx
index 69228f92c0..93c6c2c2a6 100644
--- a/packages/lib/src/layout/Icons.tsx
+++ b/packages/lib/src/layout/Icons.tsx
@@ -59,4 +59,24 @@ const layoutIcons = {
),
};
+export const dxcLogo = (
+
+ DXC Logo
+
+
+
+
+
+
+
+);
+
export default layoutIcons;
diff --git a/packages/lib/src/navigation-tree/NavigationTree.tsx b/packages/lib/src/navigation-tree/NavigationTree.tsx
index 49c248dd05..5f905ac450 100644
--- a/packages/lib/src/navigation-tree/NavigationTree.tsx
+++ b/packages/lib/src/navigation-tree/NavigationTree.tsx
@@ -33,7 +33,8 @@ export default function DxcNavigationTree({
displayBorder = true,
displayGroupLines = false,
displayControlsAfter = false,
- responsiveView = false,
+ hasPopOver = false,
+ isHorizontal = false,
}: NavigationTreePropsType) {
const [firstUpdate, setFirstUpdate] = useState(true);
const [selectedItemId, setSelectedItemId] = useState(-1);
@@ -45,9 +46,10 @@ export default function DxcNavigationTree({
setSelectedItemId,
displayGroupLines,
displayControlsAfter,
- responsiveView,
+ hasPopOver,
+ isHorizontal,
}),
- [selectedItemId, setSelectedItemId, displayGroupLines, displayControlsAfter, responsiveView]
+ [selectedItemId, setSelectedItemId, displayGroupLines, displayControlsAfter, hasPopOver, isHorizontal]
);
useLayoutEffect(() => {
@@ -71,7 +73,7 @@ export default function DxcNavigationTree({
))
) : (
-
+
{(itemsWithId as (GroupItemWithId | ItemWithId)[]).map((item, index) => (
))}
diff --git a/packages/lib/src/sidenav/Sidenav.tsx b/packages/lib/src/sidenav/Sidenav.tsx
index 8581d5c862..20f3705ebc 100644
--- a/packages/lib/src/sidenav/Sidenav.tsx
+++ b/packages/lib/src/sidenav/Sidenav.tsx
@@ -109,7 +109,7 @@ const DxcSidenav = ({
items={navItems}
displayGroupLines={displayGroupLines}
displayBorder={false}
- responsiveView={!isExpanded}
+ hasPopOver={!isExpanded}
displayControlsAfter
/>
)}
diff --git a/packages/lib/src/styles/variables.css b/packages/lib/src/styles/variables.css
index 53b0b52dc2..cf4919b5d4 100644
--- a/packages/lib/src/styles/variables.css
+++ b/packages/lib/src/styles/variables.css
@@ -4,8 +4,8 @@
/**************/
/* Application Layout */
- --z-app-layout-header: 100;
- --z-app-layout-sidenav: 110;
+ --z-app-layout-sidenav: 100;
+ --z-app-layout-header: 110;
/* Header */
--z-header-overlay: 200;