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 deleted file mode 100644 index c0675c325fd6..000000000000 --- a/app/client/packages/design-system/widgets/src/components/ActionGroup/src/ActionGroup.tsx +++ /dev/null @@ -1,114 +0,0 @@ -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 { ActionGroupProps } from "./types"; -import { IconButton } from "../../IconButton"; -import { useActionGroup } from "./useActionGroup"; -import { Item, Menu, MenuList } from "../../Menu"; -import { ActionGroupItem } from "./ActionGroupItem"; - -const _ActionGroup = ( - props: ActionGroupProps, - ref: DOMRef, -) => { - const { - alignment = "start", - color = "accent", - density = "regular", - isDisabled, - onAction, - orientation = "horizontal", - overflowMode = "collapse", - size = "medium", - variant = "filled", - ...others - } = props; - const domRef = useDOMRef(ref); - const state = useListState({ ...props, suppressTextValueWarning: true }); - const { actionGroupProps, visibleItems } = useActionGroup( - props, - state, - domRef, - ); - - let children = [...state.collection]; - const menuChildren = children.slice(visibleItems); - children = children.slice(0, visibleItems); - - return ( - -
    - {children.map((item) => { - if (Boolean(item.props.isSeparator)) { - return
    ; - } - - return ( - onAction?.(item.key)} - size={Boolean(size) ? size : undefined} - state={state} - variant={variant} - /> - ); - })} - {menuChildren?.length > 0 && ( - item.props.isSeparator) - .map((item) => item.key), - ]} - onAction={onAction} - > - - - {menuChildren.map((item) => { - return ( - - {item.rendered} - - ); - })} - - - )} -
    - - ); -}; - -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/ActionGroupItem.tsx deleted file mode 100644 index 939e28977901..000000000000 --- a/app/client/packages/design-system/widgets/src/components/ActionGroup/src/ActionGroupItem.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import type { ForwardedRef } from "react"; -import React, { forwardRef } from "react"; -import { Button } from "@design-system/widgets"; - -import type { ButtonGroupItemProps } from "../../../"; - -const _ActionGroupItem = ( - props: ButtonGroupItemProps, - ref: ForwardedRef, -) => { - const { color, item, variant, ...rest } = props; - - return ( - - ); -}; - -export const ActionGroupItem = forwardRef(_ActionGroupItem); 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 deleted file mode 100644 index 07a7f9e2162d..000000000000 --- a/app/client/packages/design-system/widgets/src/components/ActionGroup/src/index.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export * from "./types"; -export { ActionGroup } from "./ActionGroup"; -export { ActionGroupItem } from "./ActionGroupItem"; 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 deleted file mode 100644 index a1c29b16f063..000000000000 --- a/app/client/packages/design-system/widgets/src/components/ActionGroup/src/styles.module.css +++ /dev/null @@ -1,138 +0,0 @@ -@import "../../../shared/colors/colors.module.css"; - -.actionGroup { - display: flex; - width: 100%; - - & :is([data-separator]) { - inline-size: var(--sizing-5); - block-size: var(--sizing-5); - } - - [data-button]:not(:last-of-type) { - /* - We use !important here to be sure that button width and the logic of useActionGroup hook will not be changed from the outside - */ - 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; - } - - &[data-alignment="start"] { - justify-content: flex-start; - } - - &[data-alignment="end"] { - justify-content: flex-end; - } - - &[data-overflow="collapse"] { - flex-wrap: nowrap; - } - - &:not([data-density="compact"]) { - gap: var(--inner-spacing-2); - } - - &[data-density="compact"] { - /* increasing z index to make sure the focused button is on top of the others */ - & [data-button]:not([data-disabled]):focus-visible { - z-index: 1; - } - - & [data-button]:first-child { - border-bottom-right-radius: 0; - } - - & [data-button]:last-of-type { - border-top-left-radius: 0; - } - - & [data-button]:not(:first-child):not(:last-of-type) { - border-radius: 0; - } - - /** - * ---------------------------------------------------------------------------- - * Horizontal orientation - *----------------------------------------------------------------------------- - */ - &[data-orientation="horizontal"] { - width: 100%; - } - - &[data-orientation="horizontal"] [data-button]:not(:last-of-type) { - border-right-width: var(--border-width-1); - } - - &[data-orientation="horizontal"] [data-button]:first-child { - border-top-right-radius: 0; - } - - &[data-orientation="horizontal"] [data-button]:last-of-type { - border-bottom-left-radius: 0; - } - - &[data-orientation="horizontal"] [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 deleted file mode 100644 index 2a8b6631e783..000000000000 --- a/app/client/packages/design-system/widgets/src/components/ActionGroup/src/types.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { ButtonGroupProps } from "../../ButtonGroup"; - -export const ACTION_GROUP_ALIGNMENTS = { - start: "Start", - end: "End", -} as const; - -export interface ActionGroupProps extends ButtonGroupProps { - alignment?: keyof typeof ACTION_GROUP_ALIGNMENTS; -} 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 deleted file mode 100644 index 10dd802ab34d..000000000000 --- a/app/client/packages/design-system/widgets/src/components/ActionGroup/stories/ActionGroup.stories.tsx +++ /dev/null @@ -1,207 +0,0 @@ -import React from "react"; -import type { Meta, StoryObj } from "@storybook/react"; -import { - Flex, - ActionGroup, - COLORS, - BUTTON_VARIANTS, - Item, - SIZES, - objectKeys, -} 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`. - * More information about `Button` props you can find [here](?path=/docs/design-system-widgets-button--docs). - */ -const meta: Meta = { - component: ActionGroup, - title: "Design-system/Widgets/ActionGroup", -}; - -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 - - - ), -}; - -/** - * 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 - - ), -}; 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 deleted file mode 100644 index 312c6160fc5c..000000000000 --- a/app/client/packages/design-system/widgets/src/components/ButtonGroup/src/ButtonGroup.tsx +++ /dev/null @@ -1,95 +0,0 @@ -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 { useButtonGroup } from "./useButtonGroup"; - -const _ButtonGroup = ( - props: ButtonGroupProps, - ref: DOMRef, -) => { - const { - color = "accent", - density = "regular", - isDisabled, - onAction, - overflowMode = "collapse", - size = "medium", - variant = "filled", - ...others - } = props; - const domRef = useDOMRef(ref); - const state = useListState({ ...props, suppressTextValueWarning: true }); - const { buttonGroupProps, orientation } = useButtonGroup( - props, - state, - domRef, - ); - - const children = [...state.collection]; - - const style = { - flexBasis: "100%", - display: "flex", - }; - - 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 - } - /> - ); - })} - - - - ); -}; - -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/ButtonGroupItem.tsx deleted file mode 100644 index a3519ed338b2..000000000000 --- a/app/client/packages/design-system/widgets/src/components/ButtonGroup/src/ButtonGroupItem.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import type { ForwardedRef } from "react"; -import React, { forwardRef } from "react"; -import { Button } from "@design-system/widgets"; - -import type { ButtonGroupItemProps } from "./types"; - -const _ButtonGroupItem = ( - props: ButtonGroupItemProps, - ref: ForwardedRef, -) => { - const { color, item, variant, ...rest } = props; - - return ( - - ); -}; - -export const ButtonGroupItem = forwardRef(_ButtonGroupItem); 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 deleted file mode 100644 index 0c54b28010e1..000000000000 --- a/app/client/packages/design-system/widgets/src/components/ButtonGroup/src/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "./types"; -export { ButtonGroup } from "./ButtonGroup"; -export { ButtonGroupItem } from "./ButtonGroupItem"; 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 deleted file mode 100644 index 69f319804990..000000000000 --- a/app/client/packages/design-system/widgets/src/components/ButtonGroup/stories/ButtonGroup.stories.tsx +++ /dev/null @@ -1,125 +0,0 @@ -import React from "react"; -import type { Meta, StoryObj } from "@storybook/react"; -import { - ButtonGroup, - Flex, - BUTTON_VARIANTS, - COLORS, - SIZES, - Item, - objectKeys, -} from "@design-system/widgets"; - -/** - * A `ButtonGroup` is a group of buttons that are visually connected together. - * The `Item` 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 = { - component: ButtonGroup, - title: "Design-system/Widgets/ButtonGroup", -}; - -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 - - ))} - - ), -}; - -/** - * 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"} - - - - ), -}; 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/headless/src/components/Menu/index.ts b/app/client/packages/design-system/widgets/src/components/InlineButtons/index.ts similarity index 100% rename from app/client/packages/design-system/headless/src/components/Menu/index.ts rename to app/client/packages/design-system/widgets/src/components/InlineButtons/index.ts diff --git a/app/client/packages/design-system/widgets/src/components/InlineButtons/src/InlineButton.tsx b/app/client/packages/design-system/widgets/src/components/InlineButtons/src/InlineButton.tsx new file mode 100644 index 000000000000..1ea9863d6027 --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/InlineButtons/src/InlineButton.tsx @@ -0,0 +1,26 @@ +import React, { forwardRef } from "react"; +import { Button } from "@design-system/widgets"; +import type { ButtonProps } from "@design-system/widgets"; +import type { ListState } from "@react-stately/list"; +import type { Node } from "@react-types/shared"; +import type { ForwardedRef } from "react"; + +interface InlineButtonProps extends ButtonProps { + state: ListState; + item: Node; +} + +const _InlineButtonsButton = ( + props: InlineButtonProps, + ref: ForwardedRef, +) => { + const { color, item, variant, ...rest } = props; + + return ( + + ); +}; + +export const InlineButton = forwardRef(_InlineButtonsButton); diff --git a/app/client/packages/design-system/widgets/src/components/InlineButtons/src/InlineButtons.tsx b/app/client/packages/design-system/widgets/src/components/InlineButtons/src/InlineButtons.tsx new file mode 100644 index 000000000000..daa5e142b33a --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/InlineButtons/src/InlineButtons.tsx @@ -0,0 +1,90 @@ +import React, { forwardRef } from "react"; +import { FocusScope } from "@react-aria/focus"; +import { useDOMRef } from "@react-spectrum/utils"; +import { useListState } from "@react-stately/list"; +import { InlineButton } from "./InlineButton"; +import { useInlineButtons } from "./useInlineButtons"; +import { Item } from "@react-stately/collections"; +import styles from "./styles.module.css"; +import type { CollectionChildren, DOMRef } from "@react-types/shared"; +import type { InlineButtonsItem, InlineButtonsProps } from "./types"; + +interface InlineButtonsInnerProps extends InlineButtonsProps { + children?: CollectionChildren; +} + +const _InlineButtonsInner = ( + props: InlineButtonsInnerProps, + ref: DOMRef, +) => { + const { + color = "accent", + isDisabled, + onAction, + size = "medium", + variant = "filled", + } = props; + const domRef = useDOMRef(ref); + const state = useListState({ ...props, suppressTextValueWarning: true }); + const { inlineButtonsProps, orientation } = useInlineButtons( + props, + state, + domRef, + ); + + const children = [...state.collection]; + + return ( + +
    + {children.map((item) => { + if (Boolean(item.props.isSeparator)) { + return
    ; + } + + return ( + onAction?.(item.key)} + size={Boolean(size) ? size : undefined} + state={state} + variant={item.props.variant ?? variant} + /> + ); + })} +
    + + ); +}; + +const InlineButtonsInner = forwardRef(_InlineButtonsInner); + +const _InlineButtons = ( + props: InlineButtonsProps, + ref: DOMRef, +) => { + const { items, ...rest } = props; + + return ( + + {(item) => {item.label}} + + ); +}; + +export const InlineButtons = forwardRef(_InlineButtons); diff --git a/app/client/packages/design-system/widgets/src/components/InlineButtons/src/index.ts b/app/client/packages/design-system/widgets/src/components/InlineButtons/src/index.ts new file mode 100644 index 000000000000..b2f3c63a6917 --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/InlineButtons/src/index.ts @@ -0,0 +1,2 @@ +export { InlineButtons } from "./InlineButtons"; +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/InlineButtons/src/styles.module.css similarity index 66% rename from app/client/packages/design-system/widgets/src/components/ButtonGroup/src/styles.module.css rename to app/client/packages/design-system/widgets/src/components/InlineButtons/src/styles.module.css index d66743fdf2cc..701792476e6d 100644 --- a/app/client/packages/design-system/widgets/src/components/ButtonGroup/src/styles.module.css +++ b/app/client/packages/design-system/widgets/src/components/InlineButtons/src/styles.module.css @@ -1,12 +1,15 @@ @import "../../../shared/colors/colors.module.css"; -.buttonGroup { +.inlineButtons { display: flex; width: 100%; 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 useInlineButtons hook will not be changed from the outside + */ + min-inline-size: fit-content !important; } &[data-orientation="vertical"] { @@ -16,7 +19,7 @@ &[data-orientation="vertical"] :is([data-button]) { /* - We use !important here to be sure that button width and the logic of useButtonGroup hook will not be changed from the outside + We use !important here to be sure that button width and the logic of useInlineButtons hook will not be changed from the outside */ min-inline-size: 100% !important; max-inline-size: none; diff --git a/app/client/packages/design-system/widgets/src/components/ButtonGroup/src/types.ts b/app/client/packages/design-system/widgets/src/components/InlineButtons/src/types.ts similarity index 52% rename from app/client/packages/design-system/widgets/src/components/ButtonGroup/src/types.ts rename to app/client/packages/design-system/widgets/src/components/InlineButtons/src/types.ts index e10923173c96..37ac3368be0d 100644 --- a/app/client/packages/design-system/widgets/src/components/ButtonGroup/src/types.ts +++ b/app/client/packages/design-system/widgets/src/components/InlineButtons/src/types.ts @@ -1,20 +1,10 @@ import type { SpectrumActionGroupProps } from "@react-types/actiongroup"; -import type { ListState } from "@react-stately/list"; - -import type { Node, StyleProps } from "@react-types/shared"; - +import type { Key } from "@react-types/shared"; +import type { StyleProps } from "@react-types/shared"; import type { ButtonProps } from "../../Button"; import type { SIZES } from "../../../shared"; -export const BUTTON_GROUP_ORIENTATIONS = { - vertical: "vertical", - horizontal: "horizontal", -}; - -export interface InheritedActionButtonProps - extends Pick {} - -export interface ButtonGroupProps +export interface InlineButtonsProps extends Omit< SpectrumActionGroupProps, | "staticColor" @@ -29,14 +19,22 @@ export interface ButtonGroupProps | "disallowEmptySelection" | "onSelectionChange" | "selectedKeys" + | "density" + | "children" + | "overflowMode" + | "id" | keyof StyleProps >, - InheritedActionButtonProps { - orientation?: keyof typeof BUTTON_GROUP_ORIENTATIONS; + Pick { size?: Omit; } -export interface ButtonGroupItemProps extends ButtonProps { - state: ListState; - item: Node; +export interface InlineButtonsItem + extends Pick< + ButtonProps, + "icon" | "iconPosition" | "isLoading" | "isDisabled" | "variant" | "color" + > { + id: Key; + label?: string; + isSeparator?: boolean; } diff --git a/app/client/packages/design-system/widgets/src/components/ButtonGroup/src/useButtonGroup.tsx b/app/client/packages/design-system/widgets/src/components/InlineButtons/src/useInlineButtons.tsx similarity index 75% rename from app/client/packages/design-system/widgets/src/components/ButtonGroup/src/useButtonGroup.tsx rename to app/client/packages/design-system/widgets/src/components/InlineButtons/src/useInlineButtons.tsx index 4af372d6706d..767b5b41dbff 100644 --- a/app/client/packages/design-system/widgets/src/components/ButtonGroup/src/useButtonGroup.tsx +++ b/app/client/packages/design-system/widgets/src/components/InlineButtons/src/useInlineButtons.tsx @@ -3,23 +3,25 @@ import type { ListState } from "@react-stately/list"; import { useCallback, type RefObject, useMemo } from "react"; import type { DOMAttributes, FocusableElement } from "@react-types/shared"; -import type { ButtonGroupProps } from "./types"; +import type { InlineButtonsProps } from "./types"; import { useLayoutEffect, useResizeObserver, useValueEffect, } from "@react-aria/utils"; -export interface ButtonGroupAria { - buttonGroupProps: DOMAttributes; - orientation: ButtonGroupProps["orientation"]; +type Orientation = "vertical" | "horizontal"; + +export interface InlineButtonsAria { + inlineButtonsProps: DOMAttributes; + orientation: Orientation; } -export function useButtonGroup( - props: ButtonGroupProps, +export function useInlineButtons( + props: InlineButtonsProps, state: ListState, ref: RefObject, -): ButtonGroupAria { +): InlineButtonsAria { const focusManager = createFocusManager(ref); const onKeyDown = (e: React.KeyboardEvent) => { @@ -46,14 +48,11 @@ export function useButtonGroup( } }; - const [{ orientation }, setOrientation] = useValueEffect({ - orientation: props.orientation, - }); + const [orientation, setOrientation] = + useValueEffect("horizontal"); const updateOverflow = useCallback(() => { - if (props.orientation === "vertical") return; - - const computeOrientation = () => { + const computeOrientation = (): Orientation => { if (ref.current) { const listItems = Array.from(ref.current.children) as HTMLLIElement[]; const containerSize = ref.current.getBoundingClientRect().width; @@ -78,17 +77,11 @@ export function useButtonGroup( }; setOrientation(function* () { - yield { - orientation: "horizontal", - }; - - const orientation = computeOrientation(); + yield "horizontal"; - yield { - orientation: orientation, - }; + yield computeOrientation(); }); - }, [ref, state.collection, setOrientation, props.orientation]); + }, [ref, state.collection, setOrientation]); const parentRef = useMemo( () => ({ @@ -106,7 +99,7 @@ export function useButtonGroup( useLayoutEffect(updateOverflow, [updateOverflow, state.collection]); return { - buttonGroupProps: { + inlineButtonsProps: { "aria-orientation": orientation, onKeyDown, }, diff --git a/app/client/packages/design-system/widgets/src/components/InlineButtons/stories/InlineButtons.stories.tsx b/app/client/packages/design-system/widgets/src/components/InlineButtons/stories/InlineButtons.stories.tsx new file mode 100644 index 000000000000..550eec8186c4 --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/InlineButtons/stories/InlineButtons.stories.tsx @@ -0,0 +1,104 @@ +import React from "react"; +import { + InlineButtons, + Flex, + objectKeys, + BUTTON_VARIANTS, + COLORS, + SIZES, +} from "@design-system/widgets"; +import type { Meta, StoryObj } from "@storybook/react"; +import { + itemList, + longItemList, + semanticItemList, + itemListWithIcons, +} from "./inlineButtonsData"; + +/** + * A `InlineButtons` is a group of buttons that are visually connected together. + * More information about `Button` props you can find [here](?path=/docs/design-system-widgets-button--docs). + */ +const meta: Meta = { + component: InlineButtons, + title: "Design-system/Widgets/InlineButtons", +}; + +export default meta; +type Story = StoryObj; + +export const Main: Story = { + render: (args) => , +}; + +/** + * `InlineButtons` 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) => ( + + )), + )} + + ), +}; + +/** + * You can also customize the color and variant for each individual button through the item config. + */ + +export const IndividualSemantic: Story = { + render: () => , +}; + +/** + * 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) => ( + + ))} + + ), +}; + +/** + * If there is not enough space for horizontal positioning, then the themes will be positioned vertically + */ +export const Responsive: Story = { + render: () => ( + + + + ), +}; + +/** + * The items can be disabled by passing `disabledKeys` or `isDisabled` in the item configuration. + * Also, all items can be disabled by passing `isDisabled` to `InlineButtons` component. + */ +export const Disabled: Story = { + args: { + disabledKeys: [1, 2], + items: itemList, + }, +}; + +export const WithIcons: Story = { + args: { + items: itemListWithIcons, + }, +}; diff --git a/app/client/packages/design-system/widgets/src/components/InlineButtons/stories/inlineButtonsData.ts b/app/client/packages/design-system/widgets/src/components/InlineButtons/stories/inlineButtonsData.ts new file mode 100644 index 000000000000..6d275ea1f192 --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/InlineButtons/stories/inlineButtonsData.ts @@ -0,0 +1,30 @@ +import type { InlineButtonsItem } from "../src"; + +export const itemList: InlineButtonsItem[] = [ + { id: 1, label: "Aerospace" }, + { id: 2, label: "Mechanical" }, + { id: 3, label: "Civil" }, + { id: 4, label: "Biomedical" }, +]; + +export const semanticItemList: InlineButtonsItem[] = [ + { id: 1, label: "Delete", color: "negative" }, + { id: 3, label: "Cancel", variant: "outlined" }, + { id: 4, label: "Save Changes" }, +]; + +export const longItemList: InlineButtonsItem[] = [ + { id: 1, label: "Aerospace" }, + { id: 2, label: "Mechanical" }, + { id: 3, label: "Civil" }, + { id: 4, label: "Biomedical" }, + { id: 5, label: "Nuclear" }, + { id: 6, label: "Industrial" }, +]; + +export const itemListWithIcons: InlineButtonsItem[] = [ + { id: 1, label: "Aerospace", icon: "galaxy" }, + { id: 2, label: "Mechanical", icon: "automatic-gearbox" }, + { id: 3, label: "Civil", icon: "circuit-ground" }, + { id: 4, label: "Biomedical", icon: "biohazard" }, +]; 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..33d6cce117a6 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,71 @@ import React from "react"; -import { Menu as HeadlessMenu } from "@design-system/headless"; -import type { MenuProps } from "@design-system/headless"; +import { Icon, listItemStyles, Popover, Text } from "@design-system/widgets"; +import { + Menu as HeadlessMenu, + MenuItem, + Separator, + SubmenuTrigger, +} from "react-aria-components"; import styles from "./styles.module.css"; +import type { MenuProps, MenuItemProps } from "./types"; +import type { Key } from "@react-types/shared"; + +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; + + return ( + // We should put only parent Popover in the root, if we put the child ones, then Menu will work incorrectly + + + {(item) => renderFunc(item, props)} + + + ); +}; + +const renderFunc = (item: MenuItemProps, props: MenuProps) => { + const { childItems, icon, id, isDisabled, isSeparator = false, label } = item; + + const isItemDisabled = () => + Boolean((props.disabledKeys as Key[])?.includes(id)) || isDisabled; + + if (childItems != null) + return ( + + + {icon && } + + {label} + + + + + {(item) => renderFunc(item, props)} + + + ); + + if (isSeparator) + return ; -export const Menu = (props: MenuProps) => { - const { children, ...rest } = props; return ( - - {children} - + + {icon && } + + {label} + + ); }; 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..66190b14cf61 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,12 @@ -@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)); - } + max-height: inherit; + overflow-y: auto; + + /** If at least one menu item has an icon, we need to add extra padding for items that doesn't have an icon. */ + &:has([data-icon]:not([data-chevron])) + [role="menuitem"]:not(:has([data-icon])) { + padding-inline-start: calc( + var(--icon-size-2) + var(--inner-spacing-3) + var(--inner-spacing-2) + ); } } - -.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; -} 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..057600671d37 --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/Menu/src/types.ts @@ -0,0 +1,31 @@ +import type { + MenuProps as HeadlessMenuProps, + MenuItemProps as HeadlessMenuItemProps, +} from "react-aria-components"; +import type { Key } from "@react-types/shared"; +import type { IconProps } from "../../Icon"; + +export interface MenuProps + extends Omit< + HeadlessMenuProps, + "slot" | "selectionMode" | "selectedKeys" + > { + /** + * Whether the item has a submenu. + */ + hasSubmenu?: boolean; +} + +export interface MenuItem { + id: Key; + label?: string; + icon?: IconProps["name"]; + isDisabled?: boolean; + 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..f9e4670526ef 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,12 +1,11 @@ import React from "react"; +import { Button, Menu, MenuTrigger } from "@design-system/widgets"; import type { Meta, StoryObj } from "@storybook/react"; -import { Button, Menu, MenuList, Item, COLORS } from "@design-system/widgets"; +import { menuItems, submenusItems, submenusItemsWithIcons } from "./menuData"; /** * A menu displays a list of actions or options that a user can choose. * - * Additional information about functionality of the component can be found in the [headless component story](/?path=/docs/design-system-headless-menu--docs). - * * Item props are not pulled up in the ArgTypes, the data can be found [here](https://react-spectrum.adobe.com/react-aria/Menu.html#item). */ const meta: Meta = { @@ -19,31 +18,44 @@ type Story = StoryObj; export const Main: Story = { render: (args) => ( - alert(key)}> - - - Copy - Cut - Paste - - + + + alert(`Selected key: ${key}`)} + {...args} + /> + + ), +}; + +export const Submenus: Story = { + render: () => ( + + + + ), }; /** - * Just like Button component, There are 3 variants of the icon button component. + * The items can be disabled by passing `disabledKeys` or `isDisabled` in the item configuration. */ -export const ItemColor: Story = { + +export const DisabledItems: Story = { + render: () => ( + + + + + ), +}; + +export const WithIcons: Story = { render: () => ( - - - - {Object.values(COLORS).map((color) => ( - - {color} - - ))} - - + + + + ), }; diff --git a/app/client/packages/design-system/widgets/src/components/Menu/stories/menuData.ts b/app/client/packages/design-system/widgets/src/components/Menu/stories/menuData.ts new file mode 100644 index 000000000000..1ed8598cd4b3 --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/Menu/stories/menuData.ts @@ -0,0 +1,57 @@ +import type { MenuItem } from "../src"; + +export const menuItems: MenuItem[] = [ + { id: 1, label: "Aerospace" }, + { id: 2, label: "Mechanical" }, + { 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 submenusItems: MenuItem[] = [ + { id: 1, label: "Level 1-1" }, + { + id: 2, + label: "Level 1-2", + childItems: [ + { id: 21, label: "Level 2-1" }, + { + id: 22, + label: "Level 2-2", + childItems: [ + { id: 31, label: "Level 3-1" }, + { id: 32, label: "Level 3-2" }, + ], + }, + ], + }, + { id: 3, label: "Level 1-3" }, + { id: 4, label: "Level 1-4" }, + { id: 5, label: "Level 1-5" }, + { id: 6, label: "Level 1-6" }, + { id: 7, label: "Level 1-7" }, + { id: 8, label: "Level 1-8" }, +]; + +export const submenusItemsWithIcons: MenuItem[] = [ + { id: 1, label: "Level 1-1", icon: "galaxy" }, + { + id: 2, + label: "Level 1-2", + icon: "galaxy", + childItems: [ + { id: 21, label: "Level 2-1", icon: "galaxy" }, + { + id: 22, + label: "Level 2-2", + icon: "galaxy", + }, + ], + }, + { id: 3, label: "Level 1-3", icon: "galaxy" }, + { id: 4, label: "Level 1-4", icon: "galaxy" }, +]; diff --git a/app/client/packages/design-system/widgets/src/components/Modal/stories/ModalExamples.tsx b/app/client/packages/design-system/widgets/src/components/Modal/stories/ModalExamples.tsx index ddadecb1a6b4..0b0a560b4cce 100644 --- a/app/client/packages/design-system/widgets/src/components/Modal/stories/ModalExamples.tsx +++ b/app/client/packages/design-system/widgets/src/components/Modal/stories/ModalExamples.tsx @@ -1,7 +1,6 @@ import React, { useRef, useState } from "react"; import { - ActionGroup, - Item, + ToolbarButtons, Modal, ModalBody, ModalContent, @@ -31,7 +30,12 @@ export const ModalExamples = () => { return ( <> - { if (key === "small") { setSmallOpen(!isSmallOpen); @@ -45,11 +49,7 @@ export const ModalExamples = () => { setLargeOpen(!isLargeOpen); } }} - > - Small - Medium - Large - + /> { + const { children, ...rest } = props; + + return ( + + {children} + + ); +}; diff --git a/app/client/packages/design-system/widgets/src/components/Popover/src/index.ts b/app/client/packages/design-system/widgets/src/components/Popover/src/index.ts new file mode 100644 index 000000000000..72f124f6d38e --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/Popover/src/index.ts @@ -0,0 +1 @@ +export * from "./Popover"; diff --git a/app/client/packages/design-system/widgets/src/components/Popover/src/styles.module.css b/app/client/packages/design-system/widgets/src/components/Popover/src/styles.module.css new file mode 100644 index 000000000000..efbd9ecd429b --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/Popover/src/styles.module.css @@ -0,0 +1,6 @@ +.popover { + 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); +} diff --git a/app/client/packages/design-system/widgets/src/components/Select/src/ListBoxItem.tsx b/app/client/packages/design-system/widgets/src/components/Select/src/ListBoxItem.tsx new file mode 100644 index 000000000000..8b49ba8925c5 --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/Select/src/ListBoxItem.tsx @@ -0,0 +1,15 @@ +import React from "react"; +import { ListBoxItem as HeadlessListBoxItem } from "react-aria-components"; +import clsx from "clsx"; +import { getTypographyClassName } from "@design-system/theming"; +import { listItemStyles } from "@design-system/widgets"; +import type { ListBoxItemProps } from "react-aria-components"; + +export const ListBoxItem = (props: ListBoxItemProps) => { + return ( + + ); +}; diff --git a/app/client/packages/design-system/widgets/src/components/Select/src/Select.tsx b/app/client/packages/design-system/widgets/src/components/Select/src/Select.tsx index dfd2aa7563d7..16bf258d0068 100644 --- a/app/client/packages/design-system/widgets/src/components/Select/src/Select.tsx +++ b/app/client/packages/design-system/widgets/src/components/Select/src/Select.tsx @@ -5,10 +5,8 @@ import { Button, Label, ListBox, - Popover, - Select as SpectrumSelect, + Select as HeadlessSelect, SelectValue, - ListBoxItem as SpectrumListBoxItem, FieldError, } from "react-aria-components"; import { @@ -17,7 +15,9 @@ import { Spinner, ContextualHelp, Flex, + Popover, } from "@design-system/widgets"; +import { ListBoxItem } from "./ListBoxItem"; import styles from "./styles.module.css"; import type { SelectProps } from "./types"; @@ -34,17 +34,19 @@ export const Select = (props: SelectProps) => { ...rest } = props; const triggerRef = useRef(null); + // place Popover in the root theme provider to get access to the CSS tokens const root = document.body.querySelector( "[data-theme-provider]", ) as HTMLButtonElement; return ( - {({ isInvalid }) => ( <> @@ -87,22 +89,22 @@ export const Select = (props: SelectProps) => { {errorMessage} {Boolean(description) && !Boolean(isInvalid) && ( - + {description} )} - + {(item) => ( - + {item.icon && } - {item.name} - + {item.label} + )} )} - + ); }; diff --git a/app/client/packages/design-system/widgets/src/components/Select/src/styles.module.css b/app/client/packages/design-system/widgets/src/components/Select/src/styles.module.css index 871b47ec3172..f08761c61b32 100644 --- a/app/client/packages/design-system/widgets/src/components/Select/src/styles.module.css +++ b/app/client/packages/design-system/widgets/src/components/Select/src/styles.module.css @@ -1,5 +1,3 @@ -@import "../../../shared/colors/colors.module.css"; - .formField { display: flex; flex-direction: column; @@ -71,80 +69,13 @@ display: none; } -.popover { - 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); +.listBox { min-inline-size: var(--trigger-width); - max-height: inherit; - min-height: unset; - overflow-y: auto; -} - -.item { - display: flex; - align-items: center; - padding-inline: var(--inner-spacing-4); - padding-block: var(--inner-spacing-4); -} - -.item [data-icon] { - margin-inline-end: var(--inner-spacing-1); -} - -.item:first-of-type { - border-top-left-radius: var(--border-radius-elevation-3); - border-top-right-radius: var(--border-radius-elevation-3); -} - -.item:last-of-type { - border-bottom-left-radius: var(--border-radius-elevation-3); - border-bottom-right-radius: var(--border-radius-elevation-3); -} - -.item:not([data-disabled]) { - cursor: pointer; -} - -.item[data-hovered] { - background-color: var(--color-bg-accent-subtle-hover); } -.item[data-selected] { - background-color: var(--color-bg-accent-subtle-active); -} - -.item:not([data-disabled]) { - @each $color in colors { - &[data-color="$(color)"] { - color: var(--color-fg-$(color)); - } - } -} - -.item[data-disabled] { - opacity: var(--opacity-disabled); - cursor: not-allowed; -} - -.item[data-focus-visible] { - box-shadow: - inset 0 0 0 2px var(--color-bg), - inset 0 0 0 4px var(--color-bd-focus); -} - -.item [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 */ -.item [data-separator] > * { - display: none; -} - -/* making sure the first and last child are not displayed when they have the data-separator attribute */ -.item:is(:first-child, :last-child):is([data-separator]) { - display: none; +/** If at least one select item has an icon, we need to add extra padding for items that doesn't have an icon. */ +.listBox:has([data-icon]) [role="option"]:not(:has([data-icon])) { + padding-inline-start: calc( + var(--icon-size-2) + var(--inner-spacing-3) + var(--inner-spacing-2) + ); } diff --git a/app/client/packages/design-system/widgets/src/components/Select/src/types.ts b/app/client/packages/design-system/widgets/src/components/Select/src/types.ts index e2832b1efb68..f31daf6a3c02 100644 --- a/app/client/packages/design-system/widgets/src/components/Select/src/types.ts +++ b/app/client/packages/design-system/widgets/src/components/Select/src/types.ts @@ -1,3 +1,4 @@ +import type { Key } from "@react-types/shared"; import type { ReactNode } from "react"; import type { SelectProps as SpectrumSelectProps, @@ -5,7 +6,8 @@ import type { } from "react-aria-components"; import type { IconProps, SIZES } from "@design-system/widgets"; -export interface SelectProps extends SpectrumSelectProps { +export interface SelectProps + extends Omit, "slot"> { /** Item objects in the collection. */ items: Iterable; /** The content to display as the label. */ @@ -26,7 +28,7 @@ export interface SelectProps extends SpectrumSelectProps { } export interface SelectItem { - name: string; - key: number; + id: Key; + label: string; icon?: IconProps["name"]; } diff --git a/app/client/packages/design-system/widgets/src/components/Select/stories/Select.stories.tsx b/app/client/packages/design-system/widgets/src/components/Select/stories/Select.stories.tsx index d43b975a4cef..eaccf77057d6 100644 --- a/app/client/packages/design-system/widgets/src/components/Select/stories/Select.stories.tsx +++ b/app/client/packages/design-system/widgets/src/components/Select/stories/Select.stories.tsx @@ -1,7 +1,7 @@ import React from "react"; import type { Meta, StoryObj } from "@storybook/react"; import { Select, Button, Flex, SIZES } from "@design-system/widgets"; -import type { SelectItem } from "../src/types"; +import { selectItems, selectItemsWithIcons } from "./selectData"; /** * A select displays a collapsible list of options and allows a user to select one of them. @@ -14,21 +14,9 @@ const meta: Meta = { export default meta; type Story = StoryObj; -const items: SelectItem[] = [ - { key: 1, name: "Aerospace", icon: "rocket" }, - { key: 2, name: "Mechanical", icon: "settings" }, - { key: 3, name: "Civil" }, - { key: 4, name: "Biomedical" }, - { key: 5, name: "Nuclear" }, - { key: 6, name: "Industrial" }, - { key: 7, name: "Chemical" }, - { key: 8, name: "Agricultural" }, - { key: 9, name: "Electrical" }, -]; - export const Main: Story = { args: { - items: items, + items: selectItems, }, render: (args) => ( @@ -46,7 +34,12 @@ export const Sizes: Story = { {Object.keys(SIZES) .filter((size) => !["large"].includes(size)) .map((size) => ( - ))} ), @@ -56,7 +49,7 @@ export const Loading: Story = { args: { placeholder: "Loading", isLoading: true, - items: items, + items: selectItems, }, }; @@ -72,7 +65,7 @@ export const Validation: Story = {