diff --git a/packages/calcite-components/src/components/menu-item/interfaces.ts b/packages/calcite-components/src/components/menu-item/interfaces.ts index a25275470c7..8c862aa3064 100644 --- a/packages/calcite-components/src/components/menu-item/interfaces.ts +++ b/packages/calcite-components/src/components/menu-item/interfaces.ts @@ -5,3 +5,5 @@ export interface MenuItemCustomEvent { children?: MenuItem["el"][]; isSubmenuOpen?: boolean; } + +export type Layout = "horizontal" | "vertical"; diff --git a/packages/calcite-components/src/components/menu-item/menu-item.e2e.ts b/packages/calcite-components/src/components/menu-item/menu-item.e2e.ts index 9794ef22e23..fa900686bdf 100644 --- a/packages/calcite-components/src/components/menu-item/menu-item.e2e.ts +++ b/packages/calcite-components/src/components/menu-item/menu-item.e2e.ts @@ -1,8 +1,11 @@ import { newE2EPage } from "@arcgis/lumina-compiler/puppeteerTesting"; import { describe, expect, it } from "vitest"; import { html } from "../../../support/formatting"; -import { accessible, focusable, hidden, reflects, renders, t9n } from "../../tests/commonTests"; +import { accessible, focusable, hidden, reflects, renders, t9n, themed } from "../../tests/commonTests"; import { getFocusedElementProp } from "../../tests/utils"; +import { ComponentTestTokens } from "../../tests/commonTests/themed"; +import { CSS } from "../../../src/components/menu-item/resources"; +import { Layout } from "./interfaces"; describe("calcite-menu-item", () => { describe("renders", () => { @@ -116,4 +119,174 @@ describe("calcite-menu-item", () => { expect(await getFocusedElementProp(page, "id")).toBe("Nature"); expect(eventSpy).toHaveReceivedEventTimes(2); }); + + describe("theme", () => { + const menuWithSlottedSubmenuHTML = (layout: Layout): string => html` + + + + + + + `; + describe("slotted submenu", () => { + const tokens = (layout: Layout): ComponentTestTokens => { + return { + "--calcite-menu-background-color": [{ + selector: "calcite-menu-item", + shadowSelector: `calcite-action`, + targetProp: "--calcite-action-background-color-press", + state: { press: { attribute: "class", value: "dropdown-action" } }, + }, { + selector: "calcite-menu-item", + shadowSelector: `calcite-action`, + targetProp: "--calcite-action-background-color", + }], + "--calcite-menu-text-color": { + selector: "calcite-menu-item", + shadowSelector: `calcite-action`, + targetProp: "--calcite-action-text-color", + }, + "--calcite-menu-item-sub-menu-corner-radius": { + selector: "calcite-menu-item", + shadowSelector: `.${CSS.dropdownMenuItems}`, + targetProp: "borderRadius", + }, + "--calcite-menu-item-sub-menu-border-color": { + selector: "calcite-menu-item", + shadowSelector: `.${CSS.dropdownMenuItems}`, + targetProp: layout === "horizontal" ? "borderColor" : "borderBlockColor", + } + }; + } + + describe("horizontal layout", () => { + themed(menuWithSlottedSubmenuHTML("horizontal"), tokens("horizontal")); + }); + + describe("vertical layout", () => { + themed(menuWithSlottedSubmenuHTML("vertical"), tokens("vertical")); + }); + }); + + describe("default", () => { + const menuHTML = (layout: Layout): string => html` + + + + `; + const tokens: ComponentTestTokens = { + "--calcite-menu-text-color": [ + { + selector: "calcite-menu-item", + shadowSelector: `.${CSS.content}`, + targetProp: "color", + }, + { + selector: "calcite-menu-item", + shadowSelector: ` .${CSS.content} `, + targetProp: "color", + state: { press: { attribute: "role", value: `menuitem` } }, + } + ], + "--calcite-menu-background-color": [ + { + selector: "calcite-menu-item", + shadowSelector: `.${CSS.content}`, + targetProp: "backgroundColor", + }, + { + selector: "calcite-menu-item", + shadowSelector: `.${CSS.content}`, + targetProp: "backgroundColor", + state: { press: { attribute: "role", value: `menuitem` } }, + }], + } + + describe("horizontal layout", () => { + themed(menuHTML("horizontal"), { + ...tokens, "--calcite-menu-item-accent-color": { + selector: "calcite-menu-item", + shadowSelector: `.${CSS.content}`, + targetProp: "borderBlockEndColor", + state: "hover", + }, + }); + }) + + describe("vertical layout", () => { + themed(menuHTML("vertical"), tokens); + }); + }) + + describe("active", () => { + const activeMenuItemHTML = (layout: Layout): string => html` + + + + `; + const tokens = (layout: Layout): ComponentTestTokens => { + const targetBorderProp = layout === "horizontal" ? "borderBlockEndColor" : "borderInlineEndColor"; + return { + "--calcite-menu-item-accent-color": [{ + selector: "calcite-menu-item", + shadowSelector: `.${CSS.content}`, + targetProp: targetBorderProp + }, + { + selector: "calcite-menu-item", + shadowSelector: `.${CSS.content}`, + targetProp: targetBorderProp, + state: "hover", + },] + } + }; + describe("horizontal layout", () => { + themed(activeMenuItemHTML("horizontal"), tokens("horizontal")); + }) + + describe("vertical layout", () => { + themed(activeMenuItemHTML("vertical"), tokens("vertical")); + }) + }); + + describe("icons", () => { + const iconMenuItemHTML: string = html` + + + + `; + + const tokens: ComponentTestTokens = { + "--calcite-menu-text-color": [ + { + selector: "calcite-menu-item", + shadowSelector: `.${CSS.iconStart}`, + targetProp: "color", + }, + { + selector: "calcite-menu-item", + shadowSelector: `.${CSS.iconEnd}`, + targetProp: "color", + }, + { + selector: "calcite-menu-item", + shadowSelector: `.${CSS.iconBreadcrumb}`, + targetProp: "color", + }, + { + selector: "calcite-menu-item", + shadowSelector: `.${CSS.iconDropdown}`, + targetProp: "color", + }, + ], + }; + themed(iconMenuItemHTML, tokens); + }); + }); }); diff --git a/packages/calcite-components/src/components/menu-item/menu-item.scss b/packages/calcite-components/src/components/menu-item/menu-item.scss index 5b2602ea3c0..12fea8c2a5d 100644 --- a/packages/calcite-components/src/components/menu-item/menu-item.scss +++ b/packages/calcite-components/src/components/menu-item/menu-item.scss @@ -1,3 +1,15 @@ +/** + * CSS Custom Properties + * + * These properties can be overridden using the component's tag as selector. + * + * @prop --calcite-menu-item-accent-color: Specifies the border color of the component when `active`. + * @prop --calcite-menu-background-color: Specifies the background color of the component. + * @prop --calcite-menu-item-sub-menu-border-color: Specifies the border color of sub-menu. + * @prop --calcite-menu-item-sub-menu-corner-radius: Specifies the border radius of sub-menu. + * @prop --calcite-menu-text-color: Specifies the text color of the component. + */ + :host { @apply flex items-center @@ -32,46 +44,66 @@ 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]"); + border-block-end: theme("spacing[0.5]") solid var(--calcite-color-transparent); + + background-color: var( + --calcite-menu-background-color, + var(--calcite-internal-menu-background-color, var(--calcite-color-foreground-1)) + ); + color: var(--calcite-menu-text-color, var(--calcite-internal-menu-text-color, var(--calcite-color-text-2))); + &:hover { - @apply text-color-2 border-b-color-2; + border-block-end-color: var(--calcite-menu-item-accent-color, var(--calcite-color-border-2)); } + &:focus { - @apply text-color-2 focus-inset border-b-4; + @apply focus-inset border-b-4; padding-block-start: theme("spacing.1"); border-block-end-width: theme("spacing.1"); } + &:active { - @apply bg-foreground-3 text-color-1; + --calcite-internal-menu-background-color: var(--calcite-color-foreground-3); + --calcite-internal-menu-text-color: var(--calcite-color-text-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; + border-inline-end: theme("spacing.1") solid var(--calcite-color-transparent); } } -:host([active]) .content { - @apply text-color-1; - border-color: var(--calcite-color-brand); +:host([layout="vertical"]) .content { + @apply px-3; +} + +:host([active]) { + .content { + --calcite-internal-menu-text-color: var(--calcite-color-text-1); + border-color: var(--calcite-menu-item-accent-color, var(--calcite-color-brand)); + } .icon { - --calcite-icon-color: var(--calcite-color-brand); + --calcite-internal-menu-item-icon-color: var(--calcite-color-brand); } } -:host([layout="vertical"]) .content { - @apply px-3; + +.icon { + color: var( + --calcite-menu-text-color, + var(--calcite-icon-color, var(--calcite-internal-menu-item-icon-color, var(--calcite-color-text-3))) + ); } .icon--start { @@ -88,7 +120,6 @@ .icon--dropdown { @apply ms-auto me-0 ps-2 relative; - --calcite-icon-color: var(--calcite-color-text-3); } :host([layout="vertical"]) .icon--end ~ .icon--dropdown { @@ -108,7 +139,6 @@ .icon--breadcrumb { @apply ps-2 me-0; - --calcite-icon-color: var(--calcite-color-text-3); } :host([layout="vertical"]) .icon--breadcrumb { @@ -130,32 +160,41 @@ calcite-action { @apply relative h-auto; border-inline-start: 1px solid var(--calcite-color-foreground-1); + --calcite-action-background-color: var(--calcite-menu-background-color); + --calcite-action-text-color: var(--calcite-menu-text-color); + &::after { @apply block w-px absolute -start-px; content: ""; inset-block: theme("spacing.3"); background-color: var(--calcite-color-border-3); } + &:hover::after { @apply h-full; inset-block: 0; } + + &:active { + --calcite-action-background-color-press: var(--calcite-menu-background-color); + } } +// extends the broder block of calcite action when hovered on content .content:focus ~ calcite-action, .content:hover ~ calcite-action { - @apply text-color-1; - border-inline-start: 1px solid var(--calcite-color-border-3); -} + --calcite-action-text-color: var(--calcite-menu-text-color, var(--calcite-color-text-1)); -.container:hover .dropdown-action { - @apply bg-foreground-2; + &::after { + @apply h-full; + inset-block: 0; + } } .dropdown-menu-items { @apply absolute h-auto flex-col hidden overflow-visible min-w-full; - border: 1px solid var(--calcite-color-border-3); - background: var(--calcite-color-foreground-1); + border: 1px solid var(--calcite-menu-item-sub-menu-border-color, var(--calcite-color-border-3)); + border-radius: var(--calcite-menu-item-sub-menu-corner-radius, var(--calcite-corner-radius)); inset-block-start: 100%; z-index: theme("zIndex.dropdown"); &.open { @@ -173,7 +212,7 @@ calcite-action { } .dropdown--vertical.dropdown-menu-items { - @apply relative rounded-none; + @apply relative; box-shadow: none; inset-block-start: 0; transform: none; diff --git a/packages/calcite-components/src/components/menu-item/menu-item.tsx b/packages/calcite-components/src/components/menu-item/menu-item.tsx index 8052d1422f3..6609f266a34 100644 --- a/packages/calcite-components/src/components/menu-item/menu-item.tsx +++ b/packages/calcite-components/src/components/menu-item/menu-item.tsx @@ -10,7 +10,7 @@ import { state, JsxNode, } from "@arcgis/lumina"; -import { FlipContext } from "../interfaces"; +import { FlipContext, Layout } from "../interfaces"; import { Direction, getElementDir, slotChangeGetAssignedElements } from "../../utils/dom"; import { componentFocusable, @@ -33,8 +33,6 @@ declare global { } } -type Layout = "horizontal" | "vertical"; - /** @slot submenu-item - A slot for adding `calcite-menu-item`s in a submenu. */ export class MenuItem extends LitElement implements LoadableComponent { // #region Static Members diff --git a/packages/calcite-components/src/custom-theme.stories.ts b/packages/calcite-components/src/custom-theme.stories.ts index d15f02078f5..5b39ffafb7e 100644 --- a/packages/calcite-components/src/custom-theme.stories.ts +++ b/packages/calcite-components/src/custom-theme.stories.ts @@ -49,6 +49,7 @@ import { navigationLogoTokens, navigationLogos } from "./custom-theme/navigation import { navigationUserTokens, navigationUsers } from "./custom-theme/navigation-user"; import { tileTokens, tile } from "./custom-theme/tile"; import { navigationTokens, navigation } from "./custom-theme/navigation"; +import { menuItem, menuItemTokens } from "./custom-theme/menu-item"; const globalTokens = { calciteColorBrand: "#007ac2", @@ -138,6 +139,7 @@ const kitchenSink = (args: Record, useTestValues = false) => ${navigation} ${navigationLogos} ${navigationUsers} ${blockSection} ${block} ${rating}
${alert}
+
${menuItem}
`; @@ -177,6 +179,7 @@ const componentTokens = { ...textAreaTokens, ...tileTokens, ...tooltipTokens, + ...menuItemTokens, }; export default { diff --git a/packages/calcite-components/src/custom-theme/menu-item.ts b/packages/calcite-components/src/custom-theme/menu-item.ts new file mode 100644 index 00000000000..f7a5990f7fd --- /dev/null +++ b/packages/calcite-components/src/custom-theme/menu-item.ts @@ -0,0 +1,45 @@ +import { html } from "../../support/formatting"; + +export const menuItemTokens = { + calciteMenuItemAccentColor: "", + calciteMenuBackgroundColor: "", + calciteMenuItemSubMenuBorderColor: "", + calciteMenuItemSubMenuCornerRadius: "", + calciteMenuTextColor: "", +}; + +const menuHTML = (layout: string) => html` + + + + + + + + + + `; + +export const menuItem = html` ${menuHTML("horizontal")} ${menuHTML("vertical")} `;