From b6044ac21f20c3811376d564524a251d273ff236 Mon Sep 17 00:00:00 2001 From: Pawan Kumar Date: Tue, 14 May 2024 11:12:32 +0530 Subject: [PATCH 1/6] fix: refactor menu component --- app/client/package.json | 1 + .../design-system/headless/package.json | 2 +- .../src/components/Field/src/Field.tsx | 29 +- .../headless/src/components/Menu/src/Menu.tsx | 66 - .../src/components/Menu/src/MenuItem.tsx | 26 - .../src/components/Menu/src/MenuList.tsx | 27 - .../headless/src/components/Menu/src/index.ts | 4 - .../headless/src/components/Menu/src/types.ts | 25 - .../components/Menu/stories/Menu.stories.tsx | 76 - .../design-system/headless/src/index.ts | 1 - .../design-system/widgets/package.json | 3 +- .../ActionGroup/src/ActionGroup.tsx | 78 +- ...ionGroupItem.tsx => ActionGroupButton.tsx} | 11 +- .../ActionGroup/src/icons/MoreIcon.tsx | 10 - .../src/components/ActionGroup/src/index.tsx | 2 +- .../ActionGroup/src/styles.module.css | 86 +- .../src/components/ActionGroup/src/types.ts | 41 +- .../ActionGroup/src/useActionGroup.ts | 16 +- .../stories/ActionGroup.stories.tsx | 209 +- .../ButtonGroup/src/ButtonGroup.tsx | 113 +- ...tonGroupItem.tsx => ButtonGroupButton.tsx} | 8 +- .../src/components/ButtonGroup/src/index.ts | 4 +- .../ButtonGroup/src/styles.module.css | 5 +- .../src/components/ButtonGroup/src/types.ts | 21 +- .../stories/ButtonGroup.stories.tsx | 120 +- .../widgets/src/components/Icon/src/Icon.tsx | 4 +- .../widgets/src/components/Menu/src/Item.tsx | 37 - .../widgets/src/components/Menu/src/Menu.tsx | 39 +- .../src/components/Menu/src/MenuItem.tsx | 34 + .../src/components/Menu/src/MenuList.tsx | 19 - .../widgets/src/components/Menu/src/index.ts | 4 +- .../src/components/Menu/src/styles.module.css | 92 +- .../widgets/src/components/Menu/src/types.ts | 22 + .../components/Menu/stories/Menu.stories.tsx | 61 +- .../Modal/stories/ModalExamples.tsx | 12 +- .../src/components/Popover}/index.ts | 0 .../src/components/Popover/src/Popover.tsx | 14 + .../src/components/Popover/src/index.ts | 1 + .../components/Popover/src/styles.module.css | 7 + .../src/components/Select/src/ListBoxItem.tsx | 15 + .../src/components/Select/src/Select.tsx | 22 +- .../components/Select/src/styles.module.css | 79 +- .../src/components/Select/src/types.ts | 3 +- .../TextInput/stories/TextInput.stories.tsx | 28 +- .../design-system/widgets/src/index.ts | 1 + .../widgets/src/styles/src/index.ts | 1 + .../src/styles/src/list-item.module.css | 72 + .../widgets/src/testing/ComplexForm.tsx | 15 +- .../propertyControls/ButtonListControl.tsx | 1 + .../ToolbarButtonListControl.tsx | 4 +- .../component/index.tsx | 25 +- .../WDSInlineButtonsWidget/component/types.ts | 18 +- .../config/defaultsConfig.ts | 13 +- .../propertyPaneConfig/contentConfig.ts | 10 +- .../WDSInlineButtonsWidget/widget/index.tsx | 5 - .../WDSInlineButtonsWidget/widget/types.ts | 7 +- .../wds/WDSMenuButtonWidget/widget/index.tsx | 41 +- .../component/cellComponents/HeaderCell.tsx | 33 +- .../component/index.tsx | 20 +- .../component/types.ts | 6 +- .../config/defaultsConfig.ts | 21 +- .../propertyPaneConfig/contentConfig.ts | 5 +- .../config/propertyPaneConfig/styleConfig.ts | 6 +- .../WDSToolbarButtonsWidget/widget/index.tsx | 11 +- .../WDSToolbarButtonsWidget/widget/types.ts | 7 +- app/client/yarn.lock | 1909 +++++++++-------- 66 files changed, 1604 insertions(+), 2104 deletions(-) delete mode 100644 app/client/packages/design-system/headless/src/components/Menu/src/Menu.tsx delete mode 100644 app/client/packages/design-system/headless/src/components/Menu/src/MenuItem.tsx delete mode 100644 app/client/packages/design-system/headless/src/components/Menu/src/MenuList.tsx delete mode 100644 app/client/packages/design-system/headless/src/components/Menu/src/index.ts delete mode 100644 app/client/packages/design-system/headless/src/components/Menu/src/types.ts delete mode 100644 app/client/packages/design-system/headless/src/components/Menu/stories/Menu.stories.tsx rename app/client/packages/design-system/widgets/src/components/ActionGroup/src/{ActionGroupItem.tsx => ActionGroupButton.tsx} (62%) delete mode 100644 app/client/packages/design-system/widgets/src/components/ActionGroup/src/icons/MoreIcon.tsx rename app/client/packages/design-system/widgets/src/components/ButtonGroup/src/{ButtonGroupItem.tsx => ButtonGroupButton.tsx} (63%) delete mode 100644 app/client/packages/design-system/widgets/src/components/Menu/src/Item.tsx create mode 100644 app/client/packages/design-system/widgets/src/components/Menu/src/MenuItem.tsx delete mode 100644 app/client/packages/design-system/widgets/src/components/Menu/src/MenuList.tsx create mode 100644 app/client/packages/design-system/widgets/src/components/Menu/src/types.ts rename app/client/packages/design-system/{headless/src/components/Menu => widgets/src/components/Popover}/index.ts (100%) create mode 100644 app/client/packages/design-system/widgets/src/components/Popover/src/Popover.tsx create mode 100644 app/client/packages/design-system/widgets/src/components/Popover/src/index.ts create mode 100644 app/client/packages/design-system/widgets/src/components/Popover/src/styles.module.css create mode 100644 app/client/packages/design-system/widgets/src/components/Select/src/ListBoxItem.tsx create mode 100644 app/client/packages/design-system/widgets/src/styles/src/list-item.module.css diff --git a/app/client/package.json b/app/client/package.json index 931831ab3bd1..49d6eace1ff6 100644 --- a/app/client/package.json +++ b/app/client/package.json @@ -74,6 +74,7 @@ "@opentelemetry/sdk-trace-base": "^1.17.1", "@opentelemetry/sdk-trace-web": "^1.17.1", "@opentelemetry/semantic-conventions": "^1.17.1", + "@react-types/shared": "^3.23.0", "@sentry/react": "^6.2.4", "@sentry/tracing": "^6.2.4", "@shared/ast": "workspace:^", diff --git a/app/client/packages/design-system/headless/package.json b/app/client/packages/design-system/headless/package.json index bd30928ab9a5..2cb42ea96e76 100644 --- a/app/client/packages/design-system/headless/package.json +++ b/app/client/packages/design-system/headless/package.json @@ -30,7 +30,7 @@ "@react-types/checkbox": "^3.4.3", "@react-types/label": "^3.7.3", "@react-types/menu": "^3.9.5", - "@react-types/shared": "^3.22.0", + "@react-types/shared": "^3.23.1", "classnames": "*" }, "peerDependencies": { diff --git a/app/client/packages/design-system/headless/src/components/Field/src/Field.tsx b/app/client/packages/design-system/headless/src/components/Field/src/Field.tsx index 56a19598de53..0671befc430b 100644 --- a/app/client/packages/design-system/headless/src/components/Field/src/Field.tsx +++ b/app/client/packages/design-system/headless/src/components/Field/src/Field.tsx @@ -1,21 +1,36 @@ -import type { Ref } from "react"; +import type { ReactNode, Ref } from "react"; import React, { forwardRef } from "react"; import type { SpectrumFieldProps } from "@react-types/label"; import { Label } from "./Label"; import { HelpText } from "./HelpText"; -import type { StyleProps, ValidationState } from "@react-types/shared"; - -export type FieldProps = Omit< +export type FieldProps = Pick< SpectrumFieldProps, - "showErrorIcon" | "labelPosition" | "labelAlign" | keyof StyleProps + | "contextualHelp" + | "description" + | "descriptionProps" + | "elementType" + | "errorMessage" + | "errorMessageProps" + | "includeNecessityIndicatorInAccessibilityName" + | "isDisabled" + | "isRequired" + | "label" + | "labelProps" + | "necessityIndicator" + | "wrapperClassName" + | "wrapperProps" > & { fieldType?: "field" | "field-group"; labelClassName?: string; helpTextClassName?: string; validationState?: ValidationState; + children: ReactNode; + isReadOnly?: boolean; }; +import type { ValidationState } from "@react-types/shared"; + export type FieldRef = Ref; const _Field = (props: FieldProps, ref: FieldRef) => { @@ -75,7 +90,9 @@ const _Field = (props: FieldProps, ref: FieldRef) => { includeNecessityIndicatorInAccessibilityName } isRequired={isRequired} - necessityIndicator={!Boolean(isReadOnly) && necessityIndicator} + necessityIndicator={ + !Boolean(isReadOnly) ? necessityIndicator : undefined + } > {label} diff --git a/app/client/packages/design-system/headless/src/components/Menu/src/Menu.tsx b/app/client/packages/design-system/headless/src/components/Menu/src/Menu.tsx deleted file mode 100644 index f48b031749dc..000000000000 --- a/app/client/packages/design-system/headless/src/components/Menu/src/Menu.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import React, { cloneElement, useRef, Children } from "react"; -import { useMenuTriggerState } from "@react-stately/menu"; -import { useMenuTrigger } from "@react-aria/menu"; -import { Popover, PopoverTrigger, PopoverContent } from "../../Popover"; - -import type { ReactElement } from "react"; -import type { MenuTriggerProps } from "@react-stately/menu"; -import type { MenuProps } from "./types"; - -export const Menu = (props: MenuProps) => { - const { - children, - className, - defaultOpen, - isOpen, - offset, - onClose, - placement, - ...rest - } = props; - const [menuTrigger, menuList] = Children.toArray(children); - - const state = useMenuTriggerState({ - ...(menuList as MenuTriggerProps), - isOpen, - defaultOpen, - }); - const ref = useRef(null); - const { menuProps, menuTriggerProps } = useMenuTrigger( - {}, - { - ...state, - // Set focus on first item element - focusStrategy: "first", - }, - ref, - ); - - const handleOnClose = () => { - state.setOpen(false); - onClose ? onClose() : null; - }; - - return ( - state.setOpen(!state.isOpen)} - > - - {cloneElement(menuTrigger as ReactElement, { - ...menuTriggerProps, - ref, - })} - - - {cloneElement(menuList as ReactElement, { - ...rest, - ...menuProps, - onClose: handleOnClose, - })} - - - ); -}; diff --git a/app/client/packages/design-system/headless/src/components/Menu/src/MenuItem.tsx b/app/client/packages/design-system/headless/src/components/Menu/src/MenuItem.tsx deleted file mode 100644 index 5de624f1543a..000000000000 --- a/app/client/packages/design-system/headless/src/components/Menu/src/MenuItem.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import React from "react"; -import { useMenuItem } from "@react-aria/menu"; -import type { MenuItemProps } from "./types"; - -export const MenuItem = (props: MenuItemProps) => { - const { className, item, state } = props; - const ref = React.useRef(null); - const { isDisabled, isFocused, isPressed, isSelected, menuItemProps } = - useMenuItem({ key: item.key }, state, ref); - - return ( -
  • -
    {item.rendered}
    -
  • - ); -}; diff --git a/app/client/packages/design-system/headless/src/components/Menu/src/MenuList.tsx b/app/client/packages/design-system/headless/src/components/Menu/src/MenuList.tsx deleted file mode 100644 index 0aa7e4c6d354..000000000000 --- a/app/client/packages/design-system/headless/src/components/Menu/src/MenuList.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import React, { useRef } from "react"; -import { useMenu } from "@react-aria/menu"; -import { useTreeState } from "@react-stately/tree"; -import { MenuItem } from "./MenuItem"; -import type { MenuListProps } from "./types"; - -export const MenuList = (props: MenuListProps) => { - const { itemClassName, listClassName } = props; - const state = useTreeState(props); - const ref = useRef(null); - const { menuProps } = useMenu(props, state, ref); - - return ( -
      - {[...state.collection].map((item) => { - return ( - - ); - })} -
    - ); -}; diff --git a/app/client/packages/design-system/headless/src/components/Menu/src/index.ts b/app/client/packages/design-system/headless/src/components/Menu/src/index.ts deleted file mode 100644 index 8cce9a21fb2d..000000000000 --- a/app/client/packages/design-system/headless/src/components/Menu/src/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from "./Menu"; -export * from "./MenuList"; -export * from "./types"; -export { Item } from "@react-stately/collections"; diff --git a/app/client/packages/design-system/headless/src/components/Menu/src/types.ts b/app/client/packages/design-system/headless/src/components/Menu/src/types.ts deleted file mode 100644 index 6c9f947b7eea..000000000000 --- a/app/client/packages/design-system/headless/src/components/Menu/src/types.ts +++ /dev/null @@ -1,25 +0,0 @@ -import type { MenuTriggerProps } from "@react-stately/menu"; -import type { TreeState } from "@react-stately/tree"; -import type { AriaMenuProps } from "@react-types/menu"; -import type { Node } from "@react-types/shared"; -import type { ReactElement } from "react"; -import type { PopoverProps } from "../../Popover"; - -export interface MenuProps - extends AriaMenuProps, - MenuTriggerProps, - Pick { - children: ReactElement[]; - className?: string; -} - -export interface MenuItemProps { - item: Node; - state: TreeState; - className?: string; -} - -export interface MenuListProps extends AriaMenuProps { - listClassName?: string; - itemClassName?: string; -} diff --git a/app/client/packages/design-system/headless/src/components/Menu/stories/Menu.stories.tsx b/app/client/packages/design-system/headless/src/components/Menu/stories/Menu.stories.tsx deleted file mode 100644 index bd8efe574e15..000000000000 --- a/app/client/packages/design-system/headless/src/components/Menu/stories/Menu.stories.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import React from "react"; -import type { Meta, StoryObj } from "@storybook/react"; -import { Menu, MenuList, Item } from "@design-system/headless"; -import { Button } from "react-aria-components"; - -/** - * A menu displays a list of actions or options that a user can choose. - * - * Item props are not pulled up in the ArgsTable, the data can be found [here](https://react-spectrum.adobe.com/react-aria/Menu.html#item). - */ - -const meta: Meta = { - component: Menu, - title: "Design-system/headless/Menu", - subcomponents: { - //@ts-expect-error: don't need props to pass here - MenuList, - }, - render: (args) => ( - - - - Cut - Copy - Paste - - - ), -}; - -export default meta; -type Story = StoryObj; - -export const Main: Story = {}; - -/** - * The placement of the menu can be changed by passing the `placement` prop. - */ -export const Placement: Story = { - render: () => ( - <> - - - - Copy - Cut - Paste - - - - - - Copy - Cut - Paste - - - - - - Copy - Cut - Paste - - - - - - Copy - Cut - Paste - - - - ), -}; diff --git a/app/client/packages/design-system/headless/src/index.ts b/app/client/packages/design-system/headless/src/index.ts index 6566138ee1aa..39a3f8b2fd29 100644 --- a/app/client/packages/design-system/headless/src/index.ts +++ b/app/client/packages/design-system/headless/src/index.ts @@ -8,4 +8,3 @@ export * from "./components/Switch"; export * from "./components/TextInput"; export * from "./components/TextArea"; export * from "./components/Popover"; -export * from "./components/Menu"; diff --git a/app/client/packages/design-system/widgets/package.json b/app/client/packages/design-system/widgets/package.json index 6b998431e64f..39a1ac53b811 100644 --- a/app/client/packages/design-system/widgets/package.json +++ b/app/client/packages/design-system/widgets/package.json @@ -18,11 +18,12 @@ "@react-aria/utils": "^3.16.0", "@react-aria/visually-hidden": "^3.8.0", "@react-types/actiongroup": "^3.4.6", + "@react-types/shared": "^3.23.1", "@tabler/icons-react": "^2.45.0", "clsx": "^2.0.0", "colorjs.io": "^0.4.3", "lodash": "*", - "react-aria-components": "^1.1.1" + "react-aria-components": "^1.2.1" }, "devDependencies": { "@types/fs-extra": "^11.0.4", diff --git a/app/client/packages/design-system/widgets/src/components/ActionGroup/src/ActionGroup.tsx b/app/client/packages/design-system/widgets/src/components/ActionGroup/src/ActionGroup.tsx index c0675c325fd6..d1248c685518 100644 --- a/app/client/packages/design-system/widgets/src/components/ActionGroup/src/ActionGroup.tsx +++ b/app/client/packages/design-system/widgets/src/components/ActionGroup/src/ActionGroup.tsx @@ -1,18 +1,22 @@ import React, { forwardRef } from "react"; import { FocusScope } from "@react-aria/focus"; import { useDOMRef } from "@react-spectrum/utils"; -import type { DOMRef } from "@react-types/shared"; +import { Button, Menu } from "@design-system/widgets"; import { useListState } from "@react-stately/list"; - -import styles from "./styles.module.css"; -import type { ActionGroupProps } from "./types"; -import { IconButton } from "../../IconButton"; +import { MenuTrigger } from "react-aria-components"; +import { ActionGroupButton } from "./ActionGroupButton"; import { useActionGroup } from "./useActionGroup"; -import { Item, Menu, MenuList } from "../../Menu"; -import { ActionGroupItem } from "./ActionGroupItem"; +import styles from "./styles.module.css"; +import { Item } from "@react-stately/collections"; +import type { ActionGroupItem, ActionGroupProps } from "./types"; +import type { DOMRef, CollectionChildren } from "@react-types/shared"; -const _ActionGroup = ( - props: ActionGroupProps, +interface ActionGroupInnerProps extends ActionGroupProps { + children?: CollectionChildren; +} + +const _ActionGroupInner = ( + props: ActionGroupInnerProps, ref: DOMRef, ) => { const { @@ -21,7 +25,6 @@ const _ActionGroup = ( density = "regular", isDisabled, onAction, - orientation = "horizontal", overflowMode = "collapse", size = "medium", variant = "filled", @@ -36,7 +39,7 @@ const _ActionGroup = ( ); let children = [...state.collection]; - const menuChildren = children.slice(visibleItems); + const menuChildren = (props.items as ActionGroupItem[]).slice(visibleItems); children = children.slice(0, visibleItems); return ( @@ -45,7 +48,6 @@ const _ActionGroup = ( className={styles.actionGroup} data-alignment={alignment} data-density={Boolean(density) ? density : undefined} - data-orientation={orientation} data-overflow={overflowMode} ref={domRef} {...actionGroupProps} @@ -53,11 +55,11 @@ const _ActionGroup = ( > {children.map((item) => { if (Boolean(item.props.isSeparator)) { - return
    ; + return
    ; } return ( - ( ); })} {menuChildren?.length > 0 && ( - item.props.isSeparator) - .map((item) => item.key), - ]} - onAction={onAction} - > - + + + )}
    ); }; +const ActionGroupInner = forwardRef(_ActionGroupInner); + +const _ActionGroup = ( + props: Omit, "children">, + ref: DOMRef, +) => { + const { items, ...rest } = props; + + return ( + + {(item) => {item.label}} + + ); +}; + export const ActionGroup = forwardRef(_ActionGroup); diff --git a/app/client/packages/design-system/widgets/src/components/ActionGroup/src/ActionGroupItem.tsx b/app/client/packages/design-system/widgets/src/components/ActionGroup/src/ActionGroupButton.tsx similarity index 62% rename from app/client/packages/design-system/widgets/src/components/ActionGroup/src/ActionGroupItem.tsx rename to app/client/packages/design-system/widgets/src/components/ActionGroup/src/ActionGroupButton.tsx index 939e28977901..4e8622b9ec86 100644 --- a/app/client/packages/design-system/widgets/src/components/ActionGroup/src/ActionGroupItem.tsx +++ b/app/client/packages/design-system/widgets/src/components/ActionGroup/src/ActionGroupButton.tsx @@ -1,11 +1,10 @@ -import type { ForwardedRef } from "react"; import React, { forwardRef } from "react"; import { Button } from "@design-system/widgets"; +import type { ForwardedRef } from "react"; +import type { ActionGroupButtonProps } from "@design-system/widgets"; -import type { ButtonGroupItemProps } from "../../../"; - -const _ActionGroupItem = ( - props: ButtonGroupItemProps, +const _ActionGroupButton = ( + props: ActionGroupButtonProps, ref: ForwardedRef, ) => { const { color, item, variant, ...rest } = props; @@ -17,4 +16,4 @@ const _ActionGroupItem = ( ); }; -export const ActionGroupItem = forwardRef(_ActionGroupItem); +export const ActionGroupButton = forwardRef(_ActionGroupButton); diff --git a/app/client/packages/design-system/widgets/src/components/ActionGroup/src/icons/MoreIcon.tsx b/app/client/packages/design-system/widgets/src/components/ActionGroup/src/icons/MoreIcon.tsx deleted file mode 100644 index e9d55859a2b9..000000000000 --- a/app/client/packages/design-system/widgets/src/components/ActionGroup/src/icons/MoreIcon.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import React from "react"; -import type { ComponentProps } from "react"; - -export function MoreIcon(props: ComponentProps<"svg">) { - return ( - - - - ); -} diff --git a/app/client/packages/design-system/widgets/src/components/ActionGroup/src/index.tsx b/app/client/packages/design-system/widgets/src/components/ActionGroup/src/index.tsx index 07a7f9e2162d..96d633c8fefe 100644 --- a/app/client/packages/design-system/widgets/src/components/ActionGroup/src/index.tsx +++ b/app/client/packages/design-system/widgets/src/components/ActionGroup/src/index.tsx @@ -1,3 +1,3 @@ export * from "./types"; export { ActionGroup } from "./ActionGroup"; -export { ActionGroupItem } from "./ActionGroupItem"; +export { ActionGroupButton } from "./ActionGroupButton"; diff --git a/app/client/packages/design-system/widgets/src/components/ActionGroup/src/styles.module.css b/app/client/packages/design-system/widgets/src/components/ActionGroup/src/styles.module.css index a1c29b16f063..2d6e3c02e6a9 100644 --- a/app/client/packages/design-system/widgets/src/components/ActionGroup/src/styles.module.css +++ b/app/client/packages/design-system/widgets/src/components/ActionGroup/src/styles.module.css @@ -5,8 +5,13 @@ width: 100%; & :is([data-separator]) { - inline-size: var(--sizing-5); - block-size: var(--sizing-5); + min-inline-size: var(--sizing-5); + min-block-size: var(--sizing-5); + } + + & [data-separator]:is(:first-child, :last-child), + &:has([data-action-group-menu]) [data-separator]:nth-last-child(2) { + display: none; } [data-button]:not(:last-of-type) { @@ -16,16 +21,8 @@ min-inline-size: fit-content !important; } - &:has([data-icon-button]) [data-button]:nth-last-child(2) { - min-inline-size: var(--sizing-14) !important; - } - - &[data-orientation="vertical"] { - flex-direction: column; - } - - &[data-orientation="vertical"] :is([data-button]) { - max-inline-size: none; + &:has([data-action-group-menu]) [data-button]:nth-last-child(2) { + min-inline-size: var(--sizing-18) !important; } &[data-alignment="start"] { @@ -62,77 +59,20 @@ border-radius: 0; } - /** - * ---------------------------------------------------------------------------- - * Horizontal orientation - *----------------------------------------------------------------------------- - */ - &[data-orientation="horizontal"] { - width: 100%; - } - - &[data-orientation="horizontal"] [data-button]:not(:last-of-type) { + & [data-button]:not(:last-of-type) { border-right-width: var(--border-width-1); } - &[data-orientation="horizontal"] [data-button]:first-child { + & [data-button]:first-child { border-top-right-radius: 0; } - &[data-orientation="horizontal"] [data-button]:last-of-type { + & [data-button]:last-of-type { border-bottom-left-radius: 0; } - &[data-orientation="horizontal"] [data-variant="outlined"] { + & [data-variant="outlined"] { margin-right: calc(-1 * var(--border-width-1)); } - - /** - * ---------------------------------------------------------------------------- - * Vertical orientation - *----------------------------------------------------------------------------- - */ - &[data-orientation="vertical"] [data-button]:not(:last-of-type) { - border-bottom-width: var(--border-width-1); - } - - &[data-orientation="vertical"] [data-button]:first-child { - border-bottom-left-radius: 0; - } - - &[data-orientation="vertical"] [data-button]:last-of-type { - border-top-right-radius: 0; - } - - &[data-orientation="vertical"] [data-variant="outlined"] { - margin-bottom: calc(-1 * var(--border-width-1)); - } - - /** - * ---------------------------------------------------------------------------- - * Filled variant - *----------------------------------------------------------------------------- - */ - & [data-variant="filled"] { - border-width: 0; - } - - @each $color in colors { - & [data-variant="filled"][data-color="$(color)"] { - border-color: var(--color-bd-on-$(color)); - } - } - - /** - * ---------------------------------------------------------------------------- - * Outlined variant - *----------------------------------------------------------------------------- - */ - - @each $color in colors { - & [data-variant="outlined"][data-color="$(color)"] { - border-color: var(--color-bd-$(color)); - } - } } } diff --git a/app/client/packages/design-system/widgets/src/components/ActionGroup/src/types.ts b/app/client/packages/design-system/widgets/src/components/ActionGroup/src/types.ts index 2a8b6631e783..83f8257c73d4 100644 --- a/app/client/packages/design-system/widgets/src/components/ActionGroup/src/types.ts +++ b/app/client/packages/design-system/widgets/src/components/ActionGroup/src/types.ts @@ -1,10 +1,47 @@ -import type { ButtonGroupProps } from "../../ButtonGroup"; +import type { ListState } from "@react-stately/list"; +import type { SpectrumActionGroupProps } from "@react-types/actiongroup"; +import type { Node } from "@react-types/shared"; +import type { StyleProps } from "@react-types/shared"; +import type { ButtonProps } from "../../Button"; +import type { SIZES } from "../../../shared"; +import type { IconProps } from "../../Icon"; export const ACTION_GROUP_ALIGNMENTS = { start: "Start", end: "End", } as const; -export interface ActionGroupProps extends ButtonGroupProps { +export interface ActionGroupProps + extends Omit< + SpectrumActionGroupProps, + | "staticColor" + | "isQuiet" + | "isJustified" + | "isEmphasized" + | "buttonLabelBehavior" + | "summaryIcon" + | "orientation" + | "selectionMode" + | "defaultSelectedKeys" + | "disallowEmptySelection" + | "onSelectionChange" + | "selectedKeys" + | "children" + | keyof StyleProps + >, + Pick { + size?: Omit; alignment?: keyof typeof ACTION_GROUP_ALIGNMENTS; } + +export interface ActionGroupItem { + id: string | number; + label?: string; + icon?: IconProps["name"]; + isSeparator?: boolean; +} + +export interface ActionGroupButtonProps extends ButtonProps { + state: ListState; + item: Node; +} diff --git a/app/client/packages/design-system/widgets/src/components/ActionGroup/src/useActionGroup.ts b/app/client/packages/design-system/widgets/src/components/ActionGroup/src/useActionGroup.ts index 6c6d79118d13..0cde155a5f11 100644 --- a/app/client/packages/design-system/widgets/src/components/ActionGroup/src/useActionGroup.ts +++ b/app/client/packages/design-system/widgets/src/components/ActionGroup/src/useActionGroup.ts @@ -9,6 +9,7 @@ import { useResizeObserver, useValueEffect, } from "@react-aria/utils"; +import type { ActionGroupProps } from "./types"; export interface ActionGroupAria { actionGroupProps: DOMAttributes; @@ -17,11 +18,11 @@ export interface ActionGroupAria { } export function useActionGroup( - props: ButtonGroupProps, + props: ActionGroupProps, state: ListState, ref: RefObject, ): ActionGroupAria { - const { orientation, overflowMode = "collapse" } = props; + const { overflowMode = "collapse" } = props; const focusManager = createFocusManager(ref); const onKeyDown = (e: React.KeyboardEvent) => { @@ -58,11 +59,6 @@ export function useActionGroup( return; } - if (orientation === "vertical") { - // Collapsing vertical action groups with selection is currently unsupported. - return; - } - const computeVisibleItems = (visibleItems: number) => { if (ref.current) { const listItems = Array.from(ref.current.children) as HTMLLIElement[]; @@ -140,7 +136,7 @@ export function useActionGroup( }; } }); - }, [ref, state.collection, setVisibleItems, overflowMode, orientation]); + }, [ref, state.collection, setVisibleItems, overflowMode]); const parentRef = useMemo( () => ({ @@ -159,12 +155,10 @@ export function useActionGroup( return { actionGroupProps: { - "aria-orientation": orientation, onKeyDown, }, isMeasuring, - visibleItems: - orientation === "vertical" ? state.collection.size : visibleItems, + visibleItems, }; } diff --git a/app/client/packages/design-system/widgets/src/components/ActionGroup/stories/ActionGroup.stories.tsx b/app/client/packages/design-system/widgets/src/components/ActionGroup/stories/ActionGroup.stories.tsx index 10dd802ab34d..814ce01d676b 100644 --- a/app/client/packages/design-system/widgets/src/components/ActionGroup/stories/ActionGroup.stories.tsx +++ b/app/client/packages/design-system/widgets/src/components/ActionGroup/stories/ActionGroup.stories.tsx @@ -1,18 +1,11 @@ import React from "react"; import type { Meta, StoryObj } from "@storybook/react"; -import { - Flex, - ActionGroup, - COLORS, - BUTTON_VARIANTS, - Item, - SIZES, - objectKeys, -} from "@design-system/widgets"; +import type { ActionGroupItem } from "@design-system/widgets"; +import { ActionGroup } from "@design-system/widgets"; /** - * A `ActionGroup` is a group of `Item` that are visually connected together. - * The `Item` accepts the same props as the `Button` except `variant` and `color`. + * A `ActionGroup` is a group of `MenuItem` that are visually connected together. + * The `MenuItem` accepts the same props as the `Button` except `variant` and `color`. * More information about `Button` props you can find [here](?path=/docs/design-system-widgets-button--docs). */ const meta: Meta = { @@ -23,185 +16,19 @@ const meta: Meta = { export default meta; type Story = StoryObj; -export const Main: Story = { - render: (args) => ( - - - Option 1 - - Option 2 - Option 3 - Option 4 - - ), -}; - -/** - * There are 3 variants of the ActionGroup component. - */ -export const Variants: Story = { - render: () => ( - - {objectKeys(BUTTON_VARIANTS).map((variant) => ( - - {variant} - {variant} - {variant} - - ))} - - ), -}; - -/** - * `ActionGroup` component has 3 visual style variants and 5 semantic color options - */ -export const Semantic: Story = { - render: () => ( - - {objectKeys(BUTTON_VARIANTS).map((variant) => - Object.values(COLORS).map((color) => ( - - - {variant} {color} - - - {variant} {color} - - - {variant} {color} - - - )), - )} - - ), -}; - -/** - * The component supports two sizes `small` and `medium`. Default size is `medium`. - */ -export const Sizes: Story = { - render: () => ( - - {Object.keys(SIZES) - .filter((size) => !["large"].includes(size)) - .map((size) => ( - - Option 1 - Option 2 - Option 3 - Option 4 - - ))} - - ), -}; - -/** - * The `ActionGroup` can be oriented horizontally or vertically. By default, it is oriented horizontally. - */ -export const Orientation: Story = { - render: () => ( - - - Option 1 - Option 2 - Option 3 - Option 4 - - - Option 1 - Option 2 - Option 3 - Option 4 - - - ), -}; +const items: ActionGroupItem[] = [ + { id: 1, label: "Aerospace", icon: "rocket" }, + { id: 2, label: "Mechanical", icon: "settings" }, + { id: 3, label: "Civil" }, + { id: 4, label: "Biomedical" }, + { id: 5, label: "Nuclear" }, + { id: 6, label: "Industrial" }, + { id: 7, label: "Chemical" }, + { id: 99, isSeparator: true }, + { id: 8, label: "Agricultural" }, + { id: 9, label: "Electrical", icon: "settings" }, +]; -/** - * The `ActionGroup` can be aligned to the start, or end. By default, it is aligned to the start. - */ -export const Alignment: Story = { - render: () => ( - - - Option 1 - Option 2 - Option 3 - - - Option 1 - Option 2 - Option 3 - - - ), -}; - -/** - * The `ActionGroup` can be `compact` or `regular`. By default, it is regular. - */ - -export const Density: Story = { - render: () => ( - - - Option 1 - Option 2 - Option 3 - Option 4 - - - Option 1 - Option 2 - Option 3 - Option 4 - - - ), -}; - -/** - * The `ActionGroup` can be `collapse`. By default, it is not collapsed. When collpaised, the `ActionGroup` will show items based on the width available. The rest of the items will be shown under a dropdown menu. - */ - -export const Overflow: Story = { - render: () => ( - - - - Option 1 - - Option 2 - - - {"Option 3"} - - - {"Option 4"} - - - - ), -}; - -/** - * The `ActionGroup` can have disabled keys. - */ - -export const DisabledKeys: Story = { - render: () => ( - - Option 1 - Option 2 - Option 3 - Option 4 - - ), +export const Main: Story = { + render: (args) => , }; diff --git a/app/client/packages/design-system/widgets/src/components/ButtonGroup/src/ButtonGroup.tsx b/app/client/packages/design-system/widgets/src/components/ButtonGroup/src/ButtonGroup.tsx index 312c6160fc5c..7479c0bdba9f 100644 --- a/app/client/packages/design-system/widgets/src/components/ButtonGroup/src/ButtonGroup.tsx +++ b/app/client/packages/design-system/widgets/src/components/ButtonGroup/src/ButtonGroup.tsx @@ -1,21 +1,24 @@ import React, { forwardRef } from "react"; import { FocusScope } from "@react-aria/focus"; import { useDOMRef } from "@react-spectrum/utils"; -import type { DOMRef } from "@react-types/shared"; import { useListState } from "@react-stately/list"; - -import styles from "./styles.module.css"; -import type { ButtonGroupItemProps, ButtonGroupProps } from "./types"; -import { ButtonGroupItem } from "./ButtonGroupItem"; +import { ButtonGroupButton } from "./ButtonGroupButton"; import { useButtonGroup } from "./useButtonGroup"; +import { Item } from "@react-stately/collections"; +import styles from "./styles.module.css"; +import type { CollectionChildren, DOMRef } from "@react-types/shared"; +import type { ButtonGroupItem, ButtonGroupProps } from "./types"; -const _ButtonGroup = ( - props: ButtonGroupProps, +interface ButtonGroupInnerProps extends ButtonGroupProps { + children?: CollectionChildren; +} + +const _ButtonGroupInner = ( + props: ButtonGroupInnerProps, ref: DOMRef, ) => { const { color = "accent", - density = "regular", isDisabled, onAction, overflowMode = "collapse", @@ -33,63 +36,59 @@ const _ButtonGroup = ( const children = [...state.collection]; - const style = { - flexBasis: "100%", - display: "flex", - }; - return (
    -
    - {children.map((item) => { - if (Boolean(item.props.isSeparator)) { - return
    ; - } + {children.map((item) => { + if (Boolean(item.props.isSeparator)) { + return
    ; + } - return ( - ["color"]) ?? - color - } - icon={item.props.icon} - iconPosition={item.props.iconPosition} - isDisabled={ - Boolean(state.disabledKeys.has(item.key)) || - Boolean(isDisabled) || - item.props.isDisabled - } - isLoading={item.props.isLoading} - item={item} - key={item.key} - onPress={() => onAction?.(item.key)} - size={Boolean(size) ? size : undefined} - state={state} - variant={ - (item.props - .variant as ButtonGroupItemProps["variant"]) ?? - variant - } - /> - ); - })} - + return ( + onAction?.(item.key)} + size={Boolean(size) ? size : undefined} + state={state} + variant={item.props.variant ?? variant} + /> + ); + })} ); }; +const ButtonGroupInner = forwardRef(_ButtonGroupInner); + +const _ButtonGroup = ( + props: ButtonGroupProps, + ref: DOMRef, +) => { + const { items, ...rest } = props; + + return ( + + {(item) => {item.label}} + + ); +}; + export const ButtonGroup = forwardRef(_ButtonGroup); diff --git a/app/client/packages/design-system/widgets/src/components/ButtonGroup/src/ButtonGroupItem.tsx b/app/client/packages/design-system/widgets/src/components/ButtonGroup/src/ButtonGroupButton.tsx similarity index 63% rename from app/client/packages/design-system/widgets/src/components/ButtonGroup/src/ButtonGroupItem.tsx rename to app/client/packages/design-system/widgets/src/components/ButtonGroup/src/ButtonGroupButton.tsx index a3519ed338b2..3a5bdf7fa248 100644 --- a/app/client/packages/design-system/widgets/src/components/ButtonGroup/src/ButtonGroupItem.tsx +++ b/app/client/packages/design-system/widgets/src/components/ButtonGroup/src/ButtonGroupButton.tsx @@ -2,10 +2,10 @@ import type { ForwardedRef } from "react"; import React, { forwardRef } from "react"; import { Button } from "@design-system/widgets"; -import type { ButtonGroupItemProps } from "./types"; +import type { ButtonGroupButtonProps } from "./types"; -const _ButtonGroupItem = ( - props: ButtonGroupItemProps, +const _ButtonGroupButton = ( + props: ButtonGroupButtonProps, ref: ForwardedRef, ) => { const { color, item, variant, ...rest } = props; @@ -17,4 +17,4 @@ const _ButtonGroupItem = ( ); }; -export const ButtonGroupItem = forwardRef(_ButtonGroupItem); +export const ButtonGroupButton = forwardRef(_ButtonGroupButton); diff --git a/app/client/packages/design-system/widgets/src/components/ButtonGroup/src/index.ts b/app/client/packages/design-system/widgets/src/components/ButtonGroup/src/index.ts index 0c54b28010e1..56d12d924653 100644 --- a/app/client/packages/design-system/widgets/src/components/ButtonGroup/src/index.ts +++ b/app/client/packages/design-system/widgets/src/components/ButtonGroup/src/index.ts @@ -1,3 +1,3 @@ -export * from "./types"; export { ButtonGroup } from "./ButtonGroup"; -export { ButtonGroupItem } from "./ButtonGroupItem"; +export { ButtonGroupButton } from "./ButtonGroupButton"; +export * from "./types"; diff --git a/app/client/packages/design-system/widgets/src/components/ButtonGroup/src/styles.module.css b/app/client/packages/design-system/widgets/src/components/ButtonGroup/src/styles.module.css index d66743fdf2cc..205280989cf9 100644 --- a/app/client/packages/design-system/widgets/src/components/ButtonGroup/src/styles.module.css +++ b/app/client/packages/design-system/widgets/src/components/ButtonGroup/src/styles.module.css @@ -6,7 +6,10 @@ gap: var(--inner-spacing-2); & :is([data-button]) { - min-inline-size: fit-content; + /* + We use !important here to be sure that button width and the logic of useButtonGroup hook will not be changed from the outside + */ + min-inline-size: fit-content !important; } &[data-orientation="vertical"] { diff --git a/app/client/packages/design-system/widgets/src/components/ButtonGroup/src/types.ts b/app/client/packages/design-system/widgets/src/components/ButtonGroup/src/types.ts index e10923173c96..e0276cac644b 100644 --- a/app/client/packages/design-system/widgets/src/components/ButtonGroup/src/types.ts +++ b/app/client/packages/design-system/widgets/src/components/ButtonGroup/src/types.ts @@ -1,19 +1,15 @@ import type { SpectrumActionGroupProps } from "@react-types/actiongroup"; import type { ListState } from "@react-stately/list"; - import type { Node, StyleProps } from "@react-types/shared"; - import type { ButtonProps } from "../../Button"; import type { SIZES } from "../../../shared"; +import type { IconProps } from "../../Icon"; export const BUTTON_GROUP_ORIENTATIONS = { vertical: "vertical", horizontal: "horizontal", }; -export interface InheritedActionButtonProps - extends Pick {} - export interface ButtonGroupProps extends Omit< SpectrumActionGroupProps, @@ -29,14 +25,23 @@ export interface ButtonGroupProps | "disallowEmptySelection" | "onSelectionChange" | "selectedKeys" + | "density" + | "children" | keyof StyleProps >, - InheritedActionButtonProps { - orientation?: keyof typeof BUTTON_GROUP_ORIENTATIONS; + Pick { size?: Omit; + orientation?: keyof typeof BUTTON_GROUP_ORIENTATIONS; +} + +export interface ButtonGroupItem { + id: string | number; + label?: string; + icon?: IconProps["name"]; + isSeparator?: boolean; } -export interface ButtonGroupItemProps extends ButtonProps { +export interface ButtonGroupButtonProps extends ButtonProps { state: ListState; item: Node; } diff --git a/app/client/packages/design-system/widgets/src/components/ButtonGroup/stories/ButtonGroup.stories.tsx b/app/client/packages/design-system/widgets/src/components/ButtonGroup/stories/ButtonGroup.stories.tsx index 69f319804990..bd4594e613f0 100644 --- a/app/client/packages/design-system/widgets/src/components/ButtonGroup/stories/ButtonGroup.stories.tsx +++ b/app/client/packages/design-system/widgets/src/components/ButtonGroup/stories/ButtonGroup.stories.tsx @@ -1,14 +1,7 @@ import React from "react"; import type { Meta, StoryObj } from "@storybook/react"; -import { - ButtonGroup, - Flex, - BUTTON_VARIANTS, - COLORS, - SIZES, - Item, - objectKeys, -} from "@design-system/widgets"; +import type { ButtonGroupItem } from "@design-system/widgets"; +import { ButtonGroup } from "@design-system/widgets"; /** * A `ButtonGroup` is a group of buttons that are visually connected together. @@ -23,103 +16,16 @@ const meta: Meta = { export default meta; type Story = StoryObj; -export const Main: Story = { - render: (args) => ( - - Option 1 - Option 2 - Option 3 - - ), -}; - -/** - * `ButtonGroup` component has 3 visual style variants and 5 semantic color options - */ - -export const Semantic: Story = { - render: () => ( - - {objectKeys(BUTTON_VARIANTS).map((variant) => - Object.values(COLORS).map((color) => ( - - - {variant} {color} - - - {variant} {color} - - - {variant} {color} - - - )), - )} - - ), -}; - -/** - * The component supports two sizes `small` and `medium`. Default size is `medium`. - */ -export const Sizes: Story = { - render: () => ( - - {Object.keys(SIZES) - .filter((size) => !["large"].includes(size)) - .map((size) => ( - - Option 1 - Option 2 - Option 3 - - ))} - - ), -}; +const items: ButtonGroupItem[] = [ + { id: 1, label: "Aerospace", icon: "rocket" }, + { id: 2, label: "Mechanical", icon: "settings" }, + { id: 3, label: "Civil", icon: "settings" }, + { id: 4, label: "Biomedical" }, + { id: 5, label: "Nuclear" }, + { id: 6, label: "Industrial" }, + { id: 7, label: "Chemical" }, +]; -/** - * The `ButtonGroup` can be oriented horizontally or vertically. By default, it is oriented horizontally. - */ -export const Orientation: Story = { - render: () => ( - - - Option 1 - Option 2 - Option 3 - Option 4 - - - Option 1 - Option 2 - Option 3 - Option 4 - - - ), -}; - -/** - * If there is not enough space for horizontal positioning, then the themes will be positioned vertically - */ -export const Responsive: Story = { - render: () => ( - - - Option 1 - Option 2 - - {"Option 3"} - - - {"Option 4"} - - - - ), +export const Main: Story = { + render: (args) => , }; diff --git a/app/client/packages/design-system/widgets/src/components/Icon/src/Icon.tsx b/app/client/packages/design-system/widgets/src/components/Icon/src/Icon.tsx index 063cd681ca98..92821b45510f 100644 --- a/app/client/packages/design-system/widgets/src/components/Icon/src/Icon.tsx +++ b/app/client/packages/design-system/widgets/src/components/Icon/src/Icon.tsx @@ -14,7 +14,7 @@ const _Icon = (props: IconProps, ref: Ref) => { const filled = theme.iconStyle === "filled" || filledProp; const Icon = useMemo(() => { - let Icon: React.ComponentType | null = null; + let Icon: React.ComponentType | null; if (icon !== undefined) { Icon = icon as React.ComponentType; @@ -64,7 +64,7 @@ const _Icon = (props: IconProps, ref: Ref) => { ref={ref} {...rest} > - + ); diff --git a/app/client/packages/design-system/widgets/src/components/Menu/src/Item.tsx b/app/client/packages/design-system/widgets/src/components/Menu/src/Item.tsx deleted file mode 100644 index a727192b2665..000000000000 --- a/app/client/packages/design-system/widgets/src/components/Menu/src/Item.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import type { ReactNode } from "react"; -import type { ReactElement } from "react"; -import { Item as HeadlessItem } from "@design-system/headless"; -import type { ItemProps as HeadlessItemProps } from "@react-types/shared"; - -import type { IconProps } from "../../Icon"; -import type { COLORS } from "../../../shared"; -import type { ButtonProps } from "../../Button"; - -interface ItemProps extends Omit, "children"> { - color?: keyof typeof COLORS; - variant?: ButtonProps["variant"]; - icon?: IconProps["name"]; - iconPosition?: "start" | "end"; - isLoading?: boolean; - isSeparator?: boolean; - children?: ReactNode; -} - -// eslint-disable-next-line @typescript-eslint/no-unused-vars -function _Item(props: ItemProps): ReactElement | null { - return null; -} - -// Add types and decorate the method for the correct props work. -_Item.getCollectionNode = (props: ItemProps) => { - const { color, ...rest } = props; - // @ts-expect-error this method is hidden by the types. See the source code of Item from Spectrum for more context. - return HeadlessItem.getCollectionNode({ - ...rest, - color, - // TODO(pawan): Check why we need [data-color] here. - ["data-color"]: Boolean(color) ? color : undefined, - }); -}; - -export const Item = _Item as (props: ItemProps) => JSX.Element; diff --git a/app/client/packages/design-system/widgets/src/components/Menu/src/Menu.tsx b/app/client/packages/design-system/widgets/src/components/Menu/src/Menu.tsx index 4da605258b2a..9173a4edf883 100644 --- a/app/client/packages/design-system/widgets/src/components/Menu/src/Menu.tsx +++ b/app/client/packages/design-system/widgets/src/components/Menu/src/Menu.tsx @@ -1,13 +1,38 @@ import React from "react"; -import { Menu as HeadlessMenu } from "@design-system/headless"; -import type { MenuProps } from "@design-system/headless"; +import { Popover } from "@design-system/widgets"; +import { Menu as HeadlessMenu, SubmenuTrigger } from "react-aria-components"; +import { MenuItem } from "./MenuItem"; import styles from "./styles.module.css"; +import type { MenuProps, MenuItemProps } from "./types"; + +export const Menu = (props: MenuProps) => { + const { hasSubmenu = false } = props; + // place Popover in the root theme provider to get access to the CSS tokens + const root = document.body.querySelector( + "[data-theme-provider]", + ) as HTMLButtonElement; -export const Menu = (props: MenuProps) => { - const { children, ...rest } = props; return ( - - {children} - + + + {(item) => renderFunc(item, props)} + + ); }; + +const renderFunc = (item: MenuItemProps, props: MenuProps) => { + const { childItems, ...rest } = item; + if (childItems != null) { + return ( + + + + {(item) => renderFunc(item, props)} + + + ); + } else { + return ; + } +}; diff --git a/app/client/packages/design-system/widgets/src/components/Menu/src/MenuItem.tsx b/app/client/packages/design-system/widgets/src/components/Menu/src/MenuItem.tsx new file mode 100644 index 000000000000..5be914f6c8ba --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/Menu/src/MenuItem.tsx @@ -0,0 +1,34 @@ +import React from "react"; +import { MenuItem as HeadlessMenuItem, Separator } from "react-aria-components"; +import { Icon, Text, listItemStyles } from "@design-system/widgets"; +import type { MenuItemProps } from "./types"; + +export const MenuItem = (props: MenuItemProps) => { + const { hasSubmenu = false, icon, id, isSeparator, label, ...rest } = props; + + if (Boolean(isSeparator)) { + return ; + } + + return ( + + {icon && } + + {label} + + {Boolean(hasSubmenu) && ( + + )} + + ); +}; diff --git a/app/client/packages/design-system/widgets/src/components/Menu/src/MenuList.tsx b/app/client/packages/design-system/widgets/src/components/Menu/src/MenuList.tsx deleted file mode 100644 index 428df062d598..000000000000 --- a/app/client/packages/design-system/widgets/src/components/Menu/src/MenuList.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import React from "react"; -import { MenuList as HeadlessMenuList } from "@design-system/headless"; -import { getTypographyClassName } from "@design-system/theming"; -import styles from "./styles.module.css"; - -import type { MenuListProps } from "@design-system/headless"; - -export const MenuList = (props: MenuListProps) => { - const { children, ...rest } = props; - return ( - - {children} - - ); -}; diff --git a/app/client/packages/design-system/widgets/src/components/Menu/src/index.ts b/app/client/packages/design-system/widgets/src/components/Menu/src/index.ts index c16b2c59a91d..6fd1d55a3c6e 100644 --- a/app/client/packages/design-system/widgets/src/components/Menu/src/index.ts +++ b/app/client/packages/design-system/widgets/src/components/Menu/src/index.ts @@ -1,3 +1,3 @@ export * from "./Menu"; -export * from "./MenuList"; -export * from "./Item"; +export { MenuTrigger } from "react-aria-components"; +export * from "./types"; diff --git a/app/client/packages/design-system/widgets/src/components/Menu/src/styles.module.css b/app/client/packages/design-system/widgets/src/components/Menu/src/styles.module.css index 777b5edead74..eaee8d5ab170 100644 --- a/app/client/packages/design-system/widgets/src/components/Menu/src/styles.module.css +++ b/app/client/packages/design-system/widgets/src/components/Menu/src/styles.module.css @@ -1,92 +1,4 @@ -@import "../../../shared/colors/colors.module.css"; - .menu { - background-color: var(--color-bg-elevation-3); - border-radius: var(--border-radius-elevation-3); - z-index: var(--z-index-99); - box-shadow: var(--box-shadow-1); - min-inline-size: var(--sizing-30); - max-inline-size: var(--sizing-80); -} - -.menuList { - list-style: none; - padding: 0; - margin: 0; -} - -.menuList li { - display: flex; - align-items: center; - padding-inline: var(--inner-spacing-3); - padding-block: var(--inner-spacing-3); -} - -.menuList li [data-text] { - overflow: hidden; - -webkit-line-clamp: 1; - text-overflow: ellipsis; - white-space: nowrap; - display: flex; - align-items: center; -} - -.menuList li:first-of-type { - border-top-left-radius: var(--border-radius-elevation-3); - border-top-right-radius: var(--border-radius-elevation-3); -} - -.menuList li:last-of-type { - border-bottom-left-radius: var(--border-radius-elevation-3); - border-bottom-right-radius: var(--border-radius-elevation-3); -} - -.menuList li:focus { - outline: none; -} - -.menuList li:not([data-disabled]) { - cursor: pointer; -} - -.menuList [data-hovered] { - background-color: var(--color-bg-accent-subtle-hover); -} - -.menuList [data-active] { - background-color: var(--color-bg-accent-subtle-active); -} - -.menuList li:not([data-disabled]) { - @each $color in colors { - &[data-color="$(color)"] { - color: var(--color-fg-$(color)); - } - } -} - -.menuList [data-disabled] { - opacity: var(--opacity-disabled); - cursor: not-allowed; -} - -.menuList [data-focused]:focus-visible { - box-shadow: - 0 0 0 2px var(--color-bg), - 0 0 0 4px var(--color-bd-focus); -} - -.menuList li[data-separator] { - border-top: var(--border-width-1) solid var(--color-bd); - padding: 0; -} - -/* this is required so that separator ( ) if passed with a text as children, the text is hidden */ -.menuList li[data-separator] > * { - display: none; -} - -/* making sure the first and last child are not displayed when they have the data-separator attribute */ -.menuList li:is(:first-child, :last-child):is([data-separator]) { - display: none; + max-height: inherit; + overflow-y: auto; } diff --git a/app/client/packages/design-system/widgets/src/components/Menu/src/types.ts b/app/client/packages/design-system/widgets/src/components/Menu/src/types.ts new file mode 100644 index 000000000000..8dd22d053f99 --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/Menu/src/types.ts @@ -0,0 +1,22 @@ +import type { + MenuProps as HeadlessMenuProps, + MenuItemProps as HeadlessMenuItemProps, +} from "react-aria-components"; +import type { IconProps } from "../../Icon"; + +export interface MenuProps extends Omit, "slot"> { + hasSubmenu?: boolean; +} + +export interface MenuItem { + id: string | number; + label?: string; + icon?: IconProps["name"]; + isSeparator?: boolean; + childItems?: Iterable; + hasSubmenu?: boolean; +} + +export interface MenuItemProps + extends Omit, + MenuItem {} diff --git a/app/client/packages/design-system/widgets/src/components/Menu/stories/Menu.stories.tsx b/app/client/packages/design-system/widgets/src/components/Menu/stories/Menu.stories.tsx index dc9f96e44de0..09f6f62ee8b8 100644 --- a/app/client/packages/design-system/widgets/src/components/Menu/stories/Menu.stories.tsx +++ b/app/client/packages/design-system/widgets/src/components/Menu/stories/Menu.stories.tsx @@ -1,6 +1,7 @@ import React from "react"; +import { Button, Menu, MenuTrigger } from "@design-system/widgets"; +import type { MenuItem } from "@design-system/widgets"; import type { Meta, StoryObj } from "@storybook/react"; -import { Button, Menu, MenuList, Item, COLORS } from "@design-system/widgets"; /** * A menu displays a list of actions or options that a user can choose. @@ -17,33 +18,39 @@ const meta: Meta = { export default meta; type Story = StoryObj; +const items: MenuItem[] = [ + { id: 1, label: "Aerospace", icon: "rocket" }, + { + id: 2, + label: "Mechanical", + icon: "settings", + childItems: [ + { id: 21, label: "Aerospace", icon: "rocket" }, + { + id: 22, + label: "Mechanical", + icon: "settings", + childItems: [ + { id: 31, label: "Aerospace", icon: "rocket" }, + { id: 32, label: "Mechanical", icon: "settings" }, + ], + }, + ], + }, + { id: 3, label: "Civil" }, + { id: 4, label: "Biomedical" }, + { id: 5, label: "Nuclear" }, + { id: 6, label: "Industrial" }, + { id: 7, label: "Chemical" }, + { id: 8, label: "Agricultural" }, + { id: 9, label: "Electrical" }, +]; + export const Main: Story = { render: (args) => ( - alert(key)}> - - - Copy - Cut - Paste - - - ), -}; - -/** - * Just like Button component, There are 3 variants of the icon button component. - */ -export const ItemColor: Story = { - render: () => ( - - - - {Object.values(COLORS).map((color) => ( - - {color} - - ))} - - + +