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
141 changes: 141 additions & 0 deletions packages/lib/src/HalstackContext.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import ExampleContainer from "./../.storybook/components/ExampleContainer";
import Title from "./../.storybook/components/Title";
import { Meta, StoryObj } from "@storybook/react";
import { HalstackProvider } from "./HalstackContext";
import { useState } from "react";
import DxcButton from "./button/Button";
import DxcDateInput from "./date-input/DateInput";
import DxcFlex from "./flex/Flex";
import DxcSelect from "./select/Select";
import DxcDialog from "./dialog/Dialog";
import DxcInset from "./inset/Inset";
import DxcAlert from "./alert/Alert";

export default {
title: "HalstackContext",
component: HalstackProvider,
} as Meta<typeof HalstackProvider>;

const Provider = () => {
const [isDialogVisible, setDialogVisible] = useState(false);
const [isAlertVisible, setAlertVisible] = useState(false);
const handleClickDialog = () => {
setDialogVisible(!isDialogVisible);
};
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-secondary-50": "#fff9d6",
"--color-secondary-100": "#ffed99",
"--color-secondary-200": "#ffe066",
"--color-secondary-300": "#e6c84d",
"--color-secondary-400": "#ccad33",
"--color-secondary-500": "#b39426",
"--color-secondary-600": "#8f741f",
"--color-secondary-700": "#6b5517",
"--color-secondary-800": "#47370f",
"--color-secondary-900": "#241b08",
"--color-alpha-800-a": "#9a2257cc",
});
const options = [
{ label: "Option 01", value: "1" },
{ label: "Option 02", value: "2" },
{ label: "Option 03", value: "3" },
{ label: "Option 04", value: "4" },
];
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-secondary-100": "#e2c7a9",
"--color-secondary-200": "#d1a577",
"--color-secondary-300": "#b88252",
"--color-secondary-400": "#99673f",
"--color-secondary-500": "#7a5232",
"--color-secondary-600": "#5c3f26",
"--color-secondary-700": "#3e2b19",
"--color-secondary-800": "#21170d",
"--color-secondary-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" }} />
<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>
</HalstackProvider>
</ExampleContainer>
</>
);
};

type Story = StoryObj<typeof HalstackProvider>;

export const Chromatic: Story = {
render: Provider,
};
51 changes: 45 additions & 6 deletions packages/lib/src/HalstackContext.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,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";

/**
Expand All @@ -8,7 +11,6 @@ import { TranslatedLabels, defaultTranslatedComponentLabels } from "./common/var
export type DeepPartial<T> = {
[P in keyof T]?: Partial<T[P]>;
};

const HalstackLanguageContext = createContext<TranslatedLabels>(defaultTranslatedComponentLabels);

const parseLabels = (labels: DeepPartial<TranslatedLabels>): TranslatedLabels => {
Expand All @@ -27,18 +29,55 @@ const parseLabels = (labels: DeepPartial<TranslatedLabels>): TranslatedLabels =>
});
return parsedLabels;
};
type ThemeType = Record<string, string | number>;

type HalstackProviderPropsType = {
labels?: DeepPartial<TranslatedLabels>;
children: ReactNode;
opinionatedTheme?: ThemeType;
};
const HalstackProvider = ({ labels, children }: HalstackProviderPropsType): JSX.Element => {

const HalstackThemed = styled.div<{ coreTheme?: ThemeType }>`
${(props) => {
if (props.coreTheme)
return css`
${Object.keys(props.coreTheme).length
? Object.entries(props.coreTheme).map(([key, val]) => `${key}: ${val};`)
: coreTokens}
${aliasTokens}
`;
else {
return css`
${coreTokens}
${aliasTokens}
`;
}
}}
`;

const createCoreTheme = (opinionatedTheme: ThemeType | undefined = {}) => {
const newTheme: ThemeType = {};
Object.entries(coreTokens).forEach(([key, value]) => {
newTheme[key] = opinionatedTheme[key] ?? value;
});
return newTheme;
};

const HalstackProvider = ({ labels, children, opinionatedTheme }: HalstackProviderPropsType): JSX.Element => {
const parsedLabels = useMemo(() => (labels ? parseLabels(labels) : null), [labels]);
const parsedCoreTheme = useMemo(() => {
const theme = createCoreTheme(opinionatedTheme);
return theme;
}, [opinionatedTheme]);

return parsedLabels ? (
<HalstackLanguageContext.Provider value={parsedLabels}>{children}</HalstackLanguageContext.Provider>
) : (
<>{children}</>
return (
<HalstackThemed coreTheme={parsedCoreTheme}>
{parsedLabels ? (
<HalstackLanguageContext.Provider value={parsedLabels}>{children}</HalstackLanguageContext.Provider>
) : (
children
)}
</HalstackThemed>
);
};

Expand Down
29 changes: 19 additions & 10 deletions packages/lib/src/alert/ModalAlertWrapper.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { createPortal } from "react-dom";
import { useEffect } from "react";
import { useEffect, useId, useState } from "react";
import { Global, css } from "@emotion/react";
import styled from "@emotion/styled";
import { responsiveSizes } from "../common/variables";
Expand Down Expand Up @@ -46,6 +46,9 @@ const ModalAlertContainer = styled.div`
`;

const ModalAlertWrapper = ({ condition, onClose, children }: ModalAlertWrapperProps) => {
const id = useId();
const [portalContainer, setPortalContainer] = useState<HTMLElement | null>(null);

useEffect(() => {
if (condition) {
const handleModalAlertKeydown = (event: KeyboardEvent) => {
Expand All @@ -61,18 +64,24 @@ const ModalAlertWrapper = ({ condition, onClose, children }: ModalAlertWrapperPr
}
}, [condition, onClose]);

useEffect(() => {
setPortalContainer(document.getElementById(`dialog-${id}-portal`));
}, []);

return condition ? (
<>
<BodyStyle />
{createPortal(
<Modal>
<Overlay onClick={onClose} />
<ModalAlertContainer>
<FocusLock>{children}</FocusLock>
</ModalAlertContainer>
</Modal>,
document.body
)}
<div id={`dialog-${id}-portal`} style={{ position: "absolute" }} />
{portalContainer &&
createPortal(
<Modal>
<Overlay onClick={onClose} />
<ModalAlertContainer>
<FocusLock>{children}</FocusLock>
</ModalAlertContainer>
</Modal>,
portalContainer
)}
</>
) : (
children
Expand Down
105 changes: 55 additions & 50 deletions packages/lib/src/date-input/DateInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -286,58 +286,63 @@ const DxcDateInput = forwardRef<RefType, DateInputPropsType>(
}, [isOpen, disabled, calendarId]);

return (
<DateInputContainer margin={margin} size={size} ref={ref}>
{label && (
<Label
htmlFor={dateRef.current?.getElementsByTagName("input")[0]?.id}
disabled={disabled}
hasHelperText={!!helperText}
>
{label}{" "}
{optional && <OptionalLabel disabled={disabled}>{translatedLabels.formFields.optionalLabel}</OptionalLabel>}
</Label>
)}
{helperText && <HelperText disabled={disabled}>{helperText}</HelperText>}
<Popover.Root open={isOpen}>
<Popover.Trigger asChild aria-controls={undefined}>
<DxcTextInput
name={name}
defaultValue={defaultValue}
value={value ?? innerValue}
placeholder={placeholder ? format.toUpperCase() : undefined}
action={{
onClick: openCalendar,
icon: "filled_calendar_today",
title: "Select date",
}}
clearable={clearable}
<>
<DateInputContainer margin={margin} size={size} ref={ref}>
{label && (
<Label
htmlFor={dateRef.current?.getElementsByTagName("input")[0]?.id}
disabled={disabled}
readOnly={readOnly}
optional={optional}
onChange={handleOnChange}
onBlur={handleOnBlur}
error={error}
autocomplete={autocomplete}
size={size}
tabIndex={tabIndex}
ref={dateRef}
ariaLabel={ariaLabel}
/>
</Popover.Trigger>
<Popover.Portal>
<StyledPopoverContent
sideOffset={sideOffset}
align="end"
aria-modal
onBlur={handleDatePickerOnBlur}
onKeyDown={handleDatePickerEscKeydown}
ref={popoverContentRef}
hasHelperText={!!helperText}
>
<DatePicker id={calendarId} onDateSelect={handleCalendarOnClick} date={dayjsDate} />
</StyledPopoverContent>
</Popover.Portal>
</Popover.Root>
</DateInputContainer>
{label}{" "}
{optional && (
<OptionalLabel disabled={disabled}>{translatedLabels.formFields.optionalLabel}</OptionalLabel>
)}
</Label>
)}
{helperText && <HelperText disabled={disabled}>{helperText}</HelperText>}
<Popover.Root open={isOpen}>
<Popover.Trigger asChild aria-controls={undefined}>
<DxcTextInput
name={name}
defaultValue={defaultValue}
value={value ?? innerValue}
placeholder={placeholder ? format.toUpperCase() : undefined}
action={{
onClick: openCalendar,
icon: "filled_calendar_today",
title: "Select date",
}}
clearable={clearable}
disabled={disabled}
readOnly={readOnly}
optional={optional}
onChange={handleOnChange}
onBlur={handleOnBlur}
error={error}
autocomplete={autocomplete}
size={size}
tabIndex={tabIndex}
ref={dateRef}
ariaLabel={ariaLabel}
/>
</Popover.Trigger>
<Popover.Portal container={document.getElementById(`${calendarId}-portal`)}>
<StyledPopoverContent
sideOffset={sideOffset}
align="end"
aria-modal
onBlur={handleDatePickerOnBlur}
onKeyDown={handleDatePickerEscKeydown}
ref={popoverContentRef}
>
<DatePicker id={calendarId} onDateSelect={handleCalendarOnClick} date={dayjsDate} />
</StyledPopoverContent>
</Popover.Portal>
</Popover.Root>
</DateInputContainer>
<div id={`${calendarId}-portal`} style={{ position: "absolute" }} />
</>
);
}
);
Expand Down
Loading