diff --git a/app/client/packages/design-system/widgets/package.json b/app/client/packages/design-system/widgets/package.json index a1dd51130f..6b998431e6 100644 --- a/app/client/packages/design-system/widgets/package.json +++ b/app/client/packages/design-system/widgets/package.json @@ -22,7 +22,7 @@ "clsx": "^2.0.0", "colorjs.io": "^0.4.3", "lodash": "*", - "react-aria-components": "^1.0.0-rc.0" + "react-aria-components": "^1.1.1" }, "devDependencies": { "@types/fs-extra": "^11.0.4", diff --git a/app/client/packages/design-system/widgets/src/components/Button/src/Button.tsx b/app/client/packages/design-system/widgets/src/components/Button/src/Button.tsx index 8e7c6c6afd..648f195075 100644 --- a/app/client/packages/design-system/widgets/src/components/Button/src/Button.tsx +++ b/app/client/packages/design-system/widgets/src/components/Button/src/Button.tsx @@ -3,7 +3,7 @@ import { useVisuallyHidden } from "@react-aria/visually-hidden"; import { Button as HeadlessButton } from "@design-system/headless"; import type { ButtonRef as HeadlessButtonRef } from "@design-system/headless"; import type { SIZES } from "../../../shared"; - +import clsx from "clsx"; import { Text } from "../../Text"; import { Spinner } from "../../Spinner"; import styles from "./styles.module.css"; @@ -25,6 +25,7 @@ const _Button = (props: ButtonProps, ref: HeadlessButtonRef) => { onKeyUp, variant = "filled", visuallyDisabled = false, + className, ...rest } = props; const { visuallyHiddenProps } = useVisuallyHidden(); @@ -67,7 +68,7 @@ const _Button = (props: ButtonProps, ref: HeadlessButtonRef) => { aria-disabled={ visuallyDisabled || isLoading || isDisabled ? true : undefined } - className={styles.button} + className={clsx(className, styles.button)} data-button="" data-color={color} data-icon-position={iconPosition === "start" ? "start" : "end"} diff --git a/app/client/packages/design-system/widgets/src/components/CheckboxGroup/src/CheckboxGroup.tsx b/app/client/packages/design-system/widgets/src/components/CheckboxGroup/src/CheckboxGroup.tsx index 109c0734ff..8bb5cd61d2 100644 --- a/app/client/packages/design-system/widgets/src/components/CheckboxGroup/src/CheckboxGroup.tsx +++ b/app/client/packages/design-system/widgets/src/components/CheckboxGroup/src/CheckboxGroup.tsx @@ -7,7 +7,7 @@ import type { import { CheckboxGroup as HeadlessCheckboxGroup } from "@design-system/headless"; import { fieldStyles } from "../../../styles"; -import { ContextualHelp } from "../../TextInput/src/ContextualHelp"; +import { ContextualHelp } from "../../ContextualHelp"; import { getTypographyClassName } from "@design-system/theming"; export interface CheckboxGroupProps extends HeadlessCheckboxGroupProps { diff --git a/app/client/packages/design-system/widgets/src/components/ContextualHelp/index.ts b/app/client/packages/design-system/widgets/src/components/ContextualHelp/index.ts new file mode 100644 index 0000000000..3bd16e178a --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/ContextualHelp/index.ts @@ -0,0 +1 @@ +export * from "./src"; diff --git a/app/client/packages/design-system/widgets/src/components/TextInput/src/ContextualHelp.tsx b/app/client/packages/design-system/widgets/src/components/ContextualHelp/src/ContextualHelp.tsx similarity index 82% rename from app/client/packages/design-system/widgets/src/components/TextInput/src/ContextualHelp.tsx rename to app/client/packages/design-system/widgets/src/components/ContextualHelp/src/ContextualHelp.tsx index f99737741f..507c0d5a63 100644 --- a/app/client/packages/design-system/widgets/src/components/TextInput/src/ContextualHelp.tsx +++ b/app/client/packages/design-system/widgets/src/components/ContextualHelp/src/ContextualHelp.tsx @@ -1,10 +1,7 @@ import React from "react"; - import { Tooltip } from "../../Tooltip"; import { IconButton } from "../../IconButton"; -import type { TextInputProps } from "./TextInput"; - -export type ContextualProps = TextInputProps; +import type { ContextualProps } from "./types"; const _ContextualHelp = (props: ContextualProps) => { const { contextualHelp } = props; diff --git a/app/client/packages/design-system/widgets/src/components/ContextualHelp/src/index.ts b/app/client/packages/design-system/widgets/src/components/ContextualHelp/src/index.ts new file mode 100644 index 0000000000..b022020ec5 --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/ContextualHelp/src/index.ts @@ -0,0 +1 @@ +export * from "./ContextualHelp"; diff --git a/app/client/packages/design-system/widgets/src/components/ContextualHelp/src/types.ts b/app/client/packages/design-system/widgets/src/components/ContextualHelp/src/types.ts new file mode 100644 index 0000000000..fd14ce9216 --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/ContextualHelp/src/types.ts @@ -0,0 +1,5 @@ +import type { ReactNode } from "react"; + +export interface ContextualProps { + contextualHelp: ReactNode; +} diff --git a/app/client/packages/design-system/widgets/src/components/RadioGroup/src/RadioGroup.tsx b/app/client/packages/design-system/widgets/src/components/RadioGroup/src/RadioGroup.tsx index e9d1401b85..9c15b3b4e1 100644 --- a/app/client/packages/design-system/widgets/src/components/RadioGroup/src/RadioGroup.tsx +++ b/app/client/packages/design-system/widgets/src/components/RadioGroup/src/RadioGroup.tsx @@ -8,7 +8,7 @@ import { getTypographyClassName } from "@design-system/theming"; import { RadioGroup as HeadlessRadioGroup } from "@design-system/headless"; import { fieldStyles } from "../../../styles"; -import { ContextualHelp } from "../../TextInput/src/ContextualHelp"; +import { ContextualHelp } from "../../ContextualHelp"; export interface RadioGroupProps extends HeadlessRadioGroupProps { className?: string; diff --git a/app/client/packages/design-system/widgets/src/components/Select/index.ts b/app/client/packages/design-system/widgets/src/components/Select/index.ts new file mode 100644 index 0000000000..3bd16e178a --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/Select/index.ts @@ -0,0 +1 @@ +export * from "./src"; 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 new file mode 100644 index 0000000000..c9ef6f248b --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/Select/src/Select.tsx @@ -0,0 +1,108 @@ +import React, { useRef } from "react"; +import clsx from "clsx"; +import { getTypographyClassName } from "@design-system/theming"; +import { + Button, + Label, + ListBox, + Popover, + Select as SpectrumSelect, + SelectValue, + ListBoxItem as SpectrumListBoxItem, + FieldError, +} from "react-aria-components"; +import { + Text, + Icon, + Spinner, + ContextualHelp, + Flex, +} from "@design-system/widgets"; +import styles from "./styles.module.css"; +import type { SelectProps } from "./types"; + +export const Select = (props: SelectProps) => { + const { + contextualHelp, + description, + errorMessage, + isLoading, + isRequired, + items, + label, + size = "medium", + ...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 }) => ( + <> + + {Boolean(label) && ( + + )} + {Boolean(contextualHelp) && ( + + )} + + + + {errorMessage} + + {Boolean(description) && !Boolean(isInvalid) && ( + + {description} + + )} + + + {(item) => ( + + {item.icon && } + {item.name} + + )} + + + + )} + + ); +}; diff --git a/app/client/packages/design-system/widgets/src/components/Select/src/index.ts b/app/client/packages/design-system/widgets/src/components/Select/src/index.ts new file mode 100644 index 0000000000..b6e8a07c26 --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/Select/src/index.ts @@ -0,0 +1 @@ +export * from "./Select"; 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 new file mode 100644 index 0000000000..2f5ff969e6 --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/Select/src/styles.module.css @@ -0,0 +1,149 @@ +@import "../../../shared/colors/colors.module.css"; + +.formField { + display: flex; + flex-direction: column; + gap: var(--inner-spacing-3); + width: 100%; +} + +.textField { + display: flex; + position: relative; + padding: 0; + border: none; + align-items: center; + border-radius: var(--border-radius-elevation-3); + background-color: var(--color-bg-neutral-subtle); + max-inline-size: 100%; + padding-inline-start: var(--inner-spacing-2); + padding-inline-end: calc(var(--inner-spacing-3) + var(--icon-size-2)); + padding-block: var(--inner-spacing-3); + box-shadow: inset 0 0 0 var(--border-width-1) + var(--color-bd-on-neutral-subtle); + cursor: pointer; +} + +.formField[data-invalid] .textField { + box-shadow: 0 0 0 var(--border-width-1) var(--color-bd-negative); +} + +.formField[data-size="small"] .textField { + padding-block: var(--inner-spacing-2); +} + +.textField[data-focus-visible] { + box-shadow: + 0 0 0 2px var(--color-bg), + 0 0 0 4px var(--color-bd-focus); +} + +.textField[data-hovered] { + background-color: var(--color-bg-neutral-subtle-hover); + box-shadow: inset 0 0 0 var(--border-width-1) + var(--color-bd-on-neutral-subtle-hover); +} + +.textField [data-icon] { + position: absolute; + right: var(--inner-spacing-2); +} + +.necessityIndicator { + color: var(--color-fg-negative); + margin-inline-start: var(--inner-spacing-1); +} + +.errorText { + color: var(--color-fg-negative); +} + +.description { + color: var(--color-fg-neutral); +} + +.fieldValue { + text-align: left; + flex: 1; +} + +.fieldValue [data-icon] { + 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); + min-inline-size: var(--trigger-width); + max-height: var(--sizing-150); + 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; +} 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 new file mode 100644 index 0000000000..e2832b1efb --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/Select/src/types.ts @@ -0,0 +1,32 @@ +import type { ReactNode } from "react"; +import type { + SelectProps as SpectrumSelectProps, + ValidationResult, +} from "react-aria-components"; +import type { IconProps, SIZES } from "@design-system/widgets"; + +export interface SelectProps extends SpectrumSelectProps { + /** Item objects in the collection. */ + items: Iterable; + /** The content to display as the label. */ + label?: string; + /** The content to display as the description. */ + description?: string; + /** The content to display as the error message. */ + errorMessage?: string | ((validation: ValidationResult) => string); + /** size of the select + * + * @default medium + */ + size?: Omit; + /** loading state for the input */ + isLoading?: boolean; + /** A ContextualHelp element to place next to the label. */ + contextualHelp?: ReactNode; +} + +export interface SelectItem { + name: string; + key: number; + icon?: IconProps["name"]; +} diff --git a/app/client/packages/design-system/widgets/src/components/Select/stories/Select.stories.mdx b/app/client/packages/design-system/widgets/src/components/Select/stories/Select.stories.mdx new file mode 100644 index 0000000000..ce849d7401 --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/Select/stories/Select.stories.mdx @@ -0,0 +1,96 @@ +import { Canvas, Meta, Story, ArgsTable } from "@storybook/addon-docs"; +import { Select, Button, Flex, SIZES } from "@design-system/widgets"; + +export const items = [ + { 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 Template = (args) => ( + + + ))} + + + +## Loading + + + {Template.bind({})} + + +## Validation + + +
{ + e.preventDefault(); + alert("Form submitted"); + }} + > + +