From 467f71ea16bd490075e53a2e3e8c9e0471a92280 Mon Sep 17 00:00:00 2001 From: Robin Date: Tue, 26 Sep 2023 14:48:10 -0400 Subject: [PATCH] Create menu item component For the epic https://github.com/vector-im/compound/issues/226. --- src/components/MenuItem/MenuItem.module.css | 102 +++++++++++++++++ src/components/MenuItem/MenuItem.stories.tsx | 54 +++++++++ src/components/MenuItem/MenuItem.test.tsx | 44 +++++++ src/components/MenuItem/MenuItem.tsx | 92 +++++++++++++++ .../__snapshots__/MenuItem.test.tsx.snap | 108 ++++++++++++++++++ src/index.ts | 1 + 6 files changed, 401 insertions(+) create mode 100644 src/components/MenuItem/MenuItem.module.css create mode 100644 src/components/MenuItem/MenuItem.stories.tsx create mode 100644 src/components/MenuItem/MenuItem.test.tsx create mode 100644 src/components/MenuItem/MenuItem.tsx create mode 100644 src/components/MenuItem/__snapshots__/MenuItem.test.tsx.snap diff --git a/src/components/MenuItem/MenuItem.module.css b/src/components/MenuItem/MenuItem.module.css new file mode 100644 index 00000000..af76234c --- /dev/null +++ b/src/components/MenuItem/MenuItem.module.css @@ -0,0 +1,102 @@ +/* +Copyright 2023 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.item { + display: flex; + align-items: center; + padding-block: var(--cpd-space-2x); + padding-inline: var(--cpd-space-4x); + box-sizing: border-box; + inline-size: 100%; + color: var(--cpd-color-text-secondary); + background: var(--cpd-color-bg-action-secondary-rest); +} + +.item.interactive { + cursor: pointer; +} + +.icon { + flex-shrink: 0; +} + +.label { + padding-inline: var(--cpd-space-3x); + flex-basis: 100%; + text-align: start; +} + +.nav-hint { + display: none; + flex-shrink: 0; + margin-inline-end: calc(-1 * var(--cpd-space-2x)); +} + +button.item { + appearance: none; + border: none; +} + +.item.with-chevron { + padding-inline-end: var(--cpd-space-2x); +} + +.item[data-kind="primary"] > .label { + color: var(--cpd-color-text-primary); +} + +.item[data-kind="primary"] > .icon { + color: var(--cpd-color-icon-primary); +} + +.item[data-kind="primary"] > .nav-hint { + color: var(--cpd-color-icon-tertiary); +} + +.item[data-kind="critical"] > .label { + color: var(--cpd-color-text-critical-primary); +} + +.item[data-kind="critical"] > .icon, +.item[data-kind="critical"] > .nav-hint { + color: var(--cpd-color-icon-critical-primary); +} + +@media (hover) { + .item.interactive[data-kind="primary"]:hover { + background: var(--cpd-color-bg-action-secondary-hovered); + } + + .item.interactive[data-kind="critical"]:hover { + background: var(--cpd-color-bg-critical-subtle); + } + + .item.interactive:hover > .nav-hint { + display: initial; + } + + .item.interactive:hover > .nav-hint ~ * { + display: none; + } +} + +.item.interactive[data-kind="primary"]:active { + background: var(--cpd-color-bg-action-secondary-pressed); +} + +.item.interactive[data-kind="critical"]:active { + background: var(--cpd-color-bg-critical-subtle); +} diff --git a/src/components/MenuItem/MenuItem.stories.tsx b/src/components/MenuItem/MenuItem.stories.tsx new file mode 100644 index 00000000..5d83f9d0 --- /dev/null +++ b/src/components/MenuItem/MenuItem.stories.tsx @@ -0,0 +1,54 @@ +/* +Copyright 2023 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from "react"; +import { Meta, StoryFn } from "@storybook/react"; +import ExtensionsIcon from "@vector-im/compound-design-tokens/icons/extensions.svg"; +import ChatIcon from "@vector-im/compound-design-tokens/icons/chat.svg"; + +import { MenuItem as MenuItemComponent } from "./MenuItem"; +import { Text } from "../Typography/Text"; + +export default { + title: "MenuItem", + component: MenuItemComponent, + argTypes: {}, + args: {}, +} as Meta; + +const Template: StoryFn = (args) => ( +
+ + + 99 + + + +
+); + +export const Primary = Template.bind({}); +Primary.args = { kind: "primary" }; + +export const Critical = Template.bind({}); +Critical.args = { kind: "critical" }; + +Primary.parameters = Critical.parameters = { + design: { + type: "figma", + url: "https://www.figma.com/file/rTaQE2nIUSLav4Tg3nozq7/Compound-Web-Components?type=design&node-id=712-6909&mode=dev", + }, +}; diff --git a/src/components/MenuItem/MenuItem.test.tsx b/src/components/MenuItem/MenuItem.test.tsx new file mode 100644 index 00000000..add6bf87 --- /dev/null +++ b/src/components/MenuItem/MenuItem.test.tsx @@ -0,0 +1,44 @@ +/* +Copyright 2023 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { describe, it, expect } from "vitest"; +import { render } from "@testing-library/react"; +import React from "react"; +import LeaveIcon from "@vector-im/compound-design-tokens/icons/leave.svg"; +import UserProfileIcon from "@vector-im/compound-design-tokens/icons/user-profile.svg"; + +import { MenuItem } from "./MenuItem"; +import { Text } from "../Typography/Text"; + +describe("MenuItem", () => { + it("renders", () => { + const { asFragment } = render( + , + ); + expect(asFragment()).toMatchSnapshot(); + }); + + it("renders with a child", () => { + const { asFragment } = render( + + + 10 + + , + ); + expect(asFragment()).toMatchSnapshot(); + }); +}); diff --git a/src/components/MenuItem/MenuItem.tsx b/src/components/MenuItem/MenuItem.tsx new file mode 100644 index 00000000..95bea261 --- /dev/null +++ b/src/components/MenuItem/MenuItem.tsx @@ -0,0 +1,92 @@ +/* +Copyright 2023 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import classnames from "classnames"; +import React, { + ComponentPropsWithoutRef, + ComponentType, + ElementType, + SVGAttributes, +} from "react"; +import styles from "./MenuItem.module.css"; +import { Text } from "../Typography/Text"; +import ChevronRightIcon from "@vector-im/compound-design-tokens/icons/chevron-right.svg"; + +type MenuItemElement = "button" | "label" | "div"; + +type Props = { + /** + * The element type of this menu item. + * @default button + */ + as?: C; + className?: string; + /** + * The icon to show on this menu item. + */ + Icon: ComponentType>; + /** + * The label to show on this menu item. + */ + label: string; + /** + * The color variant of the menu item. + * @default primary + */ + kind?: "primary" | "critical"; + /** + * Whether to replace the children with a navigation hint on hover. + * @default true + */ + navHint?: boolean; +} & ComponentPropsWithoutRef; + +export const MenuItem = ({ + as, + className, + Icon, + label, + kind = "primary", + navHint = true, + children, + ...props +}: Props) => { + const Component = as ?? ("button" as ElementType); + + return ( + + + + {label} + + {navHint && ( + + )} + {children} + + ); +}; diff --git a/src/components/MenuItem/__snapshots__/MenuItem.test.tsx.snap b/src/components/MenuItem/__snapshots__/MenuItem.test.tsx.snap new file mode 100644 index 00000000..0e12f7b7 --- /dev/null +++ b/src/components/MenuItem/__snapshots__/MenuItem.test.tsx.snap @@ -0,0 +1,108 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`MenuItem > renders 1`] = ` + + + +`; + +exports[`MenuItem > renders with a child 1`] = ` + + + +`; diff --git a/src/index.ts b/src/index.ts index 16637933..5ffa46eb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -41,6 +41,7 @@ export { export { IconButton } from "./components/IconButton/IconButton"; export { Label } from "./components/Form/Label"; export { Link } from "./components/Link/Link"; +export { MenuItem } from "./components/MenuItem/MenuItem"; export { Message } from "./components/Form/Message"; export { PasswordControl } from "./components/Form/Controls/Password"; export { Radio } from "./components/Radio/Radio";