From 685a9163019c24ac8cecfbd10ff228ae9a590ea4 Mon Sep 17 00:00:00 2001 From: Pelayo Felgueroso Date: Tue, 13 Jan 2026 16:27:29 +0100 Subject: [PATCH 1/3] First approach to new Chip redesign --- .../components/chip/code/ChipCodePage.tsx | 37 ++++++++-- .../chip/overview/ChipOverviewPage.tsx | 68 +++++++++++++++++-- packages/lib/src/chip/Chip.stories.tsx | 21 +++++- packages/lib/src/chip/Chip.tsx | 53 +++++++++------ packages/lib/src/chip/types.ts | 30 +++++--- 5 files changed, 166 insertions(+), 43 deletions(-) diff --git a/apps/website/screens/components/chip/code/ChipCodePage.tsx b/apps/website/screens/components/chip/code/ChipCodePage.tsx index 9f25f914f1..41151ff8bf 100644 --- a/apps/website/screens/components/chip/code/ChipCodePage.tsx +++ b/apps/website/screens/components/chip/code/ChipCodePage.tsx @@ -4,7 +4,23 @@ import DocFooter from "@/common/DocFooter"; import Example from "@/common/example/Example"; import basicUsage from "./examples/basicUsage"; import icons from "./examples/icons"; -import Code, { TableCode } from "@/common/Code"; +import Code, { ExtendedTableCode, TableCode } from "@/common/Code"; + +const avatarTypeString = `{ + color?: 'primary' | 'secondary' | 'tertiary' | + 'success' | 'info' | 'neutral' | 'warning' | 'error'; + disabled?: boolean; + icon?: string | SVG; + imgSrc?: string; + label?: string; + linkHref?: string; + onClick??: () => void; + shape?: 'circle' | 'square'; + size?: 'xsmall' | 'small' | 'medium' | + 'large' | 'xlarge' | 'xxlarge'; + tabIndex?: number'; + title?: string; +}`; const sections = [ { @@ -20,6 +36,19 @@ const sections = [ + + avatar + + {avatarTypeString} + + + Avatar that will be placed before the chip label only when the chip size is 'medium' or{" "} + 'large'. + + + false + + disabled @@ -80,9 +109,9 @@ const sections = [ Material Symbol {" "} - name or SVG element as the icon that will be placed before the chip label. When using Material Symbols, - replace spaces with underscores. By default they are outlined if you want it to be filled prefix the - symbol name with "filled_". + name or SVG element as the icon that will be placed before the chip label when an avatar is + not provided. When using Material Symbols, replace spaces with underscores. By default they are outlined + if you want it to be filled prefix the symbol name with "filled_". - diff --git a/apps/website/screens/components/chip/overview/ChipOverviewPage.tsx b/apps/website/screens/components/chip/overview/ChipOverviewPage.tsx index 406df4d571..aba3f28d4e 100644 --- a/apps/website/screens/components/chip/overview/ChipOverviewPage.tsx +++ b/apps/website/screens/components/chip/overview/ChipOverviewPage.tsx @@ -28,16 +28,72 @@ const sections = [ Chip anatomy - Prefix (Optional): the prefix can be an icon or an action icon that provides - additional context or functionality. + Container (Required): + + The outer wrapper of the chip. + + Defines: + + Overall size (Small / Medium / Large) + + Interactive area (default, focus, hover, active and disabled) + + + + + Acts as the main clickable surface when no action icon is present. + + - Label: the primary text that conveys the chip's meaning, such as a tag name or a selected - option. It should be concise, clear, and relevant to the chip's function. + Left Element (Optional): Supported types: + + + Icon + + Allowed in Small, Medium and Large. + Used for status, category, or action hint. + + + + + Avatar + + Allowed only in Medium and Large. + Represents people, entities or profiles. + + + + Small size supports icons only to preserve compactness and clarity. - Suffix (Optional): the suffix can be an icon or an action icon that enhances - interactivity. + Label (Required) + + Text content displayed inside the chip + + Characteristics: + + Short, concise text + Single-line only(no-wrapping) + Truncated when exceeding maximum width + Tooltip appears on hover/focus when truncated + + + Serves as the primary identifier of the chip. + + + + Action Icon (Optional) - Appears at the end of the chip. Common usage: + + Remove / clear action (✕) + Secondary inline action (if applicable) + + Behavior: + + Has its own interaction target + Does not trigger the main chip action + Disabled when the chip is disabled + diff --git a/packages/lib/src/chip/Chip.stories.tsx b/packages/lib/src/chip/Chip.stories.tsx index 12e6e9a499..be5c066550 100644 --- a/packages/lib/src/chip/Chip.stories.tsx +++ b/packages/lib/src/chip/Chip.stories.tsx @@ -3,10 +3,25 @@ import ExampleContainer from "../../.storybook/components/ExampleContainer"; import DxcChip from "./Chip"; import { Meta, StoryObj } from "@storybook/react-vite"; import { userEvent } from "storybook/internal/test"; +import { useEffect } from "react"; export default { title: "Chip", component: DxcChip, + decorators: [ + (Story) => { + useEffect(() => { + const prev = document.body.style.cssText; + document.body.style.backgroundColor = "var(--color-bg-neutral-light)"; + document.body.style.padding = "0"; + return () => { + document.body.style.cssText = prev; + }; + }, []); + + return ; + }, + ], } satisfies Meta; const iconSVG = ( @@ -42,7 +57,7 @@ const Chip = () => ( <> - <DxcChip label="Default Chip" /> + <DxcChip label="Default Chip with lots of characteres" /> </ExampleContainer> <ExampleContainer> <Title title="Chip with prefix SVG (small icon)" theme="light" level={4} /> @@ -52,6 +67,10 @@ const Chip = () => ( <Title title="Chip with suffix SVG (large icon)" theme="light" level={4} /> <DxcChip label="Chip with suffix" suffixIcon={iconSVG} /> </ExampleContainer> + <ExampleContainer> + <Title title="Chip with Avatar" theme="light" level={4} /> + <DxcChip label="Default Chip with lots of characteres" size="small" avatar={{ color: "primary" }} /> + </ExampleContainer> <ExampleContainer> <Title title="Chip with prefix (SVG) and suffix (URL)" theme="light" level={4} /> <DxcChip label="Chip with prefix and suffix" prefixIcon={iconSVG} suffixIcon="filled_check_circle" /> diff --git a/packages/lib/src/chip/Chip.tsx b/packages/lib/src/chip/Chip.tsx index 2e83c1836a..7896170f50 100644 --- a/packages/lib/src/chip/Chip.tsx +++ b/packages/lib/src/chip/Chip.tsx @@ -1,23 +1,27 @@ import styled from "@emotion/styled"; -import { getMargin } from "../common/utils"; import { spaces } from "../common/variables"; import DxcIcon from "../icon/Icon"; import ChipPropsType from "./types"; import DxcActionIcon from "../action-icon/ActionIcon"; +import DxcAvatar from "../avatar/Avatar"; -const calculateWidth = (margin: ChipPropsType["margin"]) => - `calc(100% - ${getMargin(margin, "left")} - ${getMargin(margin, "right")})`; - -const Chip = styled.div<{ margin: ChipPropsType["margin"]; disabled: ChipPropsType["disabled"] }>` +const Chip = styled.div<{ + margin: ChipPropsType["margin"]; + size: ChipPropsType["size"]; + disabled: ChipPropsType["disabled"]; +}>` + height: ${({ size }) => + size === "small" ? "var(--height-s)" : size === "large" ? "var(--height-xl)" : "var(--height-m)"}; + min-width: ${({ size }) => (size === "small" ? "60px" : "80px")}; + max-width: 172px; box-sizing: border-box; display: inline-flex; align-items: center; - gap: var(--spacing-gap-s); - min-height: var(--height-xl); - max-width: ${(props) => calculateWidth(props.margin)}; - background-color: var(--color-bg-neutral-light); + justify-content: center; + gap: var(--spacing-gap-xs); + background-color: var(--color-bg-primary-lightest); border-radius: var(--border-radius-xl); - padding: var(--spacing-padding-none) var(--spacing-padding-m); + padding: ${({ size }) => (size === "small" ? "var(--spacing-padding-xxs)" : "var(--spacing-padding-xs)")}; margin: ${(props) => (props.margin && typeof props.margin !== "object" ? spaces[props.margin] : "0px")}; margin-top: ${(props) => props.margin && typeof props.margin === "object" && props.margin.top ? spaces[props.margin.top] : ""}; @@ -31,10 +35,10 @@ const Chip = styled.div<{ margin: ChipPropsType["margin"]; disabled: ChipPropsTy `; const LabelContainer = styled.span<{ disabled: ChipPropsType["disabled"] }>` - font-size: var(--typography-label-l); + font-size: var(--typography-label-s); font-family: var(--typography-font-family); font-weight: var(--typography-label-regular); - color: ${(props) => (props.disabled ? "var(--color-fg-neutral-medium)" : "var(--color-fg-neutral-dark)")}; + color: ${({ disabled }) => (disabled ? "var(--color-fg-neutral-medium)" : "var(--color-fg-neutral-dark)")}; text-overflow: ellipsis; white-space: nowrap; overflow: hidden; @@ -44,27 +48,33 @@ const IconContainer = styled.div<{ disabled: ChipPropsType["disabled"]; }>` display: flex; - border-radius: var(--border-radius-xs); - color: ${(props) => (props.disabled ? "var(--color-fg-neutral-medium)" : "var(--color-fg-neutral-dark)")}; - font-size: var(--height-s); + align-items: center; + justify-content: center; + color: ${({ disabled }) => (disabled ? "var(--color-fg-neutral-medium)" : "var(--color-fg-neutral-dark)")}; + font-size: var(--height-xxs); svg { - width: 24px; - height: var(--height-s); + height: var(--height-xxs); + width: var(--height-xxs); } `; const DxcChip = ({ + avatar, label, suffixIcon, prefixIcon, onClickSuffix, onClickPrefix, - disabled, + disabled = false, margin, + size = "medium", tabIndex = 0, }: ChipPropsType) => ( - <Chip disabled={disabled} margin={margin}> - {prefixIcon && + <Chip disabled={disabled} margin={margin} size={size}> + {avatar && size != "small" ? ( + <DxcAvatar {...avatar} size="xsmall" /> + ) : ( + prefixIcon && (typeof onClickPrefix === "function" ? ( <DxcActionIcon size="xsmall" @@ -78,7 +88,8 @@ const DxcChip = ({ <IconContainer disabled={disabled}> {typeof prefixIcon === "string" ? <DxcIcon icon={prefixIcon} /> : prefixIcon} </IconContainer> - ))} + )) + )} {label && <LabelContainer disabled={disabled}>{label}</LabelContainer>} {suffixIcon && (typeof onClickSuffix === "function" ? ( diff --git a/packages/lib/src/chip/types.ts b/packages/lib/src/chip/types.ts index 004385e1c6..581a1f0a87 100644 --- a/packages/lib/src/chip/types.ts +++ b/packages/lib/src/chip/types.ts @@ -1,35 +1,43 @@ import { Margin, SVG, Space } from "../common/utils"; +import AvatarProps from "../avatar/types"; + +type Size = "small" | "medium" | "large"; type Props = { + avatar?: AvatarProps; + /** + * If true, the component will be disabled. + */ + disabled?: boolean; /** * Text to be placed on the chip. */ label?: string; /** - * Element or path used as icon to be placed after the chip label. + * Size of the margin to be applied to the component ('xxsmall' | 'xsmall' | 'small' | 'medium' | 'large' | 'xlarge' | 'xxlarge'). + * You can pass an object with 'top', 'bottom', 'left' and 'right' properties in order to specify different margin sizes. */ - suffixIcon?: string | SVG; + margin?: Space | Margin; /** - * Element or path used as icon to be placed before the chip label. + * This function will be called when the prefix is clicked. */ - prefixIcon?: string | SVG; + onClickPrefix?: () => void; /** * This function will be called when the suffix is clicked. */ onClickSuffix?: () => void; /** - * This function will be called when the prefix is clicked. + * Element or path used as icon to be placed before the chip label. */ - onClickPrefix?: () => void; + prefixIcon?: string | SVG; /** - * If true, the component will be disabled. + * Size of the component. */ - disabled?: boolean; + size?: Size; /** - * Size of the margin to be applied to the component ('xxsmall' | 'xsmall' | 'small' | 'medium' | 'large' | 'xlarge' | 'xxlarge'). - * You can pass an object with 'top', 'bottom', 'left' and 'right' properties in order to specify different margin sizes. + * Element or path used as icon to be placed after the chip label. */ - margin?: Space | Margin; + suffixIcon?: string | SVG; /** * Value of the tabindex attribute. */ From 2286133f83c6fd1534d2fceda91447a9bb4e6cba Mon Sep 17 00:00:00 2001 From: Pelayo Felgueroso <pfelguerosogalguera@gmail.com> Date: Thu, 15 Jan 2026 13:28:41 +0100 Subject: [PATCH 2/3] -Refacto ActionIcon to allow past actionIcon states. -Add and improve Chip stories with new variants and states -Implement new chip functionalities from the new redesign -Refactor the Chip component API --- packages/lib/src/action-icon/ActionIcon.tsx | 43 ++++-- packages/lib/src/chip/Chip.stories.tsx | 141 +++++++++++--------- packages/lib/src/chip/Chip.tsx | 97 +++++++------- packages/lib/src/chip/types.ts | 46 ++++--- 4 files changed, 186 insertions(+), 141 deletions(-) diff --git a/packages/lib/src/action-icon/ActionIcon.tsx b/packages/lib/src/action-icon/ActionIcon.tsx index b4b521dcee..11330e4607 100644 --- a/packages/lib/src/action-icon/ActionIcon.tsx +++ b/packages/lib/src/action-icon/ActionIcon.tsx @@ -20,6 +20,7 @@ const ActionIconContainer = styled.div< hasAction?: boolean; size: ActionIconPropTypes["size"]; disabled?: ActionIconPropTypes["disabled"]; + isAvatar?: boolean; } & React.AnchorHTMLAttributes<HTMLAnchorElement> >` position: relative; @@ -40,15 +41,12 @@ const ActionIconContainer = styled.div< color: inherit; outline: none; } - ${({ hasAction, disabled, size }) => + + ${({ hasAction, disabled, size, isAvatar }) => !disabled && hasAction && css` cursor: pointer; - &:hover > div:first-child > div:first-child, - &:active > div:first-child > div:first-child { - display: block; - } &:focus:enabled > div:first-child, &:active:enabled > div:first-child { outline-style: solid; @@ -59,15 +57,37 @@ const ActionIconContainer = styled.div< &:focus-visible:enabled { outline: none; } + ${isAvatar + ? css` + &:hover > div:first-child > div:first-child, + &:active > div:first-child > div:first-child { + display: block; + } + ` + : css` + &:hover > div:first-child, + &:active > div:first-child { + background-color: var(--color-bg-alpha-light); + } + `} `} - ${({ disabled }) => + + ${({ disabled, isAvatar }) => disabled && css` cursor: not-allowed; - & > div:first-child > div:first-child { - display: block; - background-color: rgba(255, 255, 255, 0.5); - } + ${isAvatar + ? css` + & > div:first-child > div:first-child { + display: block; + background-color: rgba(255, 255, 255, 0.5); + } + ` + : css` + & > div:first-child > div:first-child { + color: var(--color-fg-neutral-medium); + } + `} `} `; @@ -155,9 +175,10 @@ const ForwardedActionIcon = forwardRef<RefType, ActionIconPropTypes>( aria-label={(onClick || linkHref) && (ariaLabel || title || "Action Icon")} disabled={disabled} ref={ref} + isAvatar={color !== "transparent"} > <ActionIconWrapper shape={shape} color={color} size={size}> - {(!!onClick || !!linkHref) && <Overlay aria-hidden="true" />} + {color !== "transparent" && (!!onClick || !!linkHref || disabled) && <Overlay aria-hidden="true" />} {content ? ( content ) : ( diff --git a/packages/lib/src/chip/Chip.stories.tsx b/packages/lib/src/chip/Chip.stories.tsx index be5c066550..bee450f3fb 100644 --- a/packages/lib/src/chip/Chip.stories.tsx +++ b/packages/lib/src/chip/Chip.stories.tsx @@ -2,7 +2,6 @@ import Title from "../../.storybook/components/Title"; import ExampleContainer from "../../.storybook/components/ExampleContainer"; import DxcChip from "./Chip"; import { Meta, StoryObj } from "@storybook/react-vite"; -import { userEvent } from "storybook/internal/test"; import { useEffect } from "react"; export default { @@ -57,61 +56,73 @@ const Chip = () => ( <> <ExampleContainer> <Title title="Basic chip" theme="light" level={4} /> - <DxcChip label="Default Chip with lots of characteres" /> + <DxcChip label="Basic Chip" /> </ExampleContainer> + + <Title title="Sizes" theme="light" level={2} /> + <ExampleContainer> + <Title title="Small" theme="light" level={4} /> + <DxcChip label="Small" size="small" /> + </ExampleContainer> + <ExampleContainer> + <Title title="Medium" theme="light" level={4} /> + <DxcChip label="Medium" /> + </ExampleContainer> + <ExampleContainer> + <Title title="Large" theme="light" level={4} /> + <DxcChip label="Large" size="large" /> + </ExampleContainer> + + <Title title="Variants" theme="light" level={2} /> <ExampleContainer> - <Title title="Chip with prefix SVG (small icon)" theme="light" level={4} /> - <DxcChip label="Chip with prefix" prefixIcon={smallIconSVG} /> + <Title title="Chip with prefix SVG" theme="light" level={4} /> + <DxcChip label="Chip with prefix SVG" prefix={smallIconSVG} /> </ExampleContainer> <ExampleContainer> - <Title title="Chip with suffix SVG (large icon)" theme="light" level={4} /> - <DxcChip label="Chip with suffix" suffixIcon={iconSVG} /> + <Title title="Chip with prefix icon" theme="light" level={4} /> + <DxcChip label="Chip with prefix icon" prefix="settings" /> </ExampleContainer> <ExampleContainer> - <Title title="Chip with Avatar" theme="light" level={4} /> - <DxcChip label="Default Chip with lots of characteres" size="small" avatar={{ color: "primary" }} /> + <Title title="Chip with prefix Avatar" theme="light" level={4} /> + <DxcChip label="Chip with prefix Avatar" prefix={{ color: "primary" }} /> </ExampleContainer> <ExampleContainer> - <Title title="Chip with prefix (SVG) and suffix (URL)" theme="light" level={4} /> - <DxcChip label="Chip with prefix and suffix" prefixIcon={iconSVG} suffixIcon="filled_check_circle" /> + <Title title="Chip with action SVG" theme="light" level={4} /> + <DxcChip label="Chip with action SVG" action={{ icon: iconSVG, onClick: () => console.log("action clicked") }} /> </ExampleContainer> <ExampleContainer> - <Title title="Disabled chip" theme="light" level={4} /> - <DxcChip label="Disabled" disabled prefixIcon={iconSVG} suffixIcon="filled_check_circle" /> + <Title title="Chip with prefix (SVG) and action (URL)" theme="light" level={4} /> + <DxcChip + label="Chip with prefix and action" + prefix={iconSVG} + action={{ icon: "filled_check_circle", onClick: () => console.log("action clicked") }} + /> </ExampleContainer> <ExampleContainer> <Title title="Chip with ellipsis" theme="light" level={4} /> - <div style={{ width: "200px" }}> - <DxcChip label="With ellipsis asdfasdf asdf asdfasdf asdf asdfasdf asdfasdf asdf asdf adfasrfasf afsdg afgasfg asdf asdf asdf asdf asdf asdf asdf afdg asfg asdfg asdf asdf asdf asdfasdf asd fas df asd asdf asdf asdfasd fg ssssssssssss ssss" /> - </div> + <DxcChip label="With ellipsis asdfasdf asdf asdfasdf asdf asdfasdf asdfasdf asdf asdf adfasrfasf afsdg afgasfg asdf asdf asdf asdf asdf asdf asdf afdg asfg asdfg asdf asdf asdf asdfasdf asd fas df asd asdf asdf asdfasd fg ssssssssssss ssss" /> </ExampleContainer> <ExampleContainer> - <Title title="Chip with ellipsis and suffix" theme="light" level={4} /> - <div style={{ width: "200px" }}> - <DxcChip - suffixIcon={iconSVG} - label="With ellipsis asdfasdf asdf asdfasdf asdf asdfasdf asdfasdf asdf asdf adfasrfasf afsdg afgasfg asdf asdf asdf asdf asdf asdf asdf afdg asfg asdfg asdf asdf asdf asdfasdf asd fas df asd asdf asdf asdfasd fgsss" - /> - </div> + <Title title="Chip with ellipsis and action" theme="light" level={4} /> + <DxcChip + action={{ icon: iconSVG, onClick: () => console.log("action clicked") }} + label="With ellipsis asdfasdf asdf asdfasdf asdf asdfasdf asdfasdf asdf asdf adfasrfasf afsdg afgasfg asdf asdf asdf asdf asdf asdf asdf afdg asfg asdfg asdf asdf asdf asdfasdf asd fas df asd asdf asdf asdfasd fgsss" + /> </ExampleContainer> <ExampleContainer> <Title title="Chip with ellipsis and prefix" theme="light" level={4} /> - <div style={{ width: "200px" }}> - <DxcChip - prefixIcon={iconSVG} - label="With ellipsis asdfasdf asdf asdfasdf asdf asdfasdf asdfasdf asdf asdf adfasrfasf afsdg afgasfg asdf asdf asdf asdf asdf asdf asdf afdg asfg asdfg asdf asdf asdf asdfasdf asd fas df asd asdf asdf asdfasd fgsss" - /> - </div> - </ExampleContainer> - <ExampleContainer> - <Title title="Chip with ellipsis, suffix and prefix" theme="light" level={4} /> - <div style={{ width: "200px" }}> - <DxcChip - prefixIcon={iconSVG} - suffixIcon={iconSVG} - label="With ellipsis asdfasdf asdf asdfasdf asdf asdfasdf asdfasdf asdf asdf adfasrfasf afsdg afgasfg asdf asdf asdf asdf asdf asdf asdf afdg asfg asdfg asdf asdf asdf asdfasdf asd fas df asd asdf asdf asdfasdf" - /> - </div> + <DxcChip + prefix={iconSVG} + label="With ellipsis asdfasdf asdf asdfasdf asdf asdfasdf asdfasdf asdf asdf adfasrfasf afsdg afgasfg asdf asdf asdf asdf asdf asdf asdf afdg asfg asdfg asdf asdf asdf asdfasdf asd fas df asd asdf asdf asdfasd fgsss" + /> + </ExampleContainer> + <ExampleContainer> + <Title title="Chip with ellipsis, action and prefix" theme="light" level={4} /> + <DxcChip + prefix={iconSVG} + action={{ icon: iconSVG, onClick: () => console.log("action clicked") }} + label="With ellipsis asdfasdf asdf asdfasdf asdf asdfasdf asdfasdf asdf asdf adfasrfasf afsdg afgasfg asdf asdf asdf asdf asdf asdf asdf afdg asfg asdfg asdf asdf asdf asdfasdf asd fas df asd asdf asdf asdfasdf" + /> </ExampleContainer> <Title title="Margins" theme="light" level={2} /> <ExampleContainer> @@ -145,18 +156,34 @@ const Chip = () => ( </> ); -const ChipPrefixFocused = () => ( - <ExampleContainer> - <Title title="Chip with prefix" theme="light" level={4} /> - <DxcChip label="Chip with prefix" prefixIcon={iconSVG} onClickPrefix={() => {}} /> - </ExampleContainer> -); - -const ChipSuffixFocused = () => ( - <ExampleContainer> - <Title title="Chip with suffix" theme="light" level={4} /> - <DxcChip label="Chip with suffix" suffixIcon="filled_delete" onClickSuffix={() => {}} /> - </ExampleContainer> +const ChipActionStates = () => ( + <> + <ExampleContainer> + <Title title="Default" theme="light" level={4} /> + <DxcChip label="Default" action={{ icon: "filled_delete", onClick: () => {} }} prefix={{ color: "primary" }} /> + </ExampleContainer> + <ExampleContainer pseudoState="pseudo-hover"> + <Title title="Hover" theme="light" level={4} /> + <DxcChip label="Hover" action={{ icon: "filled_delete", onClick: () => {} }} prefix={{ color: "primary" }} /> + </ExampleContainer> + <ExampleContainer pseudoState={["pseudo-focus", "pseudo-focus-within"]}> + <Title title="Focus" theme="light" level={4} /> + <DxcChip label="Focus" action={{ icon: "filled_delete", onClick: () => {} }} prefix={{ color: "primary" }} /> + </ExampleContainer> + <ExampleContainer pseudoState="pseudo-active"> + <Title title="Active" theme="light" level={4} /> + <DxcChip label="Active" action={{ icon: "filled_delete", onClick: () => {} }} prefix={{ color: "primary" }} /> + </ExampleContainer> + <ExampleContainer> + <Title title="Disabled" theme="light" level={4} /> + <DxcChip + label="Disabled" + action={{ icon: "filled_delete", onClick: () => {} }} + prefix={{ color: "primary" }} + disabled + /> + </ExampleContainer> + </> ); type Story = StoryObj<typeof DxcChip>; @@ -165,16 +192,6 @@ export const Chromatic: Story = { render: Chip, }; -export const PrefixFocused: Story = { - render: ChipPrefixFocused, - play: async () => { - await userEvent.tab(); - }, -}; - -export const SuffixFocused: Story = { - render: ChipSuffixFocused, - play: async () => { - await userEvent.tab(); - }, +export const ActionStates: Story = { + render: ChipActionStates, }; diff --git a/packages/lib/src/chip/Chip.tsx b/packages/lib/src/chip/Chip.tsx index 7896170f50..56aa3413a4 100644 --- a/packages/lib/src/chip/Chip.tsx +++ b/packages/lib/src/chip/Chip.tsx @@ -1,9 +1,11 @@ import styled from "@emotion/styled"; import { spaces } from "../common/variables"; import DxcIcon from "../icon/Icon"; -import ChipPropsType from "./types"; +import ChipPropsType, { ChipAvatarType } from "./types"; import DxcActionIcon from "../action-icon/ActionIcon"; import DxcAvatar from "../avatar/Avatar"; +import { isValidElement } from "react"; +import { Tooltip } from "../tooltip/Tooltip"; const Chip = styled.div<{ margin: ChipPropsType["margin"]; @@ -38,7 +40,7 @@ const LabelContainer = styled.span<{ disabled: ChipPropsType["disabled"] }>` font-size: var(--typography-label-s); font-family: var(--typography-font-family); font-weight: var(--typography-label-regular); - color: ${({ disabled }) => (disabled ? "var(--color-fg-neutral-medium)" : "var(--color-fg-neutral-dark)")}; + color: ${({ disabled }) => (disabled ? "var(--color-fg-neutral-lightest)" : "var(--color-fg-neutral-dark)")}; text-overflow: ellipsis; white-space: nowrap; overflow: hidden; @@ -58,55 +60,46 @@ const IconContainer = styled.div<{ } `; -const DxcChip = ({ - avatar, - label, - suffixIcon, - prefixIcon, - onClickSuffix, - onClickPrefix, - disabled = false, - margin, - size = "medium", - tabIndex = 0, -}: ChipPropsType) => ( - <Chip disabled={disabled} margin={margin} size={size}> - {avatar && size != "small" ? ( - <DxcAvatar {...avatar} size="xsmall" /> - ) : ( - prefixIcon && - (typeof onClickPrefix === "function" ? ( - <DxcActionIcon - size="xsmall" - disabled={disabled} - icon={prefixIcon} - onClick={onClickPrefix} - tabIndex={tabIndex} - title={!disabled ? "Prefix Action" : undefined} - /> - ) : ( - <IconContainer disabled={disabled}> - {typeof prefixIcon === "string" ? <DxcIcon icon={prefixIcon} /> : prefixIcon} - </IconContainer> - )) - )} - {label && <LabelContainer disabled={disabled}>{label}</LabelContainer>} - {suffixIcon && - (typeof onClickSuffix === "function" ? ( - <DxcActionIcon - size="xsmall" - disabled={disabled} - icon={suffixIcon} - onClick={onClickSuffix} - tabIndex={tabIndex} - title={!disabled ? "Suffix Action" : undefined} - /> - ) : ( - <IconContainer disabled={disabled}> - {typeof suffixIcon === "string" ? <DxcIcon icon={suffixIcon} /> : suffixIcon} - </IconContainer> - ))} - </Chip> -); +const DxcChip = ({ action, disabled = false, label, margin, prefix, size = "medium", tabIndex = 0 }: ChipPropsType) => { + const isAvatarPrefix = (prefix: ChipPropsType["prefix"]): prefix is ChipAvatarType => + typeof prefix === "object" && prefix !== null && "color" in prefix; + + return ( + <Tooltip label={label.length > 14 ? label : undefined}> + <Chip disabled={disabled} margin={margin} size={size}> + {prefix && + (isAvatarPrefix(prefix) && size !== "small" ? ( + <DxcAvatar + color={prefix.color} + label={prefix.profileName} + icon={prefix.icon} + imageSrc={prefix.imageSrc} + size="xsmall" + disabled={disabled} + /> + ) : typeof prefix === "string" ? ( + <IconContainer disabled={disabled}> + <DxcIcon icon={prefix} /> + </IconContainer> + ) : ( + isValidElement(prefix) && <IconContainer disabled={disabled}>{prefix}</IconContainer> + ))} + + {label && <LabelContainer disabled={disabled}>{label}</LabelContainer>} + + {action && ( + <DxcActionIcon + size="xsmall" + disabled={disabled} + icon={action.icon} + onClick={action.onClick} + tabIndex={tabIndex} + title={!disabled ? action.title : undefined} + /> + )} + </Chip> + </Tooltip> + ); +}; export default DxcChip; diff --git a/packages/lib/src/chip/types.ts b/packages/lib/src/chip/types.ts index 581a1f0a87..1fb48660c0 100644 --- a/packages/lib/src/chip/types.ts +++ b/packages/lib/src/chip/types.ts @@ -2,9 +2,34 @@ import { Margin, SVG, Space } from "../common/utils"; import AvatarProps from "../avatar/types"; type Size = "small" | "medium" | "large"; +export type ChipAvatarType = { + color?: AvatarProps["color"]; + profileName?: AvatarProps["label"]; + imageSrc?: AvatarProps["imageSrc"]; + icon?: AvatarProps["icon"]; +}; +type Action = { + /** + * Icon to be placed in the action. + */ + icon: string | SVG; + /** + * This function will be called when the user clicks the action. + */ + onClick: () => void; + /** + * Text representing advisory information related + * to the button's action. Under the hood, this prop also serves + * as an accessible label for the component. + */ + title?: string; +}; type Props = { - avatar?: AvatarProps; + /** + * Action to be displayed on the right side of the chip after the label. + */ + action?: Action; /** * If true, the component will be disabled. */ @@ -12,32 +37,21 @@ type Props = { /** * Text to be placed on the chip. */ - label?: string; + label: string; /** * Size of the margin to be applied to the component ('xxsmall' | 'xsmall' | 'small' | 'medium' | 'large' | 'xlarge' | 'xxlarge'). * You can pass an object with 'top', 'bottom', 'left' and 'right' properties in order to specify different margin sizes. */ margin?: Space | Margin; /** - * This function will be called when the prefix is clicked. - */ - onClickPrefix?: () => void; - /** - * This function will be called when the suffix is clicked. - */ - onClickSuffix?: () => void; - /** - * Element or path used as icon to be placed before the chip label. + * Element, path or avatar used as icon to be placed before the chip label. */ - prefixIcon?: string | SVG; + prefix?: string | SVG | ChipAvatarType; /** * Size of the component. */ size?: Size; - /** - * Element or path used as icon to be placed after the chip label. - */ - suffixIcon?: string | SVG; + /** * Value of the tabindex attribute. */ From 28f72b87cac88690748261f015f261284660d1b0 Mon Sep 17 00:00:00 2001 From: Pelayo Felgueroso <pfelguerosogalguera@gmail.com> Date: Thu, 15 Jan 2026 16:13:22 +0100 Subject: [PATCH 3/3] Add tests --- packages/lib/src/chip/Chip.test.tsx | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/packages/lib/src/chip/Chip.test.tsx b/packages/lib/src/chip/Chip.test.tsx index e324487347..c81230be54 100644 --- a/packages/lib/src/chip/Chip.test.tsx +++ b/packages/lib/src/chip/Chip.test.tsx @@ -6,16 +6,24 @@ describe("Chip component tests", () => { const { getByText } = render(<DxcChip label="Chip" />); expect(getByText("Chip")).toBeTruthy(); }); - test("Calls correct function when clicking on prefix icon", () => { - const onClick = jest.fn(); - const { getByText, getByRole } = render(<DxcChip label="Chip" prefixIcon="nutrition" onClickPrefix={onClick} />); - expect(getByText("Chip")).toBeTruthy(); - fireEvent.click(getByRole("button")); - expect(onClick).toHaveBeenCalled(); + test("Chip renders correctly with prefix icon", () => { + const { getByRole } = render(<DxcChip label="Chip" prefix="nutrition" />); + const avatar = getByRole("img", { hidden: true }); + expect(avatar).toBeTruthy(); + }); + test("Chip renders correctly with avatar", () => { + const { getByRole } = render(<DxcChip label="Chip" prefix={{ color: "primary" }} />); + const avatar = getByRole("img", { hidden: true }); + expect(avatar).toBeTruthy(); + }); + test("Chip doesn't render avatar when size is small", () => { + const { queryByRole } = render(<DxcChip label="Chip" prefix={{ color: "primary" }} size="small" />); + const avatar = queryByRole("img", { hidden: true }); + expect(avatar).not.toBeTruthy(); }); - test("Calls correct function when clicking on suffix icon", () => { + test("Calls correct function when clicking on action icon", () => { const onClick = jest.fn(); - const { getByText, getByRole } = render(<DxcChip label="Chip" suffixIcon="nutrition" onClickSuffix={onClick} />); + const { getByText, getByRole } = render(<DxcChip label="Chip" action={{ icon: "nutrition", onClick: onClick }} />); expect(getByText("Chip")).toBeTruthy(); fireEvent.click(getByRole("button")); expect(onClick).toHaveBeenCalled();