Skip to content

Commit 8952a49

Browse files
authored
Enhance Dropdown & ContextMenu items with 'type' prop (default/danger) and theme updates (#718)
* Add generic menu default and danger color tokens * Update colors * Refactor GenericMenu, ContextMenu and Dropdown to add a type prop * fix: update DropdownMenuItem styles to support dynamic color based on type
1 parent 3280cec commit 8952a49

File tree

18 files changed

+8639
-5292
lines changed

18 files changed

+8639
-5292
lines changed

.yarnrc.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
nodeLinker: node-modules

src/components/AutoComplete/AutoComplete.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,8 @@ export type AutoCompleteProps = (SelectOptionType & Props) | (SelectChildrenType
111111
export const SelectPopoverRoot = styled(Root)`
112112
width: 100%;
113113
${({ theme }) => `
114-
border: 1px solid ${theme.click.genericMenu.item.color.stroke.default};
115-
background: ${theme.click.genericMenu.item.color.background.default};
114+
border: 1px solid ${theme.click.genericMenu.item.color.default.stroke.default};
115+
background: ${theme.click.genericMenu.item.color.default.background.default};
116116
box-shadow: 0px 1px 3px 0px rgba(16, 24, 40, 0.1),
117117
0px 1px 2px 0px rgba(16, 24, 40, 0.06);
118118
border-radius: 0.25rem;
@@ -147,7 +147,7 @@ const SelectGroupContainer = styled.div`
147147
148148
${({ theme }) => `
149149
font: ${theme.click.genericMenu.item.typography.sectionHeader.default};
150-
color: ${theme.click.genericMenu.item.color.text.muted};
150+
color: ${theme.click.genericMenu.item.color.default.text.muted};
151151
`};
152152
&[hidden] {
153153
display: none;
@@ -163,10 +163,10 @@ const SelectGroupName = styled.div`
163163
text-overflow: ellipsis;
164164
${({ theme }) => `
165165
font: ${theme.click.genericMenu.item.typography.sectionHeader.default};
166-
color: ${theme.click.genericMenu.item.color.text.muted};
166+
color: ${theme.click.genericMenu.item.color.default.text.muted};
167167
padding: ${theme.click.genericMenu.sectionHeader.space.top} ${theme.click.genericMenu.item.space.x} ${theme.click.genericMenu.sectionHeader.space.bottom};
168168
gap: ${theme.click.genericMenu.item.space.gap};
169-
border-bottom: 1px solid ${theme.click.genericMenu.item.color.stroke.default};
169+
border-bottom: 1px solid ${theme.click.genericMenu.item.color.default.stroke.default};
170170
`}
171171
`;
172172

@@ -248,8 +248,8 @@ const SelectList = styled.div`
248248
width: inherit;
249249
max-height: var(--radix-popover-content-available-height);
250250
${({ theme }) => `
251-
border: 1px solid ${theme.click.genericMenu.item.color.stroke.default};
252-
background: ${theme.click.genericMenu.item.color.background.default};
251+
border: 1px solid ${theme.click.genericMenu.item.color.default.stroke.default};
252+
background: ${theme.click.genericMenu.item.color.default.background.default};
253253
box-shadow: ${theme.click.genericMenu.panel.shadow.default};
254254
border-radius: 0.25rem;
255255
`}

src/components/ContextMenu/ContextMenu.stories.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React from "react";
22
import { Meta, StoryObj } from "@storybook/react-vite";
33
import { ContextMenuProps } from "@radix-ui/react-context-menu";
4-
import { ContextMenu } from "./ContextMenu";
4+
import { ContextMenu, ContextMenuItemProps } from "./ContextMenu";
55
import { styled } from "styled-components";
66

77
interface ContextMenuExampleProps extends ContextMenuProps {
@@ -63,6 +63,7 @@ const ContextMenuExample = ({
6363
Content2
6464
</ContextMenu.Item>
6565
<ContextMenu.Item disabled>Content3</ContextMenu.Item>
66+
<ContextMenu.Item type="danger">Delete content</ContextMenu.Item>
6667
</ContextMenu.Content>
6768
</ContextMenu>
6869
</GridCenter>
@@ -77,7 +78,7 @@ const meta: Meta<typeof ContextMenuExample> = {
7778
"ContextMenu.SubTrigger": ContextMenu.SubTrigger as React.ComponentType<unknown>,
7879
"ContextMenu.Group": ContextMenu.Group as React.ComponentType<unknown>,
7980
"ContextMenu.Sub": ContextMenu.Sub as React.ComponentType<unknown>,
80-
"ContextMenu.Item": ContextMenu.Item as React.ComponentType<unknown>,
81+
"ContextMenu.Item": ContextMenu.Item as React.ComponentType<ContextMenuItemProps>,
8182
},
8283
title: "Display/ContextMenu",
8384
tags: ["form-field", "dropdown", "autodocs"],

src/components/ContextMenu/ContextMenu.test.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,4 +151,27 @@ describe("ContextMenu", () => {
151151
expect(item).not.toBeNull();
152152
expect(queryByText("Content2")).not.toBeNull();
153153
});
154+
155+
it("should render item with danger type", () => {
156+
const { getByText, queryByText } = renderCUI(
157+
<ContextMenu>
158+
<ContextMenu.Trigger>
159+
<div>ContextMenu Trigger</div>
160+
</ContextMenu.Trigger>
161+
<ContextMenu.Content>
162+
<ContextMenu.Item type="default">Default Item</ContextMenu.Item>
163+
<ContextMenu.Item type="danger">Danger Item</ContextMenu.Item>
164+
</ContextMenu.Content>
165+
</ContextMenu>
166+
);
167+
168+
const contextMenuTrigger = getByText("ContextMenu Trigger");
169+
fireEvent.contextMenu(contextMenuTrigger);
170+
171+
const defaultItem = queryByText("Default Item");
172+
const dangerItem = queryByText("Danger Item");
173+
174+
expect(defaultItem).not.toBeNull();
175+
expect(dangerItem).not.toBeNull();
176+
});
154177
});

src/components/ContextMenu/ContextMenu.tsx

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ ContextMenu.Content = ContextMenuContent;
138138
const RightMenuGroup = styled(RightMenu.Group)`
139139
width: 100%;
140140
border-bottom: 1px solid
141-
${({ theme }) => theme.click.genericMenu.item.color.stroke.default};
141+
${({ theme }) => theme.click.genericMenu.item.color.default.stroke.default};
142142
`;
143143

144144
const ContextMenuGroup = (props: RightMenu.ContextMenuGroupProps) => {
@@ -150,7 +150,7 @@ ContextMenu.Group = ContextMenuGroup;
150150

151151
const RightMenuSub = styled(RightMenu.Sub)`
152152
border-bottom: 1px solid
153-
${({ theme }) => theme.click.genericMenu.item.color.stroke.default};
153+
${({ theme }) => theme.click.genericMenu.item.color.default.stroke.default};
154154
`;
155155

156156
const ContextMenuSub = ({ ...props }: RightMenu.ContextMenuGroupProps) => {
@@ -164,12 +164,21 @@ export interface ContextMenuItemProps extends RightMenu.ContextMenuItemProps {
164164
icon?: IconName;
165165
/** The direction of the icon relative to the label */
166166
iconDir?: HorizontalDirection;
167+
/** The type of the menu item */
168+
type?: "default" | "danger";
167169
}
168170

169-
const ContextMenuItem = ({ icon, iconDir, children, ...props }: ContextMenuItemProps) => {
171+
const ContextMenuItem = ({
172+
icon,
173+
iconDir,
174+
type = "default",
175+
children,
176+
...props
177+
}: ContextMenuItemProps) => {
170178
return (
171179
<GenericMenuItem
172180
as={RightMenu.Item}
181+
$type={type}
173182
{...props}
174183
>
175184
<IconWrapper

src/components/Dropdown/Dropdown.stories.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { Dropdown } from "./Dropdown";
55
import { GridCenter } from "../commonElement";
66
import { Button } from "..";
77
import { Key } from "react";
8+
import type { DropdownItemProps } from "./Dropdown";
89

910
interface DropdownExampleProps extends DropdownMenuProps {
1011
disabled?: boolean;
@@ -56,9 +57,10 @@ const DropdownExample = ({
5657
icon="activity"
5758
iconDir="end"
5859
>
59-
Content2
60+
Activity
6061
</Dropdown.Item>
6162
<Dropdown.Item disabled>Content3</Dropdown.Item>
63+
<Dropdown.Item type="danger">Delete content</Dropdown.Item>
6264
</Dropdown.Content>
6365
</Dropdown>
6466
<Dropdown {...props}>
@@ -95,6 +97,7 @@ const DropdownExample = ({
9597
Content2
9698
</Dropdown.Item>
9799
<Dropdown.Item disabled>Content3</Dropdown.Item>
100+
<Dropdown.Item type="danger">Delete content</Dropdown.Item>
98101
</Dropdown.Content>
99102
</Dropdown>
100103
</GridCenter>
@@ -108,7 +111,7 @@ const meta: Meta<typeof DropdownExample> = {
108111
"Dropdown.Content": Dropdown.Content as React.ComponentType<unknown>,
109112
"Dropdown.Group": Dropdown.Group as React.ComponentType<unknown>,
110113
"Dropdown.Sub": Dropdown.Sub as React.ComponentType<unknown>,
111-
"Dropdown.Item": Dropdown.Item as React.ComponentType<unknown>,
114+
"Dropdown.Item": Dropdown.Item as React.ComponentType<DropdownItemProps>,
112115
},
113116
title: "Display/Dropdown",
114117
tags: ["form-field", "dropdown", "autodocs"],

src/components/Dropdown/Dropdown.test.tsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,4 +133,25 @@ describe("Dropdown", () => {
133133
expect(item).not.toBeNull();
134134
expect(queryByText("Content2")).not.toBeNull();
135135
});
136+
137+
it("should render item with danger type", async () => {
138+
const { getByText, queryByText } = renderCUI(
139+
<Dropdown>
140+
<Dropdown.Trigger>Dropdown Trigger</Dropdown.Trigger>
141+
<Dropdown.Content>
142+
<Dropdown.Item type="default">Default Item</Dropdown.Item>
143+
<Dropdown.Item type="danger">Danger Item</Dropdown.Item>
144+
</Dropdown.Content>
145+
</Dropdown>
146+
);
147+
148+
const dropdownTrigger = getByText("Dropdown Trigger");
149+
await userEvent.click(dropdownTrigger);
150+
151+
const defaultItem = queryByText("Default Item");
152+
const dangerItem = queryByText("Danger Item");
153+
154+
expect(defaultItem).not.toBeNull();
155+
expect(dangerItem).not.toBeNull();
156+
});
136157
});

src/components/Dropdown/Dropdown.tsx

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,23 @@ export const Dropdown = (props: DropdownMenu.DropdownMenuProps) => (
1010
<DropdownMenu.Root {...props} />
1111
);
1212

13-
const DropdownMenuItem = styled(GenericMenuItem)`
13+
const DropdownMenuItem = styled(GenericMenuItem)<{ $type?: "default" | "danger" }>`
1414
position: relative;
1515
display: flex;
1616
min-height: 32px;
1717
&[data-state="open"] {
18-
${({ theme }) => `
18+
${({ theme, $type = "default" }) => {
19+
const colorGroup =
20+
theme?.click?.genericMenu?.item?.color?.[$type] ||
21+
theme?.click?.genericMenu?.item?.color?.default;
22+
if (!colorGroup || !theme?.click?.genericMenu?.item?.typography) return "";
23+
return `
1924
font: ${theme.click.genericMenu.item.typography.label.hover};
20-
background: ${theme.click.genericMenu.item.color.background.hover};
21-
color: ${theme.click.genericMenu.item.color.text.hover};
25+
background: ${colorGroup.background.hover};
26+
color: ${colorGroup.text.hover};
2227
cursor: pointer;
23-
`}
28+
`;
29+
}}
2430
}
2531
`;
2632

@@ -138,7 +144,7 @@ Dropdown.Content = DropdownContent;
138144
const DropdownMenuGroup = styled(DropdownMenu.Group)`
139145
width: 100%;
140146
border-bottom: 1px solid
141-
${({ theme }) => theme.click.genericMenu.item.color.stroke.default};
147+
${({ theme }) => theme.click.genericMenu.item.color.default.stroke.default};
142148
`;
143149

144150
const DropdownGroup = (props: DropdownMenu.DropdownMenuGroupProps) => {
@@ -150,7 +156,7 @@ Dropdown.Group = DropdownGroup;
150156

151157
const DropdownMenuSub = styled(DropdownMenu.Sub)`
152158
border-bottom: 1px solid
153-
${({ theme }) => theme.click.genericMenu.item.color.stroke.default};
159+
${({ theme }) => theme.click.genericMenu.item.color.default.stroke.default};
154160
`;
155161

156162
const DropdownSub = ({ ...props }: DropdownMenu.DropdownMenuGroupProps) => {
@@ -165,11 +171,22 @@ interface DropdownItemProps extends DropdownMenu.DropdownMenuItemProps {
165171
icon?: IconName;
166172
/** The direction of the icon relative to the label */
167173
iconDir?: HorizontalDirection;
174+
/** The type of the menu item */
175+
type?: "default" | "danger";
168176
}
169-
const DropdownItem = ({ icon, iconDir, children, ...props }: DropdownItemProps) => {
177+
178+
export type { DropdownItemProps };
179+
const DropdownItem = ({
180+
icon,
181+
iconDir,
182+
type = "default",
183+
children,
184+
...props
185+
}: DropdownItemProps) => {
170186
return (
171187
<DropdownMenuItem
172188
as={DropdownMenu.Item}
189+
$type={type}
173190
{...props}
174191
>
175192
<IconWrapper

src/components/GenericMenu.tsx

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ export const Arrow = styled.svg`
6969
`};
7070
`;
7171

72-
export const GenericMenuItem = styled.div`
72+
export const GenericMenuItem = styled.div<{ $type?: "default" | "danger" }>`
7373
display: flex;
7474
width: 100%;
7575
width: -moz-available;
@@ -87,35 +87,38 @@ export const GenericMenuItem = styled.div`
8787
outline: none;
8888
}
8989
90-
${({ theme }) => `
90+
${({ theme, $type = "default" }) => {
91+
const colorKey = $type === "danger" ? "danger" : "default";
92+
return `
9193
padding: ${theme.click.genericMenu.item.space.y} ${theme.click.genericMenu.item.space.x};
9294
gap: ${theme.click.genericMenu.item.space.gap};
9395
font: ${theme.click.genericMenu.item.typography.label.default};
94-
background: ${theme.click.genericMenu.item.color.background.default};
95-
color: ${theme.click.genericMenu.item.color.text.default};
96+
background: ${theme.click.genericMenu.item.color[colorKey].background.default};
97+
color: ${theme.click.genericMenu.item.color[colorKey].text.default};
9698
&[data-highlighted] {
9799
font: ${theme.click.genericMenu.item.typography.label.hover};
98-
background: ${theme.click.genericMenu.item.color.background.hover};
99-
color:${theme.click.genericMenu.item.color.text.hover};
100+
background: ${theme.click.genericMenu.item.color[colorKey].background.hover};
101+
color:${theme.click.genericMenu.item.color[colorKey].text.hover};
100102
cursor: pointer;
101103
}
102104
&[data-state="open"], &[data-state="checked"], &[data-selected="true"] {
103-
background:${theme.click.genericMenu.item.color.background.active};
104-
color:${theme.click.genericMenu.item.color.text.active};
105+
background:${theme.click.genericMenu.item.color[colorKey].background.active};
106+
color:${theme.click.genericMenu.item.color[colorKey].text.active};
105107
font: ${theme.click.genericMenu.item.typography.label.active};
106108
}
107109
&[data-disabled] {
108-
color:${theme.click.genericMenu.item.color.text.disabled};
110+
color:${theme.click.genericMenu.item.color[colorKey].text.disabled};
109111
font: ${theme.click.genericMenu.item.typography.label.disabled};
110112
pointer-events: none;
111113
}
112114
&:visited {
113-
color: ${theme.click.genericMenu.item.color.text.default};
115+
color: ${theme.click.genericMenu.item.color[colorKey].text.default};
114116
a {
115-
color: ${theme.click.genericMenu.item.color.text.default};
117+
color: ${theme.click.genericMenu.item.color[colorKey].text.default};
116118
}
117119
}
118-
`};
120+
`;
121+
}};
119122
position: relative;
120123
&:hover .dropdown-arrow,
121124
&[data-state="open"] .dropdown-arrow {

src/components/Select/common/SelectStyled.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -104,8 +104,8 @@ export const SelectPopoverContent = styled(Content)<{
104104
`}
105105
106106
${({ theme }) => `
107-
border: 1px solid ${theme.click.genericMenu.item.color.stroke.default};
108-
background: ${theme.click.genericMenu.item.color.background.default};
107+
border: 1px solid ${theme.click.genericMenu.item.color.default.stroke.default};
108+
background: ${theme.click.genericMenu.item.color.default.background.default};
109109
box-shadow: 0px 1px 3px 0px rgba(16, 24, 40, 0.1),
110110
0px 1px 2px 0px rgba(16, 24, 40, 0.06);
111111
border-radius: 0.25rem;
@@ -212,7 +212,7 @@ export const SelectGroupContainer = styled.div`
212212
213213
${({ theme }) => `
214214
font: ${theme.click.genericMenu.item.typography.sectionHeader.default};
215-
color: ${theme.click.genericMenu.item.color.text.muted};
215+
color: ${theme.click.genericMenu.item.color.default.text.muted};
216216
`};
217217
&[hidden] {
218218
display: none;
@@ -228,10 +228,10 @@ export const SelectGroupName = styled.div`
228228
text-overflow: ellipsis;
229229
${({ theme }) => `
230230
font: ${theme.click.genericMenu.item.typography.sectionHeader.default};
231-
color: ${theme.click.genericMenu.item.color.text.muted};
231+
color: ${theme.click.genericMenu.item.color.default.text.muted};
232232
padding: ${theme.click.genericMenu.sectionHeader.space.top} ${theme.click.genericMenu.item.space.x} ${theme.click.genericMenu.sectionHeader.space.bottom};
233233
gap: ${theme.click.genericMenu.item.space.gap};
234-
border-bottom: 1px solid ${theme.click.genericMenu.item.color.stroke.default};
234+
border-bottom: 1px solid ${theme.click.genericMenu.item.color.default.stroke.default};
235235
`}
236236
`;
237237

0 commit comments

Comments
 (0)