Skip to content

Commit

Permalink
feat(menu, menu-item): Adds menu & menu-item components. (#6901)
Browse files Browse the repository at this point in the history
**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]>
  • Loading branch information
anveshmekala and macandcheese authored May 12, 2023
1 parent fa0fe58 commit 0990bf6
Show file tree
Hide file tree
Showing 63 changed files with 2,252 additions and 22 deletions.
3 changes: 3 additions & 0 deletions src/components/menu-item/Usage/Basic.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```html
<calcite-menu><calcite-menu-item text="Nature"></calcite-menu-item></calcite-menu>
```
9 changes: 9 additions & 0 deletions src/components/menu-item/Usage/Nested-With-Href.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Nested submenu with href.

```html
<calcite-menu>
<calcite-menu-item text="Nature" href="#">
<calcite-menu-item text="Mountains" slot="submenu-item"></calcite-menu-item>
</calcite-menu-item>
</calcite-menu>
```
5 changes: 5 additions & 0 deletions src/components/menu-item/interfaces.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface MenuItemCustomEvent {
event: KeyboardEvent;
children?: HTMLCalciteMenuItemElement[];
isSubmenuOpen?: boolean;
}
83 changes: 83 additions & 0 deletions src/components/menu-item/menu-item.e2e.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { newE2EPage } from "@stencil/core/testing";
import { html } from "../../../support/formatting";
import { accessible, focusable, hidden, reflects, renders, t9n } from "../../tests/commonTests";
import { getFocusedElementProp } from "../../tests/utils";

describe("calcite-menu-item", () => {
describe("renders", () => {
renders("calcite-menu-item", { display: "flex" });
});

it("reflects", async () =>
reflects("calcite-menu-item", [
{
propertyName: "active",
value: "true"
},

{
propertyName: "target",
value: "_blank"
}
]));

describe("honors hidden attribute", () => {
hidden("calcite-menu-item");
});

describe("accessible", () => {
accessible(html`<calcite-menu><calcite-menu-item text="calcite"></calcite-menu-item></calcite-menu>`);
});

describe("is focusable", () => {
focusable("calcite-menu-item");
});

it("supports translations", () => t9n("calcite-menu-item"));

it("should emit calciteMenuItemSelect event on user click", async () => {
const page = await newE2EPage();
await page.setContent(html` <calcite-menu-item id="Nature" text="Nature" href="#nature"> </calcite-menu-item> `);

const menuItem = await page.find("calcite-menu-item");
const eventSpy = await menuItem.spyOnEvent("calciteMenuItemSelect");

await menuItem.click();
await page.waitForChanges();
expect(await getFocusedElementProp(page, "id")).toBe("Nature");
expect(eventSpy).toHaveReceivedEventTimes(1);
});

it("should emit calciteMenuItemSelect event when user select the text area of the component using Enter or Space key", async () => {
const page = await newE2EPage();
await page.setContent(html`
<calcite-menu>
<calcite-menu-item id="Nature" text="Nature" href="#nature">
<calcite-menu-item id="Mountains" text="Mountains" slot="submenu-item"> </calcite-menu-item>
<calcite-menu-item id="Rivers" text="Rivers" slot="submenu-item"> </calcite-menu-item>
</calcite-menu-item>
</calcite-menu>
`);

const element = await page.find("calcite-menu-item");
const eventSpy = await element.spyOnEvent("calciteMenuItemSelect");

await page.keyboard.press("Tab");
await page.waitForChanges();
expect(await getFocusedElementProp(page, "id")).toBe("Nature");
expect(eventSpy).not.toHaveReceivedEvent();

await page.keyboard.press("Enter");
await page.waitForChanges();
expect(eventSpy).toHaveReceivedEventTimes(1);

await page.keyboard.press("Space");
await page.waitForChanges();
expect(eventSpy).toHaveReceivedEventTimes(2);

await page.keyboard.press("Tab");
await page.waitForChanges();
expect(await getFocusedElementProp(page, "id")).toBe("Nature");
expect(eventSpy).toHaveReceivedEventTimes(2);
});
});
170 changes: 170 additions & 0 deletions src/components/menu-item/menu-item.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
:host {
@apply flex
items-center
relative
box-border
h-full;
flex-shrink: 0;
& .container,
& .item-content,
& .content {
min-block-size: theme("spacing.12");
}
}

:host([layout="vertical"]) {
@apply w-full;
}

.container,
.item-content {
@apply flex flex-row w-full h-full items-stretch;
}

.content {
@apply flex
items-center
relative
justify-center
cursor-pointer
outline-none
text-0
text-color-2
box-border
bg-foreground-1
px-4
h-full
w-full;
text-decoration: none;
border-block-end: theme("spacing[0.5]") solid transparent;
padding-block-start: theme("spacing[0.5]");
&:hover {
@apply bg-foreground-2 text-color-2;
}
&:focus {
@apply bg-foreground-2 text-color-2 focus-inset;
}
&:active {
@apply bg-foreground-3 text-color-1;
}
& span {
display: inline-flex;
}
&.layout--vertical {
@apply flex w-full justify-start;
padding-block: 1rem;
border-block-end: 0;
border-inline-end: theme("spacing.1") solid transparent;
}
}

:host([active]) .content {
@apply text-color-1;
border-color: var(--calcite-ui-brand);
.icon {
--calcite-ui-icon-color: var(--calcite-ui-brand);
}
}

.icon--start {
@apply me-3;
}

.icon--end {
@apply ms-3;
}

.icon--dropdown {
@apply ms-auto me-0 ps-2 relative;
--calcite-ui-icon-color: var(--calcite-ui-text-3);
}

:host([layout="vertical"]) .icon--dropdown {
inset-inline-start: theme("spacing.1");
}

.icon--breadcrumb {
@apply ps-2 me-0;
--calcite-ui-icon-color: var(--calcite-ui-text-3);
}

:host([breadcrumb]) .content {
@apply pe-3;
}

calcite-action {
@apply relative h-auto;
border-inline-start: 1px solid var(--calcite-ui-foreground-1);
&:after {
@apply block w-px absolute -start-px;
content: "";
inset-block: theme("spacing.3");
background-color: var(--calcite-ui-border-3);
}
&:hover:after {
@apply h-full;
inset-block: 0;
}
}

.content:focus ~ calcite-action,
.content:hover ~ calcite-action {
@apply text-color-1;
border-inline-start: 1px solid var(--calcite-ui-border-3);
}

.container:hover .dropdown-action {
@apply bg-foreground-2;
}

.dropdown-menu-items {
@apply absolute h-auto flex-col hidden overflow-visible min-w-full;
border: 1px solid var(--calcite-ui-border-3);
background: var(--calcite-ui-foreground-1);
inset-block-start: 100%;
z-index: theme("zIndex.dropdown");
&.open {
@apply block;
}
&.nested {
@apply absolute;
inset-block-start: -1px;
transform: translateX(calc(100% - 2px));
}
}

.parent--vertical {
@apply flex-col;
}

.dropdown--vertical.dropdown-menu-items {
@apply relative rounded-none;
box-shadow: none;
inset-block-start: 0;
transform: none;
&:last-of-type {
border-inline: 0;
}
}

:host([slot="submenu-item"]) .parent--vertical {
padding-inline-start: theme("spacing.7");
}

.dropdown-menu-items.nested.calcite--rtl {
transform: translateX(calc(-100% + 2px));
}

.dropdown--vertical.dropdown-menu-items.nested.calcite--rtl {
transform: none;
}

.hover-href-icon {
@apply ps-8 ms-auto relative end-1 opacity-0;
transition: all var(--calcite-internal-animation-timing-medium) ease-in-out;
}

content:focus .hover-href-icon,
content:hover .hover-href-icon {
@apply opacity-100 -end-1;
}
78 changes: 78 additions & 0 deletions src/components/menu-item/menu-item.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { boolean, iconNames, storyFilters } from "../../../.storybook/helpers";
import readme from "./readme.md";
import { html } from "../../../support/formatting";
import { select, text } from "@storybook/addon-knobs";

export default {
title: "Components/Menu Item",
parameters: {
notes: readme
},
...storyFilters()
};

export const simple = (): string => html` <calcite-menu>
<calcite-menu-item
text="${text("text", "My nav item")}"
src="${text("src", "")}"
href="${text("href", "")}"
rel="${text("rel", "")}"
target="${text("target", "")}"
label="${text("label", "")}"
${boolean("active", false)}
${boolean("breadcrumb", false)}
/>
</calcite-menu>`;

export const iconStart = (): string => html` <calcite-menu>
<calcite-menu-item
text="${text("text", "My nav item")}"
src="${text("src", "")}"
href="${text("href", "")}"
rel="${text("rel", "")}"
target="${text("target", "")}"
label="${text("label", "")}"
icon-start="${select("icon-start", iconNames, iconNames[0])}"
${boolean("active", false)}
${boolean("breadcrumb", false)}
/>
</calcite-menu>`;

export const iconEnd = (): string => html` <calcite-menu>
<calcite-menu-item
text="${text("text", "My nav item")}"
src="${text("src", "")}"
href="${text("href", "")}"
rel="${text("rel", "")}"
target="${text("target", "")}"
label="${text("label", "")}"
icon-end="${select("icon-end", iconNames, iconNames[0])}"
${boolean("active", false)}
${boolean("breadcrumb", false)}
/>
</calcite-menu>`;

export const iconsBoth = (): string => html` <calcite-menu>
<calcite-menu-item
text="${text("text", "My nav item")}"
src="${text("src", "")}"
href="${text("href", "")}"
rel="${text("rel", "")}"
target="${text("target", "")}"
label="${text("label", "")}"
icon-end="${select("icon-end", iconNames, iconNames[0])}"
icon-start="${select("icon-start", iconNames, iconNames[0])}"
${boolean("active", false)}
${boolean("breadcrumb", false)}
/>
</calcite-menu>`;

export const darkModeRTL_TestOnly = (): string =>
html`<calcite-menu-item
text="My nav item"
active
dir="rtl"
class="calcite-mode-dark"
icon-start="Layers"
icon-end="Layers"
/>`;
Loading

0 comments on commit 0990bf6

Please sign in to comment.