From f622f0adcf7642817514d762421bb1b6a8d04b16 Mon Sep 17 00:00:00 2001 From: jialecl Date: Wed, 5 Aug 2026 11:41:36 +0200 Subject: [PATCH 1/3] Exposing ref in Avatar, badge, button, card and chip to improve popover compatibility --- .../components/avatar/code/AvatarCodePage.tsx | 14 ++ .../components/badge/code/BadgeCodePage.tsx | 14 ++ .../components/button/code/ButtonCodePage.tsx | 14 ++ .../components/card/code/CardCodePage.tsx | 13 + .../components/chip/code/ChipCodePage.tsx | 13 + packages/lib/src/avatar/Avatar.tsx | 228 ++++++++++-------- packages/lib/src/avatar/types.ts | 28 +++ packages/lib/src/badge/Badge.tsx | 53 ++-- packages/lib/src/badge/types.ts | 4 + packages/lib/src/button/Button.tsx | 76 +++--- packages/lib/src/button/types.ts | 4 + packages/lib/src/card/Card.tsx | 5 +- packages/lib/src/card/types.ts | 4 + packages/lib/src/chip/Chip.tsx | 103 ++++---- packages/lib/src/chip/types.ts | 4 + 15 files changed, 367 insertions(+), 210 deletions(-) diff --git a/apps/website/screens/components/avatar/code/AvatarCodePage.tsx b/apps/website/screens/components/avatar/code/AvatarCodePage.tsx index 1d55b83a54..1c6722f879 100644 --- a/apps/website/screens/components/avatar/code/AvatarCodePage.tsx +++ b/apps/website/screens/components/avatar/code/AvatarCodePage.tsx @@ -7,6 +7,7 @@ import basicUsage from "./examples/basicUsage"; import clickable from "./examples/clickable"; import tooltip from "./examples/tooltip"; import status from "./examples/status"; +import StatusBadge from "@/common/StatusBadge"; const statusTypeString = `{ mode: 'default' | 'info' | @@ -100,6 +101,19 @@ const sections = [ Text to be displayed as label next to the avatar. - + + + + + ref + + + + {"React.Ref"} + + Ref to the avatar element. + - + secondaryText diff --git a/apps/website/screens/components/badge/code/BadgeCodePage.tsx b/apps/website/screens/components/badge/code/BadgeCodePage.tsx index 0c8b3be9b8..b75be17c6c 100644 --- a/apps/website/screens/components/badge/code/BadgeCodePage.tsx +++ b/apps/website/screens/components/badge/code/BadgeCodePage.tsx @@ -6,6 +6,7 @@ import Example from "@/common/example/Example"; import contextual from "./examples/contextual"; import notification from "./examples/notification"; import icons from "./examples/icons"; +import StatusBadge from "@/common/StatusBadge"; const sections = [ { @@ -79,6 +80,19 @@ const sections = [ 99 + + + + + ref + + + + {"React.Ref"} + + Ref to the badge element. + - + size diff --git a/apps/website/screens/components/button/code/ButtonCodePage.tsx b/apps/website/screens/components/button/code/ButtonCodePage.tsx index 0e0217718c..a90dd39834 100644 --- a/apps/website/screens/components/button/code/ButtonCodePage.tsx +++ b/apps/website/screens/components/button/code/ButtonCodePage.tsx @@ -7,6 +7,7 @@ import semantic from "./examples/semantic"; import icons from "./examples/icons"; import sizes from "./examples/sizes"; import Code, { TableCode } from "@/common/Code"; +import StatusBadge from "@/common/StatusBadge"; const sections = [ { @@ -94,6 +95,19 @@ const sections = [ This function will be called when the user clicks the button. - + + + + + ref + + + + {"React.Ref"} + + Ref to the button element. + - + semantic diff --git a/apps/website/screens/components/card/code/CardCodePage.tsx b/apps/website/screens/components/card/code/CardCodePage.tsx index e1977c8bee..8a22a21641 100644 --- a/apps/website/screens/components/card/code/CardCodePage.tsx +++ b/apps/website/screens/components/card/code/CardCodePage.tsx @@ -264,6 +264,19 @@ const sections = [ false + + + + + ref + + + + {"React.Ref"} + + Ref to the card element. + - + diff --git a/apps/website/screens/components/chip/code/ChipCodePage.tsx b/apps/website/screens/components/chip/code/ChipCodePage.tsx index 6681764bce..7d2ea1e4c0 100644 --- a/apps/website/screens/components/chip/code/ChipCodePage.tsx +++ b/apps/website/screens/components/chip/code/ChipCodePage.tsx @@ -128,6 +128,19 @@ const sections = [ - + + + + + ref + + + + {"React.Ref"} + + Ref to the chip element. + - + diff --git a/packages/lib/src/avatar/Avatar.tsx b/packages/lib/src/avatar/Avatar.tsx index 01d3dffe66..c66929c2d4 100644 --- a/packages/lib/src/avatar/Avatar.tsx +++ b/packages/lib/src/avatar/Avatar.tsx @@ -1,5 +1,5 @@ -import { memo, useCallback, useMemo, useState } from "react"; -import AvatarPropsType from "./types"; +import { forwardRef, useCallback, useMemo, useState } from "react"; +import AvatarPropsType, { ContentType, LabelWrapperType } from "./types"; import { getBorderWidth, getFontSize, getInitials, getModeColor } from "./utils"; import DxcTypography from "../typography/Typography"; import DxcImage from "../image/Image"; @@ -63,107 +63,145 @@ const StatusContainer = styled.div<{ background-color: ${({ status }) => getModeColor(status!.mode)}; `; -const DxcAvatar = memo( - ({ - color = "neutral", - disabled = false, - icon = "person", - imageSrc, - label, - linkHref, - onClick, - primaryText, - secondaryText, - shape = "circle", - size = "medium", - status, - tabIndex = 0, - title, - }: AvatarPropsType) => { +const content = ({ + hasAction, + onClick, + linkHref, + disabled, + imageSrc, + error, + handleError, + label, + title, + size, + initials, + icon, + color, + status, +}: ContentType) => ( + <> + + {(!!onClick || !!linkHref || disabled) && + {status && } + +); + +const LabelWrapper = ({ condition, children, primaryText, secondaryText }: LabelWrapperType) => + condition ? ( + + {children} + + {primaryText && ( + + {primaryText} + + )} + {secondaryText && ( + + {secondaryText} + + )} + + + ) : ( + <>{children} + ); + +const DxcAvatar = forwardRef( + ( + { + color = "neutral", + disabled = false, + icon = "person", + imageSrc, + label, + linkHref, + onClick, + primaryText, + secondaryText, + shape = "circle", + size = "medium", + status, + tabIndex = 0, + title, + }: AvatarPropsType, + ref + ) => { const [error, setError] = useState(false); const initials = useMemo(() => getInitials(label), [label]); const handleError = useCallback(() => setError(true), []); const hasAction = !disabled && (!!onClick || !!linkHref); - const content = ( - <> - - {(!!onClick || !!linkHref || disabled) && - {status && } - - ); - - const LabelWrapper = ({ condition, children }: { condition: boolean; children: React.ReactNode }) => - condition ? ( - - {children} - - {primaryText && ( - - {primaryText} - - )} - {secondaryText && ( - - {secondaryText} - - )} - - - ) : ( - <>{children} - ); - return ( - + ; /** * Text to be displayed as sublabel next to the avatar. */ @@ -69,4 +73,28 @@ type Props = { title?: string; }; +export type LabelWrapperType = { + condition: boolean; + children: React.ReactNode; + primaryText?: string; + secondaryText?: string; +}; + +export type ContentType = { + hasAction: boolean; + onClick?: () => void; + linkHref?: string; + disabled?: boolean; + imageSrc?: string; + error: boolean; + handleError: () => void; + label?: string; + title?: string; + size: Size; + initials: string; + icon?: string | SVG; + color: Color; + status?: Status; +}; + export default Props; diff --git a/packages/lib/src/badge/Badge.tsx b/packages/lib/src/badge/Badge.tsx index cdcb0e8926..85856a2c53 100644 --- a/packages/lib/src/badge/Badge.tsx +++ b/packages/lib/src/badge/Badge.tsx @@ -2,6 +2,7 @@ import styled from "@emotion/styled"; import BadgePropsType from "./types"; import DxcIcon from "../icon/Icon"; import { Tooltip } from "../tooltip/Tooltip"; +import { forwardRef } from "react"; const contextualColorMap = { primary: { @@ -136,33 +137,31 @@ const Label = styled.span<{ size: BadgePropsType["size"] }>` line-height: normal; `; -const DxcBadge = ({ - label, - title, - mode = "contextual", - color = "neutral", - icon, - notificationLimit = 99, - size = "medium", -}: BadgePropsType): JSX.Element => ( - - - {mode === "contextual" && icon && ( - {typeof icon === "string" ? : icon} - )} - {label && ( - - )} - - +const DxcBadge = forwardRef( + ( + { label, title, mode = "contextual", color = "neutral", icon, notificationLimit = 99, size = "medium" }, + ref + ): JSX.Element => ( + + + {mode === "contextual" && icon && ( + {typeof icon === "string" ? : icon} + )} + {label && ( + + )} + + + ) ); export default DxcBadge; diff --git a/packages/lib/src/badge/types.ts b/packages/lib/src/badge/types.ts index d9c3b7c59b..0d09681537 100644 --- a/packages/lib/src/badge/types.ts +++ b/packages/lib/src/badge/types.ts @@ -47,6 +47,10 @@ export type CommonProps = { * Material Symbol name or SVG element used as the icon that will be placed next to the badge label in contextual mode. */ icon?: string | SVG; + /** + * Ref to the badge element. + */ + ref?: React.Ref; /** * Size of the component. */ diff --git a/packages/lib/src/button/Button.tsx b/packages/lib/src/button/Button.tsx index 9c3f86bd80..d510ebea7d 100644 --- a/packages/lib/src/button/Button.tsx +++ b/packages/lib/src/button/Button.tsx @@ -1,4 +1,5 @@ import styled from "@emotion/styled"; +import { forwardRef } from "react"; import { spaces } from "../common/variables"; import ButtonPropsType, { Mode, Semantic, Size } from "./types"; import DxcIcon from "../icon/Icon"; @@ -66,38 +67,49 @@ const IconContainer = styled.div<{ } `; -const DxcButton = ({ - disabled, - icon, - iconPosition = "before", - label, - margin, - mode = "primary", - onClick, - semantic = "default", - size = { height: "large", width: "fitContent" }, - tabIndex = 0, - title, - type = "button", -}: ButtonPropsType): JSX.Element => ( - - - +// add ref and useRef to forward the ref to the button element +const DxcButton = forwardRef( + ( + { + disabled, + icon, + iconPosition = "before", + label, + margin, + mode = "primary", + onClick, + semantic = "default", + size = { height: "large", width: "fitContent" }, + tabIndex = 0, + title, + type = "button", + }, + ref + ): JSX.Element => { + return ( + + + + ); + } ); export default DxcButton; diff --git a/packages/lib/src/button/types.ts b/packages/lib/src/button/types.ts index c656b4a3b6..95d6b00939 100644 --- a/packages/lib/src/button/types.ts +++ b/packages/lib/src/button/types.ts @@ -55,6 +55,10 @@ type Props = { * You can pass an object with 'top', 'bottom', 'left' and 'right' properties in order to specify different margin sizes. */ margin?: Space | Margin; + /** + * Ref to the button element. + */ + ref?: React.Ref; /** * Size of the component. */ diff --git a/packages/lib/src/card/Card.tsx b/packages/lib/src/card/Card.tsx index 8591392ccc..2c7fb92bfe 100644 --- a/packages/lib/src/card/Card.tsx +++ b/packages/lib/src/card/Card.tsx @@ -106,7 +106,7 @@ const DxcCard = forwardRef( size = { width: "fitContent", height: "fitContent" }, tabIndex = 0, }: CardPropsType, - ref: Ref + ref: Ref ) => { const isInteractive = !!(onClick || onSelectionChange) || selectable; const [internalSelected, setInternalSelected] = useState( @@ -170,7 +170,7 @@ const DxcCard = forwardRef( onClick={(event) => { handleEvent(event, onClick); }} - ref={ref} + ref={ref as Ref} onKeyDown={(event) => { handleEvent(event, onClick); }} @@ -201,6 +201,7 @@ const DxcCard = forwardRef( handleEvent(event, onClick, onSelectionChange, internalSelected, setInternalSelected, selected, selectable); }} aria-checked={selectable ? internalSelected : undefined} + ref={ref as Ref} > {image && ( diff --git a/packages/lib/src/card/types.ts b/packages/lib/src/card/types.ts index dbb85e5f1c..4766177dda 100644 --- a/packages/lib/src/card/types.ts +++ b/packages/lib/src/card/types.ts @@ -73,6 +73,10 @@ type Props = { * Callback function that is called when the card is clicked. */ onClick?: (event: React.MouseEvent | React.KeyboardEvent) => void; + /** + * Ref to the card element. + */ + ref?: React.Ref; /** * If true, the card can be selected. When the card is clicked, the onSelectionChange callback will be called with the new selected state of the card. * If href is defined, the card won't be selectable, even if this prop is true. diff --git a/packages/lib/src/chip/Chip.tsx b/packages/lib/src/chip/Chip.tsx index dc758ab540..e5dd0adce3 100644 --- a/packages/lib/src/chip/Chip.tsx +++ b/packages/lib/src/chip/Chip.tsx @@ -3,7 +3,7 @@ import DxcIcon from "../icon/Icon"; import ChipPropsType, { ChipAvatarType } from "./types"; import DxcActionIcon from "../action-icon/ActionIcon"; import DxcAvatar from "../avatar/Avatar"; -import { isValidElement, ReactNode, useState } from "react"; +import { forwardRef, isValidElement, ReactNode, useState } from "react"; import { SVG } from "../common/utils"; import { getChipStyles } from "./utils"; @@ -87,60 +87,55 @@ const renderPrefix = (prefix: string | SVG | ChipAvatarType | undefined, disable return isValidElement(prefix) ? {prefix} : undefined; }; -const DxcChip = ({ - disabled = false, - label, - mode = "selectable", - onClick, - prefix, - selected, - tabIndex = 0, -}: ChipPropsType) => { - const [innerSelected, setInnerSelected] = useState(false); - - if (mode === "selectable" && isAvatarType(prefix) && !label) { - return null; - } +const DxcChip = forwardRef( + ({ disabled = false, label, mode = "selectable", onClick, prefix, selected, tabIndex = 0 }, ref) => { + const [innerSelected, setInnerSelected] = useState(false); - const handleSelectableClick = () => { - if (selected == null) { - setInnerSelected((prev) => !prev); + if (mode === "selectable" && isAvatarType(prefix) && !label) { + return null; } - onClick?.(); - }; - - const isSelected = selected ?? innerSelected; - - return ( - - - {prefix && renderPrefix(prefix, disabled)} - {label && {label}} - - - {mode === "dismissible" && ( - - )} - - ); -}; + + const handleSelectableClick = () => { + if (selected == null) { + setInnerSelected((prev) => !prev); + } + onClick?.(); + }; + + const isSelected = selected ?? innerSelected; + + return ( + + + {prefix && renderPrefix(prefix, disabled)} + {label && {label}} + + + {mode === "dismissible" && ( + + )} + + ); + } +); export default DxcChip; diff --git a/packages/lib/src/chip/types.ts b/packages/lib/src/chip/types.ts index 24b9db08a2..4cdf0c9a0b 100644 --- a/packages/lib/src/chip/types.ts +++ b/packages/lib/src/chip/types.ts @@ -13,6 +13,10 @@ type CommonProps = { * Function to be called when the chip is clicked or the dismiss action is triggered. */ onClick?: () => void; + /** + * Ref to the chip element. + */ + ref?: React.Ref; /** * Value of the tabindex attribute. */ From b16170fd1f3a02f2e0cdd61b3bccee0b9145114c Mon Sep 17 00:00:00 2001 From: jialecl Date: Wed, 5 Aug 2026 13:09:56 +0200 Subject: [PATCH 2/3] Removing ref from types files --- packages/lib/src/avatar/types.ts | 4 ---- packages/lib/src/badge/types.ts | 4 ---- packages/lib/src/button/types.ts | 4 ---- packages/lib/src/card/types.ts | 4 ---- packages/lib/src/chip/types.ts | 4 ---- 5 files changed, 20 deletions(-) diff --git a/packages/lib/src/avatar/types.ts b/packages/lib/src/avatar/types.ts index b80dd9db89..b481eeb7c1 100644 --- a/packages/lib/src/avatar/types.ts +++ b/packages/lib/src/avatar/types.ts @@ -42,10 +42,6 @@ type Props = { * Text to be displayed as label next to the avatar. */ primaryText?: string; - /** - * Ref to the avatar element. - */ - ref?: React.Ref; /** * Text to be displayed as sublabel next to the avatar. */ diff --git a/packages/lib/src/badge/types.ts b/packages/lib/src/badge/types.ts index 0d09681537..d9c3b7c59b 100644 --- a/packages/lib/src/badge/types.ts +++ b/packages/lib/src/badge/types.ts @@ -47,10 +47,6 @@ export type CommonProps = { * Material Symbol name or SVG element used as the icon that will be placed next to the badge label in contextual mode. */ icon?: string | SVG; - /** - * Ref to the badge element. - */ - ref?: React.Ref; /** * Size of the component. */ diff --git a/packages/lib/src/button/types.ts b/packages/lib/src/button/types.ts index 95d6b00939..c656b4a3b6 100644 --- a/packages/lib/src/button/types.ts +++ b/packages/lib/src/button/types.ts @@ -55,10 +55,6 @@ type Props = { * You can pass an object with 'top', 'bottom', 'left' and 'right' properties in order to specify different margin sizes. */ margin?: Space | Margin; - /** - * Ref to the button element. - */ - ref?: React.Ref; /** * Size of the component. */ diff --git a/packages/lib/src/card/types.ts b/packages/lib/src/card/types.ts index 4766177dda..dbb85e5f1c 100644 --- a/packages/lib/src/card/types.ts +++ b/packages/lib/src/card/types.ts @@ -73,10 +73,6 @@ type Props = { * Callback function that is called when the card is clicked. */ onClick?: (event: React.MouseEvent | React.KeyboardEvent) => void; - /** - * Ref to the card element. - */ - ref?: React.Ref; /** * If true, the card can be selected. When the card is clicked, the onSelectionChange callback will be called with the new selected state of the card. * If href is defined, the card won't be selectable, even if this prop is true. diff --git a/packages/lib/src/chip/types.ts b/packages/lib/src/chip/types.ts index 4cdf0c9a0b..24b9db08a2 100644 --- a/packages/lib/src/chip/types.ts +++ b/packages/lib/src/chip/types.ts @@ -13,10 +13,6 @@ type CommonProps = { * Function to be called when the chip is clicked or the dismiss action is triggered. */ onClick?: () => void; - /** - * Ref to the chip element. - */ - ref?: React.Ref; /** * Value of the tabindex attribute. */ From cb7c7b7d42226168cad98119e52ba814e64545a4 Mon Sep 17 00:00:00 2001 From: jialecl Date: Wed, 5 Aug 2026 13:20:49 +0200 Subject: [PATCH 3/3] Comment removed and added more context in card doc --- .../components/card/code/CardCodePage.tsx | 27 ++++++++++--------- packages/lib/src/button/Button.tsx | 1 - 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/apps/website/screens/components/card/code/CardCodePage.tsx b/apps/website/screens/components/card/code/CardCodePage.tsx index 8a22a21641..0fd8191a36 100644 --- a/apps/website/screens/components/card/code/CardCodePage.tsx +++ b/apps/website/screens/components/card/code/CardCodePage.tsx @@ -249,33 +249,36 @@ const sections = [ - selectable + ref - boolean - - - If true, the card can be selected. When the card is clicked, the onChange callback will be called with the - new selected state of the card. If href is defined, the card won't be selectable, even if - this prop is set to true. + {"React.Ref"} - false + Ref to the card element. The type is determined by whether the card is rendered as a div or an anchor + element based on the props such as href. + - - ref + selectable - {"React.Ref"} + boolean + + + If true, the card can be selected. When the card is clicked, the onChange callback will be called with the + new selected state of the card. If href is defined, the card won't be selectable, even if + this prop is set to true. + + + false - Ref to the card element. - - diff --git a/packages/lib/src/button/Button.tsx b/packages/lib/src/button/Button.tsx index d510ebea7d..962dac87d7 100644 --- a/packages/lib/src/button/Button.tsx +++ b/packages/lib/src/button/Button.tsx @@ -67,7 +67,6 @@ const IconContainer = styled.div<{ } `; -// add ref and useRef to forward the ref to the button element const DxcButton = forwardRef( ( {