Skip to content

Commit 0990bf6

Browse files
feat(menu, menu-item): Adds menu & menu-item components. (#6901)
**Related Issue:** #6531 ## Summary This PR adds `calcite-menu` & `calcite-menu-item` components. Co-authored by @macandcheese Extracted from #6829 ## calcite-nav-menu <!-- Auto Generated Below --> ### Usage #### Basic ```html <calcite-menu> <calcite-menu-item id="Nature" text="Nature"> </calcite-menu-item></calcite-menu> ``` ### Properties | Property | Attribute | Description | Type | Default | | -------------------- | --------- | ----------------------------------------------------------------------- | ---------------------------- | -------------- | | `label` _(required)_ | `label` | Specifies accessible label for the component. | `string` | `undefined` | | `layout` | `layout` | Specifies the layout of the component. | `"horizontal" \| "vertical"` | `"horizontal"` | | `messageOverrides` | -- | Use this property to override individual strings used by the component. | `{ more?: string; }` | `undefined` | ## #Methods ### `setFocus() => Promise<void>` Sets focus on the component's first focusable element. #### Returns Type: `Promise<void>` ---------------------------------------------- ## calcite-nav-menu-item <!-- Auto Generated Below --> ### Usage #### Basic ```html <calcite-menu> <calcite-menu-item id="Nature" text="Nature"> </calcite-menu-item></calcite-menu> ``` #### Nested-With-Href Nested SubMenu with href. ```html <calcite-menu> <calcite-menu-item id="Nature" text="Nature" href="#"> <calcite-menu-item id="Mountains" text="Mountains" slot="sub-menu-item"> </calcite-menu-item> </calcite-menu-item> </calcite-menu> ``` ### Properties | Property | Attribute | Description | Type | Default | | -------------------- | --------------- | ----------------------------------------------------------------------------------------------------------- | ---------------------------- | ----------- | | `active` | `active` | When `true`, the component is highlighted. | `boolean` | `undefined` | | `breadcrumb` | `breadcrumb` | When true, the component displays a visual indication of breadcrumb | `boolean` | `undefined` | | `href` | `href` | Specifies the URL destination of the component, which can be set as an absolute or relative path. | `string` | `undefined` | | `iconEnd` | `icon-end` | Specifies an icon to display at the end of the component. | `string` | `undefined` | | `iconFlipRtl` | `icon-flip-rtl` | Displays the `iconStart` and/or `iconEnd` as flipped when the element direction is right-to-left (`"rtl"`). | `"both" \| "end" \| "start"` | `undefined` | | `iconStart` | `icon-start` | Specifies an icon to display at the start of the component. | `string` | `undefined` | | `label` _(required)_ | `label` | Specifices accessible name for the component. | `string` | `undefined` | | `open` | `open` | When true, the menu item will display any slotted `calcite-menu-item` in an open overflow menu | `boolean` | `false` | | `rel` | `rel` | Defines the relationship between the `href` value and the current document. | `string` | `undefined` | | `target` | `target` | Specifies where to open the linked document defined in the `href` property. | `string` | `undefined` | | `text` | `text` | Specifies the text to display. | `string` | `undefined` | ### Events | Event | Description | Type | | ----------------------- | -------------------------------------- | ------------------- | | `calciteMenuItemSelect` | Emits when user selects the component. | `CustomEvent<void>` | ### Methods #### `setFocus() => Promise<void>` Sets focus on the component. ##### Returns Type: `Promise<void>` ---------------------------------------------- *Built with [StencilJS](https://stenciljs.com/)* --------- Co-authored-by: Adam <[email protected]>
1 parent fa0fe58 commit 0990bf6

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

63 files changed

+2252
-22
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
```html
2+
<calcite-menu><calcite-menu-item text="Nature"></calcite-menu-item></calcite-menu>
3+
```
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
Nested submenu with href.
2+
3+
```html
4+
<calcite-menu>
5+
<calcite-menu-item text="Nature" href="#">
6+
<calcite-menu-item text="Mountains" slot="submenu-item"></calcite-menu-item>
7+
</calcite-menu-item>
8+
</calcite-menu>
9+
```
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export interface MenuItemCustomEvent {
2+
event: KeyboardEvent;
3+
children?: HTMLCalciteMenuItemElement[];
4+
isSubmenuOpen?: boolean;
5+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { newE2EPage } from "@stencil/core/testing";
2+
import { html } from "../../../support/formatting";
3+
import { accessible, focusable, hidden, reflects, renders, t9n } from "../../tests/commonTests";
4+
import { getFocusedElementProp } from "../../tests/utils";
5+
6+
describe("calcite-menu-item", () => {
7+
describe("renders", () => {
8+
renders("calcite-menu-item", { display: "flex" });
9+
});
10+
11+
it("reflects", async () =>
12+
reflects("calcite-menu-item", [
13+
{
14+
propertyName: "active",
15+
value: "true"
16+
},
17+
18+
{
19+
propertyName: "target",
20+
value: "_blank"
21+
}
22+
]));
23+
24+
describe("honors hidden attribute", () => {
25+
hidden("calcite-menu-item");
26+
});
27+
28+
describe("accessible", () => {
29+
accessible(html`<calcite-menu><calcite-menu-item text="calcite"></calcite-menu-item></calcite-menu>`);
30+
});
31+
32+
describe("is focusable", () => {
33+
focusable("calcite-menu-item");
34+
});
35+
36+
it("supports translations", () => t9n("calcite-menu-item"));
37+
38+
it("should emit calciteMenuItemSelect event on user click", async () => {
39+
const page = await newE2EPage();
40+
await page.setContent(html` <calcite-menu-item id="Nature" text="Nature" href="#nature"> </calcite-menu-item> `);
41+
42+
const menuItem = await page.find("calcite-menu-item");
43+
const eventSpy = await menuItem.spyOnEvent("calciteMenuItemSelect");
44+
45+
await menuItem.click();
46+
await page.waitForChanges();
47+
expect(await getFocusedElementProp(page, "id")).toBe("Nature");
48+
expect(eventSpy).toHaveReceivedEventTimes(1);
49+
});
50+
51+
it("should emit calciteMenuItemSelect event when user select the text area of the component using Enter or Space key", async () => {
52+
const page = await newE2EPage();
53+
await page.setContent(html`
54+
<calcite-menu>
55+
<calcite-menu-item id="Nature" text="Nature" href="#nature">
56+
<calcite-menu-item id="Mountains" text="Mountains" slot="submenu-item"> </calcite-menu-item>
57+
<calcite-menu-item id="Rivers" text="Rivers" slot="submenu-item"> </calcite-menu-item>
58+
</calcite-menu-item>
59+
</calcite-menu>
60+
`);
61+
62+
const element = await page.find("calcite-menu-item");
63+
const eventSpy = await element.spyOnEvent("calciteMenuItemSelect");
64+
65+
await page.keyboard.press("Tab");
66+
await page.waitForChanges();
67+
expect(await getFocusedElementProp(page, "id")).toBe("Nature");
68+
expect(eventSpy).not.toHaveReceivedEvent();
69+
70+
await page.keyboard.press("Enter");
71+
await page.waitForChanges();
72+
expect(eventSpy).toHaveReceivedEventTimes(1);
73+
74+
await page.keyboard.press("Space");
75+
await page.waitForChanges();
76+
expect(eventSpy).toHaveReceivedEventTimes(2);
77+
78+
await page.keyboard.press("Tab");
79+
await page.waitForChanges();
80+
expect(await getFocusedElementProp(page, "id")).toBe("Nature");
81+
expect(eventSpy).toHaveReceivedEventTimes(2);
82+
});
83+
});
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
:host {
2+
@apply flex
3+
items-center
4+
relative
5+
box-border
6+
h-full;
7+
flex-shrink: 0;
8+
& .container,
9+
& .item-content,
10+
& .content {
11+
min-block-size: theme("spacing.12");
12+
}
13+
}
14+
15+
:host([layout="vertical"]) {
16+
@apply w-full;
17+
}
18+
19+
.container,
20+
.item-content {
21+
@apply flex flex-row w-full h-full items-stretch;
22+
}
23+
24+
.content {
25+
@apply flex
26+
items-center
27+
relative
28+
justify-center
29+
cursor-pointer
30+
outline-none
31+
text-0
32+
text-color-2
33+
box-border
34+
bg-foreground-1
35+
px-4
36+
h-full
37+
w-full;
38+
text-decoration: none;
39+
border-block-end: theme("spacing[0.5]") solid transparent;
40+
padding-block-start: theme("spacing[0.5]");
41+
&:hover {
42+
@apply bg-foreground-2 text-color-2;
43+
}
44+
&:focus {
45+
@apply bg-foreground-2 text-color-2 focus-inset;
46+
}
47+
&:active {
48+
@apply bg-foreground-3 text-color-1;
49+
}
50+
& span {
51+
display: inline-flex;
52+
}
53+
&.layout--vertical {
54+
@apply flex w-full justify-start;
55+
padding-block: 1rem;
56+
border-block-end: 0;
57+
border-inline-end: theme("spacing.1") solid transparent;
58+
}
59+
}
60+
61+
:host([active]) .content {
62+
@apply text-color-1;
63+
border-color: var(--calcite-ui-brand);
64+
.icon {
65+
--calcite-ui-icon-color: var(--calcite-ui-brand);
66+
}
67+
}
68+
69+
.icon--start {
70+
@apply me-3;
71+
}
72+
73+
.icon--end {
74+
@apply ms-3;
75+
}
76+
77+
.icon--dropdown {
78+
@apply ms-auto me-0 ps-2 relative;
79+
--calcite-ui-icon-color: var(--calcite-ui-text-3);
80+
}
81+
82+
:host([layout="vertical"]) .icon--dropdown {
83+
inset-inline-start: theme("spacing.1");
84+
}
85+
86+
.icon--breadcrumb {
87+
@apply ps-2 me-0;
88+
--calcite-ui-icon-color: var(--calcite-ui-text-3);
89+
}
90+
91+
:host([breadcrumb]) .content {
92+
@apply pe-3;
93+
}
94+
95+
calcite-action {
96+
@apply relative h-auto;
97+
border-inline-start: 1px solid var(--calcite-ui-foreground-1);
98+
&:after {
99+
@apply block w-px absolute -start-px;
100+
content: "";
101+
inset-block: theme("spacing.3");
102+
background-color: var(--calcite-ui-border-3);
103+
}
104+
&:hover:after {
105+
@apply h-full;
106+
inset-block: 0;
107+
}
108+
}
109+
110+
.content:focus ~ calcite-action,
111+
.content:hover ~ calcite-action {
112+
@apply text-color-1;
113+
border-inline-start: 1px solid var(--calcite-ui-border-3);
114+
}
115+
116+
.container:hover .dropdown-action {
117+
@apply bg-foreground-2;
118+
}
119+
120+
.dropdown-menu-items {
121+
@apply absolute h-auto flex-col hidden overflow-visible min-w-full;
122+
border: 1px solid var(--calcite-ui-border-3);
123+
background: var(--calcite-ui-foreground-1);
124+
inset-block-start: 100%;
125+
z-index: theme("zIndex.dropdown");
126+
&.open {
127+
@apply block;
128+
}
129+
&.nested {
130+
@apply absolute;
131+
inset-block-start: -1px;
132+
transform: translateX(calc(100% - 2px));
133+
}
134+
}
135+
136+
.parent--vertical {
137+
@apply flex-col;
138+
}
139+
140+
.dropdown--vertical.dropdown-menu-items {
141+
@apply relative rounded-none;
142+
box-shadow: none;
143+
inset-block-start: 0;
144+
transform: none;
145+
&:last-of-type {
146+
border-inline: 0;
147+
}
148+
}
149+
150+
:host([slot="submenu-item"]) .parent--vertical {
151+
padding-inline-start: theme("spacing.7");
152+
}
153+
154+
.dropdown-menu-items.nested.calcite--rtl {
155+
transform: translateX(calc(-100% + 2px));
156+
}
157+
158+
.dropdown--vertical.dropdown-menu-items.nested.calcite--rtl {
159+
transform: none;
160+
}
161+
162+
.hover-href-icon {
163+
@apply ps-8 ms-auto relative end-1 opacity-0;
164+
transition: all var(--calcite-internal-animation-timing-medium) ease-in-out;
165+
}
166+
167+
content:focus .hover-href-icon,
168+
content:hover .hover-href-icon {
169+
@apply opacity-100 -end-1;
170+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { boolean, iconNames, storyFilters } from "../../../.storybook/helpers";
2+
import readme from "./readme.md";
3+
import { html } from "../../../support/formatting";
4+
import { select, text } from "@storybook/addon-knobs";
5+
6+
export default {
7+
title: "Components/Menu Item",
8+
parameters: {
9+
notes: readme
10+
},
11+
...storyFilters()
12+
};
13+
14+
export const simple = (): string => html` <calcite-menu>
15+
<calcite-menu-item
16+
text="${text("text", "My nav item")}"
17+
src="${text("src", "")}"
18+
href="${text("href", "")}"
19+
rel="${text("rel", "")}"
20+
target="${text("target", "")}"
21+
label="${text("label", "")}"
22+
${boolean("active", false)}
23+
${boolean("breadcrumb", false)}
24+
/>
25+
</calcite-menu>`;
26+
27+
export const iconStart = (): string => html` <calcite-menu>
28+
<calcite-menu-item
29+
text="${text("text", "My nav item")}"
30+
src="${text("src", "")}"
31+
href="${text("href", "")}"
32+
rel="${text("rel", "")}"
33+
target="${text("target", "")}"
34+
label="${text("label", "")}"
35+
icon-start="${select("icon-start", iconNames, iconNames[0])}"
36+
${boolean("active", false)}
37+
${boolean("breadcrumb", false)}
38+
/>
39+
</calcite-menu>`;
40+
41+
export const iconEnd = (): string => html` <calcite-menu>
42+
<calcite-menu-item
43+
text="${text("text", "My nav item")}"
44+
src="${text("src", "")}"
45+
href="${text("href", "")}"
46+
rel="${text("rel", "")}"
47+
target="${text("target", "")}"
48+
label="${text("label", "")}"
49+
icon-end="${select("icon-end", iconNames, iconNames[0])}"
50+
${boolean("active", false)}
51+
${boolean("breadcrumb", false)}
52+
/>
53+
</calcite-menu>`;
54+
55+
export const iconsBoth = (): string => html` <calcite-menu>
56+
<calcite-menu-item
57+
text="${text("text", "My nav item")}"
58+
src="${text("src", "")}"
59+
href="${text("href", "")}"
60+
rel="${text("rel", "")}"
61+
target="${text("target", "")}"
62+
label="${text("label", "")}"
63+
icon-end="${select("icon-end", iconNames, iconNames[0])}"
64+
icon-start="${select("icon-start", iconNames, iconNames[0])}"
65+
${boolean("active", false)}
66+
${boolean("breadcrumb", false)}
67+
/>
68+
</calcite-menu>`;
69+
70+
export const darkModeRTL_TestOnly = (): string =>
71+
html`<calcite-menu-item
72+
text="My nav item"
73+
active
74+
dir="rtl"
75+
class="calcite-mode-dark"
76+
icon-start="Layers"
77+
icon-end="Layers"
78+
/>`;

0 commit comments

Comments
 (0)