diff --git a/apps/website/screens/utilities/halstack-provider/examples/customThemes.tsx b/apps/website/screens/utilities/halstack-provider/examples/customThemes.tsx index 7e38c5eeb4..3aefd430dd 100644 --- a/apps/website/screens/utilities/halstack-provider/examples/customThemes.tsx +++ b/apps/website/screens/utilities/halstack-provider/examples/customThemes.tsx @@ -63,7 +63,8 @@ const code = `() => { }; return ( - + ); diff --git a/packages/lib/src/HalstackContext.stories.tsx b/packages/lib/src/HalstackContext.stories.tsx index 2c92bf72bf..74a301528c 100644 --- a/packages/lib/src/HalstackContext.stories.tsx +++ b/packages/lib/src/HalstackContext.stories.tsx @@ -1,5 +1,4 @@ import ExampleContainer from "./../.storybook/components/ExampleContainer"; -import Title from "./../.storybook/components/Title"; import { Meta, StoryObj } from "@storybook/react-vite"; import { HalstackProvider } from "./HalstackContext"; import { useState } from "react"; @@ -10,12 +9,62 @@ import DxcSelect from "./select/Select"; import DxcDialog from "./dialog/Dialog"; import DxcInset from "./inset/Inset"; import DxcAlert from "./alert/Alert"; +import DxcApplicationLayout from "./layout/ApplicationLayout"; +import { userEvent, within } from "storybook/internal/test"; export default { title: "HalstackContext", component: HalstackProvider, } as Meta; +const theme1 = { + "--color-primary-50": "#d3f0b4", + "--color-primary-100": "#a2df5e", + "--color-primary-200": "#77c81f", + "--color-primary-300": "#68ad1b", + "--color-primary-400": "#579317", + "--color-primary-500": "#487813", + "--color-primary-600": "#39600f", + "--color-primary-700": "#2b470b", + "--color-primary-800": "#1c2f07", + "--color-primary-900": "#0d1503", + "--color-semantic01-50": "#fff9d6", + "--color-semantic01-100": "#ffed99", + "--color-semantic01-200": "#ffe066", + "--color-semantic01-300": "#e6c84d", + "--color-semantic01-400": "#ccad33", + "--color-semantic01-500": "#b39426", + "--color-semantic01-600": "#8f741f", + "--color-semantic01-700": "#6b5517", + "--color-semantic01-800": "#47370f", + "--color-semantic01-900": "#241b08", + "--color-alpha-800-a": "#9a2257cc", +}; + +const theme2 = { + "--color-primary-50": "#ffd6e7", + "--color-primary-100": "#ff99c2", + "--color-primary-200": "#ff66a3", + "--color-primary-300": "#e05584", + "--color-primary-400": "#c5446d", + "--color-primary-500": "#a83659", + "--color-primary-600": "#872b47", + "--color-primary-700": "#661f35", + "--color-primary-800": "#441423", + "--color-primary-900": "#220a12", + "--color-brown-50": "#f3e6db", + "--color-semantic01-100": "#e2c7a9", + "--color-semantic01-200": "#d1a577", + "--color-semantic01-300": "#b88252", + "--color-semantic01-400": "#99673f", + "--color-semantic01-500": "#7a5232", + "--color-semantic01-600": "#5c3f26", + "--color-semantic01-700": "#3e2b19", + "--color-semantic01-800": "#21170d", + "--color-semantic01-900": "#100b06", + "--color-alpha-800-a": "#fabadacc", +}; + const Provider = () => { const [isDialogVisible, setDialogVisible] = useState(false); const [isAlertVisible, setAlertVisible] = useState(false); @@ -25,29 +74,7 @@ const Provider = () => { const handleClickAlert = () => { setAlertVisible(!isAlertVisible); }; - const [newTheme, setNewTheme] = useState>({ - "--color-primary-50": "#d3f0b4", - "--color-primary-100": "#a2df5e", - "--color-primary-200": "#77c81f", - "--color-primary-300": "#68ad1b", - "--color-primary-400": "#579317", - "--color-primary-500": "#487813", - "--color-primary-600": "#39600f", - "--color-primary-700": "#2b470b", - "--color-primary-800": "#1c2f07", - "--color-primary-900": "#0d1503", - "--color-semantic01-50": "#fff9d6", - "--color-semantic01-100": "#ffed99", - "--color-semantic01-200": "#ffe066", - "--color-semantic01-300": "#e6c84d", - "--color-semantic01-400": "#ccad33", - "--color-semantic01-500": "#b39426", - "--color-semantic01-600": "#8f741f", - "--color-semantic01-700": "#6b5517", - "--color-semantic01-800": "#47370f", - "--color-semantic01-900": "#241b08", - "--color-alpha-800-a": "#9a2257cc", - }); + const [newTheme, setNewTheme] = useState>(theme1); const options = [ { label: "Option 01", value: "1" }, { label: "Option 02", value: "2" }, @@ -57,77 +84,68 @@ const Provider = () => { return ( <> - - <HalstackProvider opinionatedTheme={newTheme}> - <DxcFlex gap="var(--spacing-padding-l)" direction="column" alignItems="baseline"> - <DxcButton - label="Primary" - semantic="default" - onClick={() => - setNewTheme({ - "--color-primary-50": "#ffd6e7", - "--color-primary-100": "#ff99c2", - "--color-primary-200": "#ff66a3", - "--color-primary-300": "#e05584", - "--color-primary-400": "#c5446d", - "--color-primary-500": "#a83659", - "--color-primary-600": "#872b47", - "--color-primary-700": "#661f35", - "--color-primary-800": "#441423", - "--color-primary-900": "#220a12", - "--color-brown-50": "#f3e6db", - "--color-semantic01-100": "#e2c7a9", - "--color-semantic01-200": "#d1a577", - "--color-semantic01-300": "#b88252", - "--color-semantic01-400": "#99673f", - "--color-semantic01-500": "#7a5232", - "--color-semantic01-600": "#5c3f26", - "--color-semantic01-700": "#3e2b19", - "--color-semantic01-800": "#21170d", - "--color-semantic01-900": "#100b06", - "--color-alpha-800-a": "#fabadacc", - }) - } - size={{ height: "small" }} - /> - <DxcButton - label="Show Dialog" - semantic="default" - mode="secondary" - size={{ height: "small" }} - onClick={handleClickDialog} - /> - {isDialogVisible && ( - <DxcDialog onCloseClick={handleClickDialog}> - <DxcInset space="var(--spacing-padding-l)"> - <DxcButton label="Primary" semantic="default" mode="tertiary" size={{ height: "small" }} /> + <HalstackProvider + opinionatedTheme={{ + tokens: newTheme, + logos: { + mainLogo: "https://picsum.photos/id/16/200/40", + footerLogo: "https://picsum.photos/id/17/200/40", + footerReducedLogo: "https://picsum.photos/id/17/200/40", + }, + }} + > + <DxcApplicationLayout + header={<DxcApplicationLayout.Header />} + footer={<DxcApplicationLayout.Footer mode="reduced" />} + > + <DxcApplicationLayout.Main> + <DxcInset space="var(--spacing-padding-l)"> + <DxcFlex gap="var(--spacing-padding-l)" direction="column" alignItems="baseline"> + <DxcButton + label="Change theme" + semantic="default" + onClick={() => setNewTheme((theme) => (theme === theme1 ? theme2 : theme1))} + size={{ height: "small" }} + /> + <DxcButton + label="Show Dialog" + semantic="default" + mode="secondary" + size={{ height: "small" }} + onClick={handleClickDialog} + /> + {isDialogVisible && ( + <DxcDialog onCloseClick={handleClickDialog}> + <DxcInset space="var(--spacing-padding-l)"> + <DxcButton label="Primary" semantic="default" mode="tertiary" size={{ height: "small" }} /> + <DxcButton label="Primary" semantic="info" size={{ height: "small" }} /> + <DxcButton label="Primary" semantic="info" mode="secondary" size={{ height: "small" }} /> + <DxcDateInput /> + <DxcSelect options={options} /> + </DxcInset> + </DxcDialog> + )} + <DxcButton + label="Alert visibility" + semantic="default" + mode="tertiary" + size={{ height: "small" }} + onClick={handleClickAlert} + /> <DxcButton label="Primary" semantic="info" size={{ height: "small" }} /> <DxcButton label="Primary" semantic="info" mode="secondary" size={{ height: "small" }} /> - <DxcDateInput /> - <DxcSelect options={options} /> - </DxcInset> - </DxcDialog> - )} - <DxcButton - label="Alert visibility" - semantic="default" - mode="tertiary" - size={{ height: "small" }} - onClick={handleClickAlert} - /> - <DxcButton label="Primary" semantic="info" size={{ height: "small" }} /> - <DxcButton label="Primary" semantic="info" mode="secondary" size={{ height: "small" }} /> - <DxcDateInput /> - <DxcSelect options={options} /> - - {isAlertVisible && ( - <DxcAlert - title="Information" - mode="modal" - message={{ text: "Your document has been auto-saved.", onClose: handleClickAlert }} - /> - )} - </DxcFlex> + <DxcSelect options={options} value="2" /> + {isAlertVisible && ( + <DxcAlert + title="Information" + mode="modal" + message={{ text: "Your document has been auto-saved.", onClose: handleClickAlert }} + /> + )} + </DxcFlex> + </DxcInset> + </DxcApplicationLayout.Main> + </DxcApplicationLayout> </HalstackProvider> </ExampleContainer> </> @@ -138,4 +156,9 @@ type Story = StoryObj<typeof HalstackProvider>; export const Chromatic: Story = { render: Provider, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await userEvent.click(canvas.getByText("Change theme")); + await userEvent.click(canvas.getByRole("combobox")); + }, }; diff --git a/packages/lib/src/HalstackContext.tsx b/packages/lib/src/HalstackContext.tsx index f1f53e54d9..d37cfa2b82 100644 --- a/packages/lib/src/HalstackContext.tsx +++ b/packages/lib/src/HalstackContext.tsx @@ -2,7 +2,7 @@ import { createContext, ReactNode, useMemo } from "react"; import styled from "@emotion/styled"; import { css } from "@emotion/react"; import { coreTokens, aliasTokens } from "./styles/tokens"; -import { TranslatedLabels, defaultTranslatedComponentLabels } from "./common/variables"; +import { ThemedLogos, TranslatedLabels, defaultTranslatedComponentLabels } from "./common/variables"; /** * This type is used to allow labels objects to be passed to the HalstackProvider. @@ -12,6 +12,7 @@ export type DeepPartial<T> = { [P in keyof T]?: Partial<T[P]>; }; const HalstackLanguageContext = createContext<TranslatedLabels>(defaultTranslatedComponentLabels); +const HalstackLogosContext = createContext<Record<string, string | undefined>>(ThemedLogos); const parseLabels = (labels: DeepPartial<TranslatedLabels>): TranslatedLabels => { const parsedLabels = defaultTranslatedComponentLabels; @@ -29,7 +30,7 @@ const parseLabels = (labels: DeepPartial<TranslatedLabels>): TranslatedLabels => }); return parsedLabels; }; -type ThemeType = Record<string, string | number>; +type ThemeType = { tokens?: Record<string, string | number>; logos?: Record<string, string | undefined> }; type HalstackProviderPropsType = { labels?: DeepPartial<TranslatedLabels>; @@ -37,7 +38,7 @@ type HalstackProviderPropsType = { opinionatedTheme?: ThemeType; }; -const HalstackThemed = styled.div<{ coreTheme?: ThemeType }>` +const HalstackThemed = styled.div<{ coreTheme?: ThemeType["tokens"] }>` ${(props) => { if (props.coreTheme) return css` @@ -55,8 +56,13 @@ const HalstackThemed = styled.div<{ coreTheme?: ThemeType }>` }} `; -const createCoreTheme = (opinionatedTheme: ThemeType | undefined = {}) => { - const newTheme: ThemeType = {}; +/** + * This function creates a theme object that will be used in the HalstackThemed component. + * It takes the opinionatedTheme tokens and overrides the coreTokens with them. + * If a token is not provided in the opinionatedTheme, it will use the value from coreTokens. + */ +const createCoreTheme = (opinionatedTheme: ThemeType["tokens"] | undefined = {}) => { + const newTheme: ThemeType["tokens"] = {}; Object.entries(coreTokens).forEach(([key, value]) => { newTheme[key] = opinionatedTheme[key] ?? value; }); @@ -66,19 +72,21 @@ const createCoreTheme = (opinionatedTheme: ThemeType | undefined = {}) => { const HalstackProvider = ({ labels, children, opinionatedTheme }: HalstackProviderPropsType): JSX.Element => { const parsedLabels = useMemo(() => (labels ? parseLabels(labels) : null), [labels]); const parsedCoreTheme = useMemo(() => { - const theme = createCoreTheme(opinionatedTheme); + const theme = createCoreTheme(opinionatedTheme?.tokens); return theme; }, [opinionatedTheme]); return ( <HalstackThemed coreTheme={parsedCoreTheme}> - {parsedLabels ? ( - <HalstackLanguageContext.Provider value={parsedLabels}>{children}</HalstackLanguageContext.Provider> - ) : ( - children - )} + <HalstackLogosContext.Provider value={opinionatedTheme?.logos ?? ThemedLogos}> + {parsedLabels ? ( + <HalstackLanguageContext.Provider value={parsedLabels}>{children}</HalstackLanguageContext.Provider> + ) : ( + children + )} + </HalstackLogosContext.Provider> </HalstackThemed> ); }; -export { HalstackProvider, HalstackLanguageContext }; +export { HalstackProvider, HalstackLanguageContext, HalstackLogosContext }; diff --git a/packages/lib/src/common/variables.ts b/packages/lib/src/common/variables.ts index 5b06093ecd..4fb7350161 100644 --- a/packages/lib/src/common/variables.ts +++ b/packages/lib/src/common/variables.ts @@ -132,3 +132,10 @@ export const defaultTranslatedComponentLabels = { }; export type TranslatedLabels = typeof defaultTranslatedComponentLabels; + +export const ThemedLogos = { + mainLogo: undefined, + footerLogo: undefined, + footerReducedLogo: undefined, + favicon: undefined, +}; diff --git a/packages/lib/src/footer/Footer.stories.tsx b/packages/lib/src/footer/Footer.stories.tsx index 6470583001..0c269f8da4 100644 --- a/packages/lib/src/footer/Footer.stories.tsx +++ b/packages/lib/src/footer/Footer.stories.tsx @@ -14,6 +14,7 @@ import DxcApplicationLayout from "../layout/ApplicationLayout"; import DxcHeader from "../header/Header"; import DxcBadge from "../badge/Badge"; import DxcButton from "../button/Button"; +import { HalstackProvider } from "../HalstackContext"; export default { title: "Footer", @@ -422,6 +423,21 @@ const Footer = () => ( <Title title="Reduced with custom logo" theme="light" level={4} /> <DxcFooter mode="reduced" logo={{ src: "https://picsum.photos/id/1000/104/34", alt: "Custom logo" }} /> </ExampleContainer> + <ExampleContainer> + <Title title="Themed footer" theme="light" level={4} /> + <HalstackProvider + opinionatedTheme={{ + logos: { + footerLogo: "https://picsum.photos/id/17/104/50", + footerReducedLogo: "https://picsum.photos/id/17/104/34", + }, + }} + > + <DxcFooter /> + <DxcFooter mode="reduced" /> + <DxcFooter logo={{ src: "https://picsum.photos/id/1000/104/34", alt: "Custom logo" }} /> + </HalstackProvider> + </ExampleContainer> </> ); diff --git a/packages/lib/src/footer/Footer.tsx b/packages/lib/src/footer/Footer.tsx index 79f487449f..41852c6e9c 100644 --- a/packages/lib/src/footer/Footer.tsx +++ b/packages/lib/src/footer/Footer.tsx @@ -4,7 +4,7 @@ import DxcIcon from "../icon/Icon"; import { Tooltip } from "../tooltip/Tooltip"; import { dxcLogo, dxcSmallLogo } from "./Icons"; import FooterPropsType from "./types"; -import { HalstackLanguageContext } from "../HalstackContext"; +import { HalstackLanguageContext, HalstackLogosContext } from "../HalstackContext"; import { getContrastColor, getResponsiveStyles } from "./utils"; import DxcLink from "../link/Link"; import useWidth from "../utils/useWidth"; @@ -216,6 +216,7 @@ const DxcFooter = ({ tabIndex = 0, }: FooterPropsType): JSX.Element => { const translatedLabels = useContext(HalstackLanguageContext); + const themedLogos = useContext(HalstackLogosContext); const footerLogo = useMemo(() => { if (logo && typeof logo.src === "string") { @@ -223,9 +224,19 @@ const DxcFooter = ({ } else if (isValidElement(logo?.src)) { return logo.src; } else { - return mode === "default" ? dxcLogo : dxcSmallLogo; + return mode === "default" ? ( + themedLogos?.footerLogo ? ( + <LogoImg mode={mode} alt={"Footer logo"} src={themedLogos.footerLogo} title={"Footer logo"} /> + ) : ( + dxcLogo + ) + ) : themedLogos?.footerReducedLogo ? ( + <LogoImg mode={mode} alt={"Footer logo"} src={themedLogos.footerReducedLogo} title={"Footer logo"} /> + ) : ( + dxcSmallLogo + ); } - }, [mode, logo]); + }, [mode, logo, themedLogos]); const footerRef = useRef<HTMLDivElement>(null); const width = useWidth(footerRef); diff --git a/packages/lib/src/layout/ApplicationLayout.stories.tsx b/packages/lib/src/layout/ApplicationLayout.stories.tsx index 729c660938..a40c0fd631 100644 --- a/packages/lib/src/layout/ApplicationLayout.stories.tsx +++ b/packages/lib/src/layout/ApplicationLayout.stories.tsx @@ -3,6 +3,7 @@ import Title from "../../.storybook/components/Title"; import DxcApplicationLayout from "./ApplicationLayout"; import { userEvent, within } from "storybook/internal/test"; import { useEffect } from "react"; +import { HalstackProvider } from "../HalstackContext"; export default { title: "Application Layout", @@ -171,6 +172,51 @@ const Tooltip = () => ( </DxcApplicationLayout> ); +const SidenavThemed = () => ( + <HalstackProvider + opinionatedTheme={{ + logos: { + mainLogo: "https://picsum.photos/id/16/100/50", + footerLogo: "https://picsum.photos/id/17/200/40", + }, + }} + > + <DxcApplicationLayout sidenav={<DxcApplicationLayout.Sidenav navItems={items} />}> + <DxcApplicationLayout.Main> + <p>Main Content</p> + <p>Main Content</p> + <p>Main Content</p> + <p>Main Content</p> + </DxcApplicationLayout.Main> + </DxcApplicationLayout> + </HalstackProvider> +); + +const HeaderThemed = () => ( + <HalstackProvider + opinionatedTheme={{ + logos: { + mainLogo: "https://picsum.photos/id/16/100/50", + footerLogo: "https://picsum.photos/id/17/200/40", + footerReducedLogo: "https://picsum.photos/id/10/200/40", + }, + }} + > + <DxcApplicationLayout + header={<DxcApplicationLayout.Header />} + sidenav={<DxcApplicationLayout.Sidenav navItems={items} />} + footer={<DxcApplicationLayout.Footer mode="reduced" />} + > + <DxcApplicationLayout.Main> + <p>Main Content</p> + <p>Main Content</p> + <p>Main Content</p> + <p>Main Content</p> + </DxcApplicationLayout.Main> + </DxcApplicationLayout> + </HalstackProvider> +); + type Story = StoryObj<typeof DxcApplicationLayout>; export const DefaultApplicationLayout: Story = { @@ -220,3 +266,11 @@ export const ApplicationLayoutTooltip: Story = { } }, }; + +export const ApplicationLayoutSidenavThemed: Story = { + render: SidenavThemed, +}; + +export const ApplicationLayoutHeaderThemed: Story = { + render: HeaderThemed, +}; diff --git a/packages/lib/src/layout/ApplicationLayout.tsx b/packages/lib/src/layout/ApplicationLayout.tsx index babb9d000a..b18e8746cd 100644 --- a/packages/lib/src/layout/ApplicationLayout.tsx +++ b/packages/lib/src/layout/ApplicationLayout.tsx @@ -1,4 +1,4 @@ -import { useMemo, useRef, useState, useCallback, ReactNode } from "react"; +import { useMemo, useRef, useState, useCallback, ReactNode, useContext } from "react"; import styled from "@emotion/styled"; import DxcFooter from "../footer/Footer"; import DxcHeader from "../header/Header"; @@ -7,6 +7,7 @@ import ApplicationLayoutPropsType, { AppLayoutMainPropsType } from "./types"; import { bottomLinks, findChildType, socialLinks, year } from "./utils"; import ApplicationLayoutContext from "./ApplicationLayoutContext"; import { responsiveSizes } from "../common/variables"; +import { HalstackLogosContext } from "../HalstackContext"; const ApplicationLayoutContainer = styled.div<{ header?: ReactNode }>` display: grid; @@ -66,6 +67,8 @@ const Main = ({ children }: AppLayoutMainPropsType): JSX.Element => <div>{childr const DxcApplicationLayout = ({ logo, header, sidenav, footer, children }: ApplicationLayoutPropsType): JSX.Element => { const [headerHeight, setHeaderHeight] = useState("0px"); const [hideMainContent, setHideMainContent] = useState(false); + const themedLogos = useContext(HalstackLogosContext); + const handleHeaderHeight = useCallback( (headerElement: HTMLDivElement | null) => { if (headerElement) { @@ -77,12 +80,16 @@ const DxcApplicationLayout = ({ logo, header, sidenav, footer, children }: Appli ); const contextValue = useMemo(() => { + const logoToUse: ApplicationLayoutPropsType["logo"] = { + src: logo?.src || themedLogos?.mainLogo || "", + alt: logo?.alt || "", + }; return { - logo, + logo: logoToUse, headerExists: !!header, setHideMainContent, }; - }, [header, logo]); + }, [header, logo, themedLogos]); const ref = useRef(null); return (