Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
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 @@ -7,14 +7,15 @@ import {
DxcGrid,
DxcContainer,
} from "@dxc-technology/halstack-react";
import resultsetImage from "../../code/images/resultsetTable.png";

const code = `() => {
const image = {
alt: "Example image",
alt: "Resultset Table image",
width: "100%",
height: "100%",
height: "150px",
objectFit: "cover",
src: "https://picsum.photos/id/11/1920/1080",
src: resultsetImage.src,
};

const content = (
Expand Down Expand Up @@ -53,6 +54,7 @@ const scope = {
DxcHeading,
DxcInset,
DxcParagraph,
resultsetImage,
};

export default { code, scope };
Binary file not shown.
22 changes: 22 additions & 0 deletions packages/lib/.storybook/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,28 @@ import createCache from "@emotion/cache";
import { INITIAL_VIEWPORTS } from "storybook/viewport";
import type { StoryFn } from "@storybook/react-vite";

const mockDate = new Date("2026-08-20");
const RealDate = Date;

class MockedDate extends RealDate {
constructor(...args: [] | ConstructorParameters<typeof Date>) {
if (args.length === 0) {
super(mockDate.getTime());
} else {
super(...args);
}
}

static now(): number {
return mockDate.getTime();
}
}

Object.setPrototypeOf(MockedDate, RealDate);
Object.setPrototypeOf(MockedDate.prototype, RealDate.prototype);

globalThis.Date = MockedDate as typeof Date;

const emotionCache = createCache({ key: "css", prepend: true });

// Prevent ResizeObserver loop limit exceeded errors from failing tests
Expand Down
42 changes: 33 additions & 9 deletions packages/lib/src/card/Card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { forwardRef, Ref, useContext, useEffect, useState } from "react";
import styled from "@emotion/styled";
import CardPropsType from "./types";
import DxcImage from "../image/Image";
import { getCardStyles, emptyIconSizes, handleEvent } from "./utils";
import { getCardStyles, emptyIconSizes, handleEvent, getSelectableWrapperStyles } from "./utils";
import DxcFlex from "../flex/Flex";
import DxcIcon from "../icon/Icon";
import DxcTypography from "../typography/Typography";
Expand All @@ -12,19 +12,29 @@ const Card = styled.div<{
mode: CardPropsType["mode"];
direction?: CardPropsType["direction"];
imagePosition?: CardPropsType["imagePosition"];
selectable?: boolean;
interactive?: boolean;
size?: CardPropsType["size"];
selected?: boolean;
size?: CardPropsType["size"];
}>`
display: flex;
flex-direction: ${({ direction, imagePosition }) =>
imagePosition === "before" ? direction : `${direction}-reverse`};
gap: var(--spacing-gap-s);
padding: ${({ selectable }) => (selectable ? "var(--spacing-padding-none)" : "var(--spacing-padding-xs)")};
box-sizing: border-box;
padding: var(--spacing-padding-xs);
border-radius: var(--border-radius-l);
border-style: var(--border-style-default);
border-color: transparent;
${({ mode, interactive, selected, size }) => getCardStyles(mode, interactive ?? false, selected, size)}
outline-offset: ${({ selected }) => (selected ? "var(--border-width-m)" : "var(--border-width-s)")};
background: var(--color-bg-neutral-lightest);
`;

const SelectableWrapper = styled.div<{ selected: boolean }>`
${({ selected }) => getSelectableWrapperStyles(selected)}
border-radius: var(--border-radius-l);
padding: var(--spacing-padding-xs);
overflow: hidden;
`;

Expand Down Expand Up @@ -190,8 +200,9 @@ const DxcCard = forwardRef(
size={size}
imagePosition={imagePosition}
mode={mode}
interactive={isInteractive}
selectable={selectable}
selected={selectable && internalSelected}
interactive={isInteractive}
tabIndex={isInteractive ? tabIndex : undefined}
role={selectable ? "checkbox" : isInteractive ? "button" : undefined}
onClick={(event) => {
Expand All @@ -203,12 +214,25 @@ const DxcCard = forwardRef(
aria-checked={selectable ? internalSelected : undefined}
ref={ref as Ref<HTMLDivElement>}
>
{image && (
<ImageContainer image={image}>
<DxcImage {...image} />
</ImageContainer>
{selectable ? (
<SelectableWrapper selected={internalSelected}>
{image && (
<ImageContainer image={image}>
<DxcImage {...image} />
</ImageContainer>
)}
{children}
</SelectableWrapper>
) : (
<>
{image && (
<ImageContainer image={image}>
<DxcImage {...image} />
</ImageContainer>
)}
{children}
</>
)}
{children}
</Card>
);
}
Expand Down
57 changes: 39 additions & 18 deletions packages/lib/src/card/utils.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import CardPropsType from "./types";

const calculateSize = (size?: CardPropsType["size"], selected?: boolean, borderWidth?: string, outlined?: boolean) => {
return `width: ${size?.width === "fillParent" ? "100%" : selected || outlined ? `calc-size(fit-content, size - (${borderWidth} * 2))` : "fit-content"};
height: ${size?.height === "fillParent" ? "100%" : selected || outlined ? `calc-size(fit-content, size - (${borderWidth} * 2))` : "fit-content"};`;
const calculateSize = (size?: CardPropsType["size"]) => {
return `width: ${size?.width === "fillParent" ? "100%" : "fit-content"};
height: ${size?.height === "fillParent" ? "100%" : "fit-content"};`;
};
export const getCardStyles = (
mode: CardPropsType["mode"],
Expand All @@ -13,6 +13,7 @@ export const getCardStyles = (
let hover = "";
let focus = "";
let active = "";

const borderWidth = selected
? "var(--border-width-m)"
: mode === "outlined"
Expand All @@ -24,40 +25,60 @@ export const getCardStyles = (
${
mode === "outlined" && !selected
? `border: ${borderWidth} var(--border-style-default) var(--border-color-neutral-light);`
: selected
? `border: ${borderWidth} var(--border-style-default) var(--border-color-primary-strong);`
: ``
: "border-width: var(--border-width-none);"
}
${interactive ? "cursor: pointer;" : ""}
${calculateSize(size, selected, borderWidth, mode === "outlined")}
${calculateSize(size)}
`;
if (interactive && !selected) {

if (interactive) {
if (mode === "elevated") {
hover = `box-shadow: var(--shadow-300);`;
focus = `box-shadow: var(--shadow-100);
outline: var(--border-width-m) var(--border-style-default) var(--border-color-secondary-medium); outline-offset: calc(var(--border-width-m) * -1);`;
outline: var(--border-width-m) var(--border-style-default) var(--border-color-secondary-medium);`;
active = `box-shadow: var(--shadow-100);`;
} else if (mode === "outlined") {
hover = `border-width: var(--border-width-m); border-color: var(--border-color-neutral-medium); ${calculateSize(size, selected, "var(--border-width-m)", true)}`;
focus = `border-width: var(--border-width-m);
outline: var(--border-width-m) var(--border-style-default) var(--border-color-secondary-medium);
${calculateSize(size, selected, "var(--border-width-m)", true)}`;
active = `border-width: var(--border-width-m); border-color: var(--border-color-neutral-strong); ${calculateSize(size, selected, "var(--border-width-m)", true)}`;
if (selected) {
hover = `border-width: var(--border-width-none);`;
focus = `border-width: var(--border-width-none); outline: var(--border-width-m) var(--border-style-default) var(--border-color-secondary-medium);`;
active = `border-width: var(--border-width-none);`;
} else {
hover = `border-width: var(--border-width-m); border-color: var(--border-color-neutral-medium);outline-offset: var(--border-width-none);`;
focus = `border-width: var(--boselectedrder-width-m);
outline: var(--border-width-m) var(--border-style-default) var(--border-color-secondary-medium);outline-offset: var(--border-width-none);`;
active = `border-width: var(--border-width-m); border-color: var(--border-color-neutral-strong);outline-offset: var(--border-width-none);`;
}
}
} else if (selected) {
hover = `border-color: var(--border-color-primary-stronger);`;
focus = `outline: var(--border-width-m) var(--border-style-default) var(--border-color-secondary-medium);`;
active = `border-color: var(--border-color-primary-strong);`;
}

return `
${commonStyles}
&:hover { ${hover} }
&:focus { ${focus} }
&:focus-visible { ${focus} }
&:active { ${active} }
`;
};

export const getSelectableWrapperStyles = (selected: boolean) => {
let hover = "";
let focus = "";
let active = "";

if (selected) {
hover = `outline-color: var(--border-color-primary-stronger);`;
focus = `outline: var(--border-width-m) var(--border-style-default) var(--border-color-secondary-medium);`;
active = `outline-color: var(--border-color-primary-strong);`;
}

return `
&:hover { ${hover} }
&:focus { ${focus} }
&:active { ${active} }
outline: ${selected ? "var(--border-width-m) var(--border-style-default) var(--border-color-primary-strong)" : ""};
`;
};

export const handleEvent = (
event: React.KeyboardEvent | React.MouseEvent,
onClick?: CardPropsType["onClick"],
Expand Down
Loading