diff --git a/apps/website/pages/components/dropdown/code.tsx b/apps/website/pages/components/dropdown/code.tsx new file mode 100644 index 0000000000..0c8172ff01 --- /dev/null +++ b/apps/website/pages/components/dropdown/code.tsx @@ -0,0 +1,19 @@ +import Head from "next/head"; +import type { ReactElement } from "react"; +import DropdownPageLayout from "screens/components/dropdown/DropdownPageLayout"; +import DropdownCodePage from "screens/components/dropdown/code/DropdownCodePage"; + +const Code = () => { + return ( + <> + + Dropdown Code — Halstack Design System + + + + ); +}; + +Code.getLayout = (page: ReactElement) => {page}; + +export default Code; diff --git a/apps/website/pages/components/dropdown/index.tsx b/apps/website/pages/components/dropdown/index.tsx index 7432d8ce33..cbfd01fbe0 100644 --- a/apps/website/pages/components/dropdown/index.tsx +++ b/apps/website/pages/components/dropdown/index.tsx @@ -1,7 +1,7 @@ import Head from "next/head"; import type { ReactElement } from "react"; import DropdownPageLayout from "screens/components/dropdown/DropdownPageLayout"; -import DropdownCodePage from "screens/components/dropdown/code/DropdownCodePage"; +import DropdownOverviewPage from "screens/components/dropdown/overview/DropdownOverviewPage"; const Index = () => { return ( @@ -9,13 +9,11 @@ const Index = () => { Dropdown — Halstack Design System - + ); }; -Index.getLayout = function getLayout(page: ReactElement) { - return {page}; -}; +Index.getLayout = (page: ReactElement) => {page}; export default Index; diff --git a/apps/website/pages/components/dropdown/specifications.tsx b/apps/website/pages/components/dropdown/specifications.tsx deleted file mode 100644 index c76e0006ae..0000000000 --- a/apps/website/pages/components/dropdown/specifications.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import Head from "next/head"; -import type { ReactElement } from "react"; -import DropdownPageLayout from "screens/components/dropdown/DropdownPageLayout"; -import DropdownSpecsPage from "screens/components/dropdown/specs/DropdownSpecsPage"; - -const Specifications = () => { - return ( - <> - - Dropdown Specs — Halstack Design System - - - - ); -}; - -Specifications.getLayout = function getLayout(page: ReactElement) { - return {page}; -}; - -export default Specifications; diff --git a/apps/website/pages/components/dropdown/usage.tsx b/apps/website/pages/components/dropdown/usage.tsx deleted file mode 100644 index 10037bd07b..0000000000 --- a/apps/website/pages/components/dropdown/usage.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import Head from "next/head"; -import type { ReactElement } from "react"; -import DropdownPageLayout from "screens/components/dropdown/DropdownPageLayout"; -import DropdownUsagePage from "screens/components/dropdown/usage/DropdownUsagePage"; - -const Usage = () => { - return ( - <> - - Dropdown Usage — Halstack Design System - - - - ); -}; - -Usage.getLayout = function getLayout(page: ReactElement) { - return {page}; -}; - -export default Usage; diff --git a/apps/website/screens/components/dropdown/DropdownPageLayout.tsx b/apps/website/screens/components/dropdown/DropdownPageLayout.tsx index 2c62cf8ee1..1fb0d5236c 100644 --- a/apps/website/screens/components/dropdown/DropdownPageLayout.tsx +++ b/apps/website/screens/components/dropdown/DropdownPageLayout.tsx @@ -6,9 +6,8 @@ import { ReactNode } from "react"; const DropdownPageHeading = ({ children }: { children: ReactNode }) => { const tabs = [ - { label: "Code", path: "/components/dropdown" }, - { label: "Usage", path: "/components/dropdown/usage" }, - { label: "Specifications", path: "/components/dropdown/specifications" }, + { label: "Overview", path: "/components/dropdown" }, + { label: "Code", path: "/components/dropdown/code" }, ]; return ( @@ -17,12 +16,10 @@ const DropdownPageHeading = ({ children }: { children: ReactNode }) => { - The use of dropdowns has its advantages but it depends on the screen support. Dropdowns are a standard - widget, so the users know how to interact with them. The options available in a dropdown component are - static, preventing erroneous data entered by the user since it only shows a range of correct values for that - input. + The dropdown component is a compact, interactive element that allows users to select from a list of options, + reducing clutter in the interface. - + {children} diff --git a/apps/website/screens/components/dropdown/code/DropdownCodePage.tsx b/apps/website/screens/components/dropdown/code/DropdownCodePage.tsx index 02163b429c..2ce387ea7e 100644 --- a/apps/website/screens/components/dropdown/code/DropdownCodePage.tsx +++ b/apps/website/screens/components/dropdown/code/DropdownCodePage.tsx @@ -25,49 +25,33 @@ const sections = [ + caretHidden - - - options - + boolean + Whether the arrow next to the label must be displayed or not. - - { - "{ label?: string; icon?: string | (React.ReactNode & React.SVGProps ); value: string }[]" - } - + false + + + disabled - An array of objects representing the options. Each object has the following properties: - + boolean + + If true, the component will be disabled. + + false - - - optionsIconPosition + expandOnHover - 'before' | 'after' + boolean - In case options include icons, whether the icon should appear after or before the label. + If true, the options are shown when the dropdown is hovered. - 'before' + false @@ -104,63 +88,79 @@ const sections = [ - - caretHidden + margin - boolean + + 'xxsmall' | 'xsmall' | 'small' | 'medium' | 'large' | 'xlarge' | 'xxlarge' | Margin + - Whether the arrow next to the label must be displayed or not. - false + Size of the margin to be applied to the component. You can pass an object with 'top', 'bottom', 'left' + and 'right' properties in order to specify different margin sizes. + - - disabled - - boolean - - If true, the component will be disabled. - false + + + onSelectOption + - - - expandOnHover - boolean + {"(value: string) => void"} - If true, the options are shown when the dropdown is hovered. - false + This function will be called every time the selection changes. The value of the selected option will be + passed as a parameter. + - - onSelectOption + options - {"(value: string) => void"} + + { + "{ label?: string; icon?: string | (React.ReactNode & React.SVGProps ); value: string }[]" + } + - This function will be called every time the selection changes. The value of the selected option will be - passed as a parameter. + An array of objects representing the options. Each object has the following properties: + - - margin + optionsIconPosition - - 'xxsmall' | 'xsmall' | 'small' | 'medium' | 'large' | 'xlarge' | 'xxlarge' | Margin - + 'before' | 'after' + In case options include icons, whether the icon should appear after or before the label. - Size of the margin to be applied to the component. You can pass an object with 'top', 'bottom', 'left' - and 'right' properties in order to specify different margin sizes. + 'before' - - size @@ -223,15 +223,13 @@ const sections = [ }, ]; -const DropdownCodePage = () => { - return ( - - - - - - - ); -}; +const DropdownCodePage = () => ( + + + + + + +); export default DropdownCodePage; diff --git a/apps/website/screens/components/dropdown/overview/DropdownOverviewPage.tsx b/apps/website/screens/components/dropdown/overview/DropdownOverviewPage.tsx new file mode 100644 index 0000000000..d70381de65 --- /dev/null +++ b/apps/website/screens/components/dropdown/overview/DropdownOverviewPage.tsx @@ -0,0 +1,149 @@ +import { DxcBulletedList, DxcFlex, DxcParagraph } from "@dxc-technology/halstack-react"; +import DocFooter from "@/common/DocFooter"; +import QuickNavContainer from "@/common/QuickNavContainer"; +import QuickNavContainerLayout from "@/common/QuickNavContainerLayout"; +import Example from "@/common/example/Example"; +import iconUsage from "./examples/iconUsage"; +import Image from "@/common/Image"; +import anatomy from "./images/dropdown_anatomy.png"; + +const sections = [ + { + title: "Introduction", + content: ( + <> + + The dropdown enhances usability by displaying a list of choices in a collapsible menu, optimizing space while + keeping options easily accessible. It supports icons, sections, and different selection behaviors to adapt to + various use cases. Designed for efficiency, it ensures keyboard navigation, accessibility, and proper contrast + for readability. + + + ), + }, + { + title: "Anatomy", + content: ( + <> + Dropdown's anatomy + + + Dropdown: the main container that triggers the list of options when clicked, allowing users + to select an item. + + + Listbox: the expanded panel displaying all available options for selection. + + + Icon: a visual aid that can accompany the selected option, helping users quickly recognize + the category or purpose. + + + Label: the textual representation of the selected option, ensuring clarity in the user's + choice. + + + Expand/collapse icon: an indicator that shows whether the dropdown is expanded or + collapsed. + + + List item: an individual option within the dropdown list, which users can click to make a + selection. + + + + ), + }, + { + title: "Using dropdowns", + content: ( + <> + + Dropdowns have a similar look and behavior to select components, the difference is that while select is only + to collect user's data into a form, dropdown can be used in various scenarios. + + + + Dropdowns display a list of options that appear when the user clicks or hovers over the parent element, + providing a compact and efficient way to make selections. + + + The arrow linked to the dropdown label indicates to the user that more options are available but currently + hidden. + + + By default, a dropdown expands below its main container if there is enough screen space to accommodate the + full size of the pop-up. + + + + If displaying the dropdown below the selector hides important information, reducing discoverability and + scanability, consider alternative ways to present the content or adjust the pop-up's position to better fit + the application's needs. + + + ), + subSections: [ + { + title: "Icon usage", + content: ( + <> + + Icons can be used within the dropdown component in various configurations. They can be placed before or + after the label or serve as the sole content of the dropdown placeholder and options. This maintains + consistency with other components in our Design System, such as buttons and selects, which follow the same + behavior. + + + + ), + }, + ], + }, + { + title: "Best practices", + content: ( + + + User clear and concise labels: ensure dropdown labels are descriptive and easily understood, + helping users quickly grasp their choices. Avoid vague terms like "Select an option.” + + + Prioritize logical ordering: arrange options in a meaningful order—alphabetically for lists, + by frequency of use for common selections, or categorically when grouping similar items. + + + Keep the list of options manageable: avoid overwhelming users with too many options. If the + list is long, consider using grouped sections or an alternative selection method like autocomplete. + + + Ensure accessibility: provide sufficient contrast, keyboard navigation, and screen reader + support. Icons should always have accessible labels to maintain clarity. + + + Avoid nesting too deep: multi-level dropdowns can be hard to navigate. If multiple selection + levels are required, consider using a different component, like a sidebar or tree structure. + + + Be mindful of placement and screen space: ensure the dropdown appears in a location where it + doesn't obscure critical content. If needed, adjust its position dynamically to fit within the viewport. + + + Use icons thoughtfully: icons can enhance usability but should only be added when they add + clarity. Overloading the dropdown with icons can create visual clutter. + + + ), + }, +]; + +const DropdownOverviewPage = () => ( + + + + + + +); + +export default DropdownOverviewPage; diff --git a/apps/website/screens/components/dropdown/usage/examples/iconUsage.ts b/apps/website/screens/components/dropdown/overview/examples/iconUsage.ts similarity index 100% rename from apps/website/screens/components/dropdown/usage/examples/iconUsage.ts rename to apps/website/screens/components/dropdown/overview/examples/iconUsage.ts diff --git a/apps/website/screens/components/dropdown/overview/images/dropdown_anatomy.png b/apps/website/screens/components/dropdown/overview/images/dropdown_anatomy.png new file mode 100644 index 0000000000..d76d897b90 Binary files /dev/null and b/apps/website/screens/components/dropdown/overview/images/dropdown_anatomy.png differ diff --git a/apps/website/screens/components/dropdown/specs/DropdownSpecsPage.tsx b/apps/website/screens/components/dropdown/specs/DropdownSpecsPage.tsx deleted file mode 100644 index 563745d180..0000000000 --- a/apps/website/screens/components/dropdown/specs/DropdownSpecsPage.tsx +++ /dev/null @@ -1,585 +0,0 @@ -import { DxcTable, DxcParagraph, DxcBulletedList, DxcFlex, DxcLink } from "@dxc-technology/halstack-react"; -import Image from "@/common/Image"; -import Figure from "@/common/Figure"; -import Code from "@/common/Code"; -import DocFooter from "@/common/DocFooter"; -import QuickNavContainer from "@/common/QuickNavContainer"; -import QuickNavContainerLayout from "@/common/QuickNavContainerLayout"; -import specsImage from "./images/dropdown_specs.png"; -import statesImage from "./images/dropdown_states.png"; -import optionListStatesImage from "./images/dropdown_option_list_states.png"; - -const sections = [ - { - title: "Specifications", - content: ( -
- Dropdown design specifications -
- ), - }, - { - title: "States", - subSections: [ - { - title: "Dropdown button", - content: ( - <> - - States: enabled, hover, focus, active{" "} - and disabled. - -
- Dropdown button states -
- - ), - }, - { - title: "Option list", - content: ( - <> - - States: Enabled, hover, focus and{" "} - selected. - -
- Option list states -
- - ), - }, - ], - }, - { - title: "Design tokens", - subSections: [ - { - title: "Color", - content: ( - - - - Component Token - Element - Core token - Value - - - - - - buttonBackgroundColor - - Button - - color-white - - #ffffff - - - - buttonFontColor - - Button - - color-black - - #000000 - - - - hoverButtonBackgroundColor - - Button:hover - - color-grey-100 - - #f2f2f2 - - - - activeButtonBackgroundColor - - Button:active - - color-grey-300 - - #cccccc - - - - buttonIconColor - - Icon - - color-black - - #000000 - - - - disabledColor - - Button font:disabled - - color-grey-500 - - #999999 - - - - disabledButtonBackgroundColor - - Button:disabled - - color-transparent - - transparent - - - - optionBackgroundColor - - Option - - color-white - - #ffffff - - - - hoverOptionBackgroundColor - - Option:hover - - color-grey-100 - - #f2f2f2 - - - - activeOptionBackgroundColor - - Option:active - - color-grey-300 - - #cccccc - - - - scrollBarThumbColor - - Scroll thumb - - color-grey-700 - - #666666 - - - - scrollBarTrackColor - - Scroll track - - color-grey-300 - - #cccccc - - - - focusColor - - Focus - - color-blue-600 - - #0095ff - - - - ), - }, - { - title: "Width", - content: ( - - - - Width - Value - - - - - - small - - 60px - - - - medium - - 240px - - - - large - - 480px - - - - fitContent - - - - - - - fillParent - - - - - - - ), - }, - { - title: "Margin", - content: ( - - - - Margin - Value - - - - - - xxsmall - - 6px - - - - xsmall - - 16px - - - - small - - 24px - - - - medium - - 36px - - - - large - - 48px - - - - xlarge - - 64px - - - - xxlarge - - 100px - - - - ), - }, - { - title: "Padding", - content: ( - - - - Property - Element - Value - - - - - - padding-left - - Dropdown button - 16px - - - - padding-left - - Options list - 16px - - - - padding-right - - Dropdown button - 16px - - - - padding-right - - Options list - 16px - - - - ), - }, - { - title: "Border", - content: ( - - - - Property - Element - Core token - Value - - - - - - border-width - - Dropdown button - - border-width-0 - - 0rem / 0px - - - - border-style - - Dropdown button - - border-style-none - - none - - - - border-radius - - Dropdown button - - border-radius-medium - - 0.25rem / 4px - - - - border-width - - Options list - - border-width-0 - - 0rem / 0px - - - - border-style - - Options list - - border-style-none - - none - - - - border-radius - - Options list - - border-radius-medium - - 0.25rem / 4px - - - - border-width - - Focus outline - - border-width-2 - - 2px - - - - border-style - - Focus outline - - border-style-solid - - solid - - - - border-radius - - Focus outline - - border-radius-medium - - 0.25rem / 4px - - - - ), - }, - { - title: "Typography", - content: ( - - - - Property - Element - Value - - - - - - font-size - - Dropdown button - 1rem / 16px - - - - font-size - - List item - 1rem / 16px - - - - font-weight - - Dropdown button - 400 - - - - font-weight - - List item - 400 - - - - ), - }, - { - title: "Iconography", - content: ( - - - - Property - Element - Value - - - - - - height / width - - Caret - 24 / 24px - - - - height / width - - Custom icon - 20 / 20px - - - - ), - }, - ], - }, - { - title: "Accessibility", - subSections: [ - { - title: "WCAG 2.2", - content: ( - <> - - - Understanding WCAG 2.2 -{" "} - - SC 1.4.13: Content on Hover or Focus - - - - Understanding WCAG 2.2 -{" "} - - SC 3.2.2: On Input - - - - - ), - }, - { - title: "WAI-ARIA 1.2", - content: ( - - - WAI-ARIA Authoring Practices 1.2 -{" "} - - 3.16 Menu button - - - - ), - }, - ], - }, -]; - -const DropdownSpecsPage = () => { - return ( - - - - - - - ); -}; - -export default DropdownSpecsPage; diff --git a/apps/website/screens/components/dropdown/specs/images/dropdown_option_list_states.png b/apps/website/screens/components/dropdown/specs/images/dropdown_option_list_states.png deleted file mode 100644 index c1adacf3f5..0000000000 Binary files a/apps/website/screens/components/dropdown/specs/images/dropdown_option_list_states.png and /dev/null differ diff --git a/apps/website/screens/components/dropdown/specs/images/dropdown_specs.png b/apps/website/screens/components/dropdown/specs/images/dropdown_specs.png deleted file mode 100644 index 4322b675e9..0000000000 Binary files a/apps/website/screens/components/dropdown/specs/images/dropdown_specs.png and /dev/null differ diff --git a/apps/website/screens/components/dropdown/specs/images/dropdown_states.png b/apps/website/screens/components/dropdown/specs/images/dropdown_states.png deleted file mode 100644 index 3ab10031cc..0000000000 Binary files a/apps/website/screens/components/dropdown/specs/images/dropdown_states.png and /dev/null differ diff --git a/apps/website/screens/components/dropdown/usage/DropdownUsagePage.tsx b/apps/website/screens/components/dropdown/usage/DropdownUsagePage.tsx deleted file mode 100644 index f529e424c1..0000000000 --- a/apps/website/screens/components/dropdown/usage/DropdownUsagePage.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import { DxcBulletedList, DxcFlex, DxcParagraph } from "@dxc-technology/halstack-react"; -import DocFooter from "@/common/DocFooter"; -import QuickNavContainer from "@/common/QuickNavContainer"; -import QuickNavContainerLayout from "@/common/QuickNavContainerLayout"; -import Example from "@/common/example/Example"; -import iconUsage from "./examples/iconUsage"; - -const sections = [ - { - title: "Usage", - content: ( - <> - - Dropdowns have a similar look and behavior to select components, the difference is that while select is only - to collect user's data into a form, dropdown can be used in a variety of scenarios. - - - - Dropdowns can be useful as a list of items that will be shown when the user clicks or hovers their main - parent that will trigger the pop up with the options. - - - An arrow linked with the label of the dropdown should be shown to indicate the user that more options are - available but are currently hidden. - - - By default, every dropdown will be extending underneath his main container if the space in the screen is - enough to contain all the size declared for the pop-up. - - - - If there is a special case when the dropdown couldn't be displayed below the selector because it is hiding - important information reducing discoverability and scanability in the website then consider using other - options to display the information or customize the position of the pop-up to fitting the necessities of the - application. - - - ), - }, - { - title: "Icon usage", - content: ( - <> - - It is allowed the use of icons within the dropdown component. There are several options of configuration, the - icon can be placed before or after the label, also the icon can be the unique content of the dropdown - placeholder and options, so the final goal of this is to keep consistency with the rest of the components of - the Design System such as buttons or selects, that have the same behavior. - - - - ), - }, - { - title: "User Interface Design Considerations", - content: ( - - - Consider the number of options (binary decisions or a few items) to decide to implement one component that - represent in a better way the data, i.e. switch toggle. - - - For a large number of well specified options, consider using an autocomplete field to filter the number of - options while typing. - - - Consider the input, might be that a text input would fit better than a dropdown. - - - ), - }, -]; - -const DropdownUsagePage = () => { - return ( - - - - - - - ); -}; - -export default DropdownUsagePage; diff --git a/apps/website/screens/components/dropdown/usage/images/dropdown_icon_usage.png b/apps/website/screens/components/dropdown/usage/images/dropdown_icon_usage.png deleted file mode 100644 index 53c2a2541f..0000000000 Binary files a/apps/website/screens/components/dropdown/usage/images/dropdown_icon_usage.png and /dev/null differ diff --git a/packages/lib/src/dropdown/Dropdown.stories.tsx b/packages/lib/src/dropdown/Dropdown.stories.tsx index 2f1f9c57d5..f7453809ed 100644 --- a/packages/lib/src/dropdown/Dropdown.stories.tsx +++ b/packages/lib/src/dropdown/Dropdown.stories.tsx @@ -3,7 +3,6 @@ import { userEvent, within } from "@storybook/test"; import { ThemeProvider } from "styled-components"; import ExampleContainer from "../../.storybook/components/ExampleContainer"; import Title from "../../.storybook/components/Title"; -import { HalstackProvider } from "../HalstackContext"; import HalstackContext from "../HalstackContext"; import DxcDropdown from "./Dropdown"; import DropdownMenu from "./DropdownMenu"; @@ -86,14 +85,6 @@ const optionWithIcon: Option[] = [ const optionsIcon: any = options.map((op, i) => ({ ...op, icon: icons[i] })); -const opinionatedTheme = { - dropdown: { - baseColor: "#fabada", - fontColor: "#fff", - optionFontColor: "#0095ff", - }, -}; - const Dropdown = () => ( <> @@ -347,48 +338,6 @@ const DropdownListStates = () => { ); }; -const OpinionatedTheme = () => ( - <> - - <ExampleContainer> - <Title title="Default" theme="light" level={4} /> - <HalstackProvider theme={opinionatedTheme}> - <DxcDropdown label="Default" options={options} onSelectOption={(value) => {}} icon={iconSVG} /> - </HalstackProvider> - </ExampleContainer> - <ExampleContainer pseudoState="pseudo-hover"> - <Title title="Hovered" theme="light" level={4} /> - <HalstackProvider theme={opinionatedTheme}> - <DxcDropdown label="Hovered" options={options} onSelectOption={(value) => {}} icon={iconSVG} /> - </HalstackProvider> - </ExampleContainer> - <ExampleContainer pseudoState="pseudo-active"> - <Title title="Active" theme="light" level={4} /> - <HalstackProvider theme={opinionatedTheme}> - <DxcDropdown label="Active" options={options} onSelectOption={(value) => {}} icon={iconSVG} /> - </HalstackProvider> - </ExampleContainer> - <ExampleContainer pseudoState="pseudo-focus"> - <Title title="Focused" theme="light" level={4} /> - <HalstackProvider theme={opinionatedTheme}> - <DxcDropdown label="Focused" options={options} onSelectOption={(value) => {}} icon={iconSVG} /> - </HalstackProvider> - </ExampleContainer> - <ExampleContainer> - <Title title="Disabled" theme="light" level={4} /> - <HalstackProvider theme={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> - </> -); - const TooltipTitle = () => ( <ExampleContainer expanded> <Title title="Tooltip" theme="light" level={3} /> @@ -415,16 +364,6 @@ export const Chromatic: Story = { }, }; -export const OpinionatedThemed: Story = { - render: OpinionatedTheme, - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - const buttonList = canvas.getAllByRole("button"); - const lastButton = buttonList[buttonList.length - 1]; - lastButton != null && (await userEvent.click(lastButton)); - }, -}; - export const MenuStates: Story = { render: DropdownListStates, play: async ({ canvasElement }) => { diff --git a/packages/lib/src/dropdown/Dropdown.tsx b/packages/lib/src/dropdown/Dropdown.tsx index 070005679d..3fed994a6e 100644 --- a/packages/lib/src/dropdown/Dropdown.tsx +++ b/packages/lib/src/dropdown/Dropdown.tsx @@ -1,6 +1,6 @@ import * as Popover from "@radix-ui/react-popover"; import { FocusEvent, KeyboardEvent, useCallback, useId, useLayoutEffect, useRef, useState, useContext } from "react"; -import styled, { ThemeProvider } from "styled-components"; +import styled from "styled-components"; import { getMargin } from "../common/utils"; import { spaces } from "../common/variables"; import DxcIcon from "../icon/Icon"; @@ -10,6 +10,108 @@ import DropdownMenu from "./DropdownMenu"; import DropdownPropsType from "./types"; import { Tooltip } from "../tooltip/Tooltip"; +const sizes = { + small: "60px", + medium: "240px", + large: "480px", + fillParent: "100%", + fitContent: "fit-content", +}; + +const calculateWidth = (margin: DropdownPropsType["margin"], size: DropdownPropsType["size"]) => + size != null && + (size === "fillParent" + ? `calc(${sizes[size]} - ${getMargin(margin, "left")} - ${getMargin(margin, "right")})` + : sizes[size]); + +const DropdownContainer = styled.div<{ + margin: DropdownPropsType["margin"]; + size: DropdownPropsType["size"]; +}>` + width: ${(props) => calculateWidth(props.margin, props.size)}; + 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] : ""}; + margin-right: ${(props) => + props.margin && typeof props.margin === "object" && props.margin.right ? spaces[props.margin.right] : ""}; + margin-bottom: ${(props) => + props.margin && typeof props.margin === "object" && props.margin.bottom ? spaces[props.margin.bottom] : ""}; + margin-left: ${(props) => + props.margin && typeof props.margin === "object" && props.margin.left ? spaces[props.margin.left] : ""}; +`; + +const DropdownTrigger = styled.button<{ + label: DropdownPropsType["label"]; + margin: DropdownPropsType["margin"]; + size: DropdownPropsType["size"]; +}>` + display: flex; + justify-content: space-between; + align-items: center; + gap: var(--spacing-gap-s); + width: 100%; + height: var(--height-m); + padding: var(--spacing-padding-none) var(--spacing-padding-xs); + min-width: ${(props) => (props.label === "" ? "0px" : calculateWidth(props.margin, props.size))}; + border: 0; + border-radius: var(--border-radius-s); + background-color: var(--color-bg-neutral-lightest); + color: ${(props) => (props.disabled ? "var(--color-fg-neutral-medium);" : "var(--color-fg-neutral-dark);")}; + cursor: ${(props) => (props.disabled ? "not-allowed" : "pointer")}; + + ${(props) => + !props.disabled && + ` + &:focus { + outline: var(--border-width-m) var(--border-style-default) var(--border-color-secondary-medium); + outline-offset: -2px; + } + &:hover, &:active { + background-color: var(--color-bg-neutral-light); + } + `}; +`; + +const DropdownTriggerContent = styled.span<{ iconPosition: DropdownPropsType["iconPosition"] }>` + display: flex; + ${({ iconPosition }) => (iconPosition === "after" ? "flex-direction: row-reverse;" : "flex-direction: row;")} + align-items: center; + gap: var(--spacing-gap-xs); + width: 100%; + overflow: hidden; +`; + +const DropdownTriggerLabel = styled.span` + font-family: var(--typography-font-family); + font-size: var(--typography-label-l); + font-weight: var(--typography-label-regular); + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; +`; + +const DropdownTriggerIcon = styled.span<{ + disabled: DropdownPropsType["disabled"]; +}>` + display: flex; + font-size: var(--height-xs); + + svg { + width: 20px; + height: var(--height-xs); + } +`; + +const CaretIcon = styled.span<{ disabled: DropdownPropsType["disabled"] }>` + display: flex; + font-size: var(--typography-label-l); + + svg { + width: 16px; + height: var(--height-xxs); + } +`; + const DxcDropdown = ({ options, optionsIconPosition = "before", @@ -24,7 +126,7 @@ const DxcDropdown = ({ size = "fitContent", tabIndex = 0, title, -}: DropdownPropsType): JSX.Element => { +}: DropdownPropsType) => { const id = useId(); const triggerId = `trigger-${id}`; const menuId = `menu-${id}`; @@ -149,189 +251,72 @@ const DxcDropdown = ({ }, [visualFocusIndex]); return ( - <ThemeProvider theme={colorsTheme.dropdown}> - <DropdownContainer - onMouseEnter={!disabled && expandOnHover ? handleOnOpenMenu : undefined} - onMouseLeave={!disabled && expandOnHover ? handleOnCloseMenu : undefined} - onBlur={!disabled ? handleOnBlur : undefined} - margin={margin} - size={size} - > - <Popover.Root open={isOpen}> - <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> + <DropdownContainer + onMouseEnter={!disabled && expandOnHover ? handleOnOpenMenu : undefined} + onMouseLeave={!disabled && expandOnHover ? handleOnCloseMenu : undefined} + onBlur={!disabled ? handleOnBlur : undefined} + margin={margin} + size={size} + > + <Popover.Root open={isOpen}> + <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 iconPosition={iconPosition}> + {icon && ( + <DropdownTriggerIcon + disabled={disabled} + role={typeof icon === "string" ? undefined : "img"} + aria-hidden + > + {typeof icon === "string" ? <DxcIcon icon={icon} /> : icon} + </DropdownTriggerIcon> )} - </DropdownTrigger> - </Popover.Trigger> - </Tooltip> - <Popover.Portal> - <Popover.Content asChild sideOffset={1}> - <DropdownMenu - id={menuId} - dropdownTriggerId={triggerId} - options={options} - iconsPosition={optionsIconPosition} - visualFocusIndex={visualFocusIndex} - menuItemOnClick={handleMenuItemOnClick} - onKeyDown={handleMenuOnKeyDown} - styles={{ width, zIndex: "2147483647" }} - ref={menuRef} - /> - </Popover.Content> - </Popover.Portal> - </Popover.Root> - </DropdownContainer> - </ThemeProvider> + {label && <DropdownTriggerLabel>{label}</DropdownTriggerLabel>} + </DropdownTriggerContent> + {!caretHidden && ( + <CaretIcon disabled={disabled}> + <DxcIcon icon={isOpen ? "keyboard_arrow_up" : "keyboard_arrow_down"} /> + </CaretIcon> + )} + </DropdownTrigger> + </Popover.Trigger> + </Tooltip> + <Popover.Portal> + <Popover.Content asChild sideOffset={1}> + <DropdownMenu + id={menuId} + dropdownTriggerId={triggerId} + options={options} + iconsPosition={optionsIconPosition} + visualFocusIndex={visualFocusIndex} + menuItemOnClick={handleMenuItemOnClick} + onKeyDown={handleMenuOnKeyDown} + styles={{ width, zIndex: "2147483647" }} + ref={menuRef} + /> + </Popover.Content> + </Popover.Portal> + </Popover.Root> + </DropdownContainer> ); }; -const sizes = { - small: "60px", - medium: "240px", - large: "480px", - fillParent: "100%", - fitContent: "fit-content", -}; - -const calculateWidth = (margin: DropdownPropsType["margin"], size: DropdownPropsType["size"]) => - size != null && - (size === "fillParent" - ? `calc(${sizes[size]} - ${getMargin(margin, "left")} - ${getMargin(margin, "right")})` - : sizes[size]); - -const DropdownContainer = styled.div<{ - margin: DropdownPropsType["margin"]; - size: DropdownPropsType["size"]; -}>` - width: ${(props) => calculateWidth(props.margin, props.size)}; - 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] : ""}; - margin-right: ${(props) => - props.margin && typeof props.margin === "object" && props.margin.right ? spaces[props.margin.right] : ""}; - margin-bottom: ${(props) => - props.margin && typeof props.margin === "object" && props.margin.bottom ? spaces[props.margin.bottom] : ""}; - margin-left: ${(props) => - props.margin && typeof props.margin === "object" && props.margin.left ? spaces[props.margin.left] : ""}; -`; - -const DropdownTrigger = styled.button<{ - label: DropdownPropsType["label"]; - margin: DropdownPropsType["margin"]; - size: DropdownPropsType["size"]; -}>` - display: flex; - justify-content: space-between; - align-items: center; - gap: ${(props) => props.theme.caretIconSpacing}; - width: 100%; - height: ${(props) => props.theme.buttonHeight}; - min-width: ${(props) => (props.label === "" ? "0px" : calculateWidth(props.margin, props.size))}; - border-radius: ${(props) => props.theme.buttonBorderRadius}; - border-width: ${(props) => props.theme.buttonBorderThickness}; - border-style: ${(props) => props.theme.buttonBorderStyle}; - border-color: ${(props) => (props.disabled ? props.theme.disabledButtonBorderColor : props.theme.buttonBorderColor)}; - padding-top: ${(props) => props.theme.buttonPaddingTop}; - padding-bottom: ${(props) => props.theme.buttonPaddingBottom}; - padding-left: ${(props) => props.theme.buttonPaddingLeft}; - padding-right: ${(props) => props.theme.buttonPaddingRight}; - background-color: ${(props) => - props.disabled ? props.theme.disabledButtonBackgroundColor : props.theme.buttonBackgroundColor}; - color: ${(props) => (props.disabled ? props.theme.disabledColor : props.theme.buttonFontColor)}; - cursor: ${(props) => (props.disabled ? "not-allowed" : "pointer")}; - - ${(props) => - !props.disabled && - ` - &:focus { - outline: 2px solid ${props.theme.focusColor}; - } - &:hover { - background-color: ${props.theme.hoverButtonBackgroundColor}; - } - &:active { - background-color: ${props.theme.activeButtonBackgroundColor}; - } - `}; -`; - -const DropdownTriggerContent = styled.span` - display: flex; - align-items: center; - gap: ${(props) => props.theme.buttonIconSpacing}; - margin-left: 0px; - margin-right: 0px; - width: 100%; - overflow: hidden; - white-space: nowrap; -`; - -const DropdownTriggerLabel = styled.span` - font-family: ${(props) => props.theme.buttonFontFamily}; - font-size: ${(props) => props.theme.buttonFontSize}; - font-style: ${(props) => props.theme.buttonFontStyle}; - font-weight: ${(props) => props.theme.buttonFontWeight}; - text-overflow: ellipsis; - overflow: hidden; -`; - -const DropdownTriggerIcon = styled.span<{ - disabled: DropdownPropsType["disabled"]; -}>` - display: flex; - color: ${(props) => (props.disabled ? props.theme.disabledColor : props.theme.buttonIconColor)}; - font-size: ${(props) => props.theme.buttonIconSize}; - - svg { - width: ${(props) => props.theme.buttonIconSize}; - height: ${(props) => props.theme.buttonIconSize}; - } -`; - -const CaretIcon = styled.span<{ disabled: DropdownPropsType["disabled"] }>` - display: flex; - color: ${(props) => (props.disabled ? props.theme.disabledColor : props.theme.caretIconColor)}; - font-size: ${(props) => props.theme.caretIconSize}; - - svg { - width: ${(props) => props.theme.caretIconSize}; - height: ${(props) => props.theme.caretIconSize}; - } -`; - export default DxcDropdown; diff --git a/packages/lib/src/dropdown/DropdownMenu.tsx b/packages/lib/src/dropdown/DropdownMenu.tsx index 900b1d6500..92bead8f4c 100644 --- a/packages/lib/src/dropdown/DropdownMenu.tsx +++ b/packages/lib/src/dropdown/DropdownMenu.tsx @@ -2,12 +2,27 @@ import { forwardRef, memo } from "react"; import styled from "styled-components"; import DropdownMenuItem from "./DropdownMenuItem"; import { DropdownMenuProps } from "./types"; +import { scrollbarStyles } from "../styles/scroll"; + +const DropdownMenuContainer = styled.ul` + max-height: 230px; + min-width: min-content; + padding: 0; + margin: 0; + background-color: var(--color-bg-neutral-lightest); + border-radius: var(--border-radius-s); + box-shadow: var(--shadow-low-x-position) var(--shadow-low-y-position) var(--shadow-low-blur) var(--shadow-low-spread) + var(--shadow-dark); + outline: none; + overflow-y: auto; + ${scrollbarStyles} +`; const DropdownMenu = forwardRef<HTMLUListElement, DropdownMenuProps>( ( { id, dropdownTriggerId, iconsPosition, visualFocusIndex, menuItemOnClick, onKeyDown, options, styles }, ref - ): JSX.Element => ( + ) => ( <DropdownMenuContainer onMouseDown={(event) => { // Prevent the onBlur event from closing menu when clicking on the menu since @@ -38,35 +53,4 @@ const DropdownMenu = forwardRef<HTMLUListElement, DropdownMenuProps>( ) ); -const DropdownMenuContainer = styled.ul` - box-sizing: border-box; - max-height: 230px; - min-width: min-content; - padding: 0; - margin: 0; - background-color: ${(props) => props.theme.optionBackgroundColor}; - border-width: ${(props) => props.theme.borderThickness}; - border-style: ${(props) => props.theme.borderStyle}; - border-color: ${(props) => props.theme.borderColor}; - border-radius: ${(props) => props.theme.borderRadius}; - border-top-right-radius: 0; - border-top-left-radius: 0; - box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); - outline: none; - - overflow-y: auto; - &::-webkit-scrollbar { - width: 8px; - height: 8px; - } - &::-webkit-scrollbar-thumb { - background-color: ${(props) => props.theme.scrollBarThumbColor}; - border-radius: 6px; - } - &::-webkit-scrollbar-track { - background-color: ${(props) => props.theme.scrollBarTrackColor}; - border-radius: 6px; - } -`; - export default memo(DropdownMenu); diff --git a/packages/lib/src/dropdown/DropdownMenuItem.tsx b/packages/lib/src/dropdown/DropdownMenuItem.tsx index 3d9f5d35d6..ddf9420a57 100644 --- a/packages/lib/src/dropdown/DropdownMenuItem.tsx +++ b/packages/lib/src/dropdown/DropdownMenuItem.tsx @@ -3,72 +3,76 @@ import styled from "styled-components"; import { DropdownMenuItemProps } from "./types"; import DxcIcon from "../icon/Icon"; -const DropdownMenuItem = ({ - id, - visuallyFocused, - iconPosition, - onClick, - option, -}: DropdownMenuItemProps): JSX.Element => ( - <DropdownMenuItemContainer - visuallyFocused={visuallyFocused} - onClick={() => { - onClick(option.value); - }} - id={id} - role="menuitem" - tabIndex={-1} - > - {iconPosition === "after" && <DropdownMenuItemLabel>{option.label}</DropdownMenuItemLabel>} - {option.icon && ( - <DropdownMenuItemIcon role={typeof option.icon === "string" ? undefined : "img"} aria-hidden> - {typeof option.icon === "string" ? <DxcIcon icon={option.icon} /> : option.icon} - </DropdownMenuItemIcon> - )} - {iconPosition === "before" && <DropdownMenuItemLabel>{option.label}</DropdownMenuItemLabel>} - </DropdownMenuItemContainer> -); - -const DropdownMenuItemContainer = styled.li<{ visuallyFocused: DropdownMenuItemProps["visuallyFocused"] }>` +const DropdownMenuItemContainer = styled.li<{ + visuallyFocused: DropdownMenuItemProps["visuallyFocused"]; + iconPosition: DropdownMenuItemProps["iconPosition"]; +}>` box-sizing: border-box; + color: var(--color-fg-neutral-dark); display: flex; align-items: center; - gap: ${(props) => props.theme.optionIconSpacing}; - min-height: 36px; - padding-top: ${(props) => props.theme.optionPaddingTop}; - padding-bottom: ${(props) => props.theme.optionPaddingBottom}; - padding-left: ${(props) => props.theme.optionPaddingLeft}; - padding-right: ${(props) => props.theme.optionPaddingRight}; + gap: var(--spacing-gap-xs); + height: var(--height-m); + padding: var(--spacing-padding-none) var(--spacing-padding-xs); cursor: pointer; - ${(props) => props.visuallyFocused && `outline: ${props.theme.focusColor} solid 2px; outline-offset: -2px;`} - &:hover { - background-color: ${(props) => props.theme.hoverOptionBackgroundColor}; + ${({ iconPosition }) => (iconPosition === "after" ? "flex-direction: row-reverse;" : "flex-direction: row;")} + + ${(props) => + props.visuallyFocused && + ` + outline: var(--border-width-m) var(--border-style-default) var(--border-color-secondary-medium); + outline-offset: calc(-1 * var(--border-width-m)); +`} + &:first-child { + border-top-left-radius: var(--border-radius-s); + border-top-right-radius: var(--border-radius-s); } + &:last-child { + border-bottom-left-radius: var(--border-radius-s); + border-bottom-right-radius: var(--border-radius-s); + } + &:hover, &:active { - background-color: ${(props) => props.theme.activeOptionBackgroundColor}; + background-color: var(--color-bg-neutral-light); } `; const DropdownMenuItemLabel = styled.span` - font-family: ${(props) => props.theme.optionFontFamily}; - font-size: ${(props) => props.theme.optionFontSize}; - font-style: ${(props) => props.theme.optionFontStyle}; - font-weight: ${(props) => props.theme.optionFontWeight}; - line-height: 1.5rem; - color: ${(props) => props.theme.optionFontColor}; + font-family: var(--typography-font-family); + font-size: var(--typography-label-l); + font-weight: var(--typography-label-regular); white-space: nowrap; `; const DropdownMenuItemIcon = styled.div` display: flex; - color: ${(props) => props.theme.optionIconColor}; - font-size: ${(props) => props.theme.optionIconSize}; + font-size: var(--height-xs); svg { - width: ${(props) => props.theme.optionIconSize}; - height: ${(props) => props.theme.optionIconSize}; + width: 20px; + height: var(--height-xs); } `; +const DropdownMenuItem = ({ id, visuallyFocused, iconPosition, onClick, option }: DropdownMenuItemProps) => ( + <DropdownMenuItemContainer + iconPosition={iconPosition} + visuallyFocused={visuallyFocused} + onClick={() => { + onClick(option.value); + }} + id={id} + role="menuitem" + tabIndex={-1} + > + {option.icon && ( + <DropdownMenuItemIcon role={typeof option.icon === "string" ? undefined : "img"} aria-hidden> + {typeof option.icon === "string" ? <DxcIcon icon={option.icon} /> : option.icon} + </DropdownMenuItemIcon> + )} + <DropdownMenuItemLabel>{option.label}</DropdownMenuItemLabel> + </DropdownMenuItemContainer> +); + export default memo(DropdownMenuItem);