Skip to content

Commit d55dfa7

Browse files
authoredMar 18, 2025··
Merge pull request #2161 from dxc-technology/Mil4n0r/tokens-dropdown
Dropdown redesign
2 parents 646cad5 + 64061fd commit d55dfa7

19 files changed

+478
-1117
lines changed
 
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import Head from "next/head";
2+
import type { ReactElement } from "react";
3+
import DropdownPageLayout from "screens/components/dropdown/DropdownPageLayout";
4+
import DropdownCodePage from "screens/components/dropdown/code/DropdownCodePage";
5+
6+
const Code = () => {
7+
return (
8+
<>
9+
<Head>
10+
<title>Dropdown Code — Halstack Design System</title>
11+
</Head>
12+
<DropdownCodePage />
13+
</>
14+
);
15+
};
16+
17+
Code.getLayout = (page: ReactElement) => <DropdownPageLayout>{page}</DropdownPageLayout>;
18+
19+
export default Code;
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,19 @@
11
import Head from "next/head";
22
import type { ReactElement } from "react";
33
import DropdownPageLayout from "screens/components/dropdown/DropdownPageLayout";
4-
import DropdownCodePage from "screens/components/dropdown/code/DropdownCodePage";
4+
import DropdownOverviewPage from "screens/components/dropdown/overview/DropdownOverviewPage";
55

66
const Index = () => {
77
return (
88
<>
99
<Head>
1010
<title>Dropdown — Halstack Design System</title>
1111
</Head>
12-
<DropdownCodePage></DropdownCodePage>
12+
<DropdownOverviewPage />
1313
</>
1414
);
1515
};
1616

17-
Index.getLayout = function getLayout(page: ReactElement) {
18-
return <DropdownPageLayout>{page}</DropdownPageLayout>;
19-
};
17+
Index.getLayout = (page: ReactElement) => <DropdownPageLayout>{page}</DropdownPageLayout>;
2018

2119
export default Index;

‎apps/website/pages/components/dropdown/specifications.tsx

-21
This file was deleted.

‎apps/website/pages/components/dropdown/usage.tsx

-21
This file was deleted.

‎apps/website/screens/components/dropdown/DropdownPageLayout.tsx

+5-8
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,8 @@ import { ReactNode } from "react";
66

77
const DropdownPageHeading = ({ children }: { children: ReactNode }) => {
88
const tabs = [
9-
{ label: "Code", path: "/components/dropdown" },
10-
{ label: "Usage", path: "/components/dropdown/usage" },
11-
{ label: "Specifications", path: "/components/dropdown/specifications" },
9+
{ label: "Overview", path: "/components/dropdown" },
10+
{ label: "Code", path: "/components/dropdown/code" },
1211
];
1312

1413
return (
@@ -17,12 +16,10 @@ const DropdownPageHeading = ({ children }: { children: ReactNode }) => {
1716
<DxcFlex direction="column" gap="2rem">
1817
<ComponentHeading name="Dropdown" />
1918
<DxcParagraph>
20-
The use of dropdowns has its advantages but it depends on the screen support. Dropdowns are a standard
21-
widget, so the users know how to interact with them. The options available in a dropdown component are
22-
static, preventing erroneous data entered by the user since it only shows a range of correct values for that
23-
input.
19+
The dropdown component is a compact, interactive element that allows users to select from a list of options,
20+
reducing clutter in the interface.
2421
</DxcParagraph>
25-
<TabsPageHeading tabs={tabs}></TabsPageHeading>
22+
<TabsPageHeading tabs={tabs} />
2623
</DxcFlex>
2724
</PageHeading>
2825
{children}

‎apps/website/screens/components/dropdown/code/DropdownCodePage.tsx

+67-69
Original file line numberDiff line numberDiff line change
@@ -25,49 +25,33 @@ const sections = [
2525
</thead>
2626
<tbody>
2727
<tr>
28+
<td>caretHidden</td>
2829
<td>
29-
<DxcFlex direction="column" gap="0.25rem" alignItems="baseline">
30-
<StatusBadge status="required" />
31-
options
32-
</DxcFlex>
30+
<TableCode>boolean</TableCode>
3331
</td>
32+
<td>Whether the arrow next to the label must be displayed or not.</td>
3433
<td>
35-
<TableCode>
36-
{
37-
"{ label?: string; icon?: string | (React.ReactNode & React.SVGProps <SVGSVGElement>); value: string }[]"
38-
}
39-
</TableCode>
34+
<TableCode>false</TableCode>
4035
</td>
36+
</tr>
37+
<tr>
38+
<td>disabled</td>
4139
<td>
42-
An array of objects representing the options. Each object has the following properties:
43-
<ul>
44-
<li>
45-
<b>label</b>: Option display value.
46-
</li>
47-
<li>
48-
<b>icon</b>:{" "}
49-
<DxcLink newWindow href="https://fonts.google.com/icons">
50-
Material Symbol
51-
</DxcLink>{" "}
52-
name or SVG element as the icon that will be placed next to the option label. When using Material
53-
Symbols, replace spaces with underscores. By default they are outlined if you want it to be filled
54-
prefix the symbol name with <TableCode>"filled_"</TableCode>.
55-
</li>
56-
<li>
57-
<b>value</b>: Option inner value.
58-
</li>
59-
</ul>
40+
<TableCode>boolean</TableCode>
41+
</td>
42+
<td>If true, the component will be disabled.</td>
43+
<td>
44+
<TableCode>false</TableCode>
6045
</td>
61-
<td>-</td>
6246
</tr>
6347
<tr>
64-
<td>optionsIconPosition</td>
48+
<td>expandOnHover</td>
6549
<td>
66-
<TableCode>'before' | 'after'</TableCode>
50+
<TableCode>boolean</TableCode>
6751
</td>
68-
<td>In case options include icons, whether the icon should appear after or before the label.</td>
52+
<td>If true, the options are shown when the dropdown is hovered.</td>
6953
<td>
70-
<TableCode>'before'</TableCode>
54+
<TableCode>false</TableCode>
7155
</td>
7256
</tr>
7357
<tr>
@@ -104,63 +88,79 @@ const sections = [
10488
<td>-</td>
10589
</tr>
10690
<tr>
107-
<td>caretHidden</td>
91+
<td>margin</td>
10892
<td>
109-
<TableCode>boolean</TableCode>
93+
<TableCode>
94+
'xxsmall' | 'xsmall' | 'small' | 'medium' | 'large' | 'xlarge' | 'xxlarge' | Margin
95+
</TableCode>
11096
</td>
111-
<td>Whether the arrow next to the label must be displayed or not.</td>
11297
<td>
113-
<TableCode>false</TableCode>
98+
Size of the margin to be applied to the component. You can pass an object with 'top', 'bottom', 'left'
99+
and 'right' properties in order to specify different margin sizes.
114100
</td>
101+
<td>-</td>
115102
</tr>
116103
<tr>
117-
<td>disabled</td>
118-
<td>
119-
<TableCode>boolean</TableCode>
120-
</td>
121-
<td>If true, the component will be disabled.</td>
122104
<td>
123-
<TableCode>false</TableCode>
105+
<DxcFlex direction="column" gap="0.25rem" alignItems="baseline">
106+
<StatusBadge status="required" />
107+
onSelectOption
108+
</DxcFlex>
124109
</td>
125-
</tr>
126-
<tr>
127-
<td>expandOnHover</td>
128110
<td>
129-
<TableCode>boolean</TableCode>
111+
<TableCode>{"(value: string) => void"}</TableCode>
130112
</td>
131-
<td>If true, the options are shown when the dropdown is hovered.</td>
132113
<td>
133-
<TableCode>false</TableCode>
114+
This function will be called every time the selection changes. The value of the selected option will be
115+
passed as a parameter.
134116
</td>
117+
<td>-</td>
135118
</tr>
136119
<tr>
137120
<td>
138121
<DxcFlex direction="column" gap="0.25rem" alignItems="baseline">
139122
<StatusBadge status="required" />
140-
onSelectOption
123+
options
141124
</DxcFlex>
142125
</td>
143126
<td>
144-
<TableCode>{"(value: string) => void"}</TableCode>
127+
<TableCode>
128+
{
129+
"{ label?: string; icon?: string | (React.ReactNode & React.SVGProps <SVGSVGElement>); value: string }[]"
130+
}
131+
</TableCode>
145132
</td>
146133
<td>
147-
This function will be called every time the selection changes. The value of the selected option will be
148-
passed as a parameter.
134+
An array of objects representing the options. Each object has the following properties:
135+
<ul>
136+
<li>
137+
<b>label</b>: Option display value.
138+
</li>
139+
<li>
140+
<b>icon</b>:{" "}
141+
<DxcLink newWindow href="https://fonts.google.com/icons">
142+
Material Symbol
143+
</DxcLink>{" "}
144+
name or SVG element as the icon that will be placed next to the option label. When using Material
145+
Symbols, replace spaces with underscores. By default they are outlined if you want it to be filled
146+
prefix the symbol name with <TableCode>"filled_"</TableCode>.
147+
</li>
148+
<li>
149+
<b>value</b>: Option inner value.
150+
</li>
151+
</ul>
149152
</td>
150153
<td>-</td>
151154
</tr>
152155
<tr>
153-
<td>margin</td>
156+
<td>optionsIconPosition</td>
154157
<td>
155-
<TableCode>
156-
'xxsmall' | 'xsmall' | 'small' | 'medium' | 'large' | 'xlarge' | 'xxlarge' | Margin
157-
</TableCode>
158+
<TableCode>'before' | 'after'</TableCode>
158159
</td>
160+
<td>In case options include icons, whether the icon should appear after or before the label.</td>
159161
<td>
160-
Size of the margin to be applied to the component. You can pass an object with 'top', 'bottom', 'left'
161-
and 'right' properties in order to specify different margin sizes.
162+
<TableCode>'before'</TableCode>
162163
</td>
163-
<td>-</td>
164164
</tr>
165165
<tr>
166166
<td>size</td>
@@ -223,15 +223,13 @@ const sections = [
223223
},
224224
];
225225

226-
const DropdownCodePage = () => {
227-
return (
228-
<DxcFlex direction="column" gap="4rem">
229-
<QuickNavContainerLayout>
230-
<QuickNavContainer sections={sections} startHeadingLevel={2}></QuickNavContainer>
231-
</QuickNavContainerLayout>
232-
<DocFooter githubLink="https://github.com/dxc-technology/halstack-react/blob/master/apps/website/screens/components/dropdown/code/DropdownCodePage.tsx" />
233-
</DxcFlex>
234-
);
235-
};
226+
const DropdownCodePage = () => (
227+
<DxcFlex direction="column" gap="4rem">
228+
<QuickNavContainerLayout>
229+
<QuickNavContainer sections={sections} startHeadingLevel={2} />
230+
</QuickNavContainerLayout>
231+
<DocFooter githubLink="https://github.com/dxc-technology/halstack-react/blob/master/apps/website/screens/components/dropdown/code/DropdownCodePage.tsx" />
232+
</DxcFlex>
233+
);
236234

237235
export default DropdownCodePage;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
import { DxcBulletedList, DxcFlex, DxcParagraph } from "@dxc-technology/halstack-react";
2+
import DocFooter from "@/common/DocFooter";
3+
import QuickNavContainer from "@/common/QuickNavContainer";
4+
import QuickNavContainerLayout from "@/common/QuickNavContainerLayout";
5+
import Example from "@/common/example/Example";
6+
import iconUsage from "./examples/iconUsage";
7+
import Image from "@/common/Image";
8+
import anatomy from "./images/dropdown_anatomy.png";
9+
10+
const sections = [
11+
{
12+
title: "Introduction",
13+
content: (
14+
<>
15+
<DxcParagraph>
16+
The dropdown enhances usability by displaying a list of choices in a collapsible menu, optimizing space while
17+
keeping options easily accessible. It supports icons, sections, and different selection behaviors to adapt to
18+
various use cases. Designed for efficiency, it ensures keyboard navigation, accessibility, and proper contrast
19+
for readability.
20+
</DxcParagraph>
21+
</>
22+
),
23+
},
24+
{
25+
title: "Anatomy",
26+
content: (
27+
<>
28+
<Image src={anatomy} alt="Dropdown's anatomy" />
29+
<DxcBulletedList type="number">
30+
<DxcBulletedList.Item>
31+
<strong>Dropdown:</strong> the main container that triggers the list of options when clicked, allowing users
32+
to select an item.
33+
</DxcBulletedList.Item>
34+
<DxcBulletedList.Item>
35+
<strong>Listbox:</strong> the expanded panel displaying all available options for selection.
36+
</DxcBulletedList.Item>
37+
<DxcBulletedList.Item>
38+
<strong>Icon:</strong> a visual aid that can accompany the selected option, helping users quickly recognize
39+
the category or purpose.
40+
</DxcBulletedList.Item>
41+
<DxcBulletedList.Item>
42+
<strong>Label:</strong> the textual representation of the selected option, ensuring clarity in the user's
43+
choice.
44+
</DxcBulletedList.Item>
45+
<DxcBulletedList.Item>
46+
<strong>Expand/collapse icon:</strong> an indicator that shows whether the dropdown is expanded or
47+
collapsed.
48+
</DxcBulletedList.Item>
49+
<DxcBulletedList.Item>
50+
<strong>List item:</strong> an individual option within the dropdown list, which users can click to make a
51+
selection.
52+
</DxcBulletedList.Item>
53+
</DxcBulletedList>
54+
</>
55+
),
56+
},
57+
{
58+
title: "Using dropdowns",
59+
content: (
60+
<>
61+
<DxcParagraph>
62+
Dropdowns have a similar look and behavior to select components, the difference is that while select is only
63+
to collect user's data into a form, dropdown can be used in various scenarios.
64+
</DxcParagraph>
65+
<DxcBulletedList>
66+
<DxcBulletedList.Item>
67+
Dropdowns display a list of options that appear when the user clicks or hovers over the parent element,
68+
providing a compact and efficient way to make selections.
69+
</DxcBulletedList.Item>
70+
<DxcBulletedList.Item>
71+
The arrow linked to the dropdown label indicates to the user that more options are available but currently
72+
hidden.
73+
</DxcBulletedList.Item>
74+
<DxcBulletedList.Item>
75+
By default, a dropdown expands below its main container if there is enough screen space to accommodate the
76+
full size of the pop-up.
77+
</DxcBulletedList.Item>
78+
</DxcBulletedList>
79+
<DxcParagraph>
80+
If displaying the dropdown below the selector hides important information, reducing discoverability and
81+
scanability, consider alternative ways to present the content or adjust the pop-up's position to better fit
82+
the application's needs.
83+
</DxcParagraph>
84+
</>
85+
),
86+
subSections: [
87+
{
88+
title: "Icon usage",
89+
content: (
90+
<>
91+
<DxcParagraph>
92+
Icons can be used within the dropdown component in various configurations. They can be placed before or
93+
after the label or serve as the sole content of the dropdown placeholder and options. This maintains
94+
consistency with other components in our Design System, such as buttons and selects, which follow the same
95+
behavior.
96+
</DxcParagraph>
97+
<Example example={iconUsage}></Example>
98+
</>
99+
),
100+
},
101+
],
102+
},
103+
{
104+
title: "Best practices",
105+
content: (
106+
<DxcBulletedList>
107+
<DxcBulletedList.Item>
108+
<strong>User clear and concise labels:</strong> ensure dropdown labels are descriptive and easily understood,
109+
helping users quickly grasp their choices. Avoid vague terms like "Select an option.”
110+
</DxcBulletedList.Item>
111+
<DxcBulletedList.Item>
112+
<strong>Prioritize logical ordering:</strong> arrange options in a meaningful order—alphabetically for lists,
113+
by frequency of use for common selections, or categorically when grouping similar items.
114+
</DxcBulletedList.Item>
115+
<DxcBulletedList.Item>
116+
<strong>Keep the list of options manageable:</strong> avoid overwhelming users with too many options. If the
117+
list is long, consider using grouped sections or an alternative selection method like autocomplete.
118+
</DxcBulletedList.Item>
119+
<DxcBulletedList.Item>
120+
<strong>Ensure accessibility:</strong> provide sufficient contrast, keyboard navigation, and screen reader
121+
support. Icons should always have accessible labels to maintain clarity.
122+
</DxcBulletedList.Item>
123+
<DxcBulletedList.Item>
124+
<strong>Avoid nesting too deep:</strong> multi-level dropdowns can be hard to navigate. If multiple selection
125+
levels are required, consider using a different component, like a sidebar or tree structure.
126+
</DxcBulletedList.Item>
127+
<DxcBulletedList.Item>
128+
<strong>Be mindful of placement and screen space:</strong> ensure the dropdown appears in a location where it
129+
doesn't obscure critical content. If needed, adjust its position dynamically to fit within the viewport.
130+
</DxcBulletedList.Item>
131+
<DxcBulletedList.Item>
132+
<strong>Use icons thoughtfully:</strong> icons can enhance usability but should only be added when they add
133+
clarity. Overloading the dropdown with icons can create visual clutter.
134+
</DxcBulletedList.Item>
135+
</DxcBulletedList>
136+
),
137+
},
138+
];
139+
140+
const DropdownOverviewPage = () => (
141+
<DxcFlex direction="column" gap="4rem">
142+
<QuickNavContainerLayout>
143+
<QuickNavContainer sections={sections} startHeadingLevel={2} />
144+
</QuickNavContainerLayout>
145+
<DocFooter githubLink="https://github.com/dxc-technology/halstack-react/blob/master/apps/website/screens/components/dropdown/overview/DropdownOverviewPage.tsx" />
146+
</DxcFlex>
147+
);
148+
149+
export default DropdownOverviewPage;
Loading

‎apps/website/screens/components/dropdown/specs/DropdownSpecsPage.tsx

-585
This file was deleted.
Binary file not shown.
Binary file not shown.

‎apps/website/screens/components/dropdown/usage/DropdownUsagePage.tsx

-85
This file was deleted.
Binary file not shown.

‎packages/lib/src/dropdown/Dropdown.stories.tsx

-61
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { userEvent, within } from "@storybook/test";
33
import { ThemeProvider } from "styled-components";
44
import ExampleContainer from "../../.storybook/components/ExampleContainer";
55
import Title from "../../.storybook/components/Title";
6-
import { HalstackProvider } from "../HalstackContext";
76
import HalstackContext from "../HalstackContext";
87
import DxcDropdown from "./Dropdown";
98
import DropdownMenu from "./DropdownMenu";
@@ -86,14 +85,6 @@ const optionWithIcon: Option[] = [
8685

8786
const optionsIcon: any = options.map((op, i) => ({ ...op, icon: icons[i] }));
8887

89-
const opinionatedTheme = {
90-
dropdown: {
91-
baseColor: "#fabada",
92-
fontColor: "#fff",
93-
optionFontColor: "#0095ff",
94-
},
95-
};
96-
9788
const Dropdown = () => (
9889
<>
9990
<ExampleContainer>
@@ -347,48 +338,6 @@ const DropdownListStates = () => {
347338
);
348339
};
349340

350-
const OpinionatedTheme = () => (
351-
<>
352-
<Title title="Opinionated theme" theme="light" level={2} />
353-
<ExampleContainer>
354-
<Title title="Default" theme="light" level={4} />
355-
<HalstackProvider theme={opinionatedTheme}>
356-
<DxcDropdown label="Default" options={options} onSelectOption={(value) => {}} icon={iconSVG} />
357-
</HalstackProvider>
358-
</ExampleContainer>
359-
<ExampleContainer pseudoState="pseudo-hover">
360-
<Title title="Hovered" theme="light" level={4} />
361-
<HalstackProvider theme={opinionatedTheme}>
362-
<DxcDropdown label="Hovered" options={options} onSelectOption={(value) => {}} icon={iconSVG} />
363-
</HalstackProvider>
364-
</ExampleContainer>
365-
<ExampleContainer pseudoState="pseudo-active">
366-
<Title title="Active" theme="light" level={4} />
367-
<HalstackProvider theme={opinionatedTheme}>
368-
<DxcDropdown label="Active" options={options} onSelectOption={(value) => {}} icon={iconSVG} />
369-
</HalstackProvider>
370-
</ExampleContainer>
371-
<ExampleContainer pseudoState="pseudo-focus">
372-
<Title title="Focused" theme="light" level={4} />
373-
<HalstackProvider theme={opinionatedTheme}>
374-
<DxcDropdown label="Focused" options={options} onSelectOption={(value) => {}} icon={iconSVG} />
375-
</HalstackProvider>
376-
</ExampleContainer>
377-
<ExampleContainer>
378-
<Title title="Disabled" theme="light" level={4} />
379-
<HalstackProvider theme={opinionatedTheme}>
380-
<DxcDropdown label="Disabled" options={options} onSelectOption={(value) => {}} icon={iconSVG} disabled />
381-
</HalstackProvider>
382-
</ExampleContainer>
383-
<ExampleContainer expanded>
384-
<Title title="List opened" theme="light" level={4} />
385-
<HalstackProvider theme={opinionatedTheme}>
386-
<DxcDropdown label="Default" options={options} onSelectOption={(value) => {}} icon={iconSVG} />
387-
</HalstackProvider>
388-
</ExampleContainer>
389-
</>
390-
);
391-
392341
const TooltipTitle = () => (
393342
<ExampleContainer expanded>
394343
<Title title="Tooltip" theme="light" level={3} />
@@ -415,16 +364,6 @@ export const Chromatic: Story = {
415364
},
416365
};
417366

418-
export const OpinionatedThemed: Story = {
419-
render: OpinionatedTheme,
420-
play: async ({ canvasElement }) => {
421-
const canvas = within(canvasElement);
422-
const buttonList = canvas.getAllByRole("button");
423-
const lastButton = buttonList[buttonList.length - 1];
424-
lastButton != null && (await userEvent.click(lastButton));
425-
},
426-
};
427-
428367
export const MenuStates: Story = {
429368
render: DropdownListStates,
430369
play: async ({ canvasElement }) => {
+168-183
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as Popover from "@radix-ui/react-popover";
22
import { FocusEvent, KeyboardEvent, useCallback, useId, useLayoutEffect, useRef, useState, useContext } from "react";
3-
import styled, { ThemeProvider } from "styled-components";
3+
import styled from "styled-components";
44
import { getMargin } from "../common/utils";
55
import { spaces } from "../common/variables";
66
import DxcIcon from "../icon/Icon";
@@ -10,6 +10,108 @@ import DropdownMenu from "./DropdownMenu";
1010
import DropdownPropsType from "./types";
1111
import { Tooltip } from "../tooltip/Tooltip";
1212

13+
const sizes = {
14+
small: "60px",
15+
medium: "240px",
16+
large: "480px",
17+
fillParent: "100%",
18+
fitContent: "fit-content",
19+
};
20+
21+
const calculateWidth = (margin: DropdownPropsType["margin"], size: DropdownPropsType["size"]) =>
22+
size != null &&
23+
(size === "fillParent"
24+
? `calc(${sizes[size]} - ${getMargin(margin, "left")} - ${getMargin(margin, "right")})`
25+
: sizes[size]);
26+
27+
const DropdownContainer = styled.div<{
28+
margin: DropdownPropsType["margin"];
29+
size: DropdownPropsType["size"];
30+
}>`
31+
width: ${(props) => calculateWidth(props.margin, props.size)};
32+
margin: ${(props) => (props.margin && typeof props.margin !== "object" ? spaces[props.margin] : "0px")};
33+
margin-top: ${(props) =>
34+
props.margin && typeof props.margin === "object" && props.margin.top ? spaces[props.margin.top] : ""};
35+
margin-right: ${(props) =>
36+
props.margin && typeof props.margin === "object" && props.margin.right ? spaces[props.margin.right] : ""};
37+
margin-bottom: ${(props) =>
38+
props.margin && typeof props.margin === "object" && props.margin.bottom ? spaces[props.margin.bottom] : ""};
39+
margin-left: ${(props) =>
40+
props.margin && typeof props.margin === "object" && props.margin.left ? spaces[props.margin.left] : ""};
41+
`;
42+
43+
const DropdownTrigger = styled.button<{
44+
label: DropdownPropsType["label"];
45+
margin: DropdownPropsType["margin"];
46+
size: DropdownPropsType["size"];
47+
}>`
48+
display: flex;
49+
justify-content: space-between;
50+
align-items: center;
51+
gap: var(--spacing-gap-s);
52+
width: 100%;
53+
height: var(--height-m);
54+
padding: var(--spacing-padding-none) var(--spacing-padding-xs);
55+
min-width: ${(props) => (props.label === "" ? "0px" : calculateWidth(props.margin, props.size))};
56+
border: 0;
57+
border-radius: var(--border-radius-s);
58+
background-color: var(--color-bg-neutral-lightest);
59+
color: ${(props) => (props.disabled ? "var(--color-fg-neutral-medium);" : "var(--color-fg-neutral-dark);")};
60+
cursor: ${(props) => (props.disabled ? "not-allowed" : "pointer")};
61+
62+
${(props) =>
63+
!props.disabled &&
64+
`
65+
&:focus {
66+
outline: var(--border-width-m) var(--border-style-default) var(--border-color-secondary-medium);
67+
outline-offset: -2px;
68+
}
69+
&:hover, &:active {
70+
background-color: var(--color-bg-neutral-light);
71+
}
72+
`};
73+
`;
74+
75+
const DropdownTriggerContent = styled.span<{ iconPosition: DropdownPropsType["iconPosition"] }>`
76+
display: flex;
77+
${({ iconPosition }) => (iconPosition === "after" ? "flex-direction: row-reverse;" : "flex-direction: row;")}
78+
align-items: center;
79+
gap: var(--spacing-gap-xs);
80+
width: 100%;
81+
overflow: hidden;
82+
`;
83+
84+
const DropdownTriggerLabel = styled.span`
85+
font-family: var(--typography-font-family);
86+
font-size: var(--typography-label-l);
87+
font-weight: var(--typography-label-regular);
88+
text-overflow: ellipsis;
89+
overflow: hidden;
90+
white-space: nowrap;
91+
`;
92+
93+
const DropdownTriggerIcon = styled.span<{
94+
disabled: DropdownPropsType["disabled"];
95+
}>`
96+
display: flex;
97+
font-size: var(--height-xs);
98+
99+
svg {
100+
width: 20px;
101+
height: var(--height-xs);
102+
}
103+
`;
104+
105+
const CaretIcon = styled.span<{ disabled: DropdownPropsType["disabled"] }>`
106+
display: flex;
107+
font-size: var(--typography-label-l);
108+
109+
svg {
110+
width: 16px;
111+
height: var(--height-xxs);
112+
}
113+
`;
114+
13115
const DxcDropdown = ({
14116
options,
15117
optionsIconPosition = "before",
@@ -24,7 +126,7 @@ const DxcDropdown = ({
24126
size = "fitContent",
25127
tabIndex = 0,
26128
title,
27-
}: DropdownPropsType): JSX.Element => {
129+
}: DropdownPropsType) => {
28130
const id = useId();
29131
const triggerId = `trigger-${id}`;
30132
const menuId = `menu-${id}`;
@@ -149,189 +251,72 @@ const DxcDropdown = ({
149251
}, [visualFocusIndex]);
150252

151253
return (
152-
<ThemeProvider theme={colorsTheme.dropdown}>
153-
<DropdownContainer
154-
onMouseEnter={!disabled && expandOnHover ? handleOnOpenMenu : undefined}
155-
onMouseLeave={!disabled && expandOnHover ? handleOnCloseMenu : undefined}
156-
onBlur={!disabled ? handleOnBlur : undefined}
157-
margin={margin}
158-
size={size}
159-
>
160-
<Popover.Root open={isOpen}>
161-
<Tooltip label={title}>
162-
<Popover.Trigger asChild type={undefined}>
163-
<DropdownTrigger
164-
onClick={handleTriggerOnClick}
165-
onKeyDown={handleTriggerOnKeyDown}
166-
onBlur={(event) => {
167-
event.stopPropagation();
168-
}}
169-
disabled={disabled}
170-
label={label}
171-
margin={margin}
172-
size={size}
173-
id={triggerId}
174-
aria-haspopup="true"
175-
aria-controls={isOpen ? menuId : undefined}
176-
aria-expanded={isOpen ? true : undefined}
177-
aria-label="Show options"
178-
tabIndex={tabIndex}
179-
ref={triggerRef}
180-
>
181-
<DropdownTriggerContent>
182-
{label && iconPosition === "after" && <DropdownTriggerLabel>{label}</DropdownTriggerLabel>}
183-
{icon && (
184-
<DropdownTriggerIcon
185-
disabled={disabled}
186-
role={typeof icon === "string" ? undefined : "img"}
187-
aria-hidden
188-
>
189-
{typeof icon === "string" ? <DxcIcon icon={icon} /> : icon}
190-
</DropdownTriggerIcon>
191-
)}
192-
{label && iconPosition === "before" && <DropdownTriggerLabel>{label}</DropdownTriggerLabel>}
193-
</DropdownTriggerContent>
194-
{!caretHidden && (
195-
<CaretIcon disabled={disabled}>
196-
<DxcIcon icon={isOpen ? "arrow_drop_up" : "arrow_drop_down"} />{" "}
197-
</CaretIcon>
254+
<DropdownContainer
255+
onMouseEnter={!disabled && expandOnHover ? handleOnOpenMenu : undefined}
256+
onMouseLeave={!disabled && expandOnHover ? handleOnCloseMenu : undefined}
257+
onBlur={!disabled ? handleOnBlur : undefined}
258+
margin={margin}
259+
size={size}
260+
>
261+
<Popover.Root open={isOpen}>
262+
<Tooltip label={title}>
263+
<Popover.Trigger asChild type={undefined}>
264+
<DropdownTrigger
265+
onClick={handleTriggerOnClick}
266+
onKeyDown={handleTriggerOnKeyDown}
267+
onBlur={(event) => {
268+
event.stopPropagation();
269+
}}
270+
disabled={disabled}
271+
label={label}
272+
margin={margin}
273+
size={size}
274+
id={triggerId}
275+
aria-haspopup="true"
276+
aria-controls={isOpen ? menuId : undefined}
277+
aria-expanded={isOpen ? true : undefined}
278+
aria-label="Show options"
279+
tabIndex={tabIndex}
280+
ref={triggerRef}
281+
>
282+
<DropdownTriggerContent iconPosition={iconPosition}>
283+
{icon && (
284+
<DropdownTriggerIcon
285+
disabled={disabled}
286+
role={typeof icon === "string" ? undefined : "img"}
287+
aria-hidden
288+
>
289+
{typeof icon === "string" ? <DxcIcon icon={icon} /> : icon}
290+
</DropdownTriggerIcon>
198291
)}
199-
</DropdownTrigger>
200-
</Popover.Trigger>
201-
</Tooltip>
202-
<Popover.Portal>
203-
<Popover.Content asChild sideOffset={1}>
204-
<DropdownMenu
205-
id={menuId}
206-
dropdownTriggerId={triggerId}
207-
options={options}
208-
iconsPosition={optionsIconPosition}
209-
visualFocusIndex={visualFocusIndex}
210-
menuItemOnClick={handleMenuItemOnClick}
211-
onKeyDown={handleMenuOnKeyDown}
212-
styles={{ width, zIndex: "2147483647" }}
213-
ref={menuRef}
214-
/>
215-
</Popover.Content>
216-
</Popover.Portal>
217-
</Popover.Root>
218-
</DropdownContainer>
219-
</ThemeProvider>
292+
{label && <DropdownTriggerLabel>{label}</DropdownTriggerLabel>}
293+
</DropdownTriggerContent>
294+
{!caretHidden && (
295+
<CaretIcon disabled={disabled}>
296+
<DxcIcon icon={isOpen ? "keyboard_arrow_up" : "keyboard_arrow_down"} />
297+
</CaretIcon>
298+
)}
299+
</DropdownTrigger>
300+
</Popover.Trigger>
301+
</Tooltip>
302+
<Popover.Portal>
303+
<Popover.Content asChild sideOffset={1}>
304+
<DropdownMenu
305+
id={menuId}
306+
dropdownTriggerId={triggerId}
307+
options={options}
308+
iconsPosition={optionsIconPosition}
309+
visualFocusIndex={visualFocusIndex}
310+
menuItemOnClick={handleMenuItemOnClick}
311+
onKeyDown={handleMenuOnKeyDown}
312+
styles={{ width, zIndex: "2147483647" }}
313+
ref={menuRef}
314+
/>
315+
</Popover.Content>
316+
</Popover.Portal>
317+
</Popover.Root>
318+
</DropdownContainer>
220319
);
221320
};
222321

223-
const sizes = {
224-
small: "60px",
225-
medium: "240px",
226-
large: "480px",
227-
fillParent: "100%",
228-
fitContent: "fit-content",
229-
};
230-
231-
const calculateWidth = (margin: DropdownPropsType["margin"], size: DropdownPropsType["size"]) =>
232-
size != null &&
233-
(size === "fillParent"
234-
? `calc(${sizes[size]} - ${getMargin(margin, "left")} - ${getMargin(margin, "right")})`
235-
: sizes[size]);
236-
237-
const DropdownContainer = styled.div<{
238-
margin: DropdownPropsType["margin"];
239-
size: DropdownPropsType["size"];
240-
}>`
241-
width: ${(props) => calculateWidth(props.margin, props.size)};
242-
margin: ${(props) => (props.margin && typeof props.margin !== "object" ? spaces[props.margin] : "0px")};
243-
margin-top: ${(props) =>
244-
props.margin && typeof props.margin === "object" && props.margin.top ? spaces[props.margin.top] : ""};
245-
margin-right: ${(props) =>
246-
props.margin && typeof props.margin === "object" && props.margin.right ? spaces[props.margin.right] : ""};
247-
margin-bottom: ${(props) =>
248-
props.margin && typeof props.margin === "object" && props.margin.bottom ? spaces[props.margin.bottom] : ""};
249-
margin-left: ${(props) =>
250-
props.margin && typeof props.margin === "object" && props.margin.left ? spaces[props.margin.left] : ""};
251-
`;
252-
253-
const DropdownTrigger = styled.button<{
254-
label: DropdownPropsType["label"];
255-
margin: DropdownPropsType["margin"];
256-
size: DropdownPropsType["size"];
257-
}>`
258-
display: flex;
259-
justify-content: space-between;
260-
align-items: center;
261-
gap: ${(props) => props.theme.caretIconSpacing};
262-
width: 100%;
263-
height: ${(props) => props.theme.buttonHeight};
264-
min-width: ${(props) => (props.label === "" ? "0px" : calculateWidth(props.margin, props.size))};
265-
border-radius: ${(props) => props.theme.buttonBorderRadius};
266-
border-width: ${(props) => props.theme.buttonBorderThickness};
267-
border-style: ${(props) => props.theme.buttonBorderStyle};
268-
border-color: ${(props) => (props.disabled ? props.theme.disabledButtonBorderColor : props.theme.buttonBorderColor)};
269-
padding-top: ${(props) => props.theme.buttonPaddingTop};
270-
padding-bottom: ${(props) => props.theme.buttonPaddingBottom};
271-
padding-left: ${(props) => props.theme.buttonPaddingLeft};
272-
padding-right: ${(props) => props.theme.buttonPaddingRight};
273-
background-color: ${(props) =>
274-
props.disabled ? props.theme.disabledButtonBackgroundColor : props.theme.buttonBackgroundColor};
275-
color: ${(props) => (props.disabled ? props.theme.disabledColor : props.theme.buttonFontColor)};
276-
cursor: ${(props) => (props.disabled ? "not-allowed" : "pointer")};
277-
278-
${(props) =>
279-
!props.disabled &&
280-
`
281-
&:focus {
282-
outline: 2px solid ${props.theme.focusColor};
283-
}
284-
&:hover {
285-
background-color: ${props.theme.hoverButtonBackgroundColor};
286-
}
287-
&:active {
288-
background-color: ${props.theme.activeButtonBackgroundColor};
289-
}
290-
`};
291-
`;
292-
293-
const DropdownTriggerContent = styled.span`
294-
display: flex;
295-
align-items: center;
296-
gap: ${(props) => props.theme.buttonIconSpacing};
297-
margin-left: 0px;
298-
margin-right: 0px;
299-
width: 100%;
300-
overflow: hidden;
301-
white-space: nowrap;
302-
`;
303-
304-
const DropdownTriggerLabel = styled.span`
305-
font-family: ${(props) => props.theme.buttonFontFamily};
306-
font-size: ${(props) => props.theme.buttonFontSize};
307-
font-style: ${(props) => props.theme.buttonFontStyle};
308-
font-weight: ${(props) => props.theme.buttonFontWeight};
309-
text-overflow: ellipsis;
310-
overflow: hidden;
311-
`;
312-
313-
const DropdownTriggerIcon = styled.span<{
314-
disabled: DropdownPropsType["disabled"];
315-
}>`
316-
display: flex;
317-
color: ${(props) => (props.disabled ? props.theme.disabledColor : props.theme.buttonIconColor)};
318-
font-size: ${(props) => props.theme.buttonIconSize};
319-
320-
svg {
321-
width: ${(props) => props.theme.buttonIconSize};
322-
height: ${(props) => props.theme.buttonIconSize};
323-
}
324-
`;
325-
326-
const CaretIcon = styled.span<{ disabled: DropdownPropsType["disabled"] }>`
327-
display: flex;
328-
color: ${(props) => (props.disabled ? props.theme.disabledColor : props.theme.caretIconColor)};
329-
font-size: ${(props) => props.theme.caretIconSize};
330-
331-
svg {
332-
width: ${(props) => props.theme.caretIconSize};
333-
height: ${(props) => props.theme.caretIconSize};
334-
}
335-
`;
336-
337322
export default DxcDropdown;

‎packages/lib/src/dropdown/DropdownMenu.tsx

+16-32
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,27 @@ import { forwardRef, memo } from "react";
22
import styled from "styled-components";
33
import DropdownMenuItem from "./DropdownMenuItem";
44
import { DropdownMenuProps } from "./types";
5+
import { scrollbarStyles } from "../styles/scroll";
6+
7+
const DropdownMenuContainer = styled.ul`
8+
max-height: 230px;
9+
min-width: min-content;
10+
padding: 0;
11+
margin: 0;
12+
background-color: var(--color-bg-neutral-lightest);
13+
border-radius: var(--border-radius-s);
14+
box-shadow: var(--shadow-low-x-position) var(--shadow-low-y-position) var(--shadow-low-blur) var(--shadow-low-spread)
15+
var(--shadow-dark);
16+
outline: none;
17+
overflow-y: auto;
18+
${scrollbarStyles}
19+
`;
520

621
const DropdownMenu = forwardRef<HTMLUListElement, DropdownMenuProps>(
722
(
823
{ id, dropdownTriggerId, iconsPosition, visualFocusIndex, menuItemOnClick, onKeyDown, options, styles },
924
ref
10-
): JSX.Element => (
25+
) => (
1126
<DropdownMenuContainer
1227
onMouseDown={(event) => {
1328
// Prevent the onBlur event from closing menu when clicking on the menu since
@@ -38,35 +53,4 @@ const DropdownMenu = forwardRef<HTMLUListElement, DropdownMenuProps>(
3853
)
3954
);
4055

41-
const DropdownMenuContainer = styled.ul`
42-
box-sizing: border-box;
43-
max-height: 230px;
44-
min-width: min-content;
45-
padding: 0;
46-
margin: 0;
47-
background-color: ${(props) => props.theme.optionBackgroundColor};
48-
border-width: ${(props) => props.theme.borderThickness};
49-
border-style: ${(props) => props.theme.borderStyle};
50-
border-color: ${(props) => props.theme.borderColor};
51-
border-radius: ${(props) => props.theme.borderRadius};
52-
border-top-right-radius: 0;
53-
border-top-left-radius: 0;
54-
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
55-
outline: none;
56-
57-
overflow-y: auto;
58-
&::-webkit-scrollbar {
59-
width: 8px;
60-
height: 8px;
61-
}
62-
&::-webkit-scrollbar-thumb {
63-
background-color: ${(props) => props.theme.scrollBarThumbColor};
64-
border-radius: 6px;
65-
}
66-
&::-webkit-scrollbar-track {
67-
background-color: ${(props) => props.theme.scrollBarTrackColor};
68-
border-radius: 6px;
69-
}
70-
`;
71-
7256
export default memo(DropdownMenu);

‎packages/lib/src/dropdown/DropdownMenuItem.tsx

+51-47
Original file line numberDiff line numberDiff line change
@@ -3,72 +3,76 @@ import styled from "styled-components";
33
import { DropdownMenuItemProps } from "./types";
44
import DxcIcon from "../icon/Icon";
55

6-
const DropdownMenuItem = ({
7-
id,
8-
visuallyFocused,
9-
iconPosition,
10-
onClick,
11-
option,
12-
}: DropdownMenuItemProps): JSX.Element => (
13-
<DropdownMenuItemContainer
14-
visuallyFocused={visuallyFocused}
15-
onClick={() => {
16-
onClick(option.value);
17-
}}
18-
id={id}
19-
role="menuitem"
20-
tabIndex={-1}
21-
>
22-
{iconPosition === "after" && <DropdownMenuItemLabel>{option.label}</DropdownMenuItemLabel>}
23-
{option.icon && (
24-
<DropdownMenuItemIcon role={typeof option.icon === "string" ? undefined : "img"} aria-hidden>
25-
{typeof option.icon === "string" ? <DxcIcon icon={option.icon} /> : option.icon}
26-
</DropdownMenuItemIcon>
27-
)}
28-
{iconPosition === "before" && <DropdownMenuItemLabel>{option.label}</DropdownMenuItemLabel>}
29-
</DropdownMenuItemContainer>
30-
);
31-
32-
const DropdownMenuItemContainer = styled.li<{ visuallyFocused: DropdownMenuItemProps["visuallyFocused"] }>`
6+
const DropdownMenuItemContainer = styled.li<{
7+
visuallyFocused: DropdownMenuItemProps["visuallyFocused"];
8+
iconPosition: DropdownMenuItemProps["iconPosition"];
9+
}>`
3310
box-sizing: border-box;
11+
color: var(--color-fg-neutral-dark);
3412
display: flex;
3513
align-items: center;
36-
gap: ${(props) => props.theme.optionIconSpacing};
37-
min-height: 36px;
38-
padding-top: ${(props) => props.theme.optionPaddingTop};
39-
padding-bottom: ${(props) => props.theme.optionPaddingBottom};
40-
padding-left: ${(props) => props.theme.optionPaddingLeft};
41-
padding-right: ${(props) => props.theme.optionPaddingRight};
14+
gap: var(--spacing-gap-xs);
15+
height: var(--height-m);
16+
padding: var(--spacing-padding-none) var(--spacing-padding-xs);
4217
cursor: pointer;
4318
44-
${(props) => props.visuallyFocused && `outline: ${props.theme.focusColor} solid 2px; outline-offset: -2px;`}
45-
&:hover {
46-
background-color: ${(props) => props.theme.hoverOptionBackgroundColor};
19+
${({ iconPosition }) => (iconPosition === "after" ? "flex-direction: row-reverse;" : "flex-direction: row;")}
20+
21+
${(props) =>
22+
props.visuallyFocused &&
23+
`
24+
outline: var(--border-width-m) var(--border-style-default) var(--border-color-secondary-medium);
25+
outline-offset: calc(-1 * var(--border-width-m));
26+
`}
27+
&:first-child {
28+
border-top-left-radius: var(--border-radius-s);
29+
border-top-right-radius: var(--border-radius-s);
4730
}
31+
&:last-child {
32+
border-bottom-left-radius: var(--border-radius-s);
33+
border-bottom-right-radius: var(--border-radius-s);
34+
}
35+
&:hover,
4836
&:active {
49-
background-color: ${(props) => props.theme.activeOptionBackgroundColor};
37+
background-color: var(--color-bg-neutral-light);
5038
}
5139
`;
5240

5341
const DropdownMenuItemLabel = styled.span`
54-
font-family: ${(props) => props.theme.optionFontFamily};
55-
font-size: ${(props) => props.theme.optionFontSize};
56-
font-style: ${(props) => props.theme.optionFontStyle};
57-
font-weight: ${(props) => props.theme.optionFontWeight};
58-
line-height: 1.5rem;
59-
color: ${(props) => props.theme.optionFontColor};
42+
font-family: var(--typography-font-family);
43+
font-size: var(--typography-label-l);
44+
font-weight: var(--typography-label-regular);
6045
white-space: nowrap;
6146
`;
6247

6348
const DropdownMenuItemIcon = styled.div`
6449
display: flex;
65-
color: ${(props) => props.theme.optionIconColor};
66-
font-size: ${(props) => props.theme.optionIconSize};
50+
font-size: var(--height-xs);
6751
6852
svg {
69-
width: ${(props) => props.theme.optionIconSize};
70-
height: ${(props) => props.theme.optionIconSize};
53+
width: 20px;
54+
height: var(--height-xs);
7155
}
7256
`;
7357

58+
const DropdownMenuItem = ({ id, visuallyFocused, iconPosition, onClick, option }: DropdownMenuItemProps) => (
59+
<DropdownMenuItemContainer
60+
iconPosition={iconPosition}
61+
visuallyFocused={visuallyFocused}
62+
onClick={() => {
63+
onClick(option.value);
64+
}}
65+
id={id}
66+
role="menuitem"
67+
tabIndex={-1}
68+
>
69+
{option.icon && (
70+
<DropdownMenuItemIcon role={typeof option.icon === "string" ? undefined : "img"} aria-hidden>
71+
{typeof option.icon === "string" ? <DxcIcon icon={option.icon} /> : option.icon}
72+
</DropdownMenuItemIcon>
73+
)}
74+
<DropdownMenuItemLabel>{option.label}</DropdownMenuItemLabel>
75+
</DropdownMenuItemContainer>
76+
);
77+
7478
export default memo(DropdownMenuItem);

0 commit comments

Comments
 (0)
Please sign in to comment.