From 6e59db227d8721aa0dd0a4dbee6316a1cfaf33dd Mon Sep 17 00:00:00 2001 From: Pawan Kumar Date: Wed, 9 Oct 2024 16:22:44 +0530 Subject: [PATCH] chore: refactor inputs (#36680) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit /ok-to-test tags="@tag.Anvil" ## Summary by CodeRabbit - **New Features** - Introduced a new `ChatInput` component for enhanced user input in chat interfaces, featuring structured input fields and dynamic height adjustments. - Added a customizable `Input` component for both single-line and multi-line text entry, with support for password visibility and loading states. - Launched a `TextAreaInput` component that integrates loading states and optional prefix/suffix elements for a versatile text area experience. - **Improvements** - Enhanced input components with new interfaces for better customization options and shared properties. - **Bug Fixes** - Resolved issues related to input sizing and alignment, ensuring a smoother user experience. - **Documentation** - Expanded documentation to include details on the new input components and their usage examples. > [!TIP] > 🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉 > Workflow run: > Commit: dc87d2de1213d23fdda1f52cee5b346d68627263 > Cypress dashboard. > Tags: `@tag.Anvil` > Spec: >
Wed, 09 Oct 2024 10:49:28 UTC --- .../src/components/Field/src/Field.tsx | 123 ---------- .../src/components/Field/src/HelpText.tsx | 48 ---- .../src/components/Field/src/Label.tsx | 76 ------ .../src/components/Field/src/index.ts | 3 - .../src/components/TextArea/src/TextArea.tsx | 120 ---------- .../src/components/TextArea/src/index.ts | 3 - .../src/components/TextArea/src/types.ts | 19 -- .../components/TextInput/src/TextInput.tsx | 55 ----- .../src/components/TextInput/src/index.ts | 3 - .../src/components/TextInput/src/types.ts | 28 --- .../TextInput/stories/TextInput.stories.ts | 19 -- .../TextInputBase/src/TextInputBase.tsx | 105 --------- .../src/components/TextInputBase/src/index.ts | 2 - .../src/components/TextInputBase/src/types.ts | 25 -- .../design-system/headless/src/index.ts | 6 +- .../src/components/Button/src/Button.tsx | 9 +- .../src/components/ChatInput}/index.ts | 0 .../components/ChatInput/src/ChatInput.tsx | 164 +++++++++++++ .../src/components/ChatInput/src/index.ts | 1 + .../src/components/ChatInput/src/types.ts | 5 + .../ChatInput/stories/ChatInput.stories.tsx | 68 ++++++ .../src/components/Checkbox/src/Checkbox.tsx | 5 +- .../src/components/ComboBox/src/ComboBox.tsx | 68 +++--- .../ComboBox/src/ComboBoxTrigger.tsx | 38 +++ .../components/ComboBox/src/styles.module.css | 84 ------- .../src/components/ComboBox/src/types.ts | 30 +-- .../ComboBox/stories/ComboBox.stories.tsx | 108 ++++----- .../src/components/ComboBox/stories/items.ts | 6 +- .../ContextualHelp/src/ContextualHelp.tsx | 6 +- .../src/components/Field}/index.ts | 0 .../widgets/src/components/Field/src/index.ts | 2 + .../components/Field/src/styles.module.css | 5 + .../widgets/src/components/Field/src/types.ts | 10 + .../FieldDescription/src/FieldDescription.tsx | 16 -- .../components/FieldDescription/src/index.ts | 2 - .../FieldDescription/src/styles.module.css | 4 - .../components/FieldDescription/src/types.ts | 6 - .../components/FieldError/src/FieldError.tsx | 20 +- .../FieldError/src/styles.module.css | 3 +- .../src/components/FieldError/src/types.ts | 2 +- .../components/FieldLabel/src/FieldLabel.tsx | 43 ++-- .../FieldLabel/src/styles.module.css | 20 +- .../src/components/FieldLabel/src/types.ts | 9 +- .../FieldListPopover/src/FieldListPopover.tsx | 36 --- .../components/FieldListPopover/src/index.ts | 2 - .../FieldListPopover/src/styles.module.css | 12 - .../components/FieldListPopover/src/types.ts | 13 - .../src/components/Input}/index.ts | 0 .../src/components/Input/src/Input.tsx | 65 +++++ .../components/Input/src/TextAreaInput.tsx | 51 ++++ .../widgets/src/components/Input/src/index.ts | 3 + .../components/Input/src/styles.module.css | 223 ++++++++++++++++++ .../widgets/src/components/Input/src/types.ts | 24 ++ .../widgets/src/components/Link/src/types.ts | 13 +- .../src/components/ListBox}/index.ts | 0 .../src/components/ListBox/src/ListBox.tsx | 15 ++ .../src/components/ListBox/src/index.ts | 2 + .../components/ListBox/src/styles.module.css | 5 + .../src/components/ListBox/src/types.ts | 3 + .../index.ts | 0 .../ListBoxItem/src/ListBoxItem.tsx | 17 ++ .../src/components/ListBoxItem/src/index.ts | 2 + .../ListBoxItem/src/styles.module.css | 83 +++++++ .../src/components/ListBoxItem/src/types.ts | 6 + .../widgets/src/components/Menu/src/Menu.tsx | 79 ++----- .../widgets/src/components/Menu/src/index.ts | 2 +- .../widgets/src/components/Menu/src/types.ts | 30 +-- .../components/Menu/stories/Menu.stories.tsx | 68 ++++-- .../src/components/Menu/stories/menuData.ts | 8 +- .../{FieldListPopover => MenuItem}/index.ts | 0 .../src/components/MenuItem/src/MenuItem.tsx | 26 ++ .../src/components/MenuItem/src/index.ts | 1 + .../src/components/MenuItem/src/types.ts | 7 + .../src/components/Popover/src/Popover.tsx | 5 +- .../src/{styles => components/Radio}/index.ts | 0 .../src/components/Radio/src/Radio.tsx | 24 ++ .../widgets/src/components/Radio/src/index.ts | 2 + .../src/styles.module.css | 18 -- .../widgets/src/components/Radio/src/types.ts | 6 + .../RadioGroup.chromatic.stories.tsx | 18 +- .../components/RadioGroup/src/RadioGroup.tsx | 54 ++--- .../src/components/RadioGroup/src/types.ts | 31 +-- .../RadioGroup/stories/RadioGroup.stories.tsx | 89 ++++--- .../RadioGroup/tests/RadioGroup.test.tsx | 52 ++-- .../src/components/Select/src/Select.tsx | 68 +++--- .../components/Select/src/SelectTrigger.tsx | 41 ++++ .../components/Select/src/styles.module.css | 61 ----- .../src/components/Select/src/types.ts | 22 +- .../Select/stories/Select.stories.tsx | 53 +++-- .../components/Select/stories/selectData.ts | 8 +- .../src/components/Switch/src/Switch.tsx | 5 +- .../src/components/TagGroup/src/TagGroup.tsx | 20 +- .../TagGroup/stories/TagGroup.stories.tsx | 9 - .../widgets/src/components/Text/src/types.ts | 2 +- .../src/components/TextArea/src/TextArea.tsx | 137 +++++++---- .../src/components/TextArea/src/index.ts | 2 +- .../components/TextArea/src/styles.module.css | 12 - .../src/components/TextArea/src/types.ts | 11 + .../TextArea/stories/TextArea.stories.tsx | 73 +++--- .../components/TextInput/src/TextInput.tsx | 115 +++------ .../src/components/TextInput/src/index.ts | 4 +- .../src/components/TextInput/src/types.ts | 10 + .../TextInput/stories/TextInput.stories.tsx | 166 +++++-------- .../chromatic/Group.chromatic.stories.tsx | 31 ++- .../ToggleGroup/src/ToggleGroup.tsx | 44 ++-- .../src/components/ToggleGroup/src/index.ts | 1 + .../ToggleGroup/src/styles.module.css | 21 +- .../src/components/ToggleGroup/src/types.ts | 33 +-- .../stories/ToggleGroup.stories.tsx | 156 +++++------- .../ToggleGroup/tests/ToggleGroup.test.tsx | 49 ++-- .../ToolbarButtons/src/ToolbarButtons.tsx | 12 +- .../chromatic/Tooltip.chromatic.stories.tsx | 35 +-- .../src/components/Tooltip/src/Tooltip.tsx | 12 +- .../components/Tooltip/src/TooltipContent.tsx | 5 +- .../src/components/Tooltip/src/index.ts | 4 +- .../Tooltip/stories/Tooltip.stories.tsx | 49 ++-- .../design-system/widgets/src/index.ts | 14 +- .../widgets/src/styles/src/field.module.css | 114 --------- .../widgets/src/styles/src/index.ts | 4 - .../src/styles/src/inline-label.module.css | 25 -- .../src/styles/src/list-item.module.css | 62 ----- .../src/styles/src/text-input.module.css | 178 -------------- .../widgets/src/testing/ComplexForm.tsx | 76 +++--- .../wds/WDSBaseInputWidget/component/types.ts | 3 +- .../WDSCheckboxGroupWidget/widget/index.tsx | 14 +- .../ui/wds/WDSComboBoxWidget/widget/index.tsx | 16 +- .../component/index.tsx | 2 +- .../ui/wds/WDSInputWidget/component/index.tsx | 4 +- .../WDSPhoneInputWidget/component/index.tsx | 2 +- .../wds/WDSRadioGroupWidget/widget/index.tsx | 11 +- .../wds/WDSRadioGroupWidget/widget/types.ts | 5 +- .../ui/wds/WDSSelectWidget/widget/index.tsx | 16 +- .../wds/WDSSwitchGroupWidget/widget/index.tsx | 13 +- .../component/cellComponents/HeaderCell.tsx | 23 +- 134 files changed, 1929 insertions(+), 2388 deletions(-) delete mode 100644 app/client/packages/design-system/headless/src/components/Field/src/Field.tsx delete mode 100644 app/client/packages/design-system/headless/src/components/Field/src/HelpText.tsx delete mode 100644 app/client/packages/design-system/headless/src/components/Field/src/Label.tsx delete mode 100644 app/client/packages/design-system/headless/src/components/Field/src/index.ts delete mode 100644 app/client/packages/design-system/headless/src/components/TextArea/src/TextArea.tsx delete mode 100644 app/client/packages/design-system/headless/src/components/TextArea/src/index.ts delete mode 100644 app/client/packages/design-system/headless/src/components/TextArea/src/types.ts delete mode 100644 app/client/packages/design-system/headless/src/components/TextInput/src/TextInput.tsx delete mode 100644 app/client/packages/design-system/headless/src/components/TextInput/src/index.ts delete mode 100644 app/client/packages/design-system/headless/src/components/TextInput/src/types.ts delete mode 100644 app/client/packages/design-system/headless/src/components/TextInput/stories/TextInput.stories.ts delete mode 100644 app/client/packages/design-system/headless/src/components/TextInputBase/src/TextInputBase.tsx delete mode 100644 app/client/packages/design-system/headless/src/components/TextInputBase/src/index.ts delete mode 100644 app/client/packages/design-system/headless/src/components/TextInputBase/src/types.ts rename app/client/packages/design-system/{headless/src/components/Field => widgets/src/components/ChatInput}/index.ts (100%) create mode 100644 app/client/packages/design-system/widgets/src/components/ChatInput/src/ChatInput.tsx create mode 100644 app/client/packages/design-system/widgets/src/components/ChatInput/src/index.ts create mode 100644 app/client/packages/design-system/widgets/src/components/ChatInput/src/types.ts create mode 100644 app/client/packages/design-system/widgets/src/components/ChatInput/stories/ChatInput.stories.tsx create mode 100644 app/client/packages/design-system/widgets/src/components/ComboBox/src/ComboBoxTrigger.tsx delete mode 100644 app/client/packages/design-system/widgets/src/components/ComboBox/src/styles.module.css rename app/client/packages/design-system/{headless/src/components/TextArea => widgets/src/components/Field}/index.ts (100%) create mode 100644 app/client/packages/design-system/widgets/src/components/Field/src/index.ts create mode 100644 app/client/packages/design-system/widgets/src/components/Field/src/styles.module.css create mode 100644 app/client/packages/design-system/widgets/src/components/Field/src/types.ts delete mode 100644 app/client/packages/design-system/widgets/src/components/FieldDescription/src/FieldDescription.tsx delete mode 100644 app/client/packages/design-system/widgets/src/components/FieldDescription/src/index.ts delete mode 100644 app/client/packages/design-system/widgets/src/components/FieldDescription/src/styles.module.css delete mode 100644 app/client/packages/design-system/widgets/src/components/FieldDescription/src/types.ts delete mode 100644 app/client/packages/design-system/widgets/src/components/FieldListPopover/src/FieldListPopover.tsx delete mode 100644 app/client/packages/design-system/widgets/src/components/FieldListPopover/src/index.ts delete mode 100644 app/client/packages/design-system/widgets/src/components/FieldListPopover/src/styles.module.css delete mode 100644 app/client/packages/design-system/widgets/src/components/FieldListPopover/src/types.ts rename app/client/packages/design-system/{headless/src/components/TextInput => widgets/src/components/Input}/index.ts (100%) create mode 100644 app/client/packages/design-system/widgets/src/components/Input/src/Input.tsx create mode 100644 app/client/packages/design-system/widgets/src/components/Input/src/TextAreaInput.tsx create mode 100644 app/client/packages/design-system/widgets/src/components/Input/src/index.ts create mode 100644 app/client/packages/design-system/widgets/src/components/Input/src/styles.module.css create mode 100644 app/client/packages/design-system/widgets/src/components/Input/src/types.ts rename app/client/packages/design-system/{headless/src/components/TextInputBase => widgets/src/components/ListBox}/index.ts (100%) create mode 100644 app/client/packages/design-system/widgets/src/components/ListBox/src/ListBox.tsx create mode 100644 app/client/packages/design-system/widgets/src/components/ListBox/src/index.ts create mode 100644 app/client/packages/design-system/widgets/src/components/ListBox/src/styles.module.css create mode 100644 app/client/packages/design-system/widgets/src/components/ListBox/src/types.ts rename app/client/packages/design-system/widgets/src/components/{FieldDescription => ListBoxItem}/index.ts (100%) create mode 100644 app/client/packages/design-system/widgets/src/components/ListBoxItem/src/ListBoxItem.tsx create mode 100644 app/client/packages/design-system/widgets/src/components/ListBoxItem/src/index.ts create mode 100644 app/client/packages/design-system/widgets/src/components/ListBoxItem/src/styles.module.css create mode 100644 app/client/packages/design-system/widgets/src/components/ListBoxItem/src/types.ts rename app/client/packages/design-system/widgets/src/components/{FieldListPopover => MenuItem}/index.ts (100%) create mode 100644 app/client/packages/design-system/widgets/src/components/MenuItem/src/MenuItem.tsx create mode 100644 app/client/packages/design-system/widgets/src/components/MenuItem/src/index.ts create mode 100644 app/client/packages/design-system/widgets/src/components/MenuItem/src/types.ts rename app/client/packages/design-system/widgets/src/{styles => components/Radio}/index.ts (100%) create mode 100644 app/client/packages/design-system/widgets/src/components/Radio/src/Radio.tsx create mode 100644 app/client/packages/design-system/widgets/src/components/Radio/src/index.ts rename app/client/packages/design-system/widgets/src/components/{RadioGroup => Radio}/src/styles.module.css (92%) create mode 100644 app/client/packages/design-system/widgets/src/components/Radio/src/types.ts create mode 100644 app/client/packages/design-system/widgets/src/components/Select/src/SelectTrigger.tsx delete mode 100644 app/client/packages/design-system/widgets/src/components/Select/src/styles.module.css delete mode 100644 app/client/packages/design-system/widgets/src/components/TextArea/src/styles.module.css create mode 100644 app/client/packages/design-system/widgets/src/components/TextArea/src/types.ts create mode 100644 app/client/packages/design-system/widgets/src/components/TextInput/src/types.ts delete mode 100644 app/client/packages/design-system/widgets/src/styles/src/field.module.css delete mode 100644 app/client/packages/design-system/widgets/src/styles/src/index.ts delete mode 100644 app/client/packages/design-system/widgets/src/styles/src/inline-label.module.css delete mode 100644 app/client/packages/design-system/widgets/src/styles/src/list-item.module.css delete mode 100644 app/client/packages/design-system/widgets/src/styles/src/text-input.module.css 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 deleted file mode 100644 index 0671befc430..00000000000 --- a/app/client/packages/design-system/headless/src/components/Field/src/Field.tsx +++ /dev/null @@ -1,123 +0,0 @@ -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"; -export type FieldProps = Pick< - SpectrumFieldProps, - | "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) => { - const { - children, - contextualHelp, - description, - descriptionProps, - elementType, - errorMessage, - errorMessageProps = {}, - fieldType = "field", - helpTextClassName, - includeNecessityIndicatorInAccessibilityName, - isDisabled = false, - isReadOnly = false, - isRequired, - label, - labelClassName, - labelProps, - necessityIndicator, - validationState, - wrapperClassName, - wrapperProps = {}, - } = props; - - // Readonly has a higher priority than disabled. - const getDisabledState = () => Boolean(isDisabled) && !Boolean(isReadOnly); - - const hasHelpText = - Boolean(description) || - (Boolean(errorMessage) && validationState === "invalid"); - - const renderHelpText = () => { - return ( - - ); - }; - - const labelAndContextualHelp = (Boolean(label) || - Boolean(contextualHelp)) && ( -
- {Boolean(label) && ( - - )} - {contextualHelp} -
- ); - - return ( -
- {labelAndContextualHelp} -
- {children} - {hasHelpText && renderHelpText()} -
-
- ); -}; - -export const Field = forwardRef(_Field); diff --git a/app/client/packages/design-system/headless/src/components/Field/src/HelpText.tsx b/app/client/packages/design-system/headless/src/components/Field/src/HelpText.tsx deleted file mode 100644 index 9ad27701202..00000000000 --- a/app/client/packages/design-system/headless/src/components/Field/src/HelpText.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import React, { forwardRef } from "react"; -import type { HTMLAttributes } from "react"; -import { useDOMRef } from "@react-spectrum/utils"; -import type { - DOMRef, - SpectrumHelpTextProps, - ValidationState, -} from "@react-types/shared"; - -interface HelpTextProps extends Omit { - /** Props for the help text description element. */ - descriptionProps?: HTMLAttributes; - /** Props for the help text error message element. */ - errorMessageProps?: HTMLAttributes; - /** classname */ - className?: string; - /** validation state for help text */ - validationState?: ValidationState; -} - -function _HelpText(props: HelpTextProps, ref: DOMRef) { - const { - className, - description, - descriptionProps, - errorMessage, - errorMessageProps, - validationState, - } = props; - const domRef = useDOMRef(ref); - const isErrorMessage = Boolean(errorMessage) && validationState === "invalid"; - - return ( -
- {isErrorMessage ? ( -
- {errorMessage} -
- ) : ( -
- {description} -
- )} -
- ); -} - -export const HelpText = forwardRef(_HelpText); diff --git a/app/client/packages/design-system/headless/src/components/Field/src/Label.tsx b/app/client/packages/design-system/headless/src/components/Field/src/Label.tsx deleted file mode 100644 index 522763a2c1d..00000000000 --- a/app/client/packages/design-system/headless/src/components/Field/src/Label.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import React, { forwardRef } from "react"; -import { useDOMRef } from "@react-spectrum/utils"; -import { filterDOMProps } from "@react-aria/utils"; -import type { DOMRef, StyleProps } from "@react-types/shared"; -import type { SpectrumLabelProps } from "@react-types/label"; - -export type LabelProps = Omit< - SpectrumLabelProps, - keyof StyleProps | "labelPosition" | "labelAlign" ->; - -const _Label = (props: LabelProps, ref: DOMRef) => { - const { - children, - className, - elementType: ElementType = "label", - for: labelFor, - htmlFor, - includeNecessityIndicatorInAccessibilityName, - isRequired, - necessityIndicator = "icon", - onClick, - ...otherProps - } = props; - - const domRef = useDOMRef(ref); - - const necessityLabel = Boolean(isRequired) ? "(required)" : "(optional)"; - const icon = ( - - * - - ); - - return ( - - {children} - {/* necessityLabel is hidden to screen readers if the field is required because - * aria-required is set on the field in that case. That will already be announced, - * so no need to duplicate it here. If optional, we do want it to be announced here. */} - {(necessityIndicator === "label" || - (necessityIndicator === "icon" && Boolean(isRequired))) && - " \u200b"} - {necessityIndicator === "label" && ( - - {necessityLabel} - - )} - {necessityIndicator === "icon" && Boolean(isRequired) && icon} - - ); -}; - -export const Label = forwardRef(_Label); diff --git a/app/client/packages/design-system/headless/src/components/Field/src/index.ts b/app/client/packages/design-system/headless/src/components/Field/src/index.ts deleted file mode 100644 index 7f94c4c65ac..00000000000 --- a/app/client/packages/design-system/headless/src/components/Field/src/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "./Field"; -export type { LabelProps } from "./Label"; -export type { FieldProps, FieldRef } from "./Field"; diff --git a/app/client/packages/design-system/headless/src/components/TextArea/src/TextArea.tsx b/app/client/packages/design-system/headless/src/components/TextArea/src/TextArea.tsx deleted file mode 100644 index 691d8d8c7b8..00000000000 --- a/app/client/packages/design-system/headless/src/components/TextArea/src/TextArea.tsx +++ /dev/null @@ -1,120 +0,0 @@ -import type { Ref } from "react"; -import React, { useCallback, useRef } from "react"; -import { useTextField } from "@react-aria/textfield"; -import { chain, useLayoutEffect } from "@react-aria/utils"; -import { useControlledState } from "@react-stately/utils"; - -import type { TextAreaProps } from "./types"; -import { TextInputBase } from "../../TextInputBase"; - -export type TextAreaRef = Ref; - -function TextArea(props: TextAreaProps, ref: TextAreaRef) { - const { - defaultValue, - isDisabled = false, - isReadOnly = false, - isRequired = false, - onChange, - value, - ...otherProps - } = props; - - const isEmpty = isReadOnly && !Boolean(value) && !Boolean(defaultValue); - - // not in stately because this is so we know when to re-measure, which is a spectrum design - const [inputValue, setInputValue] = useControlledState( - props.value, - props.defaultValue ?? "", - () => { - // - }, - ); - const inputRef = useRef(null); - - const onHeightChange = useCallback(() => { - // Quiet textareas always grow based on their text content. - // Standard textareas also grow by default, unless an explicit height is set. - if (props.height == null && inputRef.current) { - const input = inputRef.current; - const prevAlignment = input.style.alignSelf; - const prevOverflow = input.style.overflow; - // Firefox scroll position is lost when overflow: 'hidden' is applied so we skip applying it. - // The measure/applied height is also incorrect/reset if we turn on and off - // overflow: hidden in Firefox https://bugzilla.mozilla.org/show_bug.cgi?id=1787062 - const isFirefox = "MozAppearance" in input.style; - - if (!isFirefox) { - input.style.overflow = "hidden"; - } - - input.style.alignSelf = "start"; - input.style.height = "auto"; - - const computedStyle = getComputedStyle(input); - const paddingTop = parseFloat(computedStyle.paddingTop); - const paddingBottom = parseFloat(computedStyle.paddingBottom); - - input.style.height = `${ - // subtract comptued padding and border to get the actual content height - input.scrollHeight - - paddingTop - - paddingBottom + - // Also, adding 1px to fix a bug in browser where there is a scrolllbar on certain heights - 1 - }px`; - input.style.overflow = prevOverflow; - input.style.alignSelf = prevAlignment; - } - }, [inputRef, props.height]); - - useLayoutEffect(() => { - if (inputRef.current) { - onHeightChange(); - } - }, [onHeightChange, inputValue, inputRef.current]); - - if (props.placeholder != null) { - // eslint-disable-next-line no-console - console.warn( - "Placeholders are deprecated due to accessibility issues. Please use help text instead. See the docs for details: https://react-spectrum.adobe.com/react-spectrum/TextArea.html#help-text", - ); - } - - const { descriptionProps, errorMessageProps, inputProps, labelProps } = - useTextField( - { - ...props, - value: isEmpty ? "—" : value, - defaultValue, - onChange: chain(onChange, setInputValue), - inputElementType: "textarea", - }, - inputRef, - ); - - return ( - - ); -} - -/** - * TextAreas are multiline text inputs, useful for cases where users have - * a sizable amount of text to enter. They allow for all customizations that - * are available to text fields. - */ -const _TextArea = React.forwardRef(TextArea); - -export { _TextArea as TextArea }; diff --git a/app/client/packages/design-system/headless/src/components/TextArea/src/index.ts b/app/client/packages/design-system/headless/src/components/TextArea/src/index.ts deleted file mode 100644 index 5d86eab325a..00000000000 --- a/app/client/packages/design-system/headless/src/components/TextArea/src/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "./types"; -export { TextArea } from "./TextArea"; -export type { TextAreaRef } from "./TextArea"; diff --git a/app/client/packages/design-system/headless/src/components/TextArea/src/types.ts b/app/client/packages/design-system/headless/src/components/TextArea/src/types.ts deleted file mode 100644 index 59c353da581..00000000000 --- a/app/client/packages/design-system/headless/src/components/TextArea/src/types.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { TextInputBaseProps } from "../../TextInputBase"; -import type { OmitedSpectrumTextFieldProps } from "../../TextInput"; - -export interface TextAreaProps - extends OmitedSpectrumTextFieldProps, - Pick { - height?: number | string; - inputClassName?: string; - /** spell check attribute */ - spellCheck?: boolean; - /** classname for label */ - labelClassName?: string; - /** classname for errorMessage or description */ - helpTextClassName?: string; - /** classname for the field */ - fieldClassName?: string; - /** className for the text input. */ - className?: string; -} diff --git a/app/client/packages/design-system/headless/src/components/TextInput/src/TextInput.tsx b/app/client/packages/design-system/headless/src/components/TextInput/src/TextInput.tsx deleted file mode 100644 index 12687d1c282..00000000000 --- a/app/client/packages/design-system/headless/src/components/TextInput/src/TextInput.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import type { Ref } from "react"; -import React, { forwardRef, useRef } from "react"; -import { useTextField } from "@react-aria/textfield"; - -import type { TextInputProps } from "./types"; -import { TextInputBase } from "../../TextInputBase"; - -export type TextInputRef = Ref; - -function TextInput(props: TextInputProps, ref: TextInputRef) { - const inputRef = useRef(null); - const { - defaultValue, - isReadOnly = false, - spellCheck, - type: typeProp, - value, - ...rest - } = props; - - const isEmpty = isReadOnly && !Boolean(value) && !Boolean(defaultValue); - const type = typeProp === "password" && isEmpty ? "text" : typeProp; - - const { descriptionProps, errorMessageProps, inputProps, labelProps } = - useTextField( - { ...rest, type, defaultValue, value: isEmpty ? "—" : value }, - inputRef, - ); - - if (props.placeholder != null) { - // eslint-disable-next-line no-console - console.warn( - "Placeholders are deprecated due to accessibility issues. Please use help text instead. See the docs for details: https://react-spectrum.adobe.com/react-spectrum/TextField.html#help-text", - ); - } - - return ( - - ); -} - -const _TextInput = forwardRef(TextInput); - -export { _TextInput as TextInput }; diff --git a/app/client/packages/design-system/headless/src/components/TextInput/src/index.ts b/app/client/packages/design-system/headless/src/components/TextInput/src/index.ts deleted file mode 100644 index f72f0a0c706..00000000000 --- a/app/client/packages/design-system/headless/src/components/TextInput/src/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "./types"; -export { TextInput } from "./TextInput"; -export type { TextInputRef } from "./TextInput"; diff --git a/app/client/packages/design-system/headless/src/components/TextInput/src/types.ts b/app/client/packages/design-system/headless/src/components/TextInput/src/types.ts deleted file mode 100644 index 22eaf29aafb..00000000000 --- a/app/client/packages/design-system/headless/src/components/TextInput/src/types.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type { StyleProps } from "@react-types/shared"; -import type { SpectrumTextFieldProps } from "@react-types/textfield"; - -export type OmitedSpectrumTextFieldProps = Omit< - SpectrumTextFieldProps, - keyof StyleProps | "icon" | "isQuiet" | "labelPosition" | "labelAlign" ->; - -export interface TextInputProps extends OmitedSpectrumTextFieldProps { - /** classname for the input element */ - inputClassName?: string; - /** spell check attribute */ - spellCheck?: boolean; - /** classname for label */ - labelClassName?: string; - /** classname for errorMessage or description */ - helpTextClassName?: string; - /** classname for the field */ - fieldClassName?: string; - /** className for the text input. */ - className?: string; - /** indicates loading state of the text input */ - isLoading?: boolean; - /** suffix component */ - prefix?: React.ReactNode; - /** prefix component */ - suffix?: React.ReactNode; -} diff --git a/app/client/packages/design-system/headless/src/components/TextInput/stories/TextInput.stories.ts b/app/client/packages/design-system/headless/src/components/TextInput/stories/TextInput.stories.ts deleted file mode 100644 index 454be2714f7..00000000000 --- a/app/client/packages/design-system/headless/src/components/TextInput/stories/TextInput.stories.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react"; -import { TextInput } from "@appsmith/wds-headless"; - -/** - * TextInput component allows users to input text. It is mostly used in forms. - */ -const meta: Meta = { - component: TextInput, - title: "WDS/headless/TextInput", - args: { - label: "Label", - placeholder: "Placeholder", - }, -}; - -export default meta; -type Story = StoryObj; - -export const Main: Story = {}; diff --git a/app/client/packages/design-system/headless/src/components/TextInputBase/src/TextInputBase.tsx b/app/client/packages/design-system/headless/src/components/TextInputBase/src/TextInputBase.tsx deleted file mode 100644 index 25d0f401627..00000000000 --- a/app/client/packages/design-system/headless/src/components/TextInputBase/src/TextInputBase.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import type { Ref } from "react"; -import { mergeProps } from "@react-aria/utils"; -import React, { forwardRef, useRef } from "react"; -import { useHover } from "@react-aria/interactions"; -import { useFocusRing, useFocusable } from "@react-aria/focus"; - -import { Field } from "../../Field"; -import type { TextInputBaseProps } from "./types"; - -function TextInputBase(props: TextInputBaseProps, ref: Ref) { - const { - autoFocus, - descriptionProps, - errorMessageProps, - fieldClassName, - inputClassName, - inputProps, - inputRef: userInputRef, - isDisabled = false, - isLoading = false, - isReadOnly = false, - labelProps, - multiLine = false, - onBlur, - onFocus, - prefix, - suffix, - validationState, - } = props; - - // Readonly has a higher priority than disabled. - const getDisabledState = () => isDisabled && !isReadOnly; - - const { hoverProps, isHovered } = useHover({ - isDisabled: getDisabledState(), - }); - const domRef = useRef(null); - const defaultInputRef = useRef(null); - const inputRef = userInputRef ?? defaultInputRef; - - const ElementType: React.ElementType = Boolean(multiLine) - ? "textarea" - : "input"; - const isInvalid = - validationState === "invalid" && - !Boolean(isDisabled) && - !Boolean(isReadOnly); - - const { focusProps, isFocused, isFocusVisible } = useFocusRing({ - isTextInput: true, - autoFocus, - }); - - const { focusableProps } = useFocusable( - { isDisabled: getDisabledState(), onFocus: onFocus, onBlur: onBlur }, - inputRef, - ); - - // When user clicks on the startIcon or endIcon, we want to focus the input. - const focusInput: React.MouseEventHandler = () => { - inputRef.current?.focus(); - }; - - return ( - -
- {Boolean(prefix) && {prefix}} - - {Boolean(suffix) && {suffix}} -
-
- ); -} - -const _TextInputBase = forwardRef(TextInputBase); - -export { _TextInputBase as TextInputBase }; diff --git a/app/client/packages/design-system/headless/src/components/TextInputBase/src/index.ts b/app/client/packages/design-system/headless/src/components/TextInputBase/src/index.ts deleted file mode 100644 index 88f7c42bce1..00000000000 --- a/app/client/packages/design-system/headless/src/components/TextInputBase/src/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./types"; -export { TextInputBase } from "./TextInputBase"; diff --git a/app/client/packages/design-system/headless/src/components/TextInputBase/src/types.ts b/app/client/packages/design-system/headless/src/components/TextInputBase/src/types.ts deleted file mode 100644 index 51f23ee4f58..00000000000 --- a/app/client/packages/design-system/headless/src/components/TextInputBase/src/types.ts +++ /dev/null @@ -1,25 +0,0 @@ -import type { RefObject } from "react"; -import type { TextFieldAria } from "@react-aria/textfield"; - -import type { PressEvents } from "@react-types/shared"; - -import type { TextInputProps } from "../../TextInput"; - -export interface TextInputBaseProps - extends Omit, - PressEvents { - /** indicates if the component is textarea */ - multiLine?: boolean; - /** props to be passed to label component */ - labelProps?: TextFieldAria["labelProps"]; - /** props to be passed to input component */ - inputProps: TextFieldAria<"input" | "textarea">["inputProps"]; - /** props to be passed to description component */ - descriptionProps?: TextFieldAria["descriptionProps"]; - /** props to be passed to error component */ - errorMessageProps?: TextFieldAria["errorMessageProps"]; - /** ref for input component */ - inputRef?: RefObject; - /** Whether the input can be selected but not changed by the user. Readonly has a higher priority than disabled. */ - isReadOnly?: boolean; -} diff --git a/app/client/packages/design-system/headless/src/index.ts b/app/client/packages/design-system/headless/src/index.ts index 579239bb67e..a6b2628f3f8 100644 --- a/app/client/packages/design-system/headless/src/index.ts +++ b/app/client/packages/design-system/headless/src/index.ts @@ -1,6 +1,2 @@ -// components -export * from "./components/Field"; -export * from "./components/Tooltip"; -export * from "./components/TextInput"; -export * from "./components/TextArea"; export * from "./components/Popover"; +export * from "./components/Tooltip"; 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 42be29b1ece..8aeab5dee07 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 @@ -1,14 +1,13 @@ +import clsx from "clsx"; +import type { SIZES } from "@appsmith/wds"; import type { ForwardedRef } from "react"; import React, { forwardRef } from "react"; +import { Text, Spinner, Icon } from "@appsmith/wds"; import { useVisuallyHidden } from "@react-aria/visually-hidden"; import { Button as HeadlessButton } from "react-aria-components"; -import type { SIZES } from "../../../shared"; -import clsx from "clsx"; -import { Text } from "../../Text"; -import { Spinner } from "../../Spinner"; + import styles from "./styles.module.css"; import type { ButtonProps } from "./types"; -import { Icon } from "../../Icon"; const _Button = (props: ButtonProps, ref: ForwardedRef) => { props = useVisuallyDisabled(props); diff --git a/app/client/packages/design-system/headless/src/components/Field/index.ts b/app/client/packages/design-system/widgets/src/components/ChatInput/index.ts similarity index 100% rename from app/client/packages/design-system/headless/src/components/Field/index.ts rename to app/client/packages/design-system/widgets/src/components/ChatInput/index.ts diff --git a/app/client/packages/design-system/widgets/src/components/ChatInput/src/ChatInput.tsx b/app/client/packages/design-system/widgets/src/components/ChatInput/src/ChatInput.tsx new file mode 100644 index 00000000000..98feb3794d2 --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/ChatInput/src/ChatInput.tsx @@ -0,0 +1,164 @@ +import clsx from "clsx"; +import { + FieldError, + FieldLabel, + inputFieldStyles, + IconButton, + TextAreaInput, +} from "@appsmith/wds"; +import React, { useCallback, useRef, useEffect, useState } from "react"; +import { useControlledState } from "@react-stately/utils"; +import { chain, useLayoutEffect } from "@react-aria/utils"; +import { TextField as HeadlessTextField } from "react-aria-components"; + +import type { ChatInputProps } from "./types"; + +export function ChatInput(props: ChatInputProps) { + const { + contextualHelp, + errorMessage, + isDisabled, + isInvalid, + isLoading, + isReadOnly, + isRequired, + label, + onChange, + onSubmit, + prefix, + suffix: suffixProp, + value, + ...rest + } = props; + + const inputRef = useRef(null); + const [initialHeight, setInitialHeight] = useState(null); + const [inputValue, setInputValue] = useControlledState( + props.value, + props.defaultValue ?? "", + () => { + // + }, + ); + + useEffect(() => { + if (inputRef.current && initialHeight === null) { + const input = inputRef.current; + const computedStyle = window.getComputedStyle(input); + const height = parseFloat(computedStyle.height) || 0; + const paddingTop = parseFloat(computedStyle.paddingTop) || 0; + const paddingBottom = parseFloat(computedStyle.paddingBottom) || 0; + + setInitialHeight(height + paddingTop + paddingBottom); + } + }, [initialHeight]); + + const onHeightChange = useCallback(() => { + // Quiet textareas always grow based on their text content. + // Standard textareas also grow by default, unless an explicit height is set. + if (props.height == null && inputRef.current) { + const input = inputRef.current; + const prevAlignment = input.style.alignSelf; + const prevOverflow = input.style.overflow; + // Firefox scroll position is lost when overflow: 'hidden' is applied so we skip applying it. + // The measure/applied height is also incorrect/reset if we turn on and off + // overflow: hidden in Firefox https://bugzilla.mozilla.org/show_bug.cgi?id=1787062 + const isFirefox = "MozAppearance" in input.style; + + if (!isFirefox) { + input.style.overflow = "hidden"; + } + + input.style.alignSelf = "start"; + input.style.height = "auto"; + + const computedStyle = window.getComputedStyle(input); + const height = parseFloat(computedStyle.height) || 0; + const paddingTop = parseFloat(computedStyle.paddingTop) || 0; + const paddingBottom = parseFloat(computedStyle.paddingBottom) || 0; + const textHeight = input.scrollHeight - paddingTop - paddingBottom + 1; + + if (Math.abs(textHeight - height) > 10) { + input.style.height = `${textHeight}px`; + } else { + input.style.height = "auto"; + } + + input.style.overflow = prevOverflow; + input.style.alignSelf = prevAlignment; + } + }, [inputRef, props.height]); + + const handleKeyDown = useCallback( + (event: React.KeyboardEvent) => { + if (event.key === "Enter" && (event.metaKey || event.ctrlKey)) { + event.preventDefault(); + onSubmit?.(); + } + }, + [onSubmit], + ); + + useLayoutEffect(() => { + if (inputRef.current) { + onHeightChange(); + } + }, [onHeightChange, inputValue]); + + const suffix = (function () { + if (Boolean(suffixProp)) return suffixProp; + + if (Boolean(isLoading)) { + return ( + + ); + } + + return ( + + ); + })(); + + const styles = { + // The --input-height is required to make the icon button vertically centered. + // Why can't we do this with CSS? Reason is that the height of the input is calculated based on the content. + "--input-height": Boolean(initialHeight) ? `${initialHeight}px` : "auto", + } as React.CSSProperties; + + return ( + + + {label} + + + {errorMessage} + + ); +} diff --git a/app/client/packages/design-system/widgets/src/components/ChatInput/src/index.ts b/app/client/packages/design-system/widgets/src/components/ChatInput/src/index.ts new file mode 100644 index 00000000000..6499ef6fdf5 --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/ChatInput/src/index.ts @@ -0,0 +1 @@ +export { ChatInput } from "./ChatInput"; diff --git a/app/client/packages/design-system/widgets/src/components/ChatInput/src/types.ts b/app/client/packages/design-system/widgets/src/components/ChatInput/src/types.ts new file mode 100644 index 00000000000..936fc5bbf21 --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/ChatInput/src/types.ts @@ -0,0 +1,5 @@ +import type { TextAreaProps } from "@appsmith/wds"; + +export interface ChatInputProps extends TextAreaProps { + onSubmit?: () => void; +} diff --git a/app/client/packages/design-system/widgets/src/components/ChatInput/stories/ChatInput.stories.tsx b/app/client/packages/design-system/widgets/src/components/ChatInput/stories/ChatInput.stories.tsx new file mode 100644 index 00000000000..634451082af --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/ChatInput/stories/ChatInput.stories.tsx @@ -0,0 +1,68 @@ +import React from "react"; +import { Form } from "react-aria-components"; +import type { Meta, StoryObj } from "@storybook/react"; +import { Flex, ChatInput, Button } from "@appsmith/wds"; + +const meta: Meta = { + title: "WDS/Widgets/ChatInput", + component: ChatInput, + tags: ["autodocs"], + args: { + placeholder: "Write something...", + onSubmit: () => alert("Action triggered"), + }, +}; + +export default meta; +type Story = StoryObj; + +export const Main: Story = { + args: { + label: "Description", + placeholder: "Write something...", + }, +}; + +export const WithLabel: Story = { + args: { + label: "Description", + }, +}; + +export const WithContextualHelp: Story = { + args: { + label: "Description", + contextualHelp: "This is a contextual help", + }, +}; + +export const Disabled: Story = { + args: { + isDisabled: true, + label: "Disabled", + }, +}; + +export const Loading: Story = { + args: { + isLoading: true, + label: "Loading", + placeholder: "Loading...", + }, +}; + +export const Validation: Story = { + render: (args) => ( +
+ + + + +
+ ), +}; diff --git a/app/client/packages/design-system/widgets/src/components/Checkbox/src/Checkbox.tsx b/app/client/packages/design-system/widgets/src/components/Checkbox/src/Checkbox.tsx index 5dc843fa84a..685f7b26234 100644 --- a/app/client/packages/design-system/widgets/src/components/Checkbox/src/Checkbox.tsx +++ b/app/client/packages/design-system/widgets/src/components/Checkbox/src/Checkbox.tsx @@ -1,8 +1,9 @@ import React, { forwardRef } from "react"; -import { Checkbox as HeadlessCheckbox } from "react-aria-components"; +import type { ForwardedRef } from "react"; import { Text, Icon } from "@appsmith/wds"; +import { Checkbox as HeadlessCheckbox } from "react-aria-components"; + import styles from "./styles.module.css"; -import type { ForwardedRef } from "react"; import type { CheckboxProps } from "./types"; const _Checkbox = ( diff --git a/app/client/packages/design-system/widgets/src/components/ComboBox/src/ComboBox.tsx b/app/client/packages/design-system/widgets/src/components/ComboBox/src/ComboBox.tsx index 88bbcf7c229..738432dd4b3 100644 --- a/app/client/packages/design-system/widgets/src/components/ComboBox/src/ComboBox.tsx +++ b/app/client/packages/design-system/widgets/src/components/ComboBox/src/ComboBox.tsx @@ -1,65 +1,59 @@ import { - FieldError, - FieldDescription, + Popover, + ListBox, FieldLabel, - FieldListPopover, - Button, + FieldError, + inputFieldStyles, } from "@appsmith/wds"; -import { getTypographyClassName } from "@appsmith/wds-theming"; -import clsx from "clsx"; import React from "react"; -import { ComboBox as HeadlessCombobox, Input } from "react-aria-components"; -import styles from "./styles.module.css"; +import { ComboBox as HeadlessCombobox } from "react-aria-components"; + import type { ComboBoxProps } from "./types"; +import { ComboBoxTrigger } from "./ComboBoxTrigger"; export const ComboBox = (props: ComboBoxProps) => { const { + children, contextualHelp, - description, errorMessage, + isDisabled, isLoading, isRequired, - items, label, placeholder, size = "medium", ...rest } = props; + const root = document.body.querySelector( + "[data-theme-provider]", + ) as HTMLButtonElement; return ( - {({ isInvalid }) => ( - <> - -
- -
- - - - - )} + + {label} + + + {errorMessage} + + {children} +
); }; diff --git a/app/client/packages/design-system/widgets/src/components/ComboBox/src/ComboBoxTrigger.tsx b/app/client/packages/design-system/widgets/src/components/ComboBox/src/ComboBoxTrigger.tsx new file mode 100644 index 00000000000..c17bcb4cca8 --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/ComboBox/src/ComboBoxTrigger.tsx @@ -0,0 +1,38 @@ +import clsx from "clsx"; +import React, { useMemo } from "react"; +import { getTypographyClassName } from "@appsmith/wds-theming"; +import { Spinner, textInputStyles, Input, IconButton } from "@appsmith/wds"; + +import type { ComboBoxProps } from "./types"; + +interface ComboBoxTriggerProps { + size?: ComboBoxProps["size"]; + isLoading?: boolean; + isDisabled?: boolean; + placeholder?: string; +} + +export const ComboBoxTrigger: React.FC = (props) => { + const { isDisabled, isLoading, placeholder, size } = props; + + const suffix = useMemo(() => { + if (Boolean(isLoading)) return ; + + return ( + + ); + }, [isLoading, size, isDisabled]); + + return ( + + ); +}; diff --git a/app/client/packages/design-system/widgets/src/components/ComboBox/src/styles.module.css b/app/client/packages/design-system/widgets/src/components/ComboBox/src/styles.module.css deleted file mode 100644 index 7a9ecadc7e3..00000000000 --- a/app/client/packages/design-system/widgets/src/components/ComboBox/src/styles.module.css +++ /dev/null @@ -1,84 +0,0 @@ -.formField { - display: flex; - flex-direction: column; - width: 100%; -} - -.inputWrapper { - position: relative; - display: flex; - align-items: center; - border-radius: var(--border-radius-elevation-3); - background-color: var(--color-bg-neutral-subtle); - flex: 1; - max-inline-size: 100%; - isolation: isolate; - box-shadow: inset 0 0 0 1px var(--color-bd-on-neutral-subtle); - padding-inline-end: var(--inner-spacing-2); -} - -.input { - border: 0; - background-color: transparent; - font-family: inherit; - flex-grow: 1; - color: var(--color-fg); - text-overflow: ellipsis; - max-inline-size: 100%; - width: 100%; - box-sizing: content-box; - padding-block: var(--inner-spacing-1); - padding-inline: var(--inner-spacing-2); -} - -.inputWrapper:has([data-hovered]) { - background-color: var(--color-bg-neutral-subtle-hover); - box-shadow: inset 0 0 0 1px var(--color-bd-on-neutral-subtle-hover); -} - -.inputWrapper:has([data-focused]) { - background-color: transparent; - box-shadow: none; -} - -.inputWrapper:has([data-focused]):before { - content: ""; - left: 0; - width: 100%; - height: 100%; - position: absolute; - box-shadow: 0 0 0 2px var(--color-bd-focus); - border-radius: var(--border-radius-elevation-3); - z-index: -1; -} - -.inputWrapper:has([data-invalid]) { - box-shadow: 0 0 0 1px var(--color-bd-negative); -} - -.inputWrapper:has([data-invalid][data-hovered]) { - box-shadow: 0 0 0 1px var(--color-bd-negative-hover); -} - -.formField[data-size="small"] .input { - block-size: calc( - var(--body-line-height) + var(--body-margin-start) + var(--body-margin-end) - ); - padding-block: var(--inner-spacing-2); -} - -.formField[data-size="small"] .inputWrapper { - padding-inline-end: var(--inner-spacing-1); -} - -.formField .inputWrapper [data-button] { - border-radius: calc( - var(--border-radius-elevation-3) - var(--inner-spacing-1) - ); -} - -.formField[data-size="small"] .inputWrapper { - border-radius: calc( - var(--border-radius-elevation-3) - var(--inner-spacing-2) - ); -} diff --git a/app/client/packages/design-system/widgets/src/components/ComboBox/src/types.ts b/app/client/packages/design-system/widgets/src/components/ComboBox/src/types.ts index d2ae2a6341b..30e4c2de7ec 100644 --- a/app/client/packages/design-system/widgets/src/components/ComboBox/src/types.ts +++ b/app/client/packages/design-system/widgets/src/components/ComboBox/src/types.ts @@ -1,35 +1,15 @@ -import type { Key } from "@react-types/shared"; -import type { - ComboBoxProps as SpectrumComboBoxProps, - ValidationResult, -} from "react-aria-components"; -import type { IconProps, SIZES } from "@appsmith/wds"; +import type { SIZES, FieldProps } from "@appsmith/wds"; +import type { ComboBoxProps as SpectrumComboBoxProps } from "react-aria-components"; export interface ComboBoxProps - extends Omit, "slot"> { - /** Item objects in the collection. */ - items: ComboBoxItem[]; - /** 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); + extends Omit, "slot">, + FieldProps { /** 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?: string; + /** The content to display as the placeholder. */ placeholder?: string; } - -export interface ComboBoxItem { - id: Key; - label: string; - icon?: IconProps["name"]; -} diff --git a/app/client/packages/design-system/widgets/src/components/ComboBox/stories/ComboBox.stories.tsx b/app/client/packages/design-system/widgets/src/components/ComboBox/stories/ComboBox.stories.tsx index 513dcd42824..5f01ae804ca 100644 --- a/app/client/packages/design-system/widgets/src/components/ComboBox/stories/ComboBox.stories.tsx +++ b/app/client/packages/design-system/widgets/src/components/ComboBox/stories/ComboBox.stories.tsx @@ -1,14 +1,21 @@ -import { Button, ComboBox, Flex, SIZES } from "@appsmith/wds"; -import type { Meta, StoryObj } from "@storybook/react"; import React from "react"; -import { items, itemsWithIcons } from "./items"; +import { Form } from "react-aria-components"; +import type { Meta, StoryObj } from "@storybook/react"; +import { ComboBox, ListBoxItem, Flex, Button } from "@appsmith/wds"; + +import { items } from "./items"; -/** - * A select displays a collapsible list of options and allows a user to select one of them. - */ const meta: Meta = { - component: ComboBox, title: "WDS/Widgets/ComboBox", + component: ComboBox, + tags: ["autodocs"], + args: { + children: items.map((item) => ( + + {item.label} + + )), + }, }; export default meta; @@ -16,71 +23,60 @@ type Story = StoryObj; export const Main: Story = { args: { - items: items, + label: "Select an option", + placeholder: "Choose...", }, - render: (args) => ( - - - - ), }; -/** - * The component supports two sizes `small` and `medium`. Default size is `medium`. - */ -export const Sizes: Story = { - render: () => ( - - {Object.keys(SIZES) - .filter((size) => !["xSmall", "large"].includes(size)) - .map((size) => ( - - ))} - - ), +export const WithLabel: Story = { + args: { + label: "Favorite Fruit", + }, +}; + +export const WithContextualHelp: Story = { + args: { + label: "Country", + contextualHelp: "Select the country you currently reside in", + }, +}; + +export const Disabled: Story = { + args: { + isDisabled: true, + label: "Disabled ComboBox", + }, }; export const Loading: Story = { args: { - placeholder: "Loading", isLoading: true, - items: items, + label: "Loading ComboBox", + placeholder: "Loading options...", }, }; +export const Size: Story = { + render: (args) => ( + + + + + ), +}; + export const Validation: Story = { - render: () => ( -
{ - e.preventDefault(); - alert("Form submitted"); - }} - > - + render: (args) => ( + e.preventDefault()}> + - + ), }; - -export const ContextualHelp: Story = { - args: { - label: "Label", - placeholder: "Contextual Help Text", - contextualHelp: "This is a contextual help text", - items: items, - }, -}; - -export const WithIcons: Story = { - args: { - label: "With icons", - items: itemsWithIcons, - }, -}; diff --git a/app/client/packages/design-system/widgets/src/components/ComboBox/stories/items.ts b/app/client/packages/design-system/widgets/src/components/ComboBox/stories/items.ts index 7319bf939cb..3c465f7b8c5 100644 --- a/app/client/packages/design-system/widgets/src/components/ComboBox/stories/items.ts +++ b/app/client/packages/design-system/widgets/src/components/ComboBox/stories/items.ts @@ -1,6 +1,4 @@ -import type { ComboBoxItem } from "../src/types"; - -export const items: ComboBoxItem[] = [ +export const items = [ { id: 1, label: "Aerospace" }, { id: 2, @@ -15,7 +13,7 @@ export const items: ComboBoxItem[] = [ { id: 9, label: "Electrical" }, ]; -export const itemsWithIcons: ComboBoxItem[] = [ +export const itemsWithIcons = [ { id: 1, label: "Aerospace", icon: "galaxy" }, { id: 2, diff --git a/app/client/packages/design-system/widgets/src/components/ContextualHelp/src/ContextualHelp.tsx b/app/client/packages/design-system/widgets/src/components/ContextualHelp/src/ContextualHelp.tsx index bf78d5578b6..b079ad5e3e8 100644 --- a/app/client/packages/design-system/widgets/src/components/ContextualHelp/src/ContextualHelp.tsx +++ b/app/client/packages/design-system/widgets/src/components/ContextualHelp/src/ContextualHelp.tsx @@ -1,11 +1,13 @@ import React from "react"; -import { Tooltip } from "../../Tooltip"; -import { IconButton } from "../../IconButton"; +import { Tooltip, IconButton } from "@appsmith/wds"; + import type { ContextualProps } from "./types"; const _ContextualHelp = (props: ContextualProps) => { const { contextualHelp } = props; + if (!Boolean(contextualHelp)) return null; + return ( { - const { description, isInvalid } = props; - - if (!Boolean(description) || Boolean(isInvalid)) return null; - - return ( - - {description} - - ); -}; diff --git a/app/client/packages/design-system/widgets/src/components/FieldDescription/src/index.ts b/app/client/packages/design-system/widgets/src/components/FieldDescription/src/index.ts deleted file mode 100644 index 2c6b2d3578d..00000000000 --- a/app/client/packages/design-system/widgets/src/components/FieldDescription/src/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./FieldDescription"; -export type { FieldDescriptionProps } from "./types"; diff --git a/app/client/packages/design-system/widgets/src/components/FieldDescription/src/styles.module.css b/app/client/packages/design-system/widgets/src/components/FieldDescription/src/styles.module.css deleted file mode 100644 index 66f4818bdac..00000000000 --- a/app/client/packages/design-system/widgets/src/components/FieldDescription/src/styles.module.css +++ /dev/null @@ -1,4 +0,0 @@ -.description { - margin-block-start: var(--inner-spacing-3); - color: var(--color-fg-neutral); -} diff --git a/app/client/packages/design-system/widgets/src/components/FieldDescription/src/types.ts b/app/client/packages/design-system/widgets/src/components/FieldDescription/src/types.ts deleted file mode 100644 index 733ea6b20b8..00000000000 --- a/app/client/packages/design-system/widgets/src/components/FieldDescription/src/types.ts +++ /dev/null @@ -1,6 +0,0 @@ -export interface FieldDescriptionProps { - /** The content to display as the description. */ - description?: string; - /** Whether the input value is invalid. */ - isInvalid?: boolean; -} diff --git a/app/client/packages/design-system/widgets/src/components/FieldError/src/FieldError.tsx b/app/client/packages/design-system/widgets/src/components/FieldError/src/FieldError.tsx index 6b62b21869e..66548149eb0 100644 --- a/app/client/packages/design-system/widgets/src/components/FieldError/src/FieldError.tsx +++ b/app/client/packages/design-system/widgets/src/components/FieldError/src/FieldError.tsx @@ -1,18 +1,20 @@ import React from "react"; -import { getTypographyClassName } from "@appsmith/wds-theming"; -import clsx from "clsx"; -import { FieldError as HeadlessFieldError } from "react-aria-components"; +import { Text } from "@appsmith/wds"; +import { FieldError as AriaFieldError } from "react-aria-components"; + import styles from "./styles.module.css"; import type { FieldErrorProps } from "./types"; export const FieldError = (props: FieldErrorProps) => { - const { errorMessage } = props; + const { children } = props; + + if (!Boolean(children)) return null; return ( - - {errorMessage} - + + + {children} + + ); }; diff --git a/app/client/packages/design-system/widgets/src/components/FieldError/src/styles.module.css b/app/client/packages/design-system/widgets/src/components/FieldError/src/styles.module.css index bb2636b64de..f94e20aaf97 100644 --- a/app/client/packages/design-system/widgets/src/components/FieldError/src/styles.module.css +++ b/app/client/packages/design-system/widgets/src/components/FieldError/src/styles.module.css @@ -1,4 +1,3 @@ .errorText { - margin-block-start: var(--inner-spacing-3); - color: var(--color-fg-negative); + margin-block-start: var(--inner-spacing-2); } diff --git a/app/client/packages/design-system/widgets/src/components/FieldError/src/types.ts b/app/client/packages/design-system/widgets/src/components/FieldError/src/types.ts index 67de97f9072..8849a7b5b6d 100644 --- a/app/client/packages/design-system/widgets/src/components/FieldError/src/types.ts +++ b/app/client/packages/design-system/widgets/src/components/FieldError/src/types.ts @@ -2,5 +2,5 @@ import type { ValidationResult } from "react-aria-components"; export interface FieldErrorProps { /** The content to display as the error message. */ - errorMessage?: string | ((validation: ValidationResult) => string); + children?: string | ((validation: ValidationResult) => string); } diff --git a/app/client/packages/design-system/widgets/src/components/FieldLabel/src/FieldLabel.tsx b/app/client/packages/design-system/widgets/src/components/FieldLabel/src/FieldLabel.tsx index 18ce87eee19..85126a123b9 100644 --- a/app/client/packages/design-system/widgets/src/components/FieldLabel/src/FieldLabel.tsx +++ b/app/client/packages/design-system/widgets/src/components/FieldLabel/src/FieldLabel.tsx @@ -1,36 +1,37 @@ import clsx from "clsx"; import React from "react"; -import { Text, ContextualHelp } from "@appsmith/wds"; -import { Label as HeadlessLabel } from "react-aria-components"; +import { ContextualHelp, Text } from "@appsmith/wds"; +import { Label as HeadlessLabel, Group } from "react-aria-components"; + import styles from "./styles.module.css"; import type { LabelProps } from "./types"; -export const FieldLabel = (props: LabelProps) => { - const { className, contextualHelp, isDisabled, isRequired, text, ...rest } = - props; +export function FieldLabel(props: LabelProps) { + const { children, contextualHelp, isDisabled, isRequired, ...rest } = props; - if (!Boolean(text) && !Boolean(contextualHelp)) return null; + if (!Boolean(children) && !Boolean(contextualHelp)) return null; return ( - - - {text} + + + {children} + {Boolean(isRequired) && ( * )} - - {Boolean(contextualHelp) && ( - - )} - + + + ); -}; +} diff --git a/app/client/packages/design-system/widgets/src/components/FieldLabel/src/styles.module.css b/app/client/packages/design-system/widgets/src/components/FieldLabel/src/styles.module.css index a41305e0ab9..e286abff38c 100644 --- a/app/client/packages/design-system/widgets/src/components/FieldLabel/src/styles.module.css +++ b/app/client/packages/design-system/widgets/src/components/FieldLabel/src/styles.module.css @@ -1,17 +1,23 @@ -.label { +.labelGroup { display: flex; align-items: center; + gap: var(--inner-spacing-1); height: var(--sizing-3); margin-block-end: var(--inner-spacing-3); - gap: var(--inner-spacing-1); +} + +.labelGroup[data-disabled] { + opacity: var(--opacity-disabled); +} - &[data-disabled="true"] { - cursor: not-allowed; - opacity: var(--opacity-disabled); - } +.label { + display: flex; + align-items: center; + height: fit-content; + max-width: 100%; + gap: var(--inner-spacing-1); } .necessityIndicator { color: var(--color-fg-negative); - margin-inline-start: var(--inner-spacing-1); } diff --git a/app/client/packages/design-system/widgets/src/components/FieldLabel/src/types.ts b/app/client/packages/design-system/widgets/src/components/FieldLabel/src/types.ts index 1d8f083b736..10f6d021c0b 100644 --- a/app/client/packages/design-system/widgets/src/components/FieldLabel/src/types.ts +++ b/app/client/packages/design-system/widgets/src/components/FieldLabel/src/types.ts @@ -1,8 +1,7 @@ -import type { LabelProps as HeadlessLabelProps } from "react-aria-components"; +import type { LabelProps as AriaLabelProps } from "react-aria-components"; -export interface LabelProps extends HeadlessLabelProps { - text?: string; - contextualHelp?: string; +export type LabelProps = AriaLabelProps & { + contextualHelp?: React.ReactNode; isRequired?: boolean; isDisabled?: boolean; -} +}; diff --git a/app/client/packages/design-system/widgets/src/components/FieldListPopover/src/FieldListPopover.tsx b/app/client/packages/design-system/widgets/src/components/FieldListPopover/src/FieldListPopover.tsx deleted file mode 100644 index d9d7921a827..00000000000 --- a/app/client/packages/design-system/widgets/src/components/FieldListPopover/src/FieldListPopover.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import React from "react"; -import clsx from "clsx"; -import { getTypographyClassName } from "@appsmith/wds-theming"; -import { listItemStyles, Popover, Icon } from "@appsmith/wds"; -import { ListBox, ListBoxItem } from "react-aria-components"; -import styles from "./styles.module.css"; -import type { FieldListPopoverProps } from "./types"; - -export const FieldListPopover = (props: FieldListPopoverProps) => { - const { items } = 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 ( - - - {(item) => ( - - {item.icon && } - {item.label} - - )} - - - ); -}; diff --git a/app/client/packages/design-system/widgets/src/components/FieldListPopover/src/index.ts b/app/client/packages/design-system/widgets/src/components/FieldListPopover/src/index.ts deleted file mode 100644 index be087d853eb..00000000000 --- a/app/client/packages/design-system/widgets/src/components/FieldListPopover/src/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./FieldListPopover"; -export type { FieldListPopoverProps, FieldListPopoverItem } from "./types"; diff --git a/app/client/packages/design-system/widgets/src/components/FieldListPopover/src/styles.module.css b/app/client/packages/design-system/widgets/src/components/FieldListPopover/src/styles.module.css deleted file mode 100644 index 5d7dc5e49ab..00000000000 --- a/app/client/packages/design-system/widgets/src/components/FieldListPopover/src/styles.module.css +++ /dev/null @@ -1,12 +0,0 @@ -.listBox { - min-inline-size: var(--trigger-width); - max-height: inherit; - overflow-y: auto; -} - -/** 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-4) + var(--inner-spacing-3) + var(--inner-spacing-2) - ); -} diff --git a/app/client/packages/design-system/widgets/src/components/FieldListPopover/src/types.ts b/app/client/packages/design-system/widgets/src/components/FieldListPopover/src/types.ts deleted file mode 100644 index 3c98ccf204a..00000000000 --- a/app/client/packages/design-system/widgets/src/components/FieldListPopover/src/types.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { Key } from "@react-types/shared"; -import type { IconProps } from "@appsmith/wds"; - -export interface FieldListPopoverProps { - /** Item objects in the collection. */ - items: FieldListPopoverItem[]; -} - -export interface FieldListPopoverItem { - id: Key; - label: string; - icon?: IconProps["name"]; -} diff --git a/app/client/packages/design-system/headless/src/components/TextInput/index.ts b/app/client/packages/design-system/widgets/src/components/Input/index.ts similarity index 100% rename from app/client/packages/design-system/headless/src/components/TextInput/index.ts rename to app/client/packages/design-system/widgets/src/components/Input/index.ts diff --git a/app/client/packages/design-system/widgets/src/components/Input/src/Input.tsx b/app/client/packages/design-system/widgets/src/components/Input/src/Input.tsx new file mode 100644 index 00000000000..3472e2b53ac --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/Input/src/Input.tsx @@ -0,0 +1,65 @@ +import clsx from "clsx"; +import React, { forwardRef, useState } from "react"; +import { getTypographyClassName } from "@appsmith/wds-theming"; +import { IconButton, Spinner, type IconProps } from "@appsmith/wds"; +import { Group, Input as HeadlessInput } from "react-aria-components"; + +import styles from "./styles.module.css"; +import type { InputProps } from "./types"; + +function _Input(props: InputProps, ref: React.Ref) { + const { + defaultValue, + isLoading, + isReadOnly, + prefix, + size, + suffix: suffixProp, + type, + value, + ...rest + } = props; + const [showPassword, setShowPassword] = useState(false); + const togglePasswordVisibility = () => setShowPassword((prev) => !prev); + const isEmpty = !Boolean(value) && !Boolean(defaultValue); + + const suffix = (() => { + if (Boolean(isLoading)) return ; + + if (type === "password") { + const icon: IconProps["name"] = showPassword ? "eye-off" : "eye"; + + return ( + + ); + } + + return suffixProp; + })(); + + return ( + + + {Boolean(prefix) && {prefix}} + {Boolean(suffix) && {suffix}} + + ); +} + +export const Input = forwardRef(_Input); diff --git a/app/client/packages/design-system/widgets/src/components/Input/src/TextAreaInput.tsx b/app/client/packages/design-system/widgets/src/components/Input/src/TextAreaInput.tsx new file mode 100644 index 00000000000..13dab8294f5 --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/Input/src/TextAreaInput.tsx @@ -0,0 +1,51 @@ +import clsx from "clsx"; +import { Spinner } from "@appsmith/wds"; +import React, { forwardRef } from "react"; +import { getTypographyClassName } from "@appsmith/wds-theming"; +import { Group, TextArea as HeadlessTextArea } from "react-aria-components"; + +import styles from "./styles.module.css"; +import type { TextAreaInputProps } from "./types"; + +function _TextAreaInput( + props: TextAreaInputProps, + ref: React.Ref, +) { + const { + defaultValue, + isLoading, + isReadOnly, + prefix, + rows, + size, + suffix: suffixProp, + value, + ...rest + } = props; + const isEmpty = !Boolean(value) && !Boolean(defaultValue); + + const suffix = (() => { + if (Boolean(isLoading)) return ; + + return suffixProp; + })(); + + return ( + + + {Boolean(prefix) && {prefix}} + {Boolean(suffix) && {suffix}} + + ); +} + +export const TextAreaInput = forwardRef(_TextAreaInput); diff --git a/app/client/packages/design-system/widgets/src/components/Input/src/index.ts b/app/client/packages/design-system/widgets/src/components/Input/src/index.ts new file mode 100644 index 00000000000..c8628775096 --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/Input/src/index.ts @@ -0,0 +1,3 @@ +export * from "./Input"; +export * from "./TextAreaInput"; +export { default as textInputStyles } from "./styles.module.css"; diff --git a/app/client/packages/design-system/widgets/src/components/Input/src/styles.module.css b/app/client/packages/design-system/widgets/src/components/Input/src/styles.module.css new file mode 100644 index 00000000000..2f5dd660c0f --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/Input/src/styles.module.css @@ -0,0 +1,223 @@ +.inputGroup { + position: relative; + display: flex; + align-items: center; + gap: var(--inner-spacing-1); + width: 100%; +} + +.input { + position: relative; + display: flex; + flex: 1; + align-items: center; + box-sizing: content-box; + max-inline-size: 100%; + padding-block: var(--inner-spacing-1); + padding-inline: var(--inner-spacing-2); + gap: var(--inner-spacing-1); + border: 0; + border-radius: var(--border-radius-elevation-3); + background-color: var(--color-bg-neutral-subtle); + box-shadow: inset 0 0 0 1px var(--color-bd-on-neutral-subtle); + isolation: isolate; +} + +.input:has(> [data-select-text]) { + block-size: var(--body-line-height); +} + +.input:is(textarea) { + block-size: auto; + min-block-size: var(--sizing-16); + align-items: flex-start; + resize: none; + font-family: inherit; +} + +.input:is(textarea)[rows="1"] { + min-block-size: initial; +} + +.input:autofill, +.input:autofill:hover, +.input:autofill:focus, +.input:autofill:active { + font-size: initial; + -webkit-text-fill-color: var(--color-fg); + -webkit-box-shadow: 0 0 0 40rem var(--color-bg-neutral-subtle) inset; +} + +/** + * ---------------------------------------------------------------------------- + * SUFFIX and PREFIX + * ---------------------------------------------------------------------------- + */ +.inputGroup [data-input-suffix] button { + border-radius: calc( + var(--border-radius-elevation-3) - var(--inner-spacing-1) + ); +} + +.inputGroup:has(> [data-input-prefix]) .input { + padding-left: var(--sizing-8); +} + +.inputGroup:has(> [data-input-prefix]) .input[data-size="small"] { + padding-left: var(--sizing-6); +} + +.inputGroup:has(> [data-input-prefix]) [data-input-prefix] { + left: var(--inner-spacing-1); + position: absolute; +} + +.inputGroup:has(> [data-input-suffix]) .input { + padding-right: var(--sizing-8); +} + +.inputGroup:has(> [data-input-suffix]) .input[data-size="small"] { + padding-right: var(--sizing-6); +} + +.inputGroup:has(> [data-input-suffix]) [data-input-suffix] { + right: var(--inner-spacing-1); + position: absolute; +} + +/* Note: the following calculations are done so that icon button isn chat input is centered vertically */ +.inputGroup:has(.input[rows="1"]) [data-input-suffix] { + --icon-size: calc( + var(--body-line-height) + var(--body-margin-start) + var(--body-margin-end) + + var(--inner-spacing-3) * 2 + ); + --icon-offset: calc((var(--input-height) - var(--icon-size)) / 2); + + bottom: var(--icon-offset); + right: var(--icon-offset); +} + +.inputGroup :is([data-input-suffix], [data-input-prefix]) { + display: flex; + justify-content: center; + align-items: center; +} + +/** + * ---------------------------------------------------------------------------- + * HOVERED + * ---------------------------------------------------------------------------- + */ +.inputGroup[data-hovered] + .input:not(:is([data-focused], [data-readonly], [data-disabled])) { + background-color: var(--color-bg-neutral-subtle-hover); + box-shadow: inset 0 0 0 1px var(--color-bd-on-neutral-subtle-hover); +} + +/** + * ---------------------------------------------------------------------------- + * READONLY + * ---------------------------------------------------------------------------- + */ +.input[data-readonly] { + background-color: transparent; + box-shadow: none; + padding-inline: 0; +} + +/** Reason for doing this is because for readonly inputs, we want focus state to be wider than the component width */ +.inputGroup:has(> .input[data-readonly][data-focus-visible])::before { + content: ""; + left: calc(-0.5 * var(--inner-spacing-1)); + width: calc(100% + var(--inner-spacing-1)); + height: 100%; + position: absolute; + box-shadow: 0 0 0 2px var(--color-bd-focus); + border-radius: var(--border-radius-elevation-3); +} + +/** + * ---------------------------------------------------------------------------- + * PLACEHOLDER + * ---------------------------------------------------------------------------- + */ +.input::placeholder { + color: var(--color-fg-neutral-subtle) !important; + opacity: 1; +} + +.input:placeholder-shown { + text-overflow: ellipsis; +} + +/** + * ---------------------------------------------------------------------------- + * DISABLED + * ---------------------------------------------------------------------------- + */ +.input[data-disabled], +.input[data-disabled] :is(input, textarea), +.input[data-disabled] label { + cursor: not-allowed; + box-shadow: none; +} + +/** + * ---------------------------------------------------------------------------- + * INVALID + * ---------------------------------------------------------------------------- + */ +.input[data-invalid] { + box-shadow: 0 0 0 1px var(--color-bd-negative); +} + +.inputGroup[data-hovered] + .input[data-invalid]:not( + :is([data-focused], [data-readonly], [data-disabled]) + ) { + box-shadow: 0 0 0 1px var(--color-bd-negative-hover); +} + +/** + * ---------------------------------------------------------------------------- + * FOCUSSED + * ---------------------------------------------------------------------------- + */ +.input[data-focused]:not([data-readonly]) { + background-color: transparent; + box-shadow: 0 0 0 2px var(--color-bd-focus); +} + +/** + * ---------------------------------------------------------------------------- + * SIZE + * ---------------------------------------------------------------------------- + */ +.input[data-size="small"] { + block-size: calc( + var(--body-line-height) + var(--body-margin-start) + var(--body-margin-end) + ); + padding-block: var(--inner-spacing-2); +} + +.input[data-size="large"] { + block-size: calc( + var(--body-line-height) + var(--body-margin-start) + var(--body-margin-end) + ); + padding-block: var(--inner-spacing-3); + padding-inline: var(--inner-spacing-3); +} + +/** + * ---------------------------------------------------------------------------- + * SELECT BUTTON's TEXT + * ---------------------------------------------------------------------------- + */ +.input [data-select-text] { + display: flex; + align-items: center; +} + +.input [data-select-text] [data-icon] { + margin-inline-end: var(--inner-spacing-1); +} diff --git a/app/client/packages/design-system/widgets/src/components/Input/src/types.ts b/app/client/packages/design-system/widgets/src/components/Input/src/types.ts new file mode 100644 index 00000000000..97ce876f1e6 --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/Input/src/types.ts @@ -0,0 +1,24 @@ +import type { SIZES } from "@appsmith/wds"; +import type { + InputProps as HeadlessInputProps, + TextAreaProps as HeadlessTextAreaProps, +} from "react-aria-components"; + +// Common properties for both Input and TextArea +interface CommonInputProps { + prefix?: React.ReactNode; + suffix?: React.ReactNode; + isLoading?: boolean; + isReadOnly?: boolean; + size?: Omit; +} + +export interface InputProps + extends Omit, + CommonInputProps {} + +export interface TextAreaInputProps + extends Omit, + CommonInputProps { + rows?: number; +} diff --git a/app/client/packages/design-system/widgets/src/components/Link/src/types.ts b/app/client/packages/design-system/widgets/src/components/Link/src/types.ts index cc335817b93..ba3d2fe5c1d 100644 --- a/app/client/packages/design-system/widgets/src/components/Link/src/types.ts +++ b/app/client/packages/design-system/widgets/src/components/Link/src/types.ts @@ -4,4 +4,15 @@ import type { TextProps } from "../../Text"; export interface LinkProps extends Omit, - Omit {} + Omit< + AriaLinkProps, + | "style" + | "className" + | "children" + | "isDisabled" + | "onBlur" + | "onFocus" + | "onKeyDown" + | "onKeyUp" + | "slot" + > {} diff --git a/app/client/packages/design-system/headless/src/components/TextInputBase/index.ts b/app/client/packages/design-system/widgets/src/components/ListBox/index.ts similarity index 100% rename from app/client/packages/design-system/headless/src/components/TextInputBase/index.ts rename to app/client/packages/design-system/widgets/src/components/ListBox/index.ts diff --git a/app/client/packages/design-system/widgets/src/components/ListBox/src/ListBox.tsx b/app/client/packages/design-system/widgets/src/components/ListBox/src/ListBox.tsx new file mode 100644 index 00000000000..75876ebaa68 --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/ListBox/src/ListBox.tsx @@ -0,0 +1,15 @@ +import React from "react"; +import styles from "./styles.module.css"; +import { ListBox as HeadlessListBox } from "react-aria-components"; + +import type { ListBoxProps } from "./types"; + +export function ListBox(props: ListBoxProps) { + const { children, ...rest } = props; + + return ( + + {children} + + ); +} diff --git a/app/client/packages/design-system/widgets/src/components/ListBox/src/index.ts b/app/client/packages/design-system/widgets/src/components/ListBox/src/index.ts new file mode 100644 index 00000000000..ae601b28db2 --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/ListBox/src/index.ts @@ -0,0 +1,2 @@ +export { ListBox } from "./ListBox"; +export { default as listStyles } from "./styles.module.css"; diff --git a/app/client/packages/design-system/widgets/src/components/ListBox/src/styles.module.css b/app/client/packages/design-system/widgets/src/components/ListBox/src/styles.module.css new file mode 100644 index 00000000000..b253d06f7e0 --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/ListBox/src/styles.module.css @@ -0,0 +1,5 @@ +.listBox { + min-inline-size: var(--trigger-width); + max-height: inherit; + overflow-y: auto; +} diff --git a/app/client/packages/design-system/widgets/src/components/ListBox/src/types.ts b/app/client/packages/design-system/widgets/src/components/ListBox/src/types.ts new file mode 100644 index 00000000000..ec7229b2aed --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/ListBox/src/types.ts @@ -0,0 +1,3 @@ +import type { ListBoxProps as AriaListBoxProps } from "react-aria-components"; + +export interface ListBoxProps extends AriaListBoxProps {} diff --git a/app/client/packages/design-system/widgets/src/components/FieldDescription/index.ts b/app/client/packages/design-system/widgets/src/components/ListBoxItem/index.ts similarity index 100% rename from app/client/packages/design-system/widgets/src/components/FieldDescription/index.ts rename to app/client/packages/design-system/widgets/src/components/ListBoxItem/index.ts diff --git a/app/client/packages/design-system/widgets/src/components/ListBoxItem/src/ListBoxItem.tsx b/app/client/packages/design-system/widgets/src/components/ListBoxItem/src/ListBoxItem.tsx new file mode 100644 index 00000000000..353f2444a92 --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/ListBoxItem/src/ListBoxItem.tsx @@ -0,0 +1,17 @@ +import React from "react"; +import { Icon, Text } from "@appsmith/wds"; +import { ListBoxItem as HeadlessListBoxItem } from "react-aria-components"; + +import styles from "./styles.module.css"; +import type { ListBoxItemProps } from "./types"; + +export function ListBoxItem(props: ListBoxItemProps) { + const { children, icon, ...rest } = props; + + return ( + + {icon && } + {children} + + ); +} diff --git a/app/client/packages/design-system/widgets/src/components/ListBoxItem/src/index.ts b/app/client/packages/design-system/widgets/src/components/ListBoxItem/src/index.ts new file mode 100644 index 00000000000..9554394e8f4 --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/ListBoxItem/src/index.ts @@ -0,0 +1,2 @@ +export { ListBoxItem } from "./ListBoxItem"; +export { default as listBoxItemStyles } from "./styles.module.css"; diff --git a/app/client/packages/design-system/widgets/src/components/ListBoxItem/src/styles.module.css b/app/client/packages/design-system/widgets/src/components/ListBoxItem/src/styles.module.css new file mode 100644 index 00000000000..f527b5ddff7 --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/ListBoxItem/src/styles.module.css @@ -0,0 +1,83 @@ +@import "../../../shared/colors/colors.module.css"; + +.listBoxItem { + display: flex; + align-items: center; + padding-inline: var(--inner-spacing-3); + block-size: var(--sizing-11); +} + +.listBoxItem:first-of-type { + border-top-left-radius: var(--border-radius-elevation-3); + border-top-right-radius: var(--border-radius-elevation-3); +} + +.listBoxItem:last-of-type { + border-bottom-left-radius: var(--border-radius-elevation-3); + border-bottom-right-radius: var(--border-radius-elevation-3); +} + +/** +* ---------------------------------------------------------------------------- +* ICON STYLES +*----------------------------------------------------------------------------- +*/ +.listBoxItem [data-icon] { + margin-inline-end: var(--inner-spacing-2); +} + +.listBoxItem [data-submenu-icon] { + margin-inline-end: 0; + margin-inline-start: auto; +} + +/** +* ---------------------------------------------------------------------------- +* HOVER AND ACTIVE STATES +*----------------------------------------------------------------------------- +*/ +.listBoxItem:not([data-disabled]) { + cursor: pointer; +} + +.listBoxItem[data-hovered] { + background-color: var(--color-bg-accent-subtle-hover); +} + +.listBoxItem[data-selected] { + background-color: var(--color-bg-accent-subtle-active); +} + +/** +* ---------------------------------------------------------------------------- +* DISABLED STATE +*----------------------------------------------------------------------------- +*/ +.listBoxItem[data-disabled] { + opacity: var(--opacity-disabled); + cursor: not-allowed; +} + +/** +* ---------------------------------------------------------------------------- +* FOCUS VISIBLE +*----------------------------------------------------------------------------- +*/ +.listBoxItem[data-focus-visible] { + box-shadow: inset 0 0 0 2px var(--color-bd-focus); +} + +/** +* ---------------------------------------------------------------------------- +* SEPARATOR +*----------------------------------------------------------------------------- +*/ +.separator { + border-top: var(--border-width-1) solid var(--color-bd); + padding: 0; +} + +/* making sure the first and last child are not displayed when they have the data-separator attribute */ +.separator:is(:first-child, :last-child) { + display: none; +} diff --git a/app/client/packages/design-system/widgets/src/components/ListBoxItem/src/types.ts b/app/client/packages/design-system/widgets/src/components/ListBoxItem/src/types.ts new file mode 100644 index 00000000000..613c3a88bbf --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/ListBoxItem/src/types.ts @@ -0,0 +1,6 @@ +import type { ListBoxItemProps as AriaListBoxItemProps } from "react-aria-components"; +import type { IconProps } from "@appsmith/wds"; + +export interface ListBoxItemProps extends AriaListBoxItemProps { + icon?: IconProps["name"]; +} 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 830d838d02b..223ed03d7c1 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,71 +1,28 @@ -import React from "react"; -import { Icon, listItemStyles, Popover, Text } from "@appsmith/wds"; -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"; +import React, { createContext, useContext } from "react"; +import { listStyles, Popover } from "@appsmith/wds"; +import { Menu as HeadlessMenu } from "react-aria-components"; + +import type { MenuProps } from "./types"; + +const MenuNestingContext = createContext(0); export const Menu = (props: MenuProps) => { - const { hasSubmenu = false } = props; - // place Popover in the root theme provider to get access to the CSS tokens + const { children } = props; 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 ; + const nestingLevel = useContext(MenuNestingContext); + const isRootMenu = nestingLevel === 0; return ( - - {icon && } - - {label} - - + + {/* Only the parent Popover should be placed in the root. Placing child popoves in root would cause the menu to function incorrectly */} + + + {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 6fd1d55a3c6..8c312410b43 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 { MenuTrigger } from "react-aria-components"; export * from "./types"; +export { MenuTrigger, SubmenuTrigger } from "react-aria-components"; 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 index 057600671d3..4c7b10e72f2 100644 --- 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 @@ -1,31 +1,7 @@ -import type { - MenuProps as HeadlessMenuProps, - MenuItemProps as HeadlessMenuItemProps, -} from "react-aria-components"; -import type { Key } from "@react-types/shared"; -import type { IconProps } from "../../Icon"; +import type { MenuProps as AriaMenuProps } from "react-aria-components"; export interface MenuProps extends Omit< - HeadlessMenuProps, + AriaMenuProps, "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 4c741b9c6c9..1e635bc377b 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,7 +1,12 @@ import React from "react"; -import { Button, Menu, MenuTrigger } from "@appsmith/wds"; +import { + Button, + Menu, + MenuTrigger, + MenuItem, + SubmenuTrigger, +} from "@appsmith/wds"; import type { Meta, StoryObj } from "@storybook/react"; -import { menuItems, submenusItems, submenusItemsWithIcons } from "./menuData"; /** * A menu displays a list of actions or options that a user can choose. @@ -17,14 +22,24 @@ export default meta; type Story = StoryObj; export const Main: Story = { - render: (args) => ( + render: () => ( - alert(`Selected key: ${key}`)} - {...args} - /> + + Item 1 + Item 2 + Item 3 + Item 4 + + Submenu + + Submenu Item 1 + Submenu Item 2 + Submenu Item 3 + Submenu Item 4 + + + ), }; @@ -33,20 +48,38 @@ export const Submenus: Story = { render: () => ( - + + Item 1 + Item 2 + + Submenu 1 + + Submenu 1 Item 1 + Submenu 1 Item 2 + + Submenu 2 + + Submenu 2 Item 1 + Submenu 2 Item 2 + + + + + ), }; -/** - * The items can be disabled by passing `disabledKeys` or `isDisabled` in the item configuration. - */ - export const DisabledItems: Story = { render: () => ( - + + Enabled Item + Disabled Item 1 + Disabled Item 2 + Enabled Item + ), }; @@ -55,7 +88,12 @@ export const WithIcons: Story = { render: () => ( - + + Home + Files + Settings + Help + ), }; 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 index 1ed8598cd4b..b15083cbfa8 100644 --- 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 @@ -1,6 +1,4 @@ -import type { MenuItem } from "../src"; - -export const menuItems: MenuItem[] = [ +export const menuItems = [ { id: 1, label: "Aerospace" }, { id: 2, label: "Mechanical" }, { id: 3, label: "Civil" }, @@ -12,7 +10,7 @@ export const menuItems: MenuItem[] = [ { id: 9, label: "Electrical" }, ]; -export const submenusItems: MenuItem[] = [ +export const submenusItems = [ { id: 1, label: "Level 1-1" }, { id: 2, @@ -37,7 +35,7 @@ export const submenusItems: MenuItem[] = [ { id: 8, label: "Level 1-8" }, ]; -export const submenusItemsWithIcons: MenuItem[] = [ +export const submenusItemsWithIcons = [ { id: 1, label: "Level 1-1", icon: "galaxy" }, { id: 2, diff --git a/app/client/packages/design-system/widgets/src/components/FieldListPopover/index.ts b/app/client/packages/design-system/widgets/src/components/MenuItem/index.ts similarity index 100% rename from app/client/packages/design-system/widgets/src/components/FieldListPopover/index.ts rename to app/client/packages/design-system/widgets/src/components/MenuItem/index.ts diff --git a/app/client/packages/design-system/widgets/src/components/MenuItem/src/MenuItem.tsx b/app/client/packages/design-system/widgets/src/components/MenuItem/src/MenuItem.tsx new file mode 100644 index 00000000000..edd69085669 --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/MenuItem/src/MenuItem.tsx @@ -0,0 +1,26 @@ +import React from "react"; +import { + composeRenderProps, + MenuItem as HeadlessMenuItem, +} from "react-aria-components"; +import { Icon, Text, listBoxItemStyles } from "@appsmith/wds"; + +import type { MenuItemProps } from "./types"; + +export function MenuItem(props: MenuItemProps) { + const { children, icon, ...rest } = props; + + return ( + + {composeRenderProps(children, (children, { hasSubmenu }) => ( + <> + {icon && } + {children} + {Boolean(hasSubmenu) && ( + + )} + + ))} + + ); +} diff --git a/app/client/packages/design-system/widgets/src/components/MenuItem/src/index.ts b/app/client/packages/design-system/widgets/src/components/MenuItem/src/index.ts new file mode 100644 index 00000000000..4ab04ea84cd --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/MenuItem/src/index.ts @@ -0,0 +1 @@ +export { MenuItem } from "./MenuItem"; diff --git a/app/client/packages/design-system/widgets/src/components/MenuItem/src/types.ts b/app/client/packages/design-system/widgets/src/components/MenuItem/src/types.ts new file mode 100644 index 00000000000..0d52e93be02 --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/MenuItem/src/types.ts @@ -0,0 +1,7 @@ +import type { IconProps } from "@appsmith/wds"; +import type { MenuItemProps as AriaMenuItemProps } from "react-aria-components"; + +export interface MenuItemProps extends AriaMenuItemProps { + icon?: IconProps["name"]; + isSubMenuItem?: boolean; +} diff --git a/app/client/packages/design-system/widgets/src/components/Popover/src/Popover.tsx b/app/client/packages/design-system/widgets/src/components/Popover/src/Popover.tsx index d09ecdef084..2eb2a15e212 100644 --- a/app/client/packages/design-system/widgets/src/components/Popover/src/Popover.tsx +++ b/app/client/packages/design-system/widgets/src/components/Popover/src/Popover.tsx @@ -1,13 +1,14 @@ import React from "react"; +import type { PopoverProps } from "react-aria-components"; import { Popover as HeadlessPopover } from "react-aria-components"; + import styles from "./styles.module.css"; -import type { PopoverProps } from "react-aria-components"; export const Popover = (props: PopoverProps) => { const { children, ...rest } = props; return ( - + {children} ); diff --git a/app/client/packages/design-system/widgets/src/styles/index.ts b/app/client/packages/design-system/widgets/src/components/Radio/index.ts similarity index 100% rename from app/client/packages/design-system/widgets/src/styles/index.ts rename to app/client/packages/design-system/widgets/src/components/Radio/index.ts diff --git a/app/client/packages/design-system/widgets/src/components/Radio/src/Radio.tsx b/app/client/packages/design-system/widgets/src/components/Radio/src/Radio.tsx new file mode 100644 index 00000000000..e3dfd8668c8 --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/Radio/src/Radio.tsx @@ -0,0 +1,24 @@ +import { Text } from "@appsmith/wds"; +import React, { forwardRef } from "react"; +import type { ForwardedRef } from "react"; +import { Radio as AriaRadio } from "react-aria-components"; + +import styles from "./styles.module.css"; +import type { RadioProps } from "./types"; + +const _Radio = (props: RadioProps, ref: ForwardedRef) => { + const { children, labelPosition = "end", ...rest } = props; + + return ( + + {Boolean(children) && {children}} + + ); +}; + +export const Radio = forwardRef(_Radio); diff --git a/app/client/packages/design-system/widgets/src/components/Radio/src/index.ts b/app/client/packages/design-system/widgets/src/components/Radio/src/index.ts new file mode 100644 index 00000000000..867c7ecaa65 --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/Radio/src/index.ts @@ -0,0 +1,2 @@ +export { Radio } from "./Radio"; +export type { RadioProps } from "./types"; diff --git a/app/client/packages/design-system/widgets/src/components/RadioGroup/src/styles.module.css b/app/client/packages/design-system/widgets/src/components/Radio/src/styles.module.css similarity index 92% rename from app/client/packages/design-system/widgets/src/components/RadioGroup/src/styles.module.css rename to app/client/packages/design-system/widgets/src/components/Radio/src/styles.module.css index 4da345ac742..0b1d214eb27 100644 --- a/app/client/packages/design-system/widgets/src/components/RadioGroup/src/styles.module.css +++ b/app/client/packages/design-system/widgets/src/components/Radio/src/styles.module.css @@ -1,21 +1,3 @@ -.radioGroup { - display: flex; - flex-direction: column; - width: 100%; - - &[data-orientation="vertical"] { - align-items: start; - - .radio { - margin-inline-end: auto; - } - } - - &[data-disabled] { - cursor: not-allowed; - } -} - .radio { display: flex; align-items: center; diff --git a/app/client/packages/design-system/widgets/src/components/Radio/src/types.ts b/app/client/packages/design-system/widgets/src/components/Radio/src/types.ts new file mode 100644 index 00000000000..243ee62cf73 --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/Radio/src/types.ts @@ -0,0 +1,6 @@ +import type { POSITION } from "@appsmith/wds"; +import type { RadioProps as AriaRadioProps } from "react-aria-components"; + +export interface RadioProps extends AriaRadioProps { + labelPosition?: keyof typeof POSITION; +} diff --git a/app/client/packages/design-system/widgets/src/components/RadioGroup/chromatic/RadioGroup.chromatic.stories.tsx b/app/client/packages/design-system/widgets/src/components/RadioGroup/chromatic/RadioGroup.chromatic.stories.tsx index 685d5750ad3..2225cc3f1e0 100644 --- a/app/client/packages/design-system/widgets/src/components/RadioGroup/chromatic/RadioGroup.chromatic.stories.tsx +++ b/app/client/packages/design-system/widgets/src/components/RadioGroup/chromatic/RadioGroup.chromatic.stories.tsx @@ -1,6 +1,6 @@ import React from "react"; import type { Checkbox } from "@appsmith/wds"; -import { RadioGroup } from "@appsmith/wds"; +import { Radio, RadioGroup } from "@appsmith/wds"; import type { Meta, StoryObj } from "@storybook/react"; import { StoryGrid, DataAttrWrapper } from "@design-system/storybook"; @@ -22,12 +22,24 @@ export const LightMode: Story = { {states.map((state) => ( - + + {items.map(({ label, value }) => ( + + {label} + + ))} + ))} {states.map((state) => ( - + + {items.map(({ label, value }) => ( + + {label} + + ))} + ))} 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 a06bceee6c3..de3da32c4e9 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 @@ -1,14 +1,14 @@ +import type { ForwardedRef } from "react"; import React, { forwardRef, useRef } from "react"; -import { RadioGroup as HeadlessRadioGroup, Radio } from "react-aria-components"; import { - FieldLabel, - Flex, - Text, useGroupOrientation, + inputFieldStyles, + FieldLabel, FieldError, + toggleGroupStyles, } from "@appsmith/wds"; -import styles from "./styles.module.css"; -import type { ForwardedRef } from "react"; +import { RadioGroup as HeadlessRadioGroup, Group } from "react-aria-components"; + import type { RadioGroupProps } from "./types"; const _RadioGroup = ( @@ -16,11 +16,12 @@ const _RadioGroup = ( ref: ForwardedRef, ) => { const { + children, contextualHelp, errorMessage, isDisabled, + isReadOnly, isRequired, - items, label, ...rest } = props; @@ -32,31 +33,30 @@ const _RadioGroup = ( return ( - - + {label} + + )} + - {items.map(({ label, value, ...rest }, index) => ( - - {label} - - ))} - - + {children} + + {errorMessage} ); }; diff --git a/app/client/packages/design-system/widgets/src/components/RadioGroup/src/types.ts b/app/client/packages/design-system/widgets/src/components/RadioGroup/src/types.ts index dd2260c32ec..7929aca5d62 100644 --- a/app/client/packages/design-system/widgets/src/components/RadioGroup/src/types.ts +++ b/app/client/packages/design-system/widgets/src/components/RadioGroup/src/types.ts @@ -1,34 +1,13 @@ +import type { FieldProps, ORIENTATION } from "@appsmith/wds"; import type { RadioGroupProps as HeadlessRadioGroupProps } from "react-aria-components"; -import type { ORIENTATION } from "../../../shared"; -interface RadioGroupItemProps { - value: string; - label?: string; - isSelected?: boolean; - isDisabled?: boolean; - index?: number; -} - -export interface RadioGroupProps extends HeadlessRadioGroupProps { - /** - * A ContextualHelp element to place next to the label. - */ - contextualHelp?: string; - /** - * The content to display as the label. - */ - label?: string; - /** - * Radio that belong to this group. - */ - items: RadioGroupItemProps[]; +export interface RadioGroupProps extends HeadlessRadioGroupProps, FieldProps { /** - * The axis the checkboxes should align with. - * @default 'horizontal' + * The orientation of the radio group. */ orientation?: keyof typeof ORIENTATION; /** - * An error message for the field. + * children for the radio group */ - errorMessage?: string; + children?: React.ReactNode; } diff --git a/app/client/packages/design-system/widgets/src/components/RadioGroup/stories/RadioGroup.stories.tsx b/app/client/packages/design-system/widgets/src/components/RadioGroup/stories/RadioGroup.stories.tsx index 03611026940..e16fdfbc187 100644 --- a/app/client/packages/design-system/widgets/src/components/RadioGroup/stories/RadioGroup.stories.tsx +++ b/app/client/packages/design-system/widgets/src/components/RadioGroup/stories/RadioGroup.stories.tsx @@ -1,70 +1,89 @@ import React from "react"; import type { Meta, StoryObj } from "@storybook/react"; -import { RadioGroup, Flex } from "@appsmith/wds"; +import { RadioGroup, Flex, Radio } from "@appsmith/wds"; + +const items = [ + { label: "Value 1", value: "value-1" }, + { label: "Value 2", value: "value-2" }, +]; -/** - * Radio group is a component that allows users to select one option from a set of options. - */ const meta: Meta = { - component: RadioGroup, title: "WDS/Widgets/RadioGroup", + component: RadioGroup, + tags: ["autodocs"], + args: { + defaultValue: "value-1", + children: items.map((item) => ( + + {item.label} + + )), + }, }; export default meta; type Story = StoryObj; -const items = [ - { label: "Value 1", value: "value-1" }, - { label: "Value 2", value: "value-2" }, -]; - export const Main: Story = { args: { - label: "Radio Group", - defaultValue: "value-1", - items: items, + label: "Label", + }, +}; + +export const WithLabel: Story = { + args: { + label: "Description", + }, +}; + +export const WithContextualHelp: Story = { + args: { + label: "Label", + contextualHelp: "Contextual help", }, - render: (args) => , }; -/** - * The component supports two label orientations `vertical` and `horizontal`. Default size is `horizontal`. - */ export const Orientation: Story = { - render: () => ( - - - - - ), + render: () => { + return ( + + + {items.map((item) => ( + + {item.label} + + ))} + + + {items.map((item) => ( + + {item.label} + + ))} + + + ); + }, }; export const Disabled: Story = { args: { - label: "Radio Group", - defaultValue: "value-1", isDisabled: true, - items: items, + label: "Disabled", }, - render: (args) => , }; export const Required: Story = { args: { - label: "Radio Group", - defaultValue: "value-1", isRequired: true, - items: items, + label: "Required", }, - render: (args) => , }; export const Invalid: Story = { args: { - label: "Radio Group", + errorMessage: "There is an error", + label: "Invalid", isInvalid: true, - errorMessage: "This is a error message", - items: items, }, - render: (args) => , }; diff --git a/app/client/packages/design-system/widgets/src/components/RadioGroup/tests/RadioGroup.test.tsx b/app/client/packages/design-system/widgets/src/components/RadioGroup/tests/RadioGroup.test.tsx index 6be7f257879..cc5512573a9 100644 --- a/app/client/packages/design-system/widgets/src/components/RadioGroup/tests/RadioGroup.test.tsx +++ b/app/client/packages/design-system/widgets/src/components/RadioGroup/tests/RadioGroup.test.tsx @@ -2,7 +2,7 @@ import React from "react"; import "@testing-library/jest-dom"; import userEvent from "@testing-library/user-event"; import { render, screen } from "@testing-library/react"; -import { RadioGroup } from "@appsmith/wds"; +import { Radio, RadioGroup } from "@appsmith/wds"; describe("@appsmith/wds/RadioGroup", () => { const items = [ @@ -12,7 +12,13 @@ describe("@appsmith/wds/RadioGroup", () => { it("should render the Radio group", async () => { const { container } = render( - , + + {items.map(({ label, value }) => ( + + {label} + + ))} + , ); expect(screen.getByText("Value 1")).toBeInTheDocument(); @@ -46,11 +52,13 @@ describe("@appsmith/wds/RadioGroup", () => { it("should support custom props", () => { render( - , + + {items.map(({ label, value }) => ( + + {label} + + ))} + , ); const radioGroup = screen.getByTestId("t--radio-group"); @@ -60,7 +68,13 @@ describe("@appsmith/wds/RadioGroup", () => { it("should render checked checkboxes when value is passed", () => { render( - , + + {items.map(({ label, value }) => ( + + {label} + + ))} + , ); const options = screen.getAllByRole("radio"); @@ -73,11 +87,13 @@ describe("@appsmith/wds/RadioGroup", () => { const onChangeSpy = jest.fn(); render( - , + + {items.map(({ label, value }) => ( + + {label} + + ))} + , ); const options = screen.getAllByRole("radio"); @@ -87,7 +103,15 @@ describe("@appsmith/wds/RadioGroup", () => { }); it("should be able to render disabled checkboxes", () => { - render(); + render( + + {items.map(({ label, value }) => ( + + {label} + + ))} + , + ); const options = screen.getAllByRole("radio"); 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 092c3124345..0337fd709ad 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 @@ -1,40 +1,36 @@ -import React, { useRef } from "react"; +import React from "react"; import { - FieldDescription, FieldError, - Icon, FieldLabel, - Spinner, - FieldListPopover, + ListBox, + inputFieldStyles, + Popover, } from "@appsmith/wds"; -import { getTypographyClassName } from "@appsmith/wds-theming"; -import clsx from "clsx"; -import { - Button, - Select as HeadlessSelect, - SelectValue, -} from "react-aria-components"; -import styles from "./styles.module.css"; +import { Select as HeadlessSelect } from "react-aria-components"; + import type { SelectProps } from "./types"; +import { SelectTrigger } from "./SelectTrigger"; export const Select = (props: SelectProps) => { const { + children, contextualHelp, - description, errorMessage, + isDisabled, isLoading, isRequired, - items, label, + placeholder, size = "medium", ...rest } = props; - const triggerRef = useRef(null); + const root = document.body.querySelector( + "[data-theme-provider]", + ) as HTMLButtonElement; return ( { <> + {label} + + - - - - + {errorMessage} + + {children} + )} diff --git a/app/client/packages/design-system/widgets/src/components/Select/src/SelectTrigger.tsx b/app/client/packages/design-system/widgets/src/components/Select/src/SelectTrigger.tsx new file mode 100644 index 00000000000..5fcd5ca50f4 --- /dev/null +++ b/app/client/packages/design-system/widgets/src/components/Select/src/SelectTrigger.tsx @@ -0,0 +1,41 @@ +import React from "react"; +import { Icon, Spinner, textInputStyles } from "@appsmith/wds"; +import { getTypographyClassName } from "@appsmith/wds-theming"; +import { Button, Group, SelectValue } from "react-aria-components"; + +import type { SelectProps } from "./types"; + +interface SelectTriggerProps { + size?: SelectProps["size"]; + isLoading?: boolean; + isInvalid?: boolean; + placeholder?: string; + isDisabled?: boolean; +} + +export const SelectTrigger: React.FC = (props) => { + const { isDisabled, isInvalid, isLoading, placeholder, size } = props; + + return ( + + + + {Boolean(isLoading) ? : } + + + ); +}; 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 deleted file mode 100644 index f3d0aee824b..00000000000 --- a/app/client/packages/design-system/widgets/src/components/Select/src/styles.module.css +++ /dev/null @@ -1,61 +0,0 @@ -.formField { - display: flex; - flex-direction: column; - width: 100%; -} - -.textField { - display: flex; - position: relative; - padding: 0; - height: var(--sizing-9); - 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-4)); - 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-1); -} - -.fieldValue { - text-align: left; - flex: 1; -} - -.fieldValue[data-placeholder] { - color: var(--color-fg-neutral-subtle); -} - -.fieldValue [data-icon] { - 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 index 1d500e53665..d2d57a90571 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,26 +1,12 @@ -import type { - SelectProps as SpectrumSelectProps, - ValidationResult, -} from "react-aria-components"; -import type { SIZES, FieldListPopoverItem } from "@appsmith/wds"; +import type { SIZES, FieldProps } from "@appsmith/wds"; +import type { SelectProps as SpectrumSelectProps } from "react-aria-components"; export interface SelectProps - extends Omit, "slot"> { - /** Item objects in the collection. */ - items: FieldListPopoverItem[]; - /** 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); + extends Omit, "slot">, + FieldProps { /** 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?: string; } 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 a7b963f9604..84b1ae3e874 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,6 +1,7 @@ import React from "react"; import type { Meta, StoryObj } from "@storybook/react"; -import { Select, Button, Flex, SIZES } from "@appsmith/wds"; +import { Select, Button, Flex, SIZES, ListBoxItem } from "@appsmith/wds"; + import { selectItems, selectItemsWithIcons } from "./selectData"; /** @@ -9,6 +10,13 @@ import { selectItems, selectItemsWithIcons } from "./selectData"; const meta: Meta = { component: Select, title: "WDS/Widgets/Select", + args: { + children: selectItems.map((item) => ( + + {item.label} + + )), + }, }; export default meta; @@ -16,13 +24,13 @@ type Story = StoryObj; export const Main: Story = { args: { - items: selectItems, + label: "Label", + children: selectItems.map((item) => ( + + {item.label} + + )), }, - render: (args) => ( - - + ))} ), @@ -49,7 +58,13 @@ export const Loading: Story = { args: { placeholder: "Loading", isLoading: true, - items: selectItems, + }, +}; + +export const Disabled: Story = { + args: { + placeholder: "Disabled", + isDisabled: true, }, }; @@ -63,9 +78,8 @@ export const Validation: Story = { >