diff --git a/apps/website/screens/components/dropdown/code/DropdownCodePage.tsx b/apps/website/screens/components/dropdown/code/DropdownCodePage.tsx index 919696722..db39ff7f6 100644 --- a/apps/website/screens/components/dropdown/code/DropdownCodePage.tsx +++ b/apps/website/screens/components/dropdown/code/DropdownCodePage.tsx @@ -184,6 +184,17 @@ const sections = [ 0 + + title + + string + + + Text representing advisory information related to the dropdown's trigger action. Under the hood, this + prop also serves as an accessible label for the component. + + - + diff --git a/apps/website/screens/components/heading/usage/HeadingUsagePage.tsx b/apps/website/screens/components/heading/usage/HeadingUsagePage.tsx index 8fc8359fe..93e8fcd15 100644 --- a/apps/website/screens/components/heading/usage/HeadingUsagePage.tsx +++ b/apps/website/screens/components/heading/usage/HeadingUsagePage.tsx @@ -39,7 +39,7 @@ const sections = [ <> - Use Heading componments from H1 to H5 when creating content + Use Heading components from H1 to H5 when creating content hierarchy in the page. diff --git a/apps/website/screens/principles/themes/ThemesPage.tsx b/apps/website/screens/principles/themes/ThemesPage.tsx index d01a1c452..52ef41982 100644 --- a/apps/website/screens/principles/themes/ThemesPage.tsx +++ b/apps/website/screens/principles/themes/ThemesPage.tsx @@ -978,6 +978,9 @@ const sections = [ Option font color listOptionFontColor +
+
+ listOptionIconColor diff --git a/packages/lib/src/HalstackContext.tsx b/packages/lib/src/HalstackContext.tsx index a326b9f80..d99ade814 100644 --- a/packages/lib/src/HalstackContext.tsx +++ b/packages/lib/src/HalstackContext.tsx @@ -255,6 +255,7 @@ const parseTheme = (theme: DeepPartial): AdvancedTheme => { selectTokens.labelFontColor = theme?.select?.fontColor ?? selectTokens.labelFontColor; selectTokens.helperTextFontColor = theme?.select?.fontColor ?? selectTokens.helperTextFontColor; selectTokens.listOptionFontColor = theme?.select?.optionFontColor ?? selectTokens.listOptionFontColor; + selectTokens.listOptionIconColor = theme?.select?.optionFontColor ?? selectTokens.listOptionIconColor; selectTokens.placeholderFontColor = addLightness(30, theme?.select?.fontColor) ?? selectTokens.placeholderFontColor; selectTokens.collapseIndicatorColor = theme?.select?.fontColor ?? selectTokens.collapseIndicatorColor; selectTokens.hoverInputBorderColor = theme?.select?.hoverBorderColor ?? selectTokens.hoverInputBorderColor; diff --git a/packages/lib/src/action-icon/ActionIcon.tsx b/packages/lib/src/action-icon/ActionIcon.tsx index df0da9a91..dcbbb7726 100644 --- a/packages/lib/src/action-icon/ActionIcon.tsx +++ b/packages/lib/src/action-icon/ActionIcon.tsx @@ -3,28 +3,26 @@ import ActionIconPropsTypes, { RefType } from "./types"; import styled from "styled-components"; import CoreTokens from "../common/coreTokens"; import DxcIcon from "../icon/Icon"; -import DxcTooltip from "../tooltip/Tooltip"; +import { Tooltip } from "../tooltip/Tooltip"; const DxcActionIcon = forwardRef( - ({ disabled = false, title, icon, onClick, tabIndex }, ref): JSX.Element => { - return ( - - { - event.stopPropagation(); - }} - tabIndex={tabIndex} - type="button" - ref={ref} - > - {typeof icon === "string" ? : icon} - - - ); - } + ({ disabled = false, title, icon, onClick, tabIndex }, ref): JSX.Element => ( + + { + event.stopPropagation(); + }} + tabIndex={tabIndex} + type="button" + ref={ref} + > + {typeof icon === "string" ? : icon} + + + ) ); const ActionIcon = styled.button` diff --git a/packages/lib/src/badge/Badge.tsx b/packages/lib/src/badge/Badge.tsx index 937c421ab..64895758f 100644 --- a/packages/lib/src/badge/Badge.tsx +++ b/packages/lib/src/badge/Badge.tsx @@ -1,9 +1,8 @@ import styled from "styled-components"; import BadgePropsType from "./types"; -import DxcFlex from "../flex/Flex"; import CoreTokens from "../common/coreTokens"; import DxcIcon from "../icon/Icon"; -import DxcTooltip from "../tooltip/Tooltip"; +import { Tooltip } from "../tooltip/Tooltip"; const contextualColorMap = { grey: { @@ -75,14 +74,6 @@ const sizeMap = { }, }; -const Label = ({ label, notificationLimit, size }) => { - return ( - - {typeof label === "number" ? (label > notificationLimit ? "+" + notificationLimit : label) : label} - - ); -}; - const DxcBadge = ({ label, title, @@ -91,28 +82,26 @@ const DxcBadge = ({ icon, notificationLimit = 99, size = "medium", -}: BadgePropsType): JSX.Element => { - return ( - - - {(mode === "contextual" && ( - - {icon && ( - {typeof icon === "string" ? : icon} - )} - - )) || - - ); -}; +}: BadgePropsType): JSX.Element => ( + + + {mode === "contextual" && icon && ( + {typeof icon === "string" ? : icon} + )} + {label && ( + + )} + + +); const getColor = (mode, color) => (mode === "contextual" ? contextualColorMap[color].text : CoreTokens.color_white); @@ -128,17 +117,18 @@ const BadgeContainer = styled.div<{ color: BadgePropsType["color"]; size: BadgePropsType["size"]; }>` - display: flex; - color: ${(props) => getColor(props.mode, props.color)}; - background-color: ${(props) => getBackgroundColor(props.mode, props.color)}; + box-sizing: border-box; border-radius: ${(props) => sizeMap[props.size].borderRadius}; + padding: ${(props) => (props.label ? getPadding(props.mode, props.size) : "")}; width: ${(props) => (!props.label && props.mode === "notification" ? sizeMap[props.size].width : "fit-content")}; min-width: ${(props) => props.mode === "notification" && sizeMap[props.size].minWidth}; height: ${(props) => sizeMap[props.size].height}; - padding: ${(props) => (props.label ? getPadding(props.mode, props.size) : "")}; + display: flex; align-items: center; + gap: ${CoreTokens.spacing_2}; justify-content: center; - box-sizing: border-box; + background-color: ${(props) => getBackgroundColor(props.mode, props.color)}; + color: ${(props) => getColor(props.mode, props.color)}; `; const IconContainer = styled.div<{ size: BadgePropsType["size"] }>` @@ -151,7 +141,7 @@ const IconContainer = styled.div<{ size: BadgePropsType["size"] }>` } `; -const LabelContainer = styled.span<{ size: BadgePropsType["size"] }>` +const Label = styled.span<{ size: BadgePropsType["size"] }>` font-family: ${CoreTokens.type_sans}; font-size: ${(props) => sizeMap[props.size].fontSize}; font-weight: ${CoreTokens.type_semibold}; diff --git a/packages/lib/src/button/Button.tsx b/packages/lib/src/button/Button.tsx index 95829d765..54a51db86 100644 --- a/packages/lib/src/button/Button.tsx +++ b/packages/lib/src/button/Button.tsx @@ -4,7 +4,7 @@ import { getMargin } from "../common/utils"; import useTheme from "../useTheme"; import ButtonPropsType from "./types"; import DxcIcon from "../icon/Icon"; -import DxcTooltip from "../tooltip/Tooltip"; +import { Tooltip } from "../tooltip/Tooltip"; const DxcButton = ({ label = "", @@ -24,7 +24,7 @@ const DxcButton = ({ return ( - + - + ); }; diff --git a/packages/lib/src/contextual-menu/ContextualMenu.stories.tsx b/packages/lib/src/contextual-menu/ContextualMenu.stories.tsx index 3716725a2..dc8b6904e 100644 --- a/packages/lib/src/contextual-menu/ContextualMenu.stories.tsx +++ b/packages/lib/src/contextual-menu/ContextualMenu.stories.tsx @@ -6,6 +6,7 @@ import DxcContainer from "../container/Container"; import useTheme from "../useTheme"; import DxcContextualMenu, { ContextualMenuContext } from "./ContextualMenu"; import SingleItem from "./SingleItem"; +import { userEvent, within } from "@storybook/test"; export default { title: "Contextual Menu", @@ -215,3 +216,19 @@ export const SingleItemStates = () => { ); }; + +const ItemWithEllipsis = () => ( + + + <DxcContainer width="300px"> + <DxcContextualMenu items={itemsWithTruncatedText} /> + </DxcContainer> + </ExampleContainer> +); + +export const ContextualMenuTooltip = ItemWithEllipsis.bind({}); +ContextualMenuTooltip.play = async ({ canvasElement }) => { + const canvas = within(canvasElement); + await userEvent.hover(canvas.getByText("Item with a very long label that should be truncated")); + await userEvent.hover(canvas.getByText("Item with a very long label that should be truncated")); +}; diff --git a/packages/lib/src/contextual-menu/ItemAction.tsx b/packages/lib/src/contextual-menu/ItemAction.tsx index ed488074d..d16921a3a 100644 --- a/packages/lib/src/contextual-menu/ItemAction.tsx +++ b/packages/lib/src/contextual-menu/ItemAction.tsx @@ -3,18 +3,7 @@ import styled from "styled-components"; import CoreTokens from "../common/coreTokens"; import { ItemActionProps } from "./types"; import DxcIcon from "../icon/Icon"; -import DxcTooltip from "../tooltip/Tooltip"; - -// TODO: The tooltip is not working fine, text-overflow is not ellipsis due to wrapper container. -const TooltipWrapper = ({ - condition, - children, - label, -}: { - condition: boolean; - children: React.ReactNode; - label: string; -}) => (condition ? <DxcTooltip label={label}>{children}</DxcTooltip> : <>{children}</>); +import { TooltipWrapper } from "../tooltip/Tooltip"; const ItemAction = ({ badge, collapseIcon, icon, label, depthLevel, ...props }: ItemActionProps) => { const [hasTooltip, setHasTooltip] = useState(false); @@ -28,10 +17,10 @@ const ItemAction = ({ badge, collapseIcon, icon, label, depthLevel, ...props }: {icon && depthLevel === 0 && <Icon>{typeof icon === "string" ? <DxcIcon icon={icon} /> : icon}</Icon>} <Text selected={props.selected} - // onMouseEnter={(event: React.MouseEvent<HTMLSpanElement>) => { - // const text = event.currentTarget; - // if (text.title === "" && text.scrollWidth > text.clientWidth) setHasTooltip(true); - // }} + onMouseEnter={(event: React.MouseEvent<HTMLSpanElement>) => { + const text = event.currentTarget; + setHasTooltip(text.scrollWidth > text.clientWidth); + }} > {label} </Text> diff --git a/packages/lib/src/date-input/DatePicker.tsx b/packages/lib/src/date-input/DatePicker.tsx index bc799140a..a10f389f0 100644 --- a/packages/lib/src/date-input/DatePicker.tsx +++ b/packages/lib/src/date-input/DatePicker.tsx @@ -7,7 +7,7 @@ import Calendar from "./Calendar"; import YearPicker from "./YearPicker"; import useTranslatedLabels from "../useTranslatedLabels"; import DxcIcon from "../icon/Icon"; -import DxcTooltip from "../tooltip/Tooltip"; +import {Tooltip} from "../tooltip/Tooltip"; const today = dayjs(); @@ -34,14 +34,14 @@ const DatePicker = ({ date, onDateSelect, id }: DatePickerPropsType): JSX.Elemen return ( <DatePickerContainer id={id}> <PickerHeader> - <DxcTooltip label={translatedLabels.calendar.previousMonthTitle}> + <Tooltip label={translatedLabels.calendar.previousMonthTitle}> <HeaderButton aria-label={translatedLabels.calendar.previousMonthTitle} onClick={() => handleMonthChange(innerDate.set("month", innerDate.get("month") - 1))} > <DxcIcon icon="keyboard_arrow_left" /> </HeaderButton> - </DxcTooltip> + </Tooltip> <HeaderYearTrigger aria-live="polite" onClick={() => setContent((content) => (content === "yearPicker" ? "calendar" : "yearPicker"))} @@ -51,14 +51,14 @@ const DatePicker = ({ date, onDateSelect, id }: DatePickerPropsType): JSX.Elemen </HeaderYearTriggerLabel> <DxcIcon icon={content === "yearPicker" ? "arrow_drop_up" : "arrow_drop_down"} /> </HeaderYearTrigger> - <DxcTooltip label={translatedLabels.calendar.nextMonthTitle}> + <Tooltip label={translatedLabels.calendar.nextMonthTitle}> <HeaderButton aria-label={translatedLabels.calendar.nextMonthTitle} onClick={() => handleMonthChange(innerDate.set("month", innerDate.get("month") + 1))} > <DxcIcon icon="keyboard_arrow_right" /> </HeaderButton> - </DxcTooltip> + </Tooltip> </PickerHeader> {content === "calendar" && ( <Calendar diff --git a/packages/lib/src/dropdown/Dropdown.stories.tsx b/packages/lib/src/dropdown/Dropdown.stories.tsx index 995a557db..1898f06d8 100644 --- a/packages/lib/src/dropdown/Dropdown.stories.tsx +++ b/packages/lib/src/dropdown/Dropdown.stories.tsx @@ -87,9 +87,9 @@ const optionsIcon: any = options.map((op, i) => ({ ...op, icon: icons[i] })); const opinionatedTheme = { dropdown: { - baseColor: "#ffffff", - fontColor: "#000000", - optionFontColor: "#000000", + baseColor: "#fabada", + fontColor: "#fff", + optionFontColor: "#0095ff", }, }; @@ -346,32 +346,7 @@ const DropdownListStates = () => { ); }; -const DropdownRightAlignment = () => ( - <ExampleContainer expanded> - <Title title="Dropdown collisions on the right boundary (right)" theme="light" level={4} /> - <DxcFlex justifyContent="flex-end"> - <DxcDropdown label="Label" options={options} onSelectOption={(value) => {}} /> - </DxcFlex> - </ExampleContainer> -); - -const DropdownCenterAlignment = () => ( - <ExampleContainer expanded> - <Title title="Dropdown collisions on the right boundary (centered)" theme="light" level={4} /> - <DxcFlex justifyContent="flex-end"> - <DxcDropdown label="Label" options={defaultOptions} onSelectOption={(value) => {}} margin="small" /> - </DxcFlex> - </ExampleContainer> -); - -export const Chromatic = Dropdown.bind({}); -Chromatic.play = async ({ canvasElement }) => { - const canvas = within(canvasElement); - const buttonList = canvas.getAllByRole("button"); - await userEvent.click(buttonList[buttonList.length - 1]); -}; - -export const OpinionatedTheme = () => ( +const OpinionatedTheme = () => ( <> <Title title="Opinionated theme" theme="light" level={2} /> <ExampleContainer> @@ -387,9 +362,9 @@ export const OpinionatedTheme = () => ( </HalstackProvider> </ExampleContainer> <ExampleContainer pseudoState="pseudo-active"> - <Title title="Actived" theme="light" level={4} /> + <Title title="Active" theme="light" level={4} /> <HalstackProvider theme={opinionatedTheme}> - <DxcDropdown label="Actived" options={options} onSelectOption={(value) => {}} icon={iconSVG} /> + <DxcDropdown label="Active" options={options} onSelectOption={(value) => {}} icon={iconSVG} /> </HalstackProvider> </ExampleContainer> <ExampleContainer pseudoState="pseudo-focus"> @@ -404,23 +379,51 @@ export const OpinionatedTheme = () => ( <DxcDropdown label="Disabled" options={options} onSelectOption={(value) => {}} icon={iconSVG} disabled /> </HalstackProvider> </ExampleContainer> + <ExampleContainer expanded> + <Title title="List opened" theme="light" level={4} /> + <HalstackProvider theme={opinionatedTheme}> + <DxcDropdown label="Default" options={options} onSelectOption={(value) => {}} icon={iconSVG} /> + </HalstackProvider> + </ExampleContainer> </> ); -export const DropdownMenuStates = DropdownListStates.bind({}); -DropdownMenuStates.play = async ({ canvasElement }) => { +const TooltipTitle = () => ( + <ExampleContainer expanded> + <Title title="Tooltip" theme="light" level={3} /> + <DxcDropdown + title="Show options" + options={options} + onSelectOption={(value) => {}} + icon="menu" + caretHidden + margin="large" + /> + </ExampleContainer> +); + +export const Chromatic = Dropdown.bind({}); +Chromatic.play = async ({ canvasElement }) => { + const canvas = within(canvasElement); + const buttonList = canvas.getAllByRole("button"); + await userEvent.click(buttonList[buttonList.length - 1]); +}; + +export const OpinionatedThemed = OpinionatedTheme.bind({}); +OpinionatedThemed.play = async ({ canvasElement }) => { const canvas = within(canvasElement); - await userEvent.click(canvas.getAllByRole("button")[0]); + const buttonList = canvas.getAllByRole("button"); + await userEvent.click(buttonList[buttonList.length - 1]); }; -export const DropdownMenuAlignedRight = DropdownRightAlignment.bind({}); -DropdownMenuAlignedRight.play = async ({ canvasElement }) => { +export const MenuStates = DropdownListStates.bind({}); +MenuStates.play = async ({ canvasElement }) => { const canvas = within(canvasElement); - await userEvent.click(canvas.getByRole("button")); + await userEvent.click(canvas.getAllByRole("button")[0]); }; -export const DropdownMenuAlignedCenter = DropdownCenterAlignment.bind({}); -DropdownMenuAlignedCenter.play = async ({ canvasElement }) => { +export const MenuTooltip = TooltipTitle.bind({}); +MenuTooltip.play = async ({ canvasElement }) => { const canvas = within(canvasElement); - await userEvent.click(canvas.getByRole("button")); + await userEvent.hover(canvas.getByRole("button")); }; diff --git a/packages/lib/src/dropdown/Dropdown.tsx b/packages/lib/src/dropdown/Dropdown.tsx index e40104dc2..ad36a565e 100644 --- a/packages/lib/src/dropdown/Dropdown.tsx +++ b/packages/lib/src/dropdown/Dropdown.tsx @@ -8,6 +8,7 @@ import useTheme from "../useTheme"; import useWidth from "../utils/useWidth"; import DropdownMenu from "./DropdownMenu"; import DropdownPropsType from "./types"; +import { Tooltip } from "../tooltip/Tooltip"; const DxcDropdown = ({ options, @@ -22,6 +23,7 @@ const DxcDropdown = ({ margin, size = "fitContent", tabIndex = 0, + title, }: DropdownPropsType): JSX.Element => { const id = useId(); const triggerId = `trigger-${id}`; @@ -145,45 +147,47 @@ const DxcDropdown = ({ size={size} > <Popover.Root open={isOpen}> - <Popover.Trigger asChild type={undefined}> - <DropdownTrigger - onClick={handleTriggerOnClick} - onKeyDown={handleTriggerOnKeyDown} - onBlur={(event) => { - event.stopPropagation(); - }} - disabled={disabled} - label={label} - margin={margin} - size={size} - id={triggerId} - aria-haspopup="true" - aria-controls={isOpen ? menuId : undefined} - aria-expanded={isOpen ? true : undefined} - aria-label="Show options" - tabIndex={tabIndex} - ref={triggerRef} - > - <DropdownTriggerContent> - {label && iconPosition === "after" && <DropdownTriggerLabel>{label}</DropdownTriggerLabel>} - {icon && ( - <DropdownTriggerIcon - disabled={disabled} - role={typeof icon === "string" ? undefined : "img"} - aria-hidden - > - {typeof icon === "string" ? <DxcIcon icon={icon} /> : icon} - </DropdownTriggerIcon> + <Tooltip label={title}> + <Popover.Trigger asChild type={undefined}> + <DropdownTrigger + onClick={handleTriggerOnClick} + onKeyDown={handleTriggerOnKeyDown} + onBlur={(event) => { + event.stopPropagation(); + }} + disabled={disabled} + label={label} + margin={margin} + size={size} + id={triggerId} + aria-haspopup="true" + aria-controls={isOpen ? menuId : undefined} + aria-expanded={isOpen ? true : undefined} + aria-label="Show options" + tabIndex={tabIndex} + ref={triggerRef} + > + <DropdownTriggerContent> + {label && iconPosition === "after" && <DropdownTriggerLabel>{label}</DropdownTriggerLabel>} + {icon && ( + <DropdownTriggerIcon + disabled={disabled} + role={typeof icon === "string" ? undefined : "img"} + aria-hidden + > + {typeof icon === "string" ? <DxcIcon icon={icon} /> : icon} + </DropdownTriggerIcon> + )} + {label && iconPosition === "before" && <DropdownTriggerLabel>{label}</DropdownTriggerLabel>} + </DropdownTriggerContent> + {!caretHidden && ( + <CaretIcon disabled={disabled}> + <DxcIcon icon={isOpen ? "arrow_drop_up" : "arrow_drop_down"} />{" "} + </CaretIcon> )} - {label && iconPosition === "before" && <DropdownTriggerLabel>{label}</DropdownTriggerLabel>} - </DropdownTriggerContent> - {!caretHidden && ( - <CaretIcon disabled={disabled}> - <DxcIcon icon={isOpen ? "arrow_drop_up" : "arrow_drop_down"} />{" "} - </CaretIcon> - )} - </DropdownTrigger> - </Popover.Trigger> + </DropdownTrigger> + </Popover.Trigger> + </Tooltip> <Popover.Portal> <Popover.Content asChild sideOffset={1}> <DropdownMenu diff --git a/packages/lib/src/dropdown/types.ts b/packages/lib/src/dropdown/types.ts index 84f60fcf8..0fff032dc 100644 --- a/packages/lib/src/dropdown/types.ts +++ b/packages/lib/src/dropdown/types.ts @@ -76,6 +76,11 @@ type Props = { * Value of the tabindex attribute. */ tabIndex?: number; + /** + * Text representing advisory information related to the dropdown's trigger action. + * Under the hood, this prop also serves as an accessible label for the component. + */ + title?: string; }; export type DropdownMenuProps = { diff --git a/packages/lib/src/file-input/FileItem.tsx b/packages/lib/src/file-input/FileItem.tsx index dab91a72e..3943335b0 100644 --- a/packages/lib/src/file-input/FileItem.tsx +++ b/packages/lib/src/file-input/FileItem.tsx @@ -5,7 +5,6 @@ import useTheme from "../useTheme"; import useTranslatedLabels from "../useTranslatedLabels"; import { FileItemProps } from "./types"; import DxcIcon from "../icon/Icon"; -import DxcTooltip from "../tooltip/Tooltip"; import DxcActionIcon from "../action-icon/ActionIcon"; const FileItem = ({ diff --git a/packages/lib/src/footer/Footer.tsx b/packages/lib/src/footer/Footer.tsx index d80d56adc..b5df7a312 100644 --- a/packages/lib/src/footer/Footer.tsx +++ b/packages/lib/src/footer/Footer.tsx @@ -3,7 +3,7 @@ import styled, { ThemeProvider } from "styled-components"; import { responsiveSizes, spaces } from "../common/variables"; import DxcFlex from "../flex/Flex"; import DxcIcon from "../icon/Icon"; -import DxcTooltip from "../tooltip/Tooltip"; +import { Tooltip } from "../tooltip/Tooltip"; import useTheme from "../useTheme"; import useTranslatedLabels from "../useTranslatedLabels"; import { dxcLogo, dxcSmallLogo } from "./Icons"; @@ -46,7 +46,7 @@ const DxcFooter = ({ {mode === "default" && ( <DxcFlex gap={colorsTheme.footer.socialLinksGutter as Spaces}> {socialLinks?.map((link, index) => ( - <DxcTooltip label={link.title}> + <Tooltip label={link.title}> <SocialAnchor href={link.href} tabIndex={tabIndex} @@ -58,7 +58,7 @@ const DxcFooter = ({ {typeof link.logo === "string" ? <DxcIcon icon={link.logo} /> : link.logo} </SocialIconContainer> </SocialAnchor> - </DxcTooltip> + </Tooltip> ))} </DxcFlex> )} diff --git a/packages/lib/src/header/Header.tsx b/packages/lib/src/header/Header.tsx index d7be0086a..a79d5f796 100644 --- a/packages/lib/src/header/Header.tsx +++ b/packages/lib/src/header/Header.tsx @@ -6,7 +6,7 @@ import DxcIcon from "../icon/Icon"; import useTheme from "../useTheme"; import useTranslatedLabels from "../useTranslatedLabels"; import HeaderPropsType from "./types"; -import DxcTooltip from "../tooltip/Tooltip"; +import { Tooltip } from "../tooltip/Tooltip"; import DxcFlex from "../flex/Flex"; const Dropdown = (props: ComponentProps<typeof DxcDropdown>) => ( @@ -119,11 +119,11 @@ const DxcHeader = ({ <ResponsiveMenu hasVisibility={isMenuVisible}> <DxcFlex justifyContent="space-between" alignItems="center"> <ResponsiveLogoContainer>{headerResponsiveLogo}</ResponsiveLogoContainer> - <DxcTooltip label={translatedLabels.header.closeIcon}> + <Tooltip label={translatedLabels.header.closeIcon}> <CloseAction tabIndex={tabIndex} onClick={handleMenu} aria-label={translatedLabels.header.closeIcon}> <DxcIcon icon="close" /> </CloseAction> - </DxcTooltip> + </Tooltip> </DxcFlex> <Content isResponsive={isResponsive} diff --git a/packages/lib/src/layout/ApplicationLayout.tsx b/packages/lib/src/layout/ApplicationLayout.tsx index a27b2a584..74372d190 100644 --- a/packages/lib/src/layout/ApplicationLayout.tsx +++ b/packages/lib/src/layout/ApplicationLayout.tsx @@ -7,7 +7,7 @@ import DxcIcon from "../icon/Icon"; import DxcSidenav from "../sidenav/Sidenav"; import { SidenavContextProvider, useResponsiveSidenavVisibility } from "../sidenav/SidenavContext"; import useTranslatedLabels from "../useTranslatedLabels"; -import DxcTooltip from "../tooltip/Tooltip"; +import { Tooltip } from "../tooltip/Tooltip"; import layoutIcons from "./Icons"; import AppLayoutPropsType, { AppLayoutMainPropsType } from "./types"; @@ -101,7 +101,7 @@ const DxcApplicationLayout = ({ <HeaderContainer>{headerContent}</HeaderContainer> {sidenav && isResponsive && ( <VisibilityToggle> - <DxcTooltip label={translatedLabels.applicationLayout.visibilityToggleTitle}> + <Tooltip label={translatedLabels.applicationLayout.visibilityToggleTitle}> <HamburgerTrigger onClick={handleSidenavVisibility} aria-label={visibilityToggleLabel ? undefined : translatedLabels.applicationLayout.visibilityToggleTitle} @@ -109,7 +109,7 @@ const DxcApplicationLayout = ({ <DxcIcon icon="Menu" /> {visibilityToggleLabel} </HamburgerTrigger> - </DxcTooltip> + </Tooltip> </VisibilityToggle> )} <BodyContainer> diff --git a/packages/lib/src/select/ListOption.tsx b/packages/lib/src/select/ListOption.tsx index bdf43dd51..3f2b82ce2 100644 --- a/packages/lib/src/select/ListOption.tsx +++ b/packages/lib/src/select/ListOption.tsx @@ -2,6 +2,8 @@ import styled from "styled-components"; import { OptionProps } from "./types"; import DxcCheckbox from "../checkbox/Checkbox"; import DxcIcon from "../icon/Icon"; +import { useState } from "react"; +import { TooltipWrapper } from "../tooltip/Tooltip"; const ListOption = ({ id, @@ -13,50 +15,53 @@ const ListOption = ({ isLastOption, isSelected, }: OptionProps): JSX.Element => { - const handleOnMouseEnter = (event: React.MouseEvent) => { - const label = event.currentTarget; - const optionElement = document.getElementById(id); - if (optionElement.title === "" && label.scrollWidth > label.clientWidth) optionElement.title = option.label; + const [hasTooltip, setHasTooltip] = useState(false); + + const handleOnMouseEnter = (event: React.MouseEvent<HTMLSpanElement>) => { + const text = event.currentTarget; + setHasTooltip(text.scrollWidth > text.clientWidth); }; return ( - <OptionItem - id={id} - onClick={() => { - onClick(option); - }} - visualFocused={visualFocused} - selected={isSelected} - role="option" - aria-selected={!multiple ? isSelected : undefined} - > - <StyledOption + <TooltipWrapper condition={hasTooltip} label={option.label}> + <OptionItem + id={id} + onClick={() => { + onClick(option); + }} visualFocused={visualFocused} selected={isSelected} - last={isLastOption} - grouped={isGroupedOption} - multiple={multiple} + role="option" + aria-selected={!multiple ? isSelected : undefined} > - {multiple && ( - <div style={{ display: "flex", pointerEvents: "none" }}> - <DxcCheckbox checked={isSelected} tabIndex={-1} /> - </div> - )} - {option.icon && ( - <OptionIcon grouped={isGroupedOption} multiple={multiple}> - {typeof option.icon === "string" ? <DxcIcon icon={option.icon} /> : option.icon} - </OptionIcon> - )} - <OptionContent grouped={isGroupedOption} hasIcon={option.icon ? true : false} multiple={multiple}> - <OptionLabel onMouseEnter={handleOnMouseEnter}>{option.label}</OptionLabel> - {!multiple && isSelected && ( - <OptionSelectedIndicator> - <DxcIcon icon="done" /> - </OptionSelectedIndicator> + <StyledOption + visualFocused={visualFocused} + selected={isSelected} + last={isLastOption} + grouped={isGroupedOption} + multiple={multiple} + > + {multiple && ( + <div style={{ display: "flex", pointerEvents: "none" }}> + <DxcCheckbox checked={isSelected} tabIndex={-1} /> + </div> + )} + {option.icon && ( + <OptionIcon grouped={isGroupedOption} multiple={multiple}> + {typeof option.icon === "string" ? <DxcIcon icon={option.icon} /> : option.icon} + </OptionIcon> )} - </OptionContent> - </StyledOption> - </OptionItem> + <OptionContent grouped={isGroupedOption} hasIcon={option.icon ? true : false} multiple={multiple}> + <OptionLabel onMouseEnter={handleOnMouseEnter}>{option.label}</OptionLabel> + {!multiple && isSelected && ( + <OptionSelectedIndicator> + <DxcIcon icon="done" /> + </OptionSelectedIndicator> + )} + </OptionContent> + </StyledOption> + </OptionItem> + </TooltipWrapper> ); }; diff --git a/packages/lib/src/select/Select.stories.tsx b/packages/lib/src/select/Select.stories.tsx index da4896e08..8c098379c 100644 --- a/packages/lib/src/select/Select.stories.tsx +++ b/packages/lib/src/select/Select.stories.tsx @@ -212,7 +212,7 @@ const options_material = [ }, ]; -const optionsWithEllipsisMedium = [ +const optionsWithEllipsis = [ { label: "Optiond1234567890123456789012345678901234", value: "1" }, { label: "Optiond12345678901234567890123456789012345", value: "2" }, { label: "Option 031111111111111111111111111111222", value: "3" }, @@ -220,10 +220,10 @@ const optionsWithEllipsisMedium = [ const opinionatedTheme = { select: { - selectedOptionBackgroundColor: "#e6f4ff", - fontColor: "#000000", - optionFontColor: "#000000", - hoverBorderColor: "#a46ede", + selectedOptionBackgroundColor: "#fabada", + fontColor: "#333", + optionFontColor: "#a46ede", + hoverBorderColor: "#0095ff", }, }; @@ -347,63 +347,44 @@ const Select = () => ( <Title title="Multiple selection with ellipsis" theme="light" level={4} /> <DxcSelect label="Label" options={single_options} multiple defaultValue={["1", "2", "3", "4"]} /> <Title title="Value with ellipsis" theme="light" level={4} /> - <DxcSelect label="Label" options={optionsWithEllipsisMedium} defaultValue="1" /> + <DxcSelect label="Label" options={optionsWithEllipsis} defaultValue="1" /> <Title title="Options with ellipsis" theme="light" level={4} /> <DxcSelect label="Label" placeholder="Choose an option" defaultValue="1" - options={optionsWithEllipsisMedium} + options={optionsWithEllipsis} margin={{ top: "xxlarge" }} /> </ExampleContainer> + </> +); + +const Opinionated = () => ( + <> <Title title="Opinionated theme" theme="light" level={2} /> <ExampleContainer pseudoState="pseudo-hover"> - <Title title="Hovered" theme="light" level={4} /> - <HalstackProvider theme={opinionatedTheme}> - <DxcSelect label="Hovered" options={single_options} /> - </HalstackProvider> - </ExampleContainer> - <ExampleContainer pseudoState="pseudo-focus-within"> - <Title title="Focused" theme="light" level={4} /> - <HalstackProvider theme={opinionatedTheme}> - <DxcSelect label="Focused" options={single_options} /> - </HalstackProvider> - </ExampleContainer> - <ExampleContainer> - <Title title="Disabled" theme="light" level={4} /> - <HalstackProvider theme={opinionatedTheme}> - <DxcSelect label="Disabled" placeholder="Placeholder" disabled options={single_options} /> - </HalstackProvider> - </ExampleContainer> - <ExampleContainer> - <Title title="Disabled with value" theme="light" level={4} /> + <Title title="Default" theme="light" level={4} /> <HalstackProvider theme={opinionatedTheme}> - <DxcSelect label="Disabled with value" disabled options={single_options} defaultValue="1" /> + <DxcSelect label="Hovered" helperText="Helper text" placeholder="Placeholder" options={single_options} /> </HalstackProvider> </ExampleContainer> - <ExampleContainer> - <Title title="Error" theme="light" level={4} /> + <ExampleContainer pseudoState="pseudo-hover"> + <Title title="Hovered" theme="light" level={4} /> <HalstackProvider theme={opinionatedTheme}> <DxcSelect - label="Label" - options={single_options} - error="Error message." + label="Hovered" helperText="Helper text" - placeholder="Placeholder" + options={single_options} + multiple + defaultValue={["1", "2"]} /> - <ExampleContainer> - <Title title="Multiple selection" theme="light" level={4} /> - <DxcSelect label="Multiple select" searchable options={single_options} multiple defaultValue={["1", "2"]} /> - </ExampleContainer> - <ExampleContainer pseudoState="pseudo-hover"> - <Title title="Multiple clear hovered" theme="light" level={4} /> - <DxcSelect label="Multiple select" options={single_options} multiple defaultValue={["1", "2"]} /> - </ExampleContainer> - <ExampleContainer pseudoState="pseudo-active"> - <Title title="Multiple clear actived" theme="light" level={4} /> - <DxcSelect label="Multiple select" options={single_options} multiple defaultValue={["1", "2"]} /> - </ExampleContainer> + </HalstackProvider> + </ExampleContainer> + <ExampleContainer pseudoState="pseudo-hover" expanded> + <Title title="List opened" theme="light" level={4} /> + <HalstackProvider theme={opinionatedTheme}> + <DxcSelect label="Hovered" helperText="Helper text" options={icon_options_grouped_material} defaultValue="1" /> </HalstackProvider> </ExampleContainer> </> @@ -413,279 +394,165 @@ const SelectListbox = () => { const colorsTheme = useTheme(); return ( - <> - <ThemeProvider theme={colorsTheme.select}> - <Title title="Listbox" theme="light" level={2} /> - <ExampleContainer> - <Title - title="List dialog uses a Radix Popover to appear over elements with a certain z-index" - theme="light" - level={3} - /> - <div - style={{ - position: "relative", - display: "flex", - flexDirection: "column", - gap: "20px", - height: "150px", - width: "min-content", - marginBottom: "100px", - padding: "20px", - border: "1px solid black", - borderRadius: "4px", - overflow: "auto", - zIndex: "1300", - }} - > - <DxcSelect label="Label" options={single_options} optional placeholder="Choose an option" /> - <button style={{ zIndex: "1", width: "100px" }}>Submit</button> - </div> - </ExampleContainer> - <Title title="Listbox option states" theme="light" level={3} /> - <ExampleContainer pseudoState="pseudo-hover"> - <Title title="Hovered option" theme="light" level={4} /> - <Listbox - id="x8" - currentValue="" - options={one_option} - visualFocusIndex={-1} - lastOptionIndex={0} - multiple={false} - optional={false} - optionalItem={{ label: "Empty", value: "" }} - searchable={false} - handleOptionOnClick={() => {}} - styles={{ width: 360 }} - /> - </ExampleContainer> - <ExampleContainer pseudoState="pseudo-active"> - <Title title="Active option" theme="light" level={4} /> - <Listbox - id="x9" - currentValue="" - options={one_option} - visualFocusIndex={-1} - lastOptionIndex={0} - multiple={false} - optional={false} - optionalItem={{ label: "Empty", value: "" }} - searchable={false} - handleOptionOnClick={() => {}} - styles={{ width: 360 }} - /> - </ExampleContainer> - <ExampleContainer> - <Title title="Focused option" theme="light" level={4} /> - <Listbox - id="x10" - currentValue="" - options={one_option} - visualFocusIndex={0} - lastOptionIndex={0} - multiple={false} - optional={false} - optionalItem={{ label: "Empty", value: "" }} - searchable={false} - handleOptionOnClick={() => {}} - styles={{ width: 360 }} - /> - </ExampleContainer> - <ExampleContainer pseudoState="pseudo-hover"> - <Title title="Hovered selected option" theme="light" level={4} /> - <Listbox - id="x11" - currentValue="1" - options={single_options} - visualFocusIndex={-1} - lastOptionIndex={3} - multiple={false} - optional={false} - optionalItem={{ label: "Empty", value: "" }} - searchable={false} - handleOptionOnClick={() => {}} - styles={{ width: 360 }} - /> - </ExampleContainer> - <ExampleContainer pseudoState="pseudo-active"> - <Title title="Active selected option" theme="light" level={4} /> - <Listbox - id="x12" - currentValue="2" - options={single_options} - visualFocusIndex={0} - lastOptionIndex={3} - multiple={false} - optional={false} - optionalItem={{ label: "Empty", value: "" }} - searchable={false} - handleOptionOnClick={() => {}} - styles={{ width: 360 }} - /> - </ExampleContainer> - <Title title="Listbox with icons" theme="light" level={3} /> - <ExampleContainer> - <Title title="Icons (SVGs)" theme="light" level={4} /> - <Listbox - id="x13" - currentValue="3" - options={icon_options} - visualFocusIndex={-1} - lastOptionIndex={3} - multiple={false} - optional={false} - optionalItem={{ label: "Empty", value: "" }} - searchable={false} - handleOptionOnClick={() => {}} - styles={{ width: 360 }} - /> - </ExampleContainer> - <ExampleContainer> - <Title title="Grouped icons (Material Symbols)" theme="light" level={4} /> - <Listbox - id="x14" - currentValue={"4"} - options={icon_options_grouped_material} - visualFocusIndex={-1} - lastOptionIndex={3} - multiple={false} - optional={false} - optionalItem={{ label: "Empty", value: "" }} - searchable={false} - handleOptionOnClick={() => {}} - styles={{ width: 360 }} - /> - </ExampleContainer> - <ExampleContainer> - <Title title="Grouped icons (Material)" theme="light" level={4} /> - <Listbox - id="x15" - currentValue={["car", "motorcycle", "train"]} - options={options_material} - visualFocusIndex={-1} - lastOptionIndex={6} - multiple={true} - optional={false} - optionalItem={{ label: "Empty", value: "" }} - searchable={false} - handleOptionOnClick={() => {}} - styles={{ width: 360 }} - /> - </ExampleContainer> - </ThemeProvider> - <ThemeProvider theme={colorsTheme.select}> - <Title title="Opinionated theme" theme="light" level={2} /> - <ExampleContainer pseudoState="pseudo-hover"> - <Title title="Hovered option" theme="light" level={4} /> - <HalstackProvider theme={opinionatedTheme}> - <Listbox - id="x16" - currentValue="" - options={one_option} - visualFocusIndex={-1} - lastOptionIndex={0} - multiple={false} - optional={false} - optionalItem={{ label: "Empty", value: "" }} - searchable={false} - handleOptionOnClick={() => {}} - styles={{ width: 360 }} - /> - </HalstackProvider> - </ExampleContainer> - <ExampleContainer pseudoState="pseudo-active"> - <Title title="Active option" theme="light" level={4} />{" "} - <HalstackProvider theme={opinionatedTheme}> - <Listbox - id="x17" - currentValue="" - options={one_option} - visualFocusIndex={-1} - lastOptionIndex={0} - multiple={false} - optional={false} - optionalItem={{ label: "Empty", value: "" }} - searchable={false} - handleOptionOnClick={() => {}} - styles={{ width: 360 }} - /> - </HalstackProvider> - </ExampleContainer> - <ExampleContainer> - <Title title="Focused option" theme="light" level={4} />{" "} - <HalstackProvider theme={opinionatedTheme}> - <Listbox - id="x18" - currentValue="" - options={one_option} - visualFocusIndex={0} - lastOptionIndex={0} - multiple={false} - optional={false} - optionalItem={{ label: "Empty", value: "" }} - searchable={false} - handleOptionOnClick={() => {}} - styles={{ width: 360 }} - /> - </HalstackProvider> - </ExampleContainer> - <ExampleContainer pseudoState="pseudo-hover"> - <Title title="Hovered selected option" theme="light" level={4} />{" "} - <HalstackProvider theme={opinionatedTheme}> - <Listbox - id="x19" - currentValue="1" - options={single_options} - visualFocusIndex={-1} - lastOptionIndex={3} - multiple={false} - optional={false} - optionalItem={{ label: "Empty", value: "" }} - searchable={false} - handleOptionOnClick={() => {}} - styles={{ width: 360 }} - /> - </HalstackProvider> - </ExampleContainer> - <ExampleContainer pseudoState="pseudo-active"> - <Title title="Active selected option" theme="light" level={4} />{" "} - <HalstackProvider theme={opinionatedTheme}> - <Listbox - id="x20" - currentValue="2" - options={single_options} - visualFocusIndex={0} - lastOptionIndex={3} - multiple={false} - optional={false} - optionalItem={{ label: "Empty", value: "" }} - searchable={false} - handleOptionOnClick={() => {}} - styles={{ width: 360 }} - /> - </HalstackProvider> - </ExampleContainer> - <Title title="Listbox with icons" theme="light" level={3} /> - <ExampleContainer> - <Title title="Icons (SVGs)" theme="light" level={4} />{" "} - <HalstackProvider theme={opinionatedTheme}> - <Listbox - id="x21" - currentValue="3" - options={icon_options} - visualFocusIndex={-1} - lastOptionIndex={3} - multiple={false} - optional={false} - optionalItem={{ label: "Empty", value: "" }} - searchable={false} - handleOptionOnClick={() => {}} - styles={{ width: 360 }} - /> - </HalstackProvider> - </ExampleContainer> - </ThemeProvider> - </> + <ThemeProvider theme={colorsTheme.select}> + <Title title="Listbox" theme="light" level={2} /> + <ExampleContainer> + <Title + title="List dialog uses a Radix Popover to appear over elements with a certain z-index" + theme="light" + level={3} + /> + <div + style={{ + position: "relative", + display: "flex", + flexDirection: "column", + gap: "20px", + height: "150px", + width: "min-content", + marginBottom: "100px", + padding: "20px", + border: "1px solid black", + borderRadius: "4px", + overflow: "auto", + zIndex: "1300", + }} + > + <DxcSelect label="Label" options={single_options} optional placeholder="Choose an option" /> + <button style={{ zIndex: "1", width: "100px" }}>Submit</button> + </div> + </ExampleContainer> + <Title title="Listbox option states" theme="light" level={3} /> + <ExampleContainer pseudoState="pseudo-hover"> + <Title title="Hovered option" theme="light" level={4} /> + <Listbox + id="x8" + currentValue="" + options={one_option} + visualFocusIndex={-1} + lastOptionIndex={0} + multiple={false} + optional={false} + optionalItem={{ label: "Empty", value: "" }} + searchable={false} + handleOptionOnClick={() => {}} + styles={{ width: 360 }} + /> + </ExampleContainer> + <ExampleContainer pseudoState="pseudo-active"> + <Title title="Active option" theme="light" level={4} /> + <Listbox + id="x9" + currentValue="" + options={one_option} + visualFocusIndex={-1} + lastOptionIndex={0} + multiple={false} + optional={false} + optionalItem={{ label: "Empty", value: "" }} + searchable={false} + handleOptionOnClick={() => {}} + styles={{ width: 360 }} + /> + </ExampleContainer> + <ExampleContainer> + <Title title="Focused option" theme="light" level={4} /> + <Listbox + id="x10" + currentValue="" + options={one_option} + visualFocusIndex={0} + lastOptionIndex={0} + multiple={false} + optional={false} + optionalItem={{ label: "Empty", value: "" }} + searchable={false} + handleOptionOnClick={() => {}} + styles={{ width: 360 }} + /> + </ExampleContainer> + <ExampleContainer pseudoState="pseudo-hover"> + <Title title="Hovered selected option" theme="light" level={4} /> + <Listbox + id="x11" + currentValue="1" + options={single_options} + visualFocusIndex={-1} + lastOptionIndex={3} + multiple={false} + optional={false} + optionalItem={{ label: "Empty", value: "" }} + searchable={false} + handleOptionOnClick={() => {}} + styles={{ width: 360 }} + /> + </ExampleContainer> + <ExampleContainer pseudoState="pseudo-active"> + <Title title="Active selected option" theme="light" level={4} /> + <Listbox + id="x12" + currentValue="2" + options={single_options} + visualFocusIndex={0} + lastOptionIndex={3} + multiple={false} + optional={false} + optionalItem={{ label: "Empty", value: "" }} + searchable={false} + handleOptionOnClick={() => {}} + styles={{ width: 360 }} + /> + </ExampleContainer> + <Title title="Listbox with icons" theme="light" level={3} /> + <ExampleContainer> + <Title title="Icons (SVGs)" theme="light" level={4} /> + <Listbox + id="x13" + currentValue="3" + options={icon_options} + visualFocusIndex={-1} + lastOptionIndex={3} + multiple={false} + optional={false} + optionalItem={{ label: "Empty", value: "" }} + searchable={false} + handleOptionOnClick={() => {}} + styles={{ width: 360 }} + /> + </ExampleContainer> + <ExampleContainer> + <Title title="Grouped icons (Material Symbols)" theme="light" level={4} /> + <Listbox + id="x14" + currentValue={"4"} + options={icon_options_grouped_material} + visualFocusIndex={-1} + lastOptionIndex={3} + multiple={false} + optional={false} + optionalItem={{ label: "Empty", value: "" }} + searchable={false} + handleOptionOnClick={() => {}} + styles={{ width: 360 }} + /> + </ExampleContainer> + <ExampleContainer> + <Title title="Grouped icons (Material)" theme="light" level={4} /> + <Listbox + id="x15" + currentValue={["car", "motorcycle", "train"]} + options={options_material} + visualFocusIndex={-1} + lastOptionIndex={6} + multiple={true} + optional={false} + optionalItem={{ label: "Empty", value: "" }} + searchable={false} + handleOptionOnClick={() => {}} + styles={{ width: 360 }} + /> + </ExampleContainer> + </ThemeProvider> ); }; @@ -696,15 +563,6 @@ const SearchableSelect = () => ( </ExampleContainer> ); -const SearchableSelectOpinionated = () => ( - <ExampleContainer expanded> - <Title title="Searchable select" theme="light" level={4} /> - <HalstackProvider theme={opinionatedTheme}> - <DxcSelect label="Select Label" searchable options={single_options} placeholder="Choose an option" /> - </HalstackProvider> - </ExampleContainer> -); - const SearchValue = () => ( <ExampleContainer expanded> <Title title="Searchable select with value" theme="light" level={4} /> @@ -718,21 +576,6 @@ const SearchValue = () => ( </ExampleContainer> ); -const SearchValueOpinionated = () => ( - <ExampleContainer expanded> - <Title title="Searchable select with value" theme="light" level={4} /> - <HalstackProvider theme={opinionatedTheme}> - <DxcSelect - label="Select Label" - searchable - defaultValue="1" - options={single_options} - placeholder="Choose an option" - /> - </HalstackProvider> - </ExampleContainer> -); - const MultipleSelect = () => ( <> <ExampleContainer expanded> @@ -748,21 +591,6 @@ const MultipleSelect = () => ( </> ); -const MultipleSelectOpinionated = () => ( - <ExampleContainer expanded> - <Title title="Multiple select" theme="light" level={4} /> - <HalstackProvider theme={opinionatedTheme}> - <DxcSelect - label="Select label" - options={single_options} - defaultValue={["1", "4"]} - multiple - placeholder="Choose an option" - /> - </HalstackProvider> - </ExampleContainer> -); - const DefaultGroupedOptionsSelect = () => ( <ExampleContainer expanded> <Title title="Grouped options simple select" theme="light" level={4} /> @@ -792,21 +620,6 @@ const MultipleGroupedOptionsSelect = () => ( </ExampleContainer> ); -const MultipleGroupedOptionsSelectOpinionated = () => ( - <ExampleContainer expanded> - <Title title="Grouped options multiple select" theme="light" level={4} /> - <HalstackProvider theme={opinionatedTheme}> - <DxcSelect - label="Label" - options={group_options} - defaultValue={["0", "2"]} - multiple - placeholder="Choose an option" - /> - </HalstackProvider> - </ExampleContainer> -); - const MultipleSearchable = () => ( <ExampleContainer expanded> <Title title="Searchable multiple select with value" theme="light" level={4} /> @@ -821,19 +634,42 @@ const MultipleSearchable = () => ( </ExampleContainer> ); -const MultipleSearchableOpinionated = () => ( +const TooltipValue = () => ( <ExampleContainer expanded> - <Title title="Searchable multiple select with value" theme="light" level={4} /> - <HalstackProvider theme={opinionatedTheme}> - <DxcSelect - label="Select Label" - searchable - multiple - defaultValue={["1", "4"]} - options={single_options} - placeholder="Choose an option" - /> - </HalstackProvider> + <Title title="Selected value(s) have tooltip when they overflow" theme="light" level={4} /> + <DxcSelect label="Label" options={single_options} multiple defaultValue={["1", "2", "3", "4"]} /> + </ExampleContainer> +); + +const TooltipOption = () => { + const colorsTheme = useTheme(); + + return ( + <ThemeProvider theme={colorsTheme.select}> + <ExampleContainer expanded> + <Title title="List option has tooltip when it overflows" theme="light" level={4} />{" "} + <Listbox + id="x8" + currentValue="1" + options={optionsWithEllipsis} + visualFocusIndex={-1} + lastOptionIndex={2} + multiple={false} + optional={false} + optionalItem={{ label: "Empty", value: "" }} + searchable={false} + handleOptionOnClick={() => {}} + styles={{ width: 360 }} + /> + </ExampleContainer> + </ThemeProvider> + ); +}; + +const TooltipClear = () => ( + <ExampleContainer expanded> + <Title title="Clear action tooltip" theme="light" level={4} /> + <DxcSelect label="Label" options={single_options} multiple defaultValue={["1", "2", "3", "4"]} /> </ExampleContainer> ); @@ -843,6 +679,12 @@ Chromatic.play = async ({ canvasElement }) => { await userEvent.click(canvas.getAllByRole("combobox")[24]); }; +export const OpinionatedTheme = Opinionated.bind({}); +OpinionatedTheme.play = async ({ canvasElement }) => { + const canvas = within(canvasElement); + await userEvent.click(canvas.getAllByRole("combobox")[2]); +}; + export const ListboxStates = SelectListbox.bind({}); ListboxStates.play = async ({ canvasElement }) => { const canvas = within(canvasElement); @@ -856,36 +698,18 @@ Searchable.play = async ({ canvasElement }) => { await userEvent.type(canvas.getByRole("combobox"), "r"); }; -export const SearchableOpinionated = SearchableSelectOpinionated.bind({}); -SearchableOpinionated.play = async ({ canvasElement }) => { - const canvas = within(canvasElement); - await userEvent.type(canvas.getByRole("combobox"), "r"); -}; - export const SearchableWithValue = SearchValue.bind({}); SearchableWithValue.play = async ({ canvasElement }) => { const canvas = within(canvasElement); await userEvent.click(canvas.getByRole("combobox")); }; -export const SearchableWithValueOpinionated = SearchValueOpinionated.bind({}); -SearchableWithValueOpinionated.play = async ({ canvasElement }) => { - const canvas = within(canvasElement); - await userEvent.click(canvas.getByRole("combobox")); -}; - export const MultipleSearchableWithValue = MultipleSearchable.bind({}); MultipleSearchableWithValue.play = async ({ canvasElement }) => { const canvas = within(canvasElement); await userEvent.click(canvas.getAllByRole("combobox")[0]); }; -export const MultipleSearchableWithValueOpinionated = MultipleSearchableOpinionated.bind({}); -MultipleSearchableWithValueOpinionated.play = async ({ canvasElement }) => { - const canvas = within(canvasElement); - await userEvent.click(canvas.getAllByRole("combobox")[0]); -}; - export const GroupOptionsDisplayed = DefaultGroupedOptionsSelect.bind({}); GroupOptionsDisplayed.play = async ({ canvasElement }) => { const canvas = within(canvasElement); @@ -906,12 +730,6 @@ MultipleOptionsDisplayed.play = async ({ canvasElement }) => { await userEvent.click(canvas.getAllByRole("combobox")[0]); }; -export const MultipleOptionsDisplayedOpinionated = MultipleSelectOpinionated.bind({}); -MultipleOptionsDisplayedOpinionated.play = async ({ canvasElement }) => { - const canvas = within(canvasElement); - await userEvent.click(canvas.getAllByRole("combobox")[0]); -}; - export const MultipleGroupedOptionsDisplayed = MultipleGroupedOptionsSelect.bind({}); MultipleGroupedOptionsDisplayed.play = async ({ canvasElement }) => { const canvas = within(canvasElement); @@ -919,28 +737,31 @@ MultipleGroupedOptionsDisplayed.play = async ({ canvasElement }) => { await userEvent.click(select); }; -export const MultipleGroupedOptionsDisplayedOpinionated = MultipleGroupedOptionsSelectOpinionated.bind({}); -MultipleGroupedOptionsDisplayedOpinionated.play = async ({ canvasElement }) => { +export const ValueWithEllipsisTooltip = TooltipValue.bind({}); +ValueWithEllipsisTooltip.play = async ({ canvasElement }) => { const canvas = within(canvasElement); - const select = canvas.getByRole("combobox"); - await userEvent.click(select); + await userEvent.hover(canvas.getByText("Option 01, Option 02, Option 03, Option 04")); + await userEvent.hover(canvas.getByText("Option 01, Option 02, Option 03, Option 04")); }; -const Tooltip = () => { - const colorsTheme: any = useTheme(); - return ( - <ThemeProvider theme={colorsTheme}> - <Title title="Default tooltip" theme="light" level={2} /> - <ExampleContainer> - <DxcSelect label="Label" options={single_options} multiple defaultValue={["1", "2", "3", "4"]} /> - </ExampleContainer> - </ThemeProvider> - ); +export const ListboxOptionWithEllipsisTooltip = TooltipOption.bind({}); +ListboxOptionWithEllipsisTooltip.play = async ({ canvasElement }) => { + const canvas = within(canvasElement); + await userEvent.hover(canvas.getByText("Optiond12345678901234567890123456789012345")); + await userEvent.hover(canvas.getByText("Optiond12345678901234567890123456789012345")); }; -export const SelectTooltip = Tooltip.bind({}); -SelectTooltip.play = async ({ canvasElement }) => { +export const ClearActionTooltip = TooltipClear.bind({}); +ClearActionTooltip.play = async ({ canvasElement }) => { const canvas = within(canvasElement); const clearSelectionButton = canvas.getByRole("button"); await userEvent.hover(clearSelectionButton); }; + +export const SearchableClearActionTooltip = SearchableSelect.bind({}); +SearchableClearActionTooltip.play = async ({ canvasElement }) => { + const canvas = within(canvasElement); + await userEvent.type(canvas.getByRole("combobox"), "r"); + const clearSelectionButton = canvas.getByRole("button"); + await userEvent.hover(clearSelectionButton); +}; diff --git a/packages/lib/src/select/Select.tsx b/packages/lib/src/select/Select.tsx index f3eb96e16..a211d9d2e 100644 --- a/packages/lib/src/select/Select.tsx +++ b/packages/lib/src/select/Select.tsx @@ -1,10 +1,10 @@ import * as Popover from "@radix-ui/react-popover"; -import { forwardRef, useCallback, useId, useLayoutEffect, useMemo, useRef, useState } from "react"; +import { forwardRef, useCallback, useId, useMemo, useRef, useState } from "react"; import styled, { ThemeProvider } from "styled-components"; import { getMargin } from "../common/utils"; import { spaces } from "../common/variables"; import DxcIcon from "../icon/Icon"; -import DxcTooltip from "../tooltip/Tooltip"; +import { Tooltip, TooltipWrapper } from "../tooltip/Tooltip"; import useTheme from "../useTheme"; import useTranslatedLabels from "../useTranslatedLabels"; import useWidth from "../utils/useWidth"; @@ -52,10 +52,9 @@ const DxcSelect = forwardRef<RefType, SelectPropsType>( const [searchValue, setSearchValue] = useState(""); const [visualFocusIndex, changeVisualFocusIndex] = useState(-1); const [isOpen, changeIsOpen] = useState(false); - + const [hasTooltip, setHasTooltip] = useState(false); const selectRef = useRef<HTMLDivElement | null>(null); const selectSearchInputRef = useRef<HTMLInputElement | null>(null); - const selectedOptionLabelRef = useRef(null); const width = useWidth(selectRef.current); const colorsTheme = useTheme(); @@ -229,13 +228,10 @@ const DxcSelect = forwardRef<RefType, SelectPropsType>( [handleSelectChangeValue, closeListbox, multiple] ); - useLayoutEffect(() => { - if (selectedOptionLabelRef?.current != null) { - if (selectedOptionLabelRef?.current.scrollWidth > selectedOptionLabelRef?.current.clientWidth) - selectedOptionLabelRef.current.title = getSelectedOptionLabel(placeholder, selectedOption); - else selectedOptionLabelRef.current.title = ""; - } - }, [placeholder, selectedOption]); + const handleOnMouseEnter = (event: React.MouseEvent<HTMLSpanElement>) => { + const text = event.currentTarget; + setHasTooltip(text.scrollWidth > text.clientWidth); + }; return ( <ThemeProvider theme={colorsTheme.select}> @@ -279,7 +275,7 @@ const DxcSelect = forwardRef<RefType, SelectPropsType>( {multiple && Array.isArray(selectedOption) && selectedOption.length > 0 && ( <SelectionIndicator> <SelectionNumber disabled={disabled}>{selectedOption.length}</SelectionNumber> - <DxcTooltip label={translatedLabels.select.actionClearSelectionTitle}> + <Tooltip label={translatedLabels.select.actionClearSelectionTitle}> <ClearOptionsAction disabled={disabled} onMouseDown={(event) => { @@ -292,58 +288,60 @@ const DxcSelect = forwardRef<RefType, SelectPropsType>( > <DxcIcon icon="clear" /> </ClearOptionsAction> - </DxcTooltip> + </Tooltip> </SelectionIndicator> )} - <SearchableValueContainer> - <input - style={{ display: "none" }} - name={name} - disabled={disabled} - value={ - multiple - ? ( - (value && Array.isArray(value) && value) ?? - (innerValue && Array.isArray(innerValue) && innerValue) - ).join(",") - : (value ?? innerValue) - } - readOnly - aria-hidden="true" - /> - {searchable && ( - <SearchInput - value={searchValue} - disabled={disabled} - onChange={handleSearchIOnChange} - ref={selectSearchInputRef} - autoComplete="nope" - autoCorrect="nope" - size={1} - aria-labelledby={label ? selectLabelId : undefined} - /> - )} - {(!searchable || searchValue === "") && ( - <SelectedOption + <TooltipWrapper condition={hasTooltip} label={getSelectedOptionLabel(placeholder, selectedOption)}> + <SearchableValueContainer> + <input + style={{ display: "none" }} + name={name} disabled={disabled} - atBackground={ - (multiple ? (value ?? innerValue).length === 0 : !(value ?? innerValue)) || - (searchable && isOpen) + value={ + multiple + ? ( + (value && Array.isArray(value) && value) ?? + (innerValue && Array.isArray(innerValue) && innerValue) + ).join(",") + : (value ?? innerValue) } - > - <SelectedOptionLabel ref={selectedOptionLabelRef}> - {getSelectedOptionLabel(placeholder, selectedOption)} - </SelectedOptionLabel> - </SelectedOption> - )} - </SearchableValueContainer> + readOnly + aria-hidden="true" + /> + {searchable && ( + <SearchInput + value={searchValue} + disabled={disabled} + onChange={handleSearchIOnChange} + ref={selectSearchInputRef} + autoComplete="nope" + autoCorrect="nope" + size={1} + aria-labelledby={label ? selectLabelId : undefined} + /> + )} + {(!searchable || searchValue === "") && ( + <SelectedOption + disabled={disabled} + atBackground={ + (multiple ? (value ?? innerValue).length === 0 : !(value ?? innerValue)) || + (searchable && isOpen) + } + > + <SelectedOptionLabel onMouseEnter={handleOnMouseEnter}> + {getSelectedOptionLabel(placeholder, selectedOption)} + </SelectedOptionLabel> + </SelectedOption> + )} + </SearchableValueContainer> + </TooltipWrapper> {!disabled && error && ( <ErrorIcon> <DxcIcon icon="filled_error" /> </ErrorIcon> )} {searchable && searchValue.length > 0 && ( - <DxcTooltip label={translatedLabels.select.actionClearSelectionTitle}> + <Tooltip label={translatedLabels.select.actionClearSelectionTitle}> <ClearSearchAction onMouseDown={(event) => { // Avoid input to lose focus @@ -355,7 +353,7 @@ const DxcSelect = forwardRef<RefType, SelectPropsType>( > <DxcIcon icon="clear" /> </ClearSearchAction> - </DxcTooltip> + </Tooltip> )} <CollapseIndicator disabled={disabled}> <DxcIcon icon={isOpen ? "keyboard_arrow_up" : "keyboard_arrow_down"} /> diff --git a/packages/lib/src/select/selectUtils.ts b/packages/lib/src/select/selectUtils.ts index 8f549dc14..d0840beb2 100644 --- a/packages/lib/src/select/selectUtils.ts +++ b/packages/lib/src/select/selectUtils.ts @@ -130,7 +130,7 @@ const getSelectedOption = ( }; /** - * Return the label or labels of the selected option(s) for the internal input. + * Return the label or labels of the selected option(s), separated by commas. */ const getSelectedOptionLabel = (placeholder: string, selectedOption: ListOptionType | ListOptionType[]) => Array.isArray(selectedOption) diff --git a/packages/lib/src/tabs/Tab.tsx b/packages/lib/src/tabs/Tab.tsx index 1cd4abd0d..a355e9565 100644 --- a/packages/lib/src/tabs/Tab.tsx +++ b/packages/lib/src/tabs/Tab.tsx @@ -2,7 +2,7 @@ import { forwardRef, Ref, useContext, useEffect, useRef } from "react"; import styled from "styled-components"; import DxcBadge from "../badge/Badge"; import DxcIcon from "../icon/Icon"; -import DxcTooltip from "../tooltip/Tooltip"; +import { Tooltip } from "../tooltip/Tooltip"; import useTheme from "../useTheme"; import BaseTypography from "../utils/BaseTypography"; import { TabsContext } from "./TabsContext"; @@ -64,7 +64,7 @@ const DxcTab = forwardRef( }; return ( - <DxcTooltip label={title}> + <Tooltip label={title}> <TabContainer role="tab" type="button" @@ -128,7 +128,7 @@ const DxcTab = forwardRef( </BadgeContainer> )} </TabContainer> - </DxcTooltip> + </Tooltip> ); } ); diff --git a/packages/lib/src/toggle-group/ToggleGroup.tsx b/packages/lib/src/toggle-group/ToggleGroup.tsx index 30853f117..91206df98 100644 --- a/packages/lib/src/toggle-group/ToggleGroup.tsx +++ b/packages/lib/src/toggle-group/ToggleGroup.tsx @@ -3,7 +3,7 @@ import styled, { ThemeProvider } from "styled-components"; import { spaces } from "../common/variables"; import DxcFlex from "../flex/Flex"; import DxcIcon from "../icon/Icon"; -import DxcTooltip from "../tooltip/Tooltip"; +import { Tooltip } from "../tooltip/Tooltip"; import useTheme from "../useTheme"; import ToggleGroupPropsType, { OptionLabel } from "./types"; @@ -67,7 +67,7 @@ const DxcToggleGroup = ({ <HelperText disabled={disabled}>{helperText}</HelperText> <OptionsContainer aria-labelledby={toggleGroupLabelId}> {options.map((option, i) => ( - <DxcTooltip label={option.title}> + <Tooltip label={option.title}> <ToggleButton key={`toggle-${i}-${option.label}`} aria-label={option.title} @@ -109,7 +109,7 @@ const DxcToggleGroup = ({ {option.label && <LabelContainer>{option.label}</LabelContainer>} </DxcFlex> </ToggleButton> - </DxcTooltip> + </Tooltip> ))} </OptionsContainer> </ToggleGroup> diff --git a/packages/lib/src/tooltip/Tooltip.tsx b/packages/lib/src/tooltip/Tooltip.tsx index 8881ae6a4..ae46bfdc0 100644 --- a/packages/lib/src/tooltip/Tooltip.tsx +++ b/packages/lib/src/tooltip/Tooltip.tsx @@ -1,60 +1,16 @@ -import * as Tooltip from "@radix-ui/react-tooltip"; import styled from "styled-components"; import CoreTokens from "../common/coreTokens"; -import TooltipPropsType from "./types"; -import { TooltipContext } from "./TooltipContext"; -import { useContext } from "react"; - -const triangleIcon = ( - <svg - width="12" - height="6" - viewBox="0 0 12 6" - xmlns="http://www.w3.org/2000/svg" - preserveAspectRatio="none" - display="block" - > - <path - d="M0.351562 0L5.30131 4.94975C5.69184 5.34027 6.325 5.34027 6.71552 4.94975L11.6653 0H6.00842H0.351562Z" - fill={CoreTokens.color_grey_800} - /> - </svg> -); - -const DxcTooltip = ({ position = "bottom", label, children }: TooltipPropsType): JSX.Element => { - const hasTooltip = useContext(TooltipContext); - - return ( - <TooltipContext.Provider value={true}> - {label && !hasTooltip ? ( - <Tooltip.Provider delayDuration={300}> - <Tooltip.Root> - <Tooltip.Trigger asChild> - <TooltipTriggerContainer>{children}</TooltipTriggerContainer> - </Tooltip.Trigger> - <Tooltip.Portal> - <StyledTooltipContent side={position} sideOffset={8}> - <TooltipContainer>{label}</TooltipContainer> - <Tooltip.Arrow asChild aria-hidden> - {triangleIcon} - </Tooltip.Arrow> - </StyledTooltipContent> - </Tooltip.Portal> - </Tooltip.Root> - </Tooltip.Provider> - ) : ( - children - )} - </TooltipContext.Provider> - ); -}; +import TooltipPropsType, { TooltipWrapperProps } from "./types"; +import { createContext, useContext } from "react"; +import { Root, Trigger, Portal, Arrow, Content } from "@radix-ui/react-tooltip"; +import { Provider } from "@radix-ui/react-tooltip"; const TooltipTriggerContainer = styled.div` - display: inline-flex; position: relative; + display: inline-flex; `; -const StyledTooltipContent = styled(Tooltip.Content)` +const StyledTooltipContent = styled(Content)` z-index: 2147483647; animation-duration: 0.2s; @@ -120,14 +76,71 @@ const StyledTooltipContent = styled(Tooltip.Content)` const TooltipContainer = styled.div` box-sizing: border-box; - padding: 8px 12px; + max-width: 242px; border-radius: 4px; + border-color: ${CoreTokens.color_grey_800}; + padding: 8px 12px; font-size: ${CoreTokens.type_scale_01}; font-family: ${CoreTokens.type_sans}; - max-width: 242px; color: ${CoreTokens.color_white}; background-color: ${CoreTokens.color_grey_800}; - border-color: ${CoreTokens.color_grey_800}; + overflow-wrap: break-word; `; -export default DxcTooltip; +const triangleIcon = ( + <svg + width="12" + height="6" + viewBox="0 0 12 6" + xmlns="http://www.w3.org/2000/svg" + preserveAspectRatio="none" + display="block" + > + <path + d="M0.351562 0L5.30131 4.94975C5.69184 5.34027 6.325 5.34027 6.71552 4.94975L11.6653 0H6.00842H0.351562Z" + fill={CoreTokens.color_grey_800} + /> + </svg> +); + +const TooltipContext = createContext(false); + +export const Tooltip = ({ + label, + hasAdditionalContainer = false, + position = "bottom", + children, +}: { hasAdditionalContainer?: boolean } & TooltipPropsType): JSX.Element => { + const hasTooltip = useContext(TooltipContext); + + return ( + <TooltipContext.Provider value={true}> + {label && !hasTooltip ? ( + <Provider delayDuration={300}> + <Root> + <Trigger asChild> + {hasAdditionalContainer ? <TooltipTriggerContainer>{children}</TooltipTriggerContainer> : children} + </Trigger> + <Portal> + <StyledTooltipContent side={position} sideOffset={8}> + <TooltipContainer>{label}</TooltipContainer> + <Arrow asChild aria-hidden> + {triangleIcon} + </Arrow> + </StyledTooltipContent> + </Portal> + </Root> + </Provider> + ) : ( + children + )} + </TooltipContext.Provider> + ); +}; + +export const TooltipWrapper = ({ condition, children, label }: TooltipWrapperProps) => + condition ? <Tooltip label={label}>{children}</Tooltip> : <>{children}</>; + +export default function DxcTooltip(props: TooltipPropsType) { + return <Tooltip {...props} hasAdditionalContainer />; +} diff --git a/packages/lib/src/tooltip/TooltipContext.tsx b/packages/lib/src/tooltip/TooltipContext.tsx deleted file mode 100644 index 5ac842bf3..000000000 --- a/packages/lib/src/tooltip/TooltipContext.tsx +++ /dev/null @@ -1,3 +0,0 @@ -import { createContext } from "react"; - -export const TooltipContext = createContext(false); diff --git a/packages/lib/src/tooltip/types.tsx b/packages/lib/src/tooltip/types.tsx index 25a4a996a..a61ae9491 100644 --- a/packages/lib/src/tooltip/types.tsx +++ b/packages/lib/src/tooltip/types.tsx @@ -13,4 +13,10 @@ type Props = { children: React.ReactNode; }; +export type TooltipWrapperProps = { + condition?: boolean; + children: React.ReactNode; + label?: string; +}; + export default Props;