diff --git a/apps/website/screens/components/card/overview/examples/direction.ts b/apps/website/screens/components/card/overview/examples/direction.ts index 7a248e051..c17424834 100644 --- a/apps/website/screens/components/card/overview/examples/direction.ts +++ b/apps/website/screens/components/card/overview/examples/direction.ts @@ -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 = ( @@ -53,6 +54,7 @@ const scope = { DxcHeading, DxcInset, DxcParagraph, + resultsetImage, }; export default { code, scope }; diff --git a/apps/website/screens/components/image/code/images/wooden_dock.avif b/apps/website/screens/components/image/code/images/wooden_dock.avif index 4b7ff4abc..4165fc903 100644 Binary files a/apps/website/screens/components/image/code/images/wooden_dock.avif and b/apps/website/screens/components/image/code/images/wooden_dock.avif differ diff --git a/packages/lib/.storybook/preview.tsx b/packages/lib/.storybook/preview.tsx index 051b829b2..4ad4ac735 100644 --- a/packages/lib/.storybook/preview.tsx +++ b/packages/lib/.storybook/preview.tsx @@ -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) { + 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 diff --git a/packages/lib/src/card/Card.tsx b/packages/lib/src/card/Card.tsx index 2c7fb92bf..50bf8a6f6 100644 --- a/packages/lib/src/card/Card.tsx +++ b/packages/lib/src/card/Card.tsx @@ -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"; @@ -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; `; @@ -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) => { @@ -203,12 +214,25 @@ const DxcCard = forwardRef( aria-checked={selectable ? internalSelected : undefined} ref={ref as Ref} > - {image && ( - - - + {selectable ? ( + + {image && ( + + + + )} + {children} + + ) : ( + <> + {image && ( + + + + )} + {children} + )} - {children} ); } diff --git a/packages/lib/src/card/utils.ts b/packages/lib/src/card/utils.ts index 5ac47f2e5..20c84aa68 100644 --- a/packages/lib/src/card/utils.ts +++ b/packages/lib/src/card/utils.ts @@ -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"], @@ -13,6 +13,7 @@ export const getCardStyles = ( let hover = ""; let focus = ""; let active = ""; + const borderWidth = selected ? "var(--border-width-m)" : mode === "outlined" @@ -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"],