Skip to content

Commit 251f2b8

Browse files
authored
Merge branch 'master' into rarrojolopez/components-catalog
2 parents 22e351f + a454b33 commit 251f2b8

File tree

14 files changed

+586
-110
lines changed

14 files changed

+586
-110
lines changed

lib/src/accordion/Accordion.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ const AccordionLabel = styled.span`
188188
const IconContainer = styled.span<{ disabled: AccordionPropsType["disabled"] }>`
189189
display: flex;
190190
margin-left: ${(props) => props.theme.iconMarginLeft};
191-
margin-right: ${(props) => props.theme.iconMarginRigth};
191+
margin-right: ${(props) => props.theme.iconMarginRight};
192192
color: ${(props) => (props.disabled ? props.theme.disabledIconColor : props.theme.iconColor)};
193193
194194
svg,

lib/src/badge/types.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
type SVG = React.ReactNode & React.SVGProps<SVGSVGElement>;
22

3-
type ContextualProps = {
3+
export type ContextualProps = {
44
/**
55
* Text to be placed in the badge.
66
*/
@@ -19,7 +19,7 @@ type ContextualProps = {
1919
color?: "grey" | "blue" | "green" | "orange" | "red" | "yellow" | "purple";
2020
};
2121

22-
type NotificationProps = {
22+
export type NotificationProps = {
2323
/**
2424
* Text to be placed in the badge.
2525
*/
@@ -38,7 +38,7 @@ type NotificationProps = {
3838
color?: never;
3939
};
4040

41-
type CommonProps = {
41+
export type CommonProps = {
4242
/**
4343
* Text representing advisory information related to the badge. Under the hood, this prop also serves as an accessible label for the component.
4444
*/

lib/src/common/variables.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export const componentTokens = {
3838
disabledIconColor: CoreTokens.color_grey_500,
3939
iconSize: "24px",
4040
iconMarginLeft: "0px",
41-
iconMarginRigth: "12px",
41+
iconMarginRight: "12px",
4242
accordionGroupSeparatorBorderColor: CoreTokens.color_grey_200_a,
4343
accordionGroupSeparatorBorderThickness: "1px",
4444
accordionGroupSeparatorBorderRadius: "0px",
@@ -1034,7 +1034,7 @@ export const componentTokens = {
10341034
errorListDialogBackgroundColor: CoreTokens.color_red_50,
10351035
errorListDialogBorderColor: CoreTokens.color_red_700,
10361036
hoverListOptionBackgroundColor: CoreTokens.color_grey_100,
1037-
activeListOptionBackgroundColor: CoreTokens.color_grey_300,
1037+
activeListOptionBackgroundColor: CoreTokens.color_grey_200,
10381038
focusListOptionBorderColor: CoreTokens.color_blue_600,
10391039
},
10401040
toggleGroup: {
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
import React from "react";
2+
import Title from "../../.storybook/components/Title";
3+
import DxcContextualMenu from "./ContextualMenu";
4+
import DxcContainer from "../container/Container";
5+
import MenuItemAction from "./MenuItemAction";
6+
import ExampleContainer from "../../.storybook/components/ExampleContainer";
7+
8+
export default {
9+
title: "Contextual Menu",
10+
component: DxcContextualMenu,
11+
};
12+
13+
const key_icon = (
14+
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24" fill="currentColor">
15+
<path d="M280-400q-33 0-56.5-23.5T200-480q0-33 23.5-56.5T280-560q33 0 56.5 23.5T360-480q0 33-23.5 56.5T280-400Zm0 160q-100 0-170-70T40-480q0-100 70-170t170-70q67 0 121.5 33t86.5 87h352l120 120-180 180-80-60-80 60-85-60h-47q-32 54-86.5 87T280-240Zm0-80q56 0 98.5-34t56.5-86h125l58 41 82-61 71 55 75-75-40-40H435q-14-52-56.5-86T280-640q-66 0-113 47t-47 113q0 66 47 113t113 47Z" />
16+
</svg>
17+
);
18+
19+
const fav_icon = (
20+
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24" fill="currentColor">
21+
<path d="m480-120-58-52q-101-91-167-157T150-447.5Q111-500 95.5-544T80-634q0-94 63-157t157-63q52 0 99 22t81 62q34-40 81-62t99-22q94 0 157 63t63 157q0 46-15.5 90T810-447.5Q771-395 705-329T538-172l-58 52Zm0-108q96-86 158-147.5t98-107q36-45.5 50-81t14-70.5q0-60-40-100t-100-40q-47 0-87 26.5T518-680h-76q-15-41-55-67.5T300-774q-60 0-100 40t-40 100q0 35 14 70.5t50 81q36 45.5 98 107T480-228Zm0-273Z" />
22+
</svg>
23+
);
24+
25+
const items = [{ label: "Item 1" }, { label: "Item 2" }, { label: "Item 3" }, { label: "Item 4" }];
26+
27+
const sections = [
28+
{
29+
title: "Team repositories",
30+
items: [{ label: "Approved locations" }, { label: "Approved locations" }, { label: "Approved locations" }],
31+
},
32+
{
33+
items: [{ label: "Approved locations" }, { label: "Approved locations" }, { label: "Approved locations" }],
34+
},
35+
];
36+
37+
const itemsWithIcon = [
38+
{
39+
label: "Item 1",
40+
icon: key_icon,
41+
},
42+
{
43+
label: "Item 2",
44+
icon: fav_icon,
45+
},
46+
];
47+
48+
const itemsWithSlot = [
49+
{
50+
label: "Item 1",
51+
slot: <DxcContextualMenu.Badge color="green" label="New" />,
52+
},
53+
{
54+
label: "Item 2",
55+
slot: (
56+
<svg width="12" height="12" viewBox="0 0 12 12" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
57+
<path
58+
d="M10.6667 10.6667H1.33333V1.33333H6V0H1.33333C0.593333 0 0 0.6 0 1.33333V10.6667C0 11.4 0.593333 12 1.33333 12H10.6667C11.4 12 12 11.4 12 10.6667V6H10.6667V10.6667ZM7.33333 0V1.33333H9.72667L3.17333 7.88667L4.11333 8.82667L10.6667 2.27333V4.66667H12V0H7.33333Z"
59+
fill="#323232"
60+
/>
61+
</svg>
62+
),
63+
},
64+
];
65+
66+
const sectionsWithScroll = [
67+
{
68+
title: "Team repositories",
69+
items: [{ label: "Approved locations" }, { label: "Approved locations" }, { label: "Approved locations" }],
70+
},
71+
{
72+
items: [
73+
{ label: "Approved locations" },
74+
{ label: "Approved locations" },
75+
{ label: "Approved locations" },
76+
{ label: "Approved locations" },
77+
{ label: "Approved locations" },
78+
{ label: "Approved locations" },
79+
{ label: "Approved locations" },
80+
{ label: "Approved locations" },
81+
{ label: "Approved locations" },
82+
],
83+
},
84+
];
85+
86+
const itemsWithTruncatedText = [
87+
{
88+
label: "Item with a very long label that should be truncated",
89+
slot: <DxcContextualMenu.Badge color="green" label="New" />,
90+
icon: key_icon,
91+
},
92+
{
93+
label: "Item 2",
94+
slot: (
95+
<svg width="12" height="12" viewBox="0 0 12 12" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
96+
<path
97+
d="M10.6667 10.6667H1.33333V1.33333H6V0H1.33333C0.593333 0 0 0.6 0 1.33333V10.6667C0 11.4 0.593333 12 1.33333 12H10.6667C11.4 12 12 11.4 12 10.6667V6H10.6667V10.6667ZM7.33333 0V1.33333H9.72667L3.17333 7.88667L4.11333 8.82667L10.6667 2.27333V4.66667H12V0H7.33333Z"
98+
fill="#323232"
99+
/>
100+
</svg>
101+
),
102+
icon: fav_icon,
103+
},
104+
];
105+
106+
export const Chromatic = () => (
107+
<>
108+
<Title title="Default" theme="light" level={3} />
109+
<ExampleContainer>
110+
<DxcContextualMenu items={items} />
111+
</ExampleContainer>
112+
<Title title="With sections" theme="light" level={3} />
113+
<ExampleContainer>
114+
<DxcContainer width="300px">
115+
<DxcContextualMenu items={sections} />
116+
</DxcContainer>
117+
</ExampleContainer>
118+
<Title title="With icons" theme="light" level={3} />
119+
<ExampleContainer>
120+
<DxcContainer width="300px">
121+
<DxcContextualMenu items={itemsWithIcon} defaultSelectedItemIndex={0} />
122+
</DxcContainer>
123+
</ExampleContainer>
124+
<Title title="With slot" theme="light" level={3} />
125+
<ExampleContainer>
126+
<DxcContainer width="300px">
127+
<DxcContextualMenu items={itemsWithSlot} />
128+
</DxcContainer>
129+
</ExampleContainer>
130+
<Title title="With label truncated" theme="light" level={3} />
131+
<ExampleContainer>
132+
<DxcContainer width="300px">
133+
<DxcContextualMenu items={itemsWithTruncatedText} />
134+
</DxcContainer>
135+
</ExampleContainer>
136+
<Title title="With scroll" theme="light" level={3} />
137+
<ExampleContainer>
138+
<DxcContainer height="300px" width="300px">
139+
<DxcContextualMenu items={sectionsWithScroll} />
140+
</DxcContainer>
141+
</ExampleContainer>
142+
<Title title="Width doesn't go below 248px" theme="light" level={3} />
143+
<ExampleContainer>
144+
<DxcContainer width="200px">
145+
<DxcContextualMenu items={items} />
146+
</DxcContainer>
147+
</ExampleContainer>
148+
</>
149+
);
150+
151+
export const MenuItemStates = () => (
152+
<>
153+
<Title title="Default" theme="light" level={3} />
154+
<ExampleContainer>
155+
<MenuItemAction {...items[0]} selected={false} />
156+
</ExampleContainer>
157+
<Title title="Focus" theme="light" level={3} />
158+
<ExampleContainer pseudoState="pseudo-focus">
159+
<MenuItemAction {...items[0]} selected={false} />
160+
</ExampleContainer>
161+
<Title title="Hover" theme="light" level={3} />
162+
<ExampleContainer pseudoState="pseudo-hover">
163+
<MenuItemAction {...items[0]} selected={false} />
164+
</ExampleContainer>
165+
<Title title="Active" theme="light" level={3} />
166+
<ExampleContainer pseudoState="pseudo-active">
167+
<MenuItemAction {...items[0]} selected={false} />
168+
</ExampleContainer>
169+
<Title title="Selected" theme="light" level={3} />
170+
<ExampleContainer>
171+
<MenuItemAction {...items[0]} selected />
172+
</ExampleContainer>
173+
<Title title="Selected hover" theme="light" level={3} />
174+
<ExampleContainer pseudoState="pseudo-hover">
175+
<MenuItemAction {...items[0]} selected />
176+
</ExampleContainer>
177+
<Title title="Selected active" theme="light" level={3} />
178+
<ExampleContainer pseudoState="pseudo-active">
179+
<MenuItemAction {...items[0]} selected />
180+
</ExampleContainer>
181+
</>
182+
);
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import React from "react";
2+
import { render, fireEvent } from "@testing-library/react";
3+
import DxcContextualMenu from "./ContextualMenu.tsx";
4+
5+
const items = [{ label: "Item 1" }, { label: "Item 2" }, { label: "Item 3" }, { label: "Item 4" }];
6+
7+
const sections = [
8+
{
9+
title: "Team repositories",
10+
items: [{ label: "Approved locations" }, { label: "Approved locations" }, { label: "Approved locations" }],
11+
},
12+
{
13+
items: [{ label: "Approved locations" }, { label: "Approved locations" }, { label: "Approved locations" }],
14+
},
15+
];
16+
17+
describe("Context menu component tests", () => {
18+
test("Context menu renders with correct aria attributes", () => {
19+
const { getAllByRole, getByRole } = render(<DxcContextualMenu items={items} defaultSelectedItemIndex={0} />);
20+
expect(getAllByRole("menuitem").length).toBe(4);
21+
const actions = getAllByRole("button");
22+
expect(actions[0].getAttribute("aria-selected")).toBeTruthy();
23+
expect(getByRole("menu")).toBeTruthy();
24+
});
25+
test("Context menu (with sections) renders with correct aria attributes", () => {
26+
const { getAllByRole } = render(<DxcContextualMenu items={sections} defaultSelectedItemIndex={4} />);
27+
expect(getAllByRole("menuitem").length).toBe(6);
28+
const actions = getAllByRole("button");
29+
expect(actions[4].getAttribute("aria-selected")).toBeTruthy();
30+
expect(getAllByRole("group").length).toBe(2);
31+
});
32+
test("onSelect event from each item is called correctly", () => {
33+
const test = [
34+
{
35+
label: "Tested item",
36+
onSelect: jest.fn(),
37+
},
38+
];
39+
const { getByRole } = render(<DxcContextualMenu items={test} />);
40+
const item = getByRole("button");
41+
fireEvent.click(item);
42+
expect(test[0].onSelect).toHaveBeenCalled();
43+
});
44+
});
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import React, { useState, Fragment } from "react";
2+
import styled from "styled-components";
3+
import CoreTokens from "../common/coreTokens";
4+
import MenuPropsType, { Item, Section as SectionType, BadgeProps as BadgePropsType } from "./types";
5+
import DxcBadge from "../badge/Badge";
6+
import MenuItemAction from "./MenuItemAction";
7+
8+
const DxcContextualMenu = ({ items, defaultSelectedItemIndex = -1 }: MenuPropsType) => {
9+
const [selectedItemIndex, setSelectedItemIndex] = useState<number>(defaultSelectedItemIndex);
10+
11+
const renderSingleItem = (item: Item, index: number) => (
12+
<Li key={`option-${index}`} role="menuitem">
13+
<MenuItemAction
14+
{...item}
15+
selected={selectedItemIndex === index}
16+
onSelect={() => {
17+
setSelectedItemIndex(index);
18+
item.onSelect?.();
19+
}}
20+
/>
21+
</Li>
22+
);
23+
24+
let accLength = 0;
25+
const renderSection = (section: SectionType, currentSectionIndex: number, items: SectionType[]) => {
26+
const startingIndex = accLength;
27+
accLength += section.items.length;
28+
return (
29+
<Fragment key={`separator-${currentSectionIndex}`}>
30+
<Li role="group">
31+
{section.title != null && <Title>{section.title}</Title>}
32+
<Section>{section.items.map((item, index) => renderSingleItem(item, startingIndex + index))}</Section>
33+
</Li>
34+
{currentSectionIndex !== items.length - 1 && <Divider aria-hidden />}
35+
</Fragment>
36+
);
37+
};
38+
39+
return (
40+
<Menu role="menu">
41+
{items.map((item: Item | SectionType, index: number, items: MenuPropsType["items"]) =>
42+
"items" in item ? renderSection(item, index, items as SectionType[]) : renderSingleItem(item, index)
43+
)}
44+
</Menu>
45+
);
46+
};
47+
48+
const Menu = styled.ul`
49+
box-sizing: border-box;
50+
margin: 0;
51+
border: 1px solid ${CoreTokens.color_grey_200};
52+
border-radius: 0.25rem;
53+
padding: ${CoreTokens.spacing_16} ${CoreTokens.spacing_8};
54+
55+
display: grid;
56+
gap: ${CoreTokens.spacing_4};
57+
min-width: 248px;
58+
max-height: 100%;
59+
background-color: ${CoreTokens.color_white};
60+
61+
overflow-y: auto;
62+
&::-webkit-scrollbar {
63+
width: 8px;
64+
height: 8px;
65+
}
66+
&::-webkit-scrollbar-thumb {
67+
background-color: ${CoreTokens.color_grey_700};
68+
border-radius: 0.25rem;
69+
}
70+
&::-webkit-scrollbar-track {
71+
background-color: ${CoreTokens.color_grey_300};
72+
border-radius: 0.25rem;
73+
}
74+
`;
75+
76+
const Li = styled.li`
77+
display: grid;
78+
`;
79+
80+
const Section = styled.ul`
81+
list-style: none;
82+
margin: 0;
83+
padding: 0;
84+
display: grid;
85+
gap: ${CoreTokens.spacing_4};
86+
`;
87+
88+
const Title = styled.h2`
89+
margin: 0 0 ${CoreTokens.spacing_4} 0;
90+
padding: ${CoreTokens.spacing_4};
91+
color: ${CoreTokens.color_grey_900};
92+
font-family: ${CoreTokens.type_sans};
93+
font-size: ${CoreTokens.type_scale_03};
94+
font-weight: ${CoreTokens.type_semibold};
95+
line-height: 24px;
96+
97+
& + ul > li > button {
98+
padding-left: ${CoreTokens.spacing_12} !important;
99+
}
100+
`;
101+
102+
const Divider = styled.hr`
103+
margin: ${CoreTokens.spacing_4} 0;
104+
border: none;
105+
height: 1px;
106+
background: ${CoreTokens.color_grey_200};
107+
`;
108+
109+
DxcContextualMenu.Badge = (props: BadgePropsType) => <DxcBadge {...props} size="small" />;
110+
111+
export default DxcContextualMenu;

0 commit comments

Comments
 (0)