From 04edf544d9a09592f14f6b1ac20a70b6ec89792f Mon Sep 17 00:00:00 2001 From: Marcus Kernohan <135075821+mkernohanbc@users.noreply.github.com> Date: Wed, 4 Dec 2024 16:51:55 -0800 Subject: [PATCH 01/10] implement RAC Popover component --- .../src/components/Popover/Popover.css | 12 ++++++++++++ .../src/components/Popover/Popover.tsx | 12 ++++++++++++ .../react-components/src/components/Popover/index.ts | 2 ++ packages/react-components/src/components/index.ts | 1 + 4 files changed, 27 insertions(+) create mode 100644 packages/react-components/src/components/Popover/Popover.css create mode 100644 packages/react-components/src/components/Popover/Popover.tsx create mode 100644 packages/react-components/src/components/Popover/index.ts diff --git a/packages/react-components/src/components/Popover/Popover.css b/packages/react-components/src/components/Popover/Popover.css new file mode 100644 index 00000000..f0d4b854 --- /dev/null +++ b/packages/react-components/src/components/Popover/Popover.css @@ -0,0 +1,12 @@ +.bcds-react-aria-Popover { + background-color: var(--surface-color-forms-default); + border: var(--layout-border-width-small) solid + var(--surface-color-border-default); + border-radius: var(--layout-border-radius-medium); + box-shadow: var(--surface-shadow-medium); + box-sizing: border-box; + padding: var(--layout-padding-small) var(--layout-padding-small); + width: var( + --trigger-width + ); /* Variable provided by Menu component: https://react-spectrum.adobe.com/react-aria/Menu.html#popover-1 */ +} diff --git a/packages/react-components/src/components/Popover/Popover.tsx b/packages/react-components/src/components/Popover/Popover.tsx new file mode 100644 index 00000000..7c153c7f --- /dev/null +++ b/packages/react-components/src/components/Popover/Popover.tsx @@ -0,0 +1,12 @@ +import { Popover as ReactAriaPopover } from "react-aria-components"; +import type { PopoverProps } from "react-aria-components"; + +import "./Popover.css"; + +export default function Popover(props: PopoverProps) { + return ( + + {props.children} + + ); +} diff --git a/packages/react-components/src/components/Popover/index.ts b/packages/react-components/src/components/Popover/index.ts new file mode 100644 index 00000000..a2546c9c --- /dev/null +++ b/packages/react-components/src/components/Popover/index.ts @@ -0,0 +1,2 @@ +export { default } from "./Popover"; +export type { PopoverProps } from "react-aria-components"; diff --git a/packages/react-components/src/components/index.ts b/packages/react-components/src/components/index.ts index db0dcff9..ffe67252 100644 --- a/packages/react-components/src/components/index.ts +++ b/packages/react-components/src/components/index.ts @@ -15,6 +15,7 @@ export { default as Form } from "./Form"; export { default as Header } from "./Header"; export { default as InlineAlert } from "./InlineAlert"; export { default as Modal } from "./Modal"; +export { default as Popover } from "./Popover"; export { default as Radio } from "./Radio"; export { default as RadioGroup } from "./RadioGroup"; export { default as Select } from "./Select"; From 8f55e4095e1d3e01cf63d00325de85367671fca1 Mon Sep 17 00:00:00 2001 From: Marcus Kernohan <135075821+mkernohanbc@users.noreply.github.com> Date: Wed, 4 Dec 2024 17:21:10 -0800 Subject: [PATCH 02/10] scaffolding popover docs and stories --- .../react-components/src/stories/Popover.mdx | 43 +++++++++++++ .../src/stories/Popover.stories.tsx | 63 +++++++++++++++++++ 2 files changed, 106 insertions(+) create mode 100644 packages/react-components/src/stories/Popover.mdx create mode 100644 packages/react-components/src/stories/Popover.stories.tsx diff --git a/packages/react-components/src/stories/Popover.mdx b/packages/react-components/src/stories/Popover.mdx new file mode 100644 index 00000000..390a6530 --- /dev/null +++ b/packages/react-components/src/stories/Popover.mdx @@ -0,0 +1,43 @@ +{/* Popover.mdx */} + +import { + Canvas, + Controls, + Meta, + Primary, + Source, + Story, + Subtitle, +} from "@storybook/blocks"; + +import * as PopoverStories from "./Popover.stories"; + + + +# Popover + + + A popover is a content container that overlays the interface. It may be shown + and hidden programmatically, or using a trigger element like a button. + + + + +## Usage and resources + +Learn more about working with the Popover component: + +- [Usage and best practice guidance]() +- [View the popover component in Figma](https://www2.gov.bc.ca/gov/content?id=8E36BE1D10E04A17B0CD4D913FA7AC43#designers) + +This component is based on [React Aria Popover](https://react-spectrum.adobe.com/react-aria/Popover.html). + +## Controls + + + + +## Configuration diff --git a/packages/react-components/src/stories/Popover.stories.tsx b/packages/react-components/src/stories/Popover.stories.tsx new file mode 100644 index 00000000..c6b34148 --- /dev/null +++ b/packages/react-components/src/stories/Popover.stories.tsx @@ -0,0 +1,63 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { Text } from "react-aria-components"; +import { Button, DialogTrigger, Popover } from "../components"; +import { PopoverProps } from "../components/Popover"; + +const meta = { + title: "Components/Popover", + component: Popover, + parameters: { layout: "centered" }, + argTypes: { + placement: { + control: { type: "radio" }, + options: ["top", "bottom", "left", "right"], + description: "Position of the popover relative to its anchor element", + }, + children: { + control: { type: "object" }, + description: "Populate the content of the popover", + }, + shouldFlip: { + control: { type: "boolean" }, + description: + "Controls whether a popover can flip its orientation if there's not enough space for it to render fully", + }, + offset: { + control: { type: "number" }, + description: + "Adjust offset along the main axis from the popvoer's anchor element", + }, + crossOffset: { + control: { type: "number" }, + description: + "Adjust offset along the cross-axis, relative to the popover's anchor element", + }, + containerPadding: { + control: { type: "number" }, + description: + "Apply additional padding between the popover and its surrounding container", + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const PopoverTemplate: Story = { + args: { + children: [ + + Pass some content or components as children to populate a popover's + content. + , + ], + placement: "bottom", + shouldFlip: true, + }, + render: ({ ...args }: PopoverProps) => ( + + + + + ), +}; From 3276fef39e3cf321ac9e51e4ac73b08ea7ed2a1b Mon Sep 17 00:00:00 2001 From: Marcus Kernohan <135075821+mkernohanbc@users.noreply.github.com> Date: Tue, 10 Dec 2024 13:48:13 -0800 Subject: [PATCH 03/10] refit Select to use Popover component --- .../src/components/Popover/Popover.css | 2 +- .../src/components/Select/Select.css | 12 ------------ .../src/components/Select/Select.tsx | 4 ++-- 3 files changed, 3 insertions(+), 15 deletions(-) diff --git a/packages/react-components/src/components/Popover/Popover.css b/packages/react-components/src/components/Popover/Popover.css index f0d4b854..d60d6849 100644 --- a/packages/react-components/src/components/Popover/Popover.css +++ b/packages/react-components/src/components/Popover/Popover.css @@ -5,7 +5,7 @@ border-radius: var(--layout-border-radius-medium); box-shadow: var(--surface-shadow-medium); box-sizing: border-box; - padding: var(--layout-padding-small) var(--layout-padding-small); + padding: var(--layout-padding-xsmall) var(--layout-padding-xsmall); width: var( --trigger-width ); /* Variable provided by Menu component: https://react-spectrum.adobe.com/react-aria/Menu.html#popover-1 */ diff --git a/packages/react-components/src/components/Select/Select.css b/packages/react-components/src/components/Select/Select.css index 582ab4dd..7da3f5f3 100644 --- a/packages/react-components/src/components/Select/Select.css +++ b/packages/react-components/src/components/Select/Select.css @@ -90,18 +90,6 @@ } /* Dropdown menu panel */ -.bcds-react-aria-Select--Popover { - background-color: var(--surface-color-forms-default); - border: 1px solid var(--surface-color-border-default); - border-radius: var(--layout-border-radius-medium); - box-shadow: var(--surface-shadow-medium); - box-sizing: border-box; - overflow-y: auto; - padding: var(--layout-padding-hair) var(--layout-padding-xsmall); - width: var( - --trigger-width - ); /* Variable provided by Select component https://react-spectrum.adobe.com/react-aria/Select.html#popover-1 */ -} .bcds-react-aria-Select--ListBox[data-focused] { outline: none; } diff --git a/packages/react-components/src/components/Select/Select.tsx b/packages/react-components/src/components/Select/Select.tsx index 398569bd..977478c7 100644 --- a/packages/react-components/src/components/Select/Select.tsx +++ b/packages/react-components/src/components/Select/Select.tsx @@ -9,7 +9,6 @@ import { ListBoxItem, ListBoxItemProps as ReactAriaListBoxItemProps, ListBoxSection, - Popover, Select as ReactAriaSelect, SelectProps as ReactAriaSelectProps, SelectValue, @@ -17,6 +16,7 @@ import { ValidationResult, } from "react-aria-components"; +import Popover from "../Popover"; import SvgExclamationIcon from "../Icons/SvgExclamationIcon"; import SvgChevronUpIcon from "../Icons/SvgChevronUpIcon"; import SvgChevronDownIcon from "../Icons/SvgChevronDownIcon"; @@ -108,7 +108,7 @@ export default function Select({ {errorMessage} - + Date: Thu, 12 Dec 2024 14:18:31 -0800 Subject: [PATCH 04/10] port WIP from menu branch --- .../SvgChevronRightIcon.tsx | 18 +++++++ .../Icons/SvgChevronRightIcon/index.ts | 1 + .../src/components/Menu/Menu.css | 28 ++++++++++ .../src/components/Menu/Menu.tsx | 26 ++++++++++ .../src/components/Menu/index.ts | 8 +++ .../src/components/MenuItem/MenuItem.css | 52 +++++++++++++++++++ .../src/components/MenuItem/MenuItem.tsx | 31 +++++++++++ .../src/components/MenuItem/index.ts | 2 + .../react-components/src/components/index.ts | 9 ++++ 9 files changed, 175 insertions(+) create mode 100644 packages/react-components/src/components/Icons/SvgChevronRightIcon/SvgChevronRightIcon.tsx create mode 100644 packages/react-components/src/components/Icons/SvgChevronRightIcon/index.ts create mode 100644 packages/react-components/src/components/Menu/Menu.css create mode 100644 packages/react-components/src/components/Menu/Menu.tsx create mode 100644 packages/react-components/src/components/Menu/index.ts create mode 100644 packages/react-components/src/components/MenuItem/MenuItem.css create mode 100644 packages/react-components/src/components/MenuItem/MenuItem.tsx create mode 100644 packages/react-components/src/components/MenuItem/index.ts diff --git a/packages/react-components/src/components/Icons/SvgChevronRightIcon/SvgChevronRightIcon.tsx b/packages/react-components/src/components/Icons/SvgChevronRightIcon/SvgChevronRightIcon.tsx new file mode 100644 index 00000000..cf0f402c --- /dev/null +++ b/packages/react-components/src/components/Icons/SvgChevronRightIcon/SvgChevronRightIcon.tsx @@ -0,0 +1,18 @@ +/* The component implements the Chevron Right icon from Font Awesome: https://fontawesome.com/icons/chevron-right */ +export default function SvgChevronRightIcon({ id = "chevron-right-icon" }) { + return ( + + + + ); +} diff --git a/packages/react-components/src/components/Icons/SvgChevronRightIcon/index.ts b/packages/react-components/src/components/Icons/SvgChevronRightIcon/index.ts new file mode 100644 index 00000000..9fd6d5a2 --- /dev/null +++ b/packages/react-components/src/components/Icons/SvgChevronRightIcon/index.ts @@ -0,0 +1 @@ +export { default } from "./SvgChevronRightIcon"; diff --git a/packages/react-components/src/components/Menu/Menu.css b/packages/react-components/src/components/Menu/Menu.css new file mode 100644 index 00000000..84bdf52e --- /dev/null +++ b/packages/react-components/src/components/Menu/Menu.css @@ -0,0 +1,28 @@ +.bcds-react-aria-Menu { + display: flex; + flex-direction: column; + gap: var(--layout-margin-small); +} + +.bcds-react-aria-Popover { + background-color: var(--surface-color-forms-default); + border: 1px solid var(--surface-color-border-default); + border-radius: var(--layout-border-radius-medium); + box-shadow: var(--surface-shadow-medium); + box-sizing: border-box; + padding: var(--layout-padding-small) var(--layout-padding-small); + width: var( + --trigger-width + ); /* Variable provided by Menu component: https://react-spectrum.adobe.com/react-aria/Menu.html#popover-1 */ +} + +/* Sections */ +.bcds-react-aria-Menu .react-aria-Header { + font: var(--typography-bold-small-body); +} + +.bcds-react-aria-Menu .react-aria-MenuSection { + display: flex; + flex-direction: column; + gap: var(--layout-margin-small); +} diff --git a/packages/react-components/src/components/Menu/Menu.tsx b/packages/react-components/src/components/Menu/Menu.tsx new file mode 100644 index 00000000..c464fd6e --- /dev/null +++ b/packages/react-components/src/components/Menu/Menu.tsx @@ -0,0 +1,26 @@ +import { + MenuTrigger, + Menu as ReactAriaMenu, + MenuProps as ReactAriaMenuProps, + MenuSection, + Header as MenuSectionHeader, + SubmenuTrigger, +} from "react-aria-components"; + +import Popover from "../Popover"; +import "./Menu.css"; + +export default function Menu({ + children, + ...props +}: ReactAriaMenuProps) { + return ( + + + {children} + + + ); +} + +export { MenuTrigger, SubmenuTrigger, MenuSection, MenuSectionHeader }; diff --git a/packages/react-components/src/components/Menu/index.ts b/packages/react-components/src/components/Menu/index.ts new file mode 100644 index 00000000..1564e0de --- /dev/null +++ b/packages/react-components/src/components/Menu/index.ts @@ -0,0 +1,8 @@ +export { + default, + MenuTrigger, + SubmenuTrigger, + MenuSection, + MenuSectionHeader, +} from "./Menu"; +export type { MenuProps } from "react-aria-components"; diff --git a/packages/react-components/src/components/MenuItem/MenuItem.css b/packages/react-components/src/components/MenuItem/MenuItem.css new file mode 100644 index 00000000..ed1b6bb1 --- /dev/null +++ b/packages/react-components/src/components/MenuItem/MenuItem.css @@ -0,0 +1,52 @@ +.bcds-react-aria-Menu-Item { + color: var(--typography-color-secondary); + display: inline-flex; + gap: var(--layout-margin-xsmall); + cursor: pointer; +} + +/* Link styling */ +a.bcds-react-aria-Menu-Item { + color: var(--typography-color-link); + text-decoration: underline; + text-underline-offset: var(--layout-padding-hair); +} + +a.bcds-react-aria-Menu-Item[data-hovered] { + color: var(--surface-color-border-active); +} + +/* Sizing */ +.bcds-react-aria-Menu-Item.small { + font: var(--typography-regular-small-body); +} + +.bcds-react-aria-Menu-Item.medium { + font: var(--typography-regular-body); +} + +/* Icon displayed when an item has a submenu */ +.bcds-react-aria-Menu-Item > svg { + width: var(--icons-size-xsmall); + height: var(--icons-size-xsmall); + padding: var(--layout-padding-hair); + align-self: center; +} + +/* States */ + +.bcds-react-aria-Menu-Item[data-focus-visible] { + outline: solid var(--layout-border-width-medium) + var(--surface-color-border-active); + outline-offset: var(--layout-margin-xsmall); + border-radius: var(--layout-border-radius-small); +} + +.bcds-react-aria-Menu-Item[data-hovered] { + color: var(--surface-color-border-active); +} + +.bcds-react-aria-Menu-Item[data-disabled] { + color: var(--typography-color-disabled); + cursor: not-allowed; +} diff --git a/packages/react-components/src/components/MenuItem/MenuItem.tsx b/packages/react-components/src/components/MenuItem/MenuItem.tsx new file mode 100644 index 00000000..3595044f --- /dev/null +++ b/packages/react-components/src/components/MenuItem/MenuItem.tsx @@ -0,0 +1,31 @@ +import { + MenuItem as ReactAriaMenuItem, + MenuItemProps as ReactAriaMenuItemProps, +} from "react-aria-components"; + +import "./MenuItem.css"; +import SvgChevronRightIcon from "../Icons/SvgChevronRightIcon"; + +export interface MenuItemProps extends ReactAriaMenuItemProps { + size?: "small" | "medium"; +} + +export default function MenuItem({ size = "small", ...props }: MenuItemProps) { + const textValue = + props.textValue || + (typeof props.children === "string" ? props.children : undefined); + return ( + + {({ hasSubmenu }) => ( + <> + {props.children} + {hasSubmenu && } + + )} + + ); +} diff --git a/packages/react-components/src/components/MenuItem/index.ts b/packages/react-components/src/components/MenuItem/index.ts new file mode 100644 index 00000000..434b3138 --- /dev/null +++ b/packages/react-components/src/components/MenuItem/index.ts @@ -0,0 +1,2 @@ +export { default } from "./MenuItem"; +export type { MenuItemProps } from "./MenuItem"; diff --git a/packages/react-components/src/components/index.ts b/packages/react-components/src/components/index.ts index ffe67252..8dcdf6b2 100644 --- a/packages/react-components/src/components/index.ts +++ b/packages/react-components/src/components/index.ts @@ -14,6 +14,14 @@ export { default as Footer, FooterLinks } from "./Footer"; export { default as Form } from "./Form"; export { default as Header } from "./Header"; export { default as InlineAlert } from "./InlineAlert"; +export { + default as Menu, + MenuTrigger, + SubmenuTrigger, + MenuSection, + MenuSectionHeader, +} from "./Menu"; +export { default as MenuItem } from "./MenuItem"; export { default as Modal } from "./Modal"; export { default as Popover } from "./Popover"; export { default as Radio } from "./Radio"; @@ -29,6 +37,7 @@ export { default as SvgExclamationIcon } from "./Icons/SvgExclamationIcon"; export { default as SvgExclamationCircleIcon } from "./Icons/SvgExclamationCircleIcon"; export { default as SvgChevronUpIcon } from "./Icons/SvgChevronUpIcon"; export { default as SvgChevronDownIcon } from "./Icons/SvgChevronDownIcon"; +export { default as SvgChevronRightIcon } from "./Icons/SvgChevronRightIcon"; export { default as SvgCloseIcon } from "./Icons/SvgCloseIcon"; export { default as SvgInfoIcon } from "./Icons/SvgInfoIcon"; export { default as SvgTooltipArrowUp } from "./Icons/SvgTooltipArrowUp"; From 2169c5fc4b1eed8ebfcaae1565fc904b8de1aade Mon Sep 17 00:00:00 2001 From: Marcus Kernohan <135075821+mkernohanbc@users.noreply.github.com> Date: Fri, 13 Dec 2024 11:01:44 -0800 Subject: [PATCH 05/10] remove Popover from Storybook --- .../react-components/src/stories/Popover.mdx | 43 ------------- .../src/stories/Popover.stories.tsx | 63 ------------------- 2 files changed, 106 deletions(-) delete mode 100644 packages/react-components/src/stories/Popover.mdx delete mode 100644 packages/react-components/src/stories/Popover.stories.tsx diff --git a/packages/react-components/src/stories/Popover.mdx b/packages/react-components/src/stories/Popover.mdx deleted file mode 100644 index 390a6530..00000000 --- a/packages/react-components/src/stories/Popover.mdx +++ /dev/null @@ -1,43 +0,0 @@ -{/* Popover.mdx */} - -import { - Canvas, - Controls, - Meta, - Primary, - Source, - Story, - Subtitle, -} from "@storybook/blocks"; - -import * as PopoverStories from "./Popover.stories"; - - - -# Popover - - - A popover is a content container that overlays the interface. It may be shown - and hidden programmatically, or using a trigger element like a button. - - - - -## Usage and resources - -Learn more about working with the Popover component: - -- [Usage and best practice guidance]() -- [View the popover component in Figma](https://www2.gov.bc.ca/gov/content?id=8E36BE1D10E04A17B0CD4D913FA7AC43#designers) - -This component is based on [React Aria Popover](https://react-spectrum.adobe.com/react-aria/Popover.html). - -## Controls - - - - -## Configuration diff --git a/packages/react-components/src/stories/Popover.stories.tsx b/packages/react-components/src/stories/Popover.stories.tsx deleted file mode 100644 index c6b34148..00000000 --- a/packages/react-components/src/stories/Popover.stories.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react"; -import { Text } from "react-aria-components"; -import { Button, DialogTrigger, Popover } from "../components"; -import { PopoverProps } from "../components/Popover"; - -const meta = { - title: "Components/Popover", - component: Popover, - parameters: { layout: "centered" }, - argTypes: { - placement: { - control: { type: "radio" }, - options: ["top", "bottom", "left", "right"], - description: "Position of the popover relative to its anchor element", - }, - children: { - control: { type: "object" }, - description: "Populate the content of the popover", - }, - shouldFlip: { - control: { type: "boolean" }, - description: - "Controls whether a popover can flip its orientation if there's not enough space for it to render fully", - }, - offset: { - control: { type: "number" }, - description: - "Adjust offset along the main axis from the popvoer's anchor element", - }, - crossOffset: { - control: { type: "number" }, - description: - "Adjust offset along the cross-axis, relative to the popover's anchor element", - }, - containerPadding: { - control: { type: "number" }, - description: - "Apply additional padding between the popover and its surrounding container", - }, - }, -} satisfies Meta; - -export default meta; -type Story = StoryObj; - -export const PopoverTemplate: Story = { - args: { - children: [ - - Pass some content or components as children to populate a popover's - content. - , - ], - placement: "bottom", - shouldFlip: true, - }, - render: ({ ...args }: PopoverProps) => ( - - - - - ), -}; From f6749eae278b3138e5b7068ab50ab37c7a9373a2 Mon Sep 17 00:00:00 2001 From: Marcus Kernohan <135075821+mkernohanbc@users.noreply.github.com> Date: Thu, 16 Jan 2025 11:36:42 -0800 Subject: [PATCH 06/10] adding docs and stories for Menu --- .../react-components/src/stories/Menu.mdx | 92 +++++++++++++++++++ .../src/stories/Menu.stories.tsx | 86 +++++++++++++++++ 2 files changed, 178 insertions(+) create mode 100644 packages/react-components/src/stories/Menu.mdx create mode 100644 packages/react-components/src/stories/Menu.stories.tsx diff --git a/packages/react-components/src/stories/Menu.mdx b/packages/react-components/src/stories/Menu.mdx new file mode 100644 index 00000000..41d5bc76 --- /dev/null +++ b/packages/react-components/src/stories/Menu.mdx @@ -0,0 +1,92 @@ +{/* Menu.mdx */} + +import { + Canvas, + Controls, + Meta, + Primary, + Source, + Story, + Subtitle, +} from "@storybook/blocks"; + +import * as MenuStories from "./Menu.stories"; + + + +# Menu + + + The menu component displays a collapsible list of items in a dropdown menu. + + + + +## Usage and resources + +Learn more about working with the select component: + +- [Usage and best practice guidance](https://www2.gov.bc.ca/gov/content?id=94129932F3384565A638034B69E0C943) +- [View the menu component in Figma](https://www2.gov.bc.ca/gov/content?id=8E36BE1D10E04A17B0CD4D913FA7AC43#designers) + +This component is based on [React Aria Menu](https://react-spectrum.adobe.com/react-aria/Menu.html). + +### Anatomy + +A menu is composed similarly to a [dialog](/docs/components-dialogs--docs). You need: + +- A `` element +- A trigger element, positioned as the first child inside the `` +- The `` itself +- Some number of `` components + +Structurally, these components are assembled like this: + +```javascript + + + + Item 1 + Item 2 + Item 3 + + +``` + +## Controls + + + + +## Configuration + +### Menu items + +### Dynamic items + +You can populate a menu dynamically using [React Aria's Collections API](https://react-spectrum.adobe.com/react-aria/collections.html). Pass an array or object containing your options using the `items` prop on your `Menu`: + + + +The example above is implemented like this: + + + + + {(item) => {item.name}} + + + +`} language="typescript"/> diff --git a/packages/react-components/src/stories/Menu.stories.tsx b/packages/react-components/src/stories/Menu.stories.tsx new file mode 100644 index 00000000..934e051b --- /dev/null +++ b/packages/react-components/src/stories/Menu.stories.tsx @@ -0,0 +1,86 @@ +import type { Meta, StoryObj } from "@storybook/react"; + +import { + Button, + Menu, + MenuItem, + MenuTrigger, + SubmenuTrigger, + SvgChevronDownIcon, +} from "../components"; +import { MenuProps } from "../components/Menu"; + +const meta = { + title: "Components/Menu/Menu", + component: Menu, + parameters: { layout: "centered" }, + argTypes: { + children: { + control: { type: "object" }, + description: "Expects an array of `MenuItem` components", + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +const MenuItems = [ + { id: 1, name: "Item 1" }, + { id: 2, name: "Item 2" }, + { id: 3, name: "Item 3" }, + { id: 4, name: "Item 4" }, +]; + +export const MenuTemplate: Story = { + args: { + children: [Item 1], + }, + render: ({ ...args }: MenuProps) => ( + + + + + ), +}; + +export const MenuWithSubmenu: Story = { + args: { + children: [ + Item 1, + Item 2, + [ + + Submenu + + Submenu item 1 + Submenu item 2 + + , + ], + ], + }, + render: ({ ...args }: MenuProps) => ( + + + + + ), +}; + +export const MenuWithDynamicItems: Story = { + ...MenuTemplate.args, + args: { items: MenuItems }, + render: () => ( + + + + {(item) => {item.name}} + + + ), +}; From f50658502623dbe716d402e41d9c0f47c9435044 Mon Sep 17 00:00:00 2001 From: Marcus Kernohan <135075821+mkernohanbc@users.noreply.github.com> Date: Thu, 16 Jan 2025 14:16:43 -0800 Subject: [PATCH 07/10] WIP on Menu stories --- .../react-components/src/stories/Menu.mdx | 8 +- .../src/stories/MenuItem.stories.tsx | 84 +++++++++++++++++++ 2 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 packages/react-components/src/stories/MenuItem.stories.tsx diff --git a/packages/react-components/src/stories/Menu.mdx b/packages/react-components/src/stories/Menu.mdx index 41d5bc76..8f1feaba 100644 --- a/packages/react-components/src/stories/Menu.mdx +++ b/packages/react-components/src/stories/Menu.mdx @@ -11,6 +11,7 @@ import { } from "@storybook/blocks"; import * as MenuStories from "./Menu.stories"; +import * as MenuItemStories from "./MenuItem.stories"; @@ -65,7 +66,12 @@ Structurally, these components are assembled like this: ### Menu items -### Dynamic items +The `MenuItem` subcomponent is used inside `Menu` to compose a static list of options: + + + + +#### Dynamic lists You can populate a menu dynamically using [React Aria's Collections API](https://react-spectrum.adobe.com/react-aria/collections.html). Pass an array or object containing your options using the `items` prop on your `Menu`: diff --git a/packages/react-components/src/stories/MenuItem.stories.tsx b/packages/react-components/src/stories/MenuItem.stories.tsx new file mode 100644 index 00000000..5938684b --- /dev/null +++ b/packages/react-components/src/stories/MenuItem.stories.tsx @@ -0,0 +1,84 @@ +import type { Meta, StoryObj } from "@storybook/react"; + +import { + Button, + Menu, + MenuItem, + MenuTrigger, + SvgChevronDownIcon, +} from "../components"; + +import { MenuItemProps } from "../components/MenuItem"; + +const meta = { + title: "Components/Menu/MenuItem", + component: MenuItem, + parameters: { layout: "centered" }, + argTypes: { + children: { + control: { type: "object" }, + description: "Populates menu item label", + }, + size: { + control: { type: "radio" }, + options: ["small", "medium"], + description: "Sets the label text size", + }, + id: { + control: { type: "text" }, + description: "Unique identifier for the menu item (required)", + }, + href: { + control: { type: "text" }, + description: "A URL to link to", + }, + hrefLang: { + control: { type: "text" }, + description: "Hints at the human language of the linked URL", + }, + rel: { + control: { type: "text" }, + description: + "The relationship between the linked resource and the current page", + }, + target: { + control: { type: "text" }, + description: "The target window for the link", + }, + value: { + control: { type: "object" }, + description: "The object value that this item represents", + }, + textValue: { + control: { type: "text" }, + description: + " A string representation of the item's contents, used for features like typeahead", + }, + isDisabled: { + control: { type: "boolean" }, + description: "Whether the menu item is disabled", + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const MenuItemTemplate: Story = { + args: { + size: "medium", + children: ["Menu item"], + id: "1", + isDisabled: false, + }, + render: ({ ...args }: MenuItemProps) => ( + + + + + + + ), +}; From ece84e1d6f34837f7cd1153e89ee43af6943056a Mon Sep 17 00:00:00 2001 From: Marcus Kernohan <135075821+mkernohanbc@users.noreply.github.com> Date: Thu, 16 Jan 2025 14:54:05 -0800 Subject: [PATCH 08/10] fixing type errors in MenuItem --- .../src/components/MenuItem/MenuItem.tsx | 26 ++++++++++++++----- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/packages/react-components/src/components/MenuItem/MenuItem.tsx b/packages/react-components/src/components/MenuItem/MenuItem.tsx index 3595044f..8321549f 100644 --- a/packages/react-components/src/components/MenuItem/MenuItem.tsx +++ b/packages/react-components/src/components/MenuItem/MenuItem.tsx @@ -1,31 +1,43 @@ import { MenuItem as ReactAriaMenuItem, MenuItemProps as ReactAriaMenuItemProps, + MenuItemRenderProps, } from "react-aria-components"; - +import { ReactNode } from "react"; import "./MenuItem.css"; import SvgChevronRightIcon from "../Icons/SvgChevronRightIcon"; export interface MenuItemProps extends ReactAriaMenuItemProps { size?: "small" | "medium"; + children?: + | ReactNode + | (( + props: MenuItemRenderProps & { defaultChildren: ReactNode } + ) => ReactNode); } export default function MenuItem({ size = "small", ...props }: MenuItemProps) { const textValue = props.textValue || (typeof props.children === "string" ? props.children : undefined); + return ( - {({ hasSubmenu }) => ( - <> - {props.children} - {hasSubmenu && } - - )} + {(renderProps: MenuItemRenderProps & { defaultChildren: ReactNode }) => { + if (typeof props.children === "function") { + return props.children(renderProps); + } + return ( + <> + {props.children} + {renderProps.hasSubmenu && } + + ); + }} ); } From d5aa8d71e9ad79a5b04d9d2e71afce1e25355deb Mon Sep 17 00:00:00 2001 From: Marcus Kernohan <135075821+mkernohanbc@users.noreply.github.com> Date: Thu, 16 Jan 2025 16:13:44 -0800 Subject: [PATCH 09/10] Menu docs and stories --- .../react-components/src/stories/Menu.mdx | 64 ++++++++++++++++++- .../src/stories/Menu.stories.tsx | 30 ++++++++- .../src/stories/MenuItem.stories.tsx | 22 +++++++ 3 files changed, 112 insertions(+), 4 deletions(-) diff --git a/packages/react-components/src/stories/Menu.mdx b/packages/react-components/src/stories/Menu.mdx index 8f1feaba..da7f0940 100644 --- a/packages/react-components/src/stories/Menu.mdx +++ b/packages/react-components/src/stories/Menu.mdx @@ -13,7 +13,7 @@ import { import * as MenuStories from "./Menu.stories"; import * as MenuItemStories from "./MenuItem.stories"; - + # Menu @@ -96,3 +96,65 @@ const MenuItems = [ `} language="typescript"/> + +#### Disabled menu items + +Pass the `isDisabled` prop to a `MenuItem` to disable it: + + + +### Menu sections + +You can use the `MenuSection` and `MenuSectionHeader` subcomponents to create sections within a menu: + + + +The example above is implemented like this: + + +