Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ const code = `() => {
};
return (
<HalstackProvider opinionatedTheme={theme === "light" ? lightPalette : darkPalette}>
<HalstackProvider opinionatedTheme={theme === "light" ?
{tokens: lightPalette} : {tokens: darkPalette}}>
<DxcButton label="Toggle theme" onClick={toggleTheme} />
</HalstackProvider>
);
Expand Down
209 changes: 116 additions & 93 deletions packages/lib/src/HalstackContext.stories.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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<typeof HalstackProvider>;

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);
Expand All @@ -25,29 +74,7 @@ const Provider = () => {
const handleClickAlert = () => {
setAlertVisible(!isAlertVisible);
};
const [newTheme, setNewTheme] = useState<Record<string, string | number>>({
"--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<Record<string, string | number>>(theme1);
const options = [
{ label: "Option 01", value: "1" },
{ label: "Option 02", value: "2" },
Expand All @@ -57,77 +84,68 @@ const Provider = () => {
return (
<>
<ExampleContainer>
<Title title="Default" theme="light" level={4} />
<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>
</>
Expand All @@ -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"));
},
};
32 changes: 20 additions & 12 deletions packages/lib/src/HalstackContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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;
Expand All @@ -29,15 +30,15 @@ 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>;
children: ReactNode;
opinionatedTheme?: ThemeType;
};

const HalstackThemed = styled.div<{ coreTheme?: ThemeType }>`
const HalstackThemed = styled.div<{ coreTheme?: ThemeType["tokens"] }>`
${(props) => {
if (props.coreTheme)
return css`
Expand All @@ -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;
});
Expand All @@ -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 };
7 changes: 7 additions & 0 deletions packages/lib/src/common/variables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,3 +132,10 @@ export const defaultTranslatedComponentLabels = {
};

export type TranslatedLabels = typeof defaultTranslatedComponentLabels;

export const ThemedLogos = {
mainLogo: undefined,
footerLogo: undefined,
footerReducedLogo: undefined,
favicon: undefined,
};
16 changes: 16 additions & 0 deletions packages/lib/src/footer/Footer.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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>
</>
);

Expand Down
Loading