diff --git a/apps/website/pages/_app.tsx b/apps/website/pages/_app.tsx index 928f4ec85..2603ad349 100644 --- a/apps/website/pages/_app.tsx +++ b/apps/website/pages/_app.tsx @@ -88,9 +88,7 @@ export default function App({ Component, pageProps, emotionCache = clientSideEmo const navItems: Section[] = useMemo(() => { return LinksSections.map((section) => { const baseItems = normalizeNavTabs(section.links); - const items = filter ? filterNavTree(baseItems, filter.trim().toLowerCase()) : baseItems; - return { title: section.label, items, @@ -104,11 +102,12 @@ export default function App({ Component, pageProps, emotionCache = clientSideEmo } + logo={{ src: dxcLogo, alt: "DXC Technology" }} + header={} sidenav={ } + appTitle={isExpanded && } topContent={ isExpanded && ( void; +}`; + const ApplicationLayoutPropsTable = () => ( @@ -61,6 +68,22 @@ const ApplicationLayoutPropsTable = () => ( - + + + + + logo + + + + {logoTypeString} + + + Object used to configure the header logo. The logo will be placed in the header, but if no global app header + exists, it will be shown in the sidenav instead. + + - + sidenav diff --git a/apps/website/screens/components/header/code/HeaderCodePage.tsx b/apps/website/screens/components/header/code/HeaderCodePage.tsx index ef737346e..6f330593b 100644 --- a/apps/website/screens/components/header/code/HeaderCodePage.tsx +++ b/apps/website/screens/components/header/code/HeaderCodePage.tsx @@ -2,14 +2,6 @@ import { DxcFlex, DxcTable } from "@dxc-technology/halstack-react"; import DocFooter from "@/common/DocFooter"; import QuickNavContainer from "@/common/QuickNavContainer"; import Code, { ExtendedTableCode, TableCode } from "@/common/Code"; -import StatusBadge from "@/common/StatusBadge"; - -const logoTypeString = `{ - src: string | SVG; - alt: string; - href?: string; - onClick?: () => void; -}`; const navItemsTypeString = `(GroupItem | Item)[]`; @@ -50,19 +42,6 @@ const sections = [ Object used to configure the header application title. - - - - - - logo - - - - {logoTypeString} - - Object used to configure the header logo. - - - navItems diff --git a/apps/website/screens/components/header/overview/HeaderOverviewPage.tsx b/apps/website/screens/components/header/overview/HeaderOverviewPage.tsx index 443815f60..322c1bd60 100644 --- a/apps/website/screens/components/header/overview/HeaderOverviewPage.tsx +++ b/apps/website/screens/components/header/overview/HeaderOverviewPage.tsx @@ -37,7 +37,7 @@ const sections = [ {/* It is also where the searchbar will be displayed once that feature is triggered. */} - Right slot: it's the place where all utilities related to an application are hosted. It’s + Right slot: it's the place where all utilities related to an application are hosted. It's highly adaptable for each product's needs. diff --git a/apps/website/screens/components/sidenav/code/SidenavCodePage.tsx b/apps/website/screens/components/sidenav/code/SidenavCodePage.tsx index 9237e9723..8eb283ba6 100644 --- a/apps/website/screens/components/sidenav/code/SidenavCodePage.tsx +++ b/apps/website/screens/components/sidenav/code/SidenavCodePage.tsx @@ -4,18 +4,6 @@ import Code, { ExtendedTableCode, TableCode } from "@/common/Code"; import { DxcFlex, DxcTable } from "@dxc-technology/halstack-react"; import StatusBadge from "@/common/StatusBadge"; -const brandingTypeString = `{ - logo?: Logo; - appTitle?: string; -}`; - -const logoTypeString = `{ - alt: string; - href?: string; - onClick?: (event: MouseEvent) => void; - src: string; -}`; - const commonItemTypeString = `badge?: ReactElement; icon?: string | SVG; label: string;`; @@ -63,27 +51,6 @@ const sections = [ The content rendered in the bottom part of the sidenav, under the navigation menu. - - - - - - branding - - - - {"Branding | ReactNode"} -

- being Branding an object with the following properties: -

- {brandingTypeString} -

- and Logo an object with the following properties: -

- {logoTypeString} - - Object with the properties of the branding placed at the top of the sidenav. - - - diff --git a/apps/website/screens/components/sidenav/overview/SidenavOverviewPage.tsx b/apps/website/screens/components/sidenav/overview/SidenavOverviewPage.tsx index 1c0c7c819..36675aa7b 100644 --- a/apps/website/screens/components/sidenav/overview/SidenavOverviewPage.tsx +++ b/apps/website/screens/components/sidenav/overview/SidenavOverviewPage.tsx @@ -40,8 +40,7 @@ const sections = [ Contains collapse/expand toggle for the sidenav. - If no global app header exists, this area should be used for branding (logo, - product/app name). + If no global app header exists, this area will be used for logo and application name. diff --git a/packages/lib/src/footer/Footer.stories.tsx b/packages/lib/src/footer/Footer.stories.tsx index 003a178fb..73b52fb5a 100644 --- a/packages/lib/src/footer/Footer.stories.tsx +++ b/packages/lib/src/footer/Footer.stories.tsx @@ -468,9 +468,9 @@ const items = [ const FooterInLayout = () => ( isResponsive ? ( @@ -546,9 +546,9 @@ const FooterInLayout = () => ( const ReducedFooterInLayout = () => ( isResponsive ? ( diff --git a/packages/lib/src/header/Header.accessibility.test.tsx b/packages/lib/src/header/Header.accessibility.test.tsx index 5dd429bb0..e337488f2 100644 --- a/packages/lib/src/header/Header.accessibility.test.tsx +++ b/packages/lib/src/header/Header.accessibility.test.tsx @@ -16,20 +16,6 @@ const disabledRules = { rules: formatRules(rules), }; -const iconSVG = ( - - - - -); - -const iconUrl = "https://iconape.com/wp-content/files/yd/367773/svg/logo-linkedin-logo-icon-png-svg.png"; - -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"; @@ -71,7 +57,6 @@ describe("Header component accessibility tests", () => { it("Should not have basic accessibility issues", async () => { const { container } = render( ); -const logo = { - src: "https://picsum.photos/id/1000/104/34", - alt: "DXC Logo", - href: "https://www.dxc.com", -}; - const dxcBrandedLogo = { src: dxcLogo, alt: "DXC Logo", @@ -73,12 +67,6 @@ const dxcBrandedLogo = { 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.`; const items = [ @@ -122,30 +110,26 @@ const longItems = [ const Header = () => ( - <DxcHeader logo={logo} /> - <DxcHeader logo={logo} /> - <DxcHeader logo={logo} sideContent={<div>Side Content</div>} /> - <DxcHeader logo={logo} navItems={items} sideContent={<div>Side Content</div>} /> + <DxcHeader /> + <DxcHeader /> + <DxcHeader sideContent={<div>Side Content</div>} /> + <DxcHeader navItems={items} sideContent={<div>Side Content</div>} /> <Title title="Header with long content" theme="light" level={3} /> - <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>} - /> + <DxcHeader navItems={items} sideContent={<div>{longSideContent}</div>} /> + <DxcHeader appTitle={longAppTitle} navItems={items} /> + <DxcHeader appTitle={longAppTitle} navItems={items} sideContent={<div>{longSideContent}</div>} /> + <DxcHeader appTitle={longAppTitle} sideContent={<div>{longSideContent}</div>} /> + <DxcHeader appTitle={longAppTitle} navItems={longItems} /> + <DxcHeader appTitle={longAppTitle} navItems={longItems} sideContent={<div>{longSideContent}</div>} /> </DxcFlex> ); const HeaderInLayout = () => ( <DxcApplicationLayout + logo={dxcBrandedLogo} header={ - <DxcHeader - logo={dxcBrandedLogo} + <DxcApplicationLayout.Header + appTitle="Application Layout with Header" navItems={items} sideContent={(isResponsive) => isResponsive ? ( diff --git a/packages/lib/src/header/Header.test.tsx b/packages/lib/src/header/Header.test.tsx index e835df1f2..a1546a6d4 100644 --- a/packages/lib/src/header/Header.test.tsx +++ b/packages/lib/src/header/Header.test.tsx @@ -3,11 +3,6 @@ import "@testing-library/jest-dom"; import DxcHeader from "./Header"; import { Item, GroupItem } from "../base-menu/types"; -const defaultHeaderBranding = { - src: "url-to-dxc-logo", - alt: "DXC Logo", -}; - describe("Header component tests", () => { const mockMatchMedia = jest.fn(); @@ -26,11 +21,6 @@ describe("Header component tests", () => { })); }); - test("Header renders with default logo", () => { - const { getByAltText } = render(<DxcHeader logo={defaultHeaderBranding} />); - expect(getByAltText("DXC Logo")).toBeTruthy(); - }); - test("hamburger button triggers onClick when clicked", () => { mockMatchMedia.mockImplementation(() => ({ matches: true, @@ -39,7 +29,7 @@ describe("Header component tests", () => { })); const navItems: Item[] = [{ label: "Home", onSelect: jest.fn() }]; - render(<DxcHeader logo={defaultHeaderBranding} navItems={navItems} />); + render(<DxcHeader navItems={navItems} />); const menuButton = screen.getByRole("button", { name: /toggle menu/i }); fireEvent.click(menuButton); @@ -63,7 +53,7 @@ describe("Header component tests", () => { }, ]; - render(<DxcHeader logo={defaultHeaderBranding} navItems={deepNestedItems} />); + render(<DxcHeader navItems={deepNestedItems} />); expect(screen.getByText("Services")).toBeInTheDocument(); expect(screen.queryByText("Development")).not.toBeInTheDocument(); expect(screen.queryByText("Frontend")).not.toBeInTheDocument(); diff --git a/packages/lib/src/header/Header.tsx b/packages/lib/src/header/Header.tsx index 72c8151bc..d175649c1 100644 --- a/packages/lib/src/header/Header.tsx +++ b/packages/lib/src/header/Header.tsx @@ -6,11 +6,12 @@ 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 { useContext, 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"; +import ApplicationLayoutContext from "../layout/ApplicationLayoutContext"; const MAX_MAIN_NAV_SIZE = "60%"; const LEVEL_LIMIT = 1; @@ -54,10 +55,10 @@ const RightSideContainer = styled(SideContainer)` justify-content: flex-end; `; -const LogoContainer = styled.div<{ isClickable: boolean }>` +const LogoContainer = styled.div<{ hasAction?: boolean; href?: string }>` display: flex; align-items: center; - cursor: ${(props) => (props.isClickable ? "pointer" : "default")}; + cursor: ${(props) => (props.hasAction ? "pointer" : "default")}; svg { height: var(--height-m); width: auto; @@ -131,9 +132,10 @@ const sanitizeNavItems = (navItems: HeaderProps["navItems"], level?: number): (G return sanitizedItems; }; -const DxcHeader = ({ logo, appTitle, navItems, sideContent, responsiveBottomContent }: HeaderProps): JSX.Element => { +const DxcHeader = ({ appTitle, navItems, sideContent, responsiveBottomContent }: HeaderProps): JSX.Element => { const [isResponsive, setIsResponsive] = useState(false); const [isMenuVisible, setIsMenuVisible] = useState(false); + const logo = useContext(ApplicationLayoutContext).logo || undefined; useEffect(() => { const handleResize = () => { @@ -161,11 +163,7 @@ const DxcHeader = ({ logo, appTitle, navItems, sideContent, responsiveBottomCont <DxcGrid templateColumns={ !isResponsive && sanitizedNavItems && sanitizedNavItems.length > 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`, `minmax(auto, ${MAX_MAIN_NAV_SIZE})`, `auto`] : ["auto", "auto"] } templateRows={["var(--height-xxxl)"]} @@ -173,21 +171,24 @@ const DxcHeader = ({ logo, appTitle, navItems, sideContent, responsiveBottomCont placeItems="center" > <BrandingContainer> - <LogoContainer - role={logo.onClick ? "button" : undefined} - onClick={typeof logo.onClick === "function" ? logo.onClick : undefined} - as={logo.href ? "a" : undefined} - isClickable={!!(logo.onClick || logo.href)} - > - {typeof logo.src === "string" ? ( - <DxcImage src={logo.src} alt={logo.alt} height="var(--height-m)" objectFit="contain" /> - ) : ( - logo.src - )} - </LogoContainer> + {logo && ( + <LogoContainer + role={logo.onClick ? "button" : undefined} + onClick={typeof logo.onClick === "function" ? logo.onClick : undefined} + as={logo.href ? "a" : undefined} + href={logo.href} + hasAction={!!(logo.onClick || logo.href)} + > + {typeof logo.src === "string" ? ( + <DxcImage src={logo.src} alt={logo.alt} height="var(--height-m)" objectFit="contain" /> + ) : ( + logo.src + )} + </LogoContainer> + )} {appTitle && !isResponsive && ( <> - <DxcDivider orientation="vertical" /> + {logo && <DxcDivider orientation="vertical" />} <DxcHeading text={appTitle} as="h1" level={5} /> </> )} diff --git a/packages/lib/src/header/types.ts b/packages/lib/src/header/types.ts index 3c053a81d..472004b61 100644 --- a/packages/lib/src/header/types.ts +++ b/packages/lib/src/header/types.ts @@ -1,13 +1,5 @@ import { ReactNode } from "react"; import { CommonItemProps, Item } from "../base-menu/types"; -import { SVG } from "../common/utils"; - -type LogoPropsType = { - src: string | SVG; - alt: string; - href?: string; - onClick?: () => void; -}; type GroupItem = CommonItemProps & { items: Item[]; @@ -16,7 +8,6 @@ type GroupItem = CommonItemProps & { type MainNavPropsType = (GroupItem | Item)[]; type Props = { - logo: LogoPropsType; appTitle?: string; navItems?: MainNavPropsType; responsiveBottomContent?: ReactNode; diff --git a/packages/lib/src/layout/ApplicationLayout.stories.tsx b/packages/lib/src/layout/ApplicationLayout.stories.tsx index 446b2f508..4f1fa5b29 100644 --- a/packages/lib/src/layout/ApplicationLayout.stories.tsx +++ b/packages/lib/src/layout/ApplicationLayout.stories.tsx @@ -8,9 +8,35 @@ export default { component: DxcApplicationLayout, } satisfies Meta<typeof DxcApplicationLayout>; +const dxcLogo = ( + <svg xmlns="http://www.w3.org/2000/svg" width="73" height="40" viewBox="0 0 73 40"> + <title>DXC Logo + + + + + + + +); + +const logo = { + src: dxcLogo, + alt: "DXC Logo", + href: "https://www.dxc.com", +}; + const ApplicationLayout = () => ( <> - + }> <p>Main Content</p> @@ -47,14 +73,7 @@ const items = [ const ApplicationLayoutDefaultSidenav = () => ( <> - <DxcApplicationLayout - sidenav={ - <DxcApplicationLayout.Sidenav - branding={{ appTitle: "Application layout with push sidenav" }} - navItems={items} - /> - } - > + <DxcApplicationLayout logo={logo} sidenav={<DxcApplicationLayout.Sidenav navItems={items} />}> <DxcApplicationLayout.Main> <p>Main Content</p> <p>Main Content</p> @@ -68,9 +87,10 @@ const ApplicationLayoutDefaultSidenav = () => ( const ApplicationLayoutResponsiveSidenav = () => ( <> <DxcApplicationLayout + logo={logo} sidenav={ <DxcApplicationLayout.Sidenav - branding={{ appTitle: "Application layout with push sidenav" }} + appTitle="Application layout with push sidenav" navItems={items} defaultExpanded={false} /> @@ -89,13 +109,9 @@ const ApplicationLayoutResponsiveSidenav = () => ( const ApplicationLayoutCustomHeader = () => ( <> <DxcApplicationLayout + logo={logo} header={<p>Custom Header</p>} - sidenav={ - <DxcApplicationLayout.Sidenav - branding={{ appTitle: "Application layout with push sidenav" }} - navItems={items} - /> - } + sidenav={<DxcApplicationLayout.Sidenav appTitle="Application layout with push sidenav" navItems={items} />} > <DxcApplicationLayout.Main> <p>Main Content</p> @@ -110,13 +126,10 @@ const ApplicationLayoutCustomHeader = () => ( const ApplicationLayoutCustomFooter = () => ( <> <DxcApplicationLayout + logo={logo} footer={<p>Custom Footer</p>} - sidenav={ - <DxcApplicationLayout.Sidenav - branding={{ appTitle: "Application layout with push sidenav" }} - navItems={items} - /> - } + header={<DxcApplicationLayout.Header appTitle="Application layout with custom footer" />} + sidenav={<DxcApplicationLayout.Sidenav appTitle="Application layout with push sidenav" navItems={items} />} > <DxcApplicationLayout.Main> <p>Main Content</p> @@ -130,9 +143,8 @@ const ApplicationLayoutCustomFooter = () => ( const Tooltip = () => ( <DxcApplicationLayout - sidenav={ - <DxcApplicationLayout.Sidenav branding={{ appTitle: "Application layout with push sidenav" }} navItems={items} /> - } + logo={logo} + sidenav={<DxcApplicationLayout.Sidenav appTitle="Application layout with push sidenav" navItems={items} />} > <DxcApplicationLayout.Main> <p>Main Content</p> diff --git a/packages/lib/src/layout/ApplicationLayout.tsx b/packages/lib/src/layout/ApplicationLayout.tsx index e65c8aa74..16b55ab42 100644 --- a/packages/lib/src/layout/ApplicationLayout.tsx +++ b/packages/lib/src/layout/ApplicationLayout.tsx @@ -1,10 +1,11 @@ -import { useRef } from "react"; +import { useMemo, useRef } from "react"; import styled from "@emotion/styled"; import DxcFooter from "../footer/Footer"; import DxcHeader from "../header/Header"; import DxcSidenav from "../sidenav/Sidenav"; import ApplicationLayoutPropsType, { AppLayoutMainPropsType } from "./types"; import { bottomLinks, findChildType, socialLinks, year } from "./utils"; +import ApplicationLayoutContext from "./ApplicationLayoutContext"; const ApplicationLayoutContainer = styled.div<{ header?: React.ReactNode }>` top: 0; @@ -62,29 +63,37 @@ const MainContentContainer = styled.main` const Main = ({ children }: AppLayoutMainPropsType): JSX.Element => <div>{children}</div>; -const DxcApplicationLayout = ({ header, sidenav, footer, children }: ApplicationLayoutPropsType): JSX.Element => { +const DxcApplicationLayout = ({ logo, header, sidenav, footer, children }: ApplicationLayoutPropsType): JSX.Element => { + const contextValue = useMemo(() => { + return { + logo, + headerExists: !!header, + }; + }, [header, logo]); const ref = useRef(null); return ( <ApplicationLayoutContainer ref={ref} header={header}> - {header && <HeaderContainer>{header}</HeaderContainer>} - <BodyContainer hasSidenav={!!sidenav}> - {sidenav && <SidenavContainer>{sidenav}</SidenavContainer>} - <MainContainer> - <MainContentContainer> - {findChildType(children, Main)} - <FooterContainer> - {footer ?? ( - <DxcFooter - copyright={`© DXC Technology ${year}. All rights reserved.`} - bottomLinks={bottomLinks} - socialLinks={socialLinks} - /> - )} - </FooterContainer> - </MainContentContainer> - </MainContainer> - </BodyContainer> + <ApplicationLayoutContext.Provider value={contextValue}> + {header && <HeaderContainer>{header}</HeaderContainer>} + <BodyContainer hasSidenav={!!sidenav}> + {sidenav && <SidenavContainer>{sidenav}</SidenavContainer>} + <MainContainer> + <MainContentContainer> + {findChildType(children, Main)} + <FooterContainer> + {footer ?? ( + <DxcFooter + copyright={`© DXC Technology ${year}. All rights reserved.`} + bottomLinks={bottomLinks} + socialLinks={socialLinks} + /> + )} + </FooterContainer> + </MainContentContainer> + </MainContainer> + </BodyContainer> + </ApplicationLayoutContext.Provider> </ApplicationLayoutContainer> ); }; diff --git a/packages/lib/src/layout/ApplicationLayoutContext.tsx b/packages/lib/src/layout/ApplicationLayoutContext.tsx new file mode 100644 index 000000000..bbcf0edf9 --- /dev/null +++ b/packages/lib/src/layout/ApplicationLayoutContext.tsx @@ -0,0 +1,7 @@ +import { createContext } from "react"; +import { ApplicationLayoutContextType } from "./types"; + +export default createContext<ApplicationLayoutContextType>({ + logo: undefined, + headerExists: false, +}); diff --git a/packages/lib/src/layout/types.ts b/packages/lib/src/layout/types.ts index a00cddee6..fed8502e0 100644 --- a/packages/lib/src/layout/types.ts +++ b/packages/lib/src/layout/types.ts @@ -1,4 +1,5 @@ import { ReactElement, ReactNode } from "react"; +import { SVG } from "../common/utils"; export type AppLayoutMainPropsType = { /** @@ -7,6 +8,25 @@ export type AppLayoutMainPropsType = { children: ReactNode; }; +type LogoPropsType = { + /** + * Alternative text for the logo image. + */ + alt: string; + /** + * URL to navigate when the logo is clicked. + */ + href?: string; + /** + * Function to be called on logo click. + */ + onClick?: () => void; + /** + * URL or SVG of the image that will be placed in the logo. + */ + src: string | SVG; +}; + export type AppLayoutSidenavPropsType = { /** * The area inside the sidenav. This area can be used to render the content inside the sidenav. @@ -18,7 +38,22 @@ export type AppLayoutSidenavPropsType = { title?: ReactNode; }; +export type ApplicationLayoutContextType = { + /** + * Logo properties. + */ + logo?: LogoPropsType; + /** + * Indicates if the header exists. + */ + headerExists: boolean; +}; + type ApplicationLayoutPropsType = { + /** + * Logo properties. + */ + logo?: LogoPropsType; /** * Header content. */ diff --git a/packages/lib/src/sidenav/Sidenav.accessibility.test.tsx b/packages/lib/src/sidenav/Sidenav.accessibility.test.tsx index a12a6dc69..157d20d52 100644 --- a/packages/lib/src/sidenav/Sidenav.accessibility.test.tsx +++ b/packages/lib/src/sidenav/Sidenav.accessibility.test.tsx @@ -47,18 +47,7 @@ describe("Sidenav component accessibility tests", () => { ], }, ]; - const { container } = render( - <DxcSidenav - navItems={groupItems} - branding={{ - appTitle: "Application Name", - logo: { - src: "https://picsum.photos/id/1022/200/300", - alt: "Alt text", - }, - }} - /> - ); + const { container } = render(<DxcSidenav navItems={groupItems} appTitle="Application Name" />); const results = await axe(container); expect(results.violations).toHaveLength(0); }); diff --git a/packages/lib/src/sidenav/Sidenav.stories.tsx b/packages/lib/src/sidenav/Sidenav.stories.tsx index 6efff792e..8c47a0845 100644 --- a/packages/lib/src/sidenav/Sidenav.stories.tsx +++ b/packages/lib/src/sidenav/Sidenav.stories.tsx @@ -139,13 +139,7 @@ const Sidenav = () => ( <Title title="Default sidenav" theme="light" level={4} /> <DxcSidenav navItems={groupItems} - branding={{ - appTitle: "Application Name", - logo: { - src: "https://images.ctfassets.net/hrltx12pl8hq/5596z2BCR9KmT1KeRBrOQa/4070fd4e2f1a13f71c2c46afeb18e41c/shutterstock_451077043-hero1.jpg", - alt: "TEST", - }, - }} + appTitle="Application Name" bottomContent={ <> <DetailedAvatar /> @@ -174,13 +168,7 @@ const Sidenav = () => ( <Title title="Sidenav with group lines" theme="light" level={4} /> <DxcSidenav navItems={groupItems} - branding={{ - appTitle: "Application Name", - logo: { - src: "https://images.ctfassets.net/hrltx12pl8hq/5596z2BCR9KmT1KeRBrOQa/4070fd4e2f1a13f71c2c46afeb18e41c/shutterstock_451077043-hero1.jpg", - alt: "TEST", - }, - }} + appTitle="Application Name" bottomContent={ <> <DetailedAvatar /> @@ -219,7 +207,7 @@ const Collapsed = () => { <Title title="Collapsed sidenav" theme="light" level={4} /> <DxcSidenav navItems={groupItems} - branding={{ appTitle: "App Name" }} + appTitle="App Name" bottomContent={ isExpanded ? ( <> @@ -272,7 +260,7 @@ const Collapsed = () => { <Title title="Collapsed sidenav with groups expanded (no lines)" theme="light" level={4} /> <DxcSidenav navItems={groupItems} - branding={{ appTitle: "App Name" }} + appTitle="App Name" bottomContent={ isExpandedGroupsNoLines ? ( <> @@ -325,7 +313,7 @@ const Collapsed = () => { <Title title="Collapsed sidenav with groups expanded (lines)" theme="light" level={4} /> <DxcSidenav navItems={groupItems} - branding={{ appTitle: "App Name" }} + appTitle="App Name" bottomContent={ isExpandedGroups ? ( <> @@ -384,13 +372,7 @@ const Hovered = () => ( <Title title="Hover state for groups" theme="light" level={4} /> <DxcSidenav navItems={groupItems} - branding={{ - appTitle: "Application Name", - logo: { - src: "https://images.ctfassets.net/hrltx12pl8hq/5596z2BCR9KmT1KeRBrOQa/4070fd4e2f1a13f71c2c46afeb18e41c/shutterstock_451077043-hero1.jpg", - alt: "TEST", - }, - }} + appTitle="Application Name" bottomContent={ <> <DetailedAvatar /> @@ -422,13 +404,7 @@ const SelectedGroup = () => ( <Title title="Default sidenav" theme="light" level={4} /> <DxcSidenav navItems={selectedGroupItems} - branding={{ - appTitle: "Application Name", - logo: { - src: "https://images.ctfassets.net/hrltx12pl8hq/5596z2BCR9KmT1KeRBrOQa/4070fd4e2f1a13f71c2c46afeb18e41c/shutterstock_451077043-hero1.jpg", - alt: "TEST", - }, - }} + appTitle="Application name" bottomContent={ <> <DetailedAvatar /> diff --git a/packages/lib/src/sidenav/Sidenav.test.tsx b/packages/lib/src/sidenav/Sidenav.test.tsx index 4bf1b2e2e..eb0edfc1c 100644 --- a/packages/lib/src/sidenav/Sidenav.test.tsx +++ b/packages/lib/src/sidenav/Sidenav.test.tsx @@ -17,7 +17,7 @@ describe("DxcSidenav component", () => { test("Sidenav renders title and children correctly", () => { const { getByText, getByRole } = render( <DxcSidenav - branding={{ appTitle: "Main Menu" }} + appTitle="Main Menu" topContent={<p>Custom top content</p>} bottomContent={<p>Custom bottom content</p>} /> @@ -31,7 +31,7 @@ describe("DxcSidenav component", () => { }); test("Sidenav collapses and expands correctly on button click", () => { - const { getByRole } = render(<DxcSidenav branding={{ appTitle: "Main Menu" }} />); + const { getByRole } = render(<DxcSidenav appTitle="Main Menu" />); const collapseButton = getByRole("button", { name: "Collapse" }); expect(collapseButton).toBeTruthy(); @@ -41,15 +41,6 @@ describe("DxcSidenav component", () => { fireEvent.click(expandButton); }); - test("Sidenav renders logo correctly when provided", () => { - const logo = { src: "logo.png", alt: "Company Logo", href: "https://example.com" }; - const { getByRole, getByAltText } = render(<DxcSidenav branding={{ appTitle: "App", logo: logo }} />); - - const link = getByRole("link"); - expect(link).toHaveAttribute("href", "https://example.com"); - expect(getByAltText("Company Logo")).toBeTruthy(); - }); - test("Sidenav renders contextual menu with items", () => { const items = [{ label: "Dashboard" }, { label: "Settings" }]; const { getByText } = render(<DxcSidenav navItems={items} />); @@ -87,7 +78,7 @@ describe("DxcSidenav component", () => { test("Sidenav uses controlled expanded prop instead of internal state", () => { const onExpandedChange = jest.fn(); const { getByRole, rerender } = render( - <DxcSidenav branding={{ appTitle: "Controlled Menu" }} expanded={false} onExpandedChange={onExpandedChange} /> + <DxcSidenav appTitle="Controlled Menu" expanded={false} onExpandedChange={onExpandedChange} /> ); const expandButton = getByRole("button", { name: "Expand" }); @@ -96,16 +87,14 @@ describe("DxcSidenav component", () => { fireEvent.click(expandButton); expect(onExpandedChange).toHaveBeenCalledWith(true); - rerender( - <DxcSidenav branding={{ appTitle: "Controlled Menu" }} expanded={true} onExpandedChange={onExpandedChange} /> - ); + rerender(<DxcSidenav appTitle="Controlled Menu" expanded={true} onExpandedChange={onExpandedChange} />); const collapseButton = getByRole("button", { name: "Collapse" }); expect(collapseButton).toBeTruthy(); }); test("Sidenav toggles internal state correctly", () => { - const { getByRole } = render(<DxcSidenav branding={{ appTitle: "App" }} defaultExpanded={false} />); + const { getByRole } = render(<DxcSidenav appTitle="App" defaultExpanded={false} />); const expandButton = getByRole("button", { name: "Expand" }); expect(expandButton).toBeTruthy(); diff --git a/packages/lib/src/sidenav/Sidenav.tsx b/packages/lib/src/sidenav/Sidenav.tsx index 5ecdf675b..8d2292058 100644 --- a/packages/lib/src/sidenav/Sidenav.tsx +++ b/packages/lib/src/sidenav/Sidenav.tsx @@ -1,13 +1,14 @@ import styled from "@emotion/styled"; import { responsiveSizes } from "../common/variables"; import DxcFlex from "../flex/Flex"; -import SidenavPropsType, { Logo } from "./types"; +import SidenavPropsType from "./types"; import DxcDivider from "../divider/Divider"; import DxcButton from "../button/Button"; import DxcImage from "../image/Image"; -import { useState } from "react"; +import { useContext, useState } from "react"; import DxcNavigationTree from "../navigation-tree/NavigationTree"; import DxcInset from "../inset/Inset"; +import ApplicationLayoutContext from "../layout/ApplicationLayoutContext"; const SidenavContainer = styled.div<{ expanded: boolean }>` box-sizing: border-box; @@ -43,26 +44,32 @@ const SidenavTitle = styled.div` const LogoContainer = styled.div<{ hasAction?: boolean; - href?: Logo["href"]; + href?: string; }>` position: relative; display: flex; justify-content: center; align-items: center; text-decoration: none; + cursor: ${(props) => (props.hasAction ? "pointer" : "default")}; + svg { + max-width: 100%; + max-height: 100%; + } `; const DxcSidenav = ({ topContent, bottomContent, navItems, - branding, + appTitle, displayGroupLines = false, expanded, defaultExpanded = true, onExpandedChange, }: SidenavPropsType): JSX.Element => { const [internalExpanded, setInternalExpanded] = useState(defaultExpanded); + const { logo, headerExists } = useContext(ApplicationLayoutContext); const isControlled = expanded !== undefined; const isExpanded = isControlled ? !!expanded : internalExpanded; @@ -72,10 +79,6 @@ const DxcSidenav = ({ onExpandedChange?.(nextState); }; - const isBrandingObject = (branding: SidenavPropsType["branding"]): branding is { logo?: Logo; appTitle?: string } => { - return typeof branding === "object" && branding !== null && ("logo" in branding || "appTitle" in branding); - }; - return ( <SidenavContainer expanded={isExpanded}> <DxcFlex @@ -91,25 +94,25 @@ const DxcSidenav = ({ title={isExpanded ? "Collapse" : "Expand"} onClick={handleToggle} /> - {isBrandingObject(branding) ? ( - <DxcFlex direction="column" gap="var(--spacing-gap-m)" justifyContent="center" alignItems="flex-start"> - {branding.logo && ( - <LogoContainer - onClick={branding.logo.onClick} - hasAction={!!branding.logo.onClick || !!branding.logo.href} - role={branding.logo.onClick ? "button" : branding.logo.href ? "link" : "presentation"} - as={branding.logo.href ? "a" : undefined} - href={branding.logo.href} - aria-label={(branding.logo.onClick || branding.logo.href) && (branding.appTitle || "Avatar")} - > - <DxcImage alt={branding.logo.alt ?? ""} src={branding.logo.src} height="100%" width="100%" /> - </LogoContainer> - )} - <SidenavTitle>{branding.appTitle}</SidenavTitle> - </DxcFlex> - ) : ( - branding - )} + + <DxcFlex direction="column" gap="var(--spacing-gap-m)" justifyContent="center" alignItems="flex-start"> + {logo && !headerExists && ( + <LogoContainer + onClick={logo.onClick} + hasAction={!!logo.onClick || !!logo.href} + role={logo.onClick ? "button" : logo.href ? "link" : "presentation"} + as={logo.href ? "a" : undefined} + href={logo.href} + > + {typeof logo.src === "string" ? ( + <DxcImage alt={logo.alt ?? ""} src={logo.src} height="100%" width="100%" /> + ) : ( + logo.src + )} + </LogoContainer> + )} + <SidenavTitle>{appTitle}</SidenavTitle> + </DxcFlex> </DxcFlex> {topContent && ( <DxcFlex direction="column" gap={"var(--spacing-gap-l)"}> diff --git a/packages/lib/src/sidenav/SidenavContext.tsx b/packages/lib/src/sidenav/SidenavContext.tsx deleted file mode 100644 index 7d16fa201..000000000 --- a/packages/lib/src/sidenav/SidenavContext.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { createContext, Dispatch, SetStateAction } from "react"; - -type SidenavContextType = (_isSidenavVisible: boolean) => void; - -const SidenavContext = createContext<SidenavContextType | null>(null); - -export const GroupContext = createContext<Dispatch<SetStateAction<boolean>> | null>(null); - -export const SidenavContextProvider = SidenavContext.Provider; - -export const GroupContextProvider = GroupContext.Provider; diff --git a/packages/lib/src/sidenav/types.ts b/packages/lib/src/sidenav/types.ts index 4cdfc4b95..378fd196f 100644 --- a/packages/lib/src/sidenav/types.ts +++ b/packages/lib/src/sidenav/types.ts @@ -1,25 +1,6 @@ -import { MouseEvent, ReactElement, ReactNode } from "react"; +import { ReactElement, ReactNode } from "react"; import { SVG } from "../common/utils"; -export type Logo = { - /** - * Alternative text for the logo image. - */ - alt: string; - /** - * URL to navigate when the logo is clicked. - */ - href?: string; - /** - * URL to navigate to when the logo is clicked. If not provided, the logo will not be clickable. - */ - onClick?: (event: MouseEvent<HTMLDivElement>) => void; - /** - * URL of the image that will be placed in the logo. - */ - src: string; -}; - type Section = { items: (Item | GroupItem)[]; title?: string }; type Props = { @@ -30,7 +11,7 @@ type Props = { /** * Object with the properties of the branding placed at the top of the sidenav. */ - branding?: { logo?: Logo; appTitle?: string } | ReactNode; + appTitle?: string | ReactNode; /** * Initial state of the expansion of the sidenav, only when it is uncontrolled. */