-
Notifications
You must be signed in to change notification settings - Fork 77
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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]>
- Loading branch information
1 parent
fa0fe58
commit 0990bf6
Showing
63 changed files
with
2,252 additions
and
22 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
export interface MenuItemCustomEvent { | ||
event: KeyboardEvent; | ||
children?: HTMLCalciteMenuItemElement[]; | ||
isSubmenuOpen?: boolean; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
/>`; |
Oops, something went wrong.