diff --git a/packages/bento-design-system/src/NumberInput/NumberInput.tsx b/packages/bento-design-system/src/NumberField/BaseNumberInput.tsx similarity index 94% rename from packages/bento-design-system/src/NumberInput/NumberInput.tsx rename to packages/bento-design-system/src/NumberField/BaseNumberInput.tsx index 87ab3d2dc..aa414d370 100644 --- a/packages/bento-design-system/src/NumberInput/NumberInput.tsx +++ b/packages/bento-design-system/src/NumberField/BaseNumberInput.tsx @@ -4,23 +4,20 @@ import useDimensions from "react-cool-dimensions"; import { Label, LocalizedString, Box, Children, Columns } from ".."; import { inputRecipe } from "../Field/Field.css"; import { bodyRecipe } from "../Typography/Body/Body.css"; -import { FormatProps } from "./FormatProps"; +import { BaseNumberProps, FormatProps } from "./types"; import { useBentoConfig } from "../BentoConfigContext"; import { match, not, __ } from "ts-pattern"; import { getReadOnlyBackgroundStyle } from "../Field/utils"; import { getRadiusPropsFromConfig } from "../util/BorderRadiusConfig"; -type Props = { +type Props = BaseNumberProps & { inputProps: React.InputHTMLAttributes; inputRef: React.Ref; - placeholder?: LocalizedString; validationState: "valid" | "invalid"; disabled?: boolean; - isReadOnly?: boolean; - rightAccessory?: Children; } & FormatProps; -export function NumberInput(props: Props) { +export function BaseNumberInput(props: Props) { const config = useBentoConfig().input; const { locale } = useLocale(); @@ -132,5 +129,3 @@ export function NumberInput(props: Props) { ); } - -export type { Props as NumberInputProps }; diff --git a/packages/bento-design-system/src/NumberField/NumberField.tsx b/packages/bento-design-system/src/NumberField/NumberField.tsx index 7269c3ddf..44ec676fc 100644 --- a/packages/bento-design-system/src/NumberField/NumberField.tsx +++ b/packages/bento-design-system/src/NumberField/NumberField.tsx @@ -2,18 +2,15 @@ import { useLocale } from "@react-aria/i18n"; import { useNumberField } from "@react-aria/numberfield"; import { NumberFieldStateOptions, useNumberFieldState } from "@react-stately/numberfield"; import { useRef } from "react"; -import { Children, LocalizedString } from ".."; import { FieldProps } from "../Field/FieldProps"; -import { FormatProps } from "../NumberInput/FormatProps"; -import { useFormatOptions } from "../NumberInput/formatOptions"; +import { BaseNumberProps, FormatProps } from "./types"; +import { useFormatOptions } from "./formatOptions"; import { Field } from "../Field/Field"; -import { NumberInput } from "../NumberInput/NumberInput"; +import { BaseNumberInput } from "./BaseNumberInput"; -type Props = FieldProps & { - placeholder?: LocalizedString; - isReadOnly?: boolean; - rightAccessory?: Children; -} & FormatProps & +type Props = FieldProps & + BaseNumberProps & + FormatProps & Pick; export function NumberField(props: Props) { @@ -45,7 +42,7 @@ export function NumberField(props: Props) { assistiveTextProps={descriptionProps} errorMessageProps={errorMessageProps} > - , "aria-label" | "aria-labelledby">> & + Pick< + FieldProps, + "autoFocus" | "disabled" | "name" | "onBlur" | "onChange" | "value" + > & + BaseNumberProps & { + validationState: "valid" | "invalid"; + } & FormatProps & + Pick; + +export function NumberInput(props: Props) { + const { locale } = useLocale(); + const formatOptions = useFormatOptions(props); + const state = useNumberFieldState({ ...props, locale, formatOptions }); + const inputRef = useRef(null); + + const { inputProps } = useNumberField( + { + ...props, + isDisabled: props.disabled, + formatOptions, + }, + state, + inputRef + ); + + return ; +} + +export type { Props as NumberInputProps }; diff --git a/packages/bento-design-system/src/NumberInput/formatOptions.ts b/packages/bento-design-system/src/NumberField/formatOptions.ts similarity index 92% rename from packages/bento-design-system/src/NumberInput/formatOptions.ts rename to packages/bento-design-system/src/NumberField/formatOptions.ts index 5914012da..39b38789b 100644 --- a/packages/bento-design-system/src/NumberInput/formatOptions.ts +++ b/packages/bento-design-system/src/NumberField/formatOptions.ts @@ -1,5 +1,5 @@ import { useMemo } from "react"; -import { FormatProps } from "./FormatProps"; +import { FormatProps } from "./types"; export function useFormatOptions({ kind }: FormatProps) { // This function must be memoized, see this relevant issue: diff --git a/packages/bento-design-system/src/NumberField/types.ts b/packages/bento-design-system/src/NumberField/types.ts new file mode 100644 index 000000000..a36e0b980 --- /dev/null +++ b/packages/bento-design-system/src/NumberField/types.ts @@ -0,0 +1,20 @@ +import { Children } from "../util/Children"; +import { LocalizedString } from "../util/LocalizedString"; + +export type BaseNumberProps = { + placeholder?: LocalizedString; + isReadOnly?: boolean; + rightAccessory?: Children; +}; + +export type FormatProps = + | { + kind: "currency"; + currency: string; + } + | { + kind: "percentage"; + } + | { + kind?: "decimal"; + }; diff --git a/packages/bento-design-system/src/NumberInput/FormatProps.ts b/packages/bento-design-system/src/NumberInput/FormatProps.ts deleted file mode 100644 index ccba243b4..000000000 --- a/packages/bento-design-system/src/NumberInput/FormatProps.ts +++ /dev/null @@ -1,11 +0,0 @@ -export type FormatProps = - | { - kind: "currency"; - currency: string; - } - | { - kind: "percentage"; - } - | { - kind?: "decimal"; - }; diff --git a/packages/bento-design-system/src/SelectField/BaseSelect.tsx b/packages/bento-design-system/src/SelectField/BaseSelect.tsx new file mode 100644 index 000000000..2aa4a9c9a --- /dev/null +++ b/packages/bento-design-system/src/SelectField/BaseSelect.tsx @@ -0,0 +1,136 @@ +import Select, { MultiValue as MultiValueT, SingleValue as SingleValueT } from "react-select"; +import { useDefaultMessages } from ".."; +import { FieldProps } from "../Field/FieldProps"; +import { BentoConfigProvider, useBentoConfig } from "../BentoConfigContext"; +import { AriaLabelingProps, DOMProps } from "@react-types/shared"; +import * as selectComponents from "./components"; +import { useEffect, useRef } from "react"; +import { BaseMultiProps, BaseSelectProps, BaseSingleProps, SelectOption } from "./types"; + +type MultiProps = BaseMultiProps & + Pick, "autoFocus" | "disabled" | "name" | "onBlur" | "onChange" | "value">; + +type SingleProps = BaseSingleProps & + Pick< + FieldProps, + "autoFocus" | "disabled" | "name" | "onBlur" | "onChange" | "value" + >; + +type Props = BaseSelectProps & { + fieldProps: AriaLabelingProps & DOMProps; + validationState: "valid" | "invalid"; +} & (SingleProps | MultiProps); + +export function BaseSelect(props: Props) { + const dropdownConfig = useBentoConfig().dropdown; + const { defaultMessages } = useDefaultMessages(); + + const menuPortalTarget = useRef(); + useEffect(() => { + if (!menuPortalTarget.current) { + menuPortalTarget.current = document.createElement("div"); + } + document.body.appendChild(menuPortalTarget.current); + + return () => { + if (document.body.contains(menuPortalTarget.current!)) { + document.body.removeChild(menuPortalTarget.current!); + } + }; + }, [menuPortalTarget]); + + const { + fieldProps, + validationState, + value, + onChange, + options, + onBlur, + name, + placeholder, + disabled, + isReadOnly, + isMulti, + noOptionsMessage, + autoFocus, + menuSize = dropdownConfig.defaultMenuSize, + searchable, + } = props; + + return ( + // NOTE(gabro): SelectField has its own config for List, so we override it here using BentoConfigProvider + + ((value ?? []) as readonly A[]).includes(o.value)) - : options.find((o) => o.value === value) - } - onChange={(o) => { - if (isMulti) { - const multiValue = o as MultiValueT>; - onChange(multiValue.map((a) => a.value)); - } else { - const singleValue = o as SingleValueT>; - onChange(singleValue == null ? undefined : singleValue.value); - } - }} - onBlur={onBlur} - options={options - .slice() // avoid mutating the original array - .sort((a, b) => { - // In case of multi-select, we display the selected options first - if (isMulti) { - const selectedValues = (value ?? []) as readonly A[]; - const isSelected = (a: SelectOption) => selectedValues.includes(a.value); - if (isSelected(a) && !isSelected(b)) { - return -1; - } - if (!isSelected(a) && isSelected(b)) { - return 1; - } - } - return 0; - })} - placeholder={placeholder} - menuPortalTarget={menuPortalTarget.current} - components={{ - ...selectComponents, - MultiValue, - }} - openMenuOnFocus - styles={selectComponents.styles>()} - validationState={validationState} - isMulti={isMulti} - isClearable={false} - noOptionsMessage={() => noOptionsMessage ?? defaultMessages.SelectField.noOptionsMessage} - multiValueMessage={ - props.isMulti && (!props.multiSelectMode || props.multiSelectMode === "summary") - ? props.multiValueMessage ?? defaultMessages.SelectField.multiOptionsSelected - : undefined - } - closeMenuOnSelect={!isMulti} - hideSelectedOptions={false} - menuSize={menuSize} - menuIsOpen={isReadOnly ? false : undefined} - isSearchable={isReadOnly ? false : searchable ?? true} - showMultiSelectBulkActions={isMulti ? props.showMultiSelectBulkActions : false} - clearAllButtonLabel={ - isMulti - ? props.clearAllButtonLabel ?? defaultMessages.SelectField.clearAllButtonLabel - : undefined - } - selectAllButtonLabel={ - isMulti - ? props.selectAllButtonLabel ?? defaultMessages.SelectField.selectAllButtonLabel - : undefined - } - multiSelectMode={isMulti ? props.multiSelectMode : undefined} - /> - - + + + ); } - -// NOTE(gabro): we override MultiValue instead of ValueContainer (which would be more natural) -// because overriding ValueContainer breaks the logic for closing the menu when clicking away. -// See: https://github.com/JedWatson/react-select/issues/2239#issuecomment-861848975 -function MultiValue>(props: MultiValueProps) { - const inputConfig = useBentoConfig().input; - const dropdownConfig = useBentoConfig().dropdown; - switch (props.selectProps.multiSelectMode ?? "summary") { - case "summary": - const numberOfSelectedOptions = props.getValue().length; - - if (props.index > 0 || !props.selectProps.multiValueMessage) { - return null; - } - - if (numberOfSelectedOptions === 1) { - return selectComponents.SingleValue(props); - } - - return ( - - {props.selectProps.multiValueMessage(numberOfSelectedOptions)} - - ); - case "chips": - return ( - void} - /> - ); - } -} diff --git a/packages/bento-design-system/src/SelectField/SelectInput.tsx b/packages/bento-design-system/src/SelectField/SelectInput.tsx new file mode 100644 index 000000000..67bdf7e4a --- /dev/null +++ b/packages/bento-design-system/src/SelectField/SelectInput.tsx @@ -0,0 +1,30 @@ +import { useField } from "@react-aria/label"; +import { FieldProps } from "../Field/FieldProps"; +import { BaseMultiProps, BaseSelectProps, BaseSingleProps } from "./types"; +import { AtLeast } from "../util/AtLeast"; +import { HTMLAttributes } from "react"; +import { BaseSelect } from "./BaseSelect"; + +type MultiProps = BaseMultiProps & + Pick, "autoFocus" | "disabled" | "name" | "onBlur" | "onChange" | "value">; + +type SingleProps = BaseSingleProps & + Pick< + FieldProps, + "autoFocus" | "disabled" | "name" | "onBlur" | "onChange" | "value" + >; + +type Props = AtLeast, "aria-label" | "aria-labelledby">> & + BaseSelectProps & { + validationState: "valid" | "invalid"; + } & (SingleProps | MultiProps); + +export function SelectInput(props: Props) { + const { fieldProps } = useField({ + ...props, + }); + + return ; +} + +export type { Props as SelectInputProps }; diff --git a/packages/bento-design-system/src/SelectField/components.tsx b/packages/bento-design-system/src/SelectField/components.tsx index abbf44d1f..a4f946371 100644 --- a/packages/bento-design-system/src/SelectField/components.tsx +++ b/packages/bento-design-system/src/SelectField/components.tsx @@ -10,6 +10,7 @@ import { SingleValueProps, StylesConfig, ValueContainerProps, + MultiValueProps, } from "react-select"; import { MenuPortalProps, @@ -27,11 +28,13 @@ import { Inline, Inset, Button, + Chip, + LocalizedString, } from ".."; import { singleValue, placeholder, menu, control } from "./SelectField.css"; import { bodyRecipe } from "../Typography/Body/Body.css"; import { clsx } from "clsx"; -import type { SelectOption } from "./SelectField"; +import { SelectOption } from "./types"; import { InternalList } from "../List/InternalList"; import { ListItem } from "../List/ListItem"; import { useBentoConfig } from "../BentoConfigContext"; @@ -257,6 +260,42 @@ export function NoOptionsMessage(props: NoticeProps) { ); } +// NOTE(gabro): we override MultiValue instead of ValueContainer (which would be more natural) +// because overriding ValueContainer breaks the logic for closing the menu when clicking away. +// See: https://github.com/JedWatson/react-select/issues/2239#issuecomment-861848975 +export function MultiValue>(props: MultiValueProps) { + const inputConfig = useBentoConfig().input; + const dropdownConfig = useBentoConfig().dropdown; + switch (props.selectProps.multiSelectMode ?? "summary") { + case "summary": + const numberOfSelectedOptions = props.getValue().length; + + if (props.index > 0 || !props.selectProps.multiValueMessage) { + return null; + } + + // note(fede): we split the SingleValue instantiation from the return + // in order to avoid the conditional calling of hooks that would result from + // the short-circuiting + const singleValue = SingleValue(props); + if (numberOfSelectedOptions === 1) return singleValue; + + return ( + + {props.selectProps.multiValueMessage(numberOfSelectedOptions)} + + ); + case "chips": + return ( + void} + /> + ); + } +} + export const IndicatorSeparator = null; export function styles(): StylesConfig { diff --git a/packages/bento-design-system/src/SelectField/types.ts b/packages/bento-design-system/src/SelectField/types.ts new file mode 100644 index 000000000..88e7a2a6a --- /dev/null +++ b/packages/bento-design-system/src/SelectField/types.ts @@ -0,0 +1,39 @@ +import { ListItemProps, ListSize } from ".."; +import { LocalizedString } from "../util/LocalizedString"; +import { Omit } from "../util/Omit"; + +export type SelectOption = Omit< + ListItemProps, + "trailingIcon" | "onPress" | "href" | "isFocused" | "ignoreTabIndex" | "size" | "isSelected" +> & { + value: A; +}; + +export type BaseSingleProps = { + isMulti?: false; +}; + +export type BaseMultiProps = { + isMulti: true; + showMultiSelectBulkActions?: boolean; + selectAllButtonLabel?: LocalizedString; + clearAllButtonLabel?: LocalizedString; +} & ( + | { + multiSelectMode?: "summary"; + multiValueMessage?: (numberOfSelectedOptions: number) => LocalizedString; + } + | { + multiSelectMode: "chips"; + multiValueMessage?: never; + } +); + +export type BaseSelectProps = { + menuSize?: ListSize; + placeholder?: LocalizedString; + options: Array>; + noOptionsMessage?: LocalizedString; + isReadOnly?: boolean; + searchable?: boolean; +}; diff --git a/packages/bento-design-system/src/SliderField/SliderField.tsx b/packages/bento-design-system/src/SliderField/SliderField.tsx index f835aecfa..2f3248823 100644 --- a/packages/bento-design-system/src/SliderField/SliderField.tsx +++ b/packages/bento-design-system/src/SliderField/SliderField.tsx @@ -6,8 +6,8 @@ import { ValueBase } from "@react-types/shared"; import { useRef } from "react"; import { Field, Slider } from ".."; import { FieldProps } from "../Field/FieldProps"; -import { useFormatOptions } from "../NumberInput/formatOptions"; -import { FormatProps } from "../NumberInput/FormatProps"; +import { useFormatOptions } from "../NumberField/formatOptions"; +import { FormatProps } from "../NumberField/types"; type Props = ( | ({ diff --git a/packages/bento-design-system/src/TextField/BaseTextInput.tsx b/packages/bento-design-system/src/TextField/BaseTextInput.tsx new file mode 100644 index 000000000..2dc34297d --- /dev/null +++ b/packages/bento-design-system/src/TextField/BaseTextInput.tsx @@ -0,0 +1,117 @@ +import { useBentoConfig } from "../BentoConfigContext"; +import { Box } from "../Box/Box"; +import { Children } from "../util/Children"; +import { LocalizedString } from "../util/LocalizedString"; +import { getRadiusPropsFromConfig } from "../util/BorderRadiusConfig"; +import { inputRecipe } from "../Field/Field.css"; +import { bodyRecipe } from "../Typography/Body/Body.css"; +import { getReadOnlyBackgroundStyle } from "../Field/utils"; +import useDimensions from "react-cool-dimensions"; +import { match } from "ts-pattern"; +import { Columns } from "../Layout/Columns"; +import { IconButton } from "../IconButton/IconButton"; +import { useDefaultMessages } from "../util/useDefaultMessages"; +import { useState } from "react"; + +type Props = { + inputProps: React.InputHTMLAttributes; + inputRef: React.Ref; + placeholder?: LocalizedString; + validationState?: "valid" | "invalid"; + type?: "text" | "email" | "url" | "password"; + disabled?: boolean; + isReadOnly?: boolean; + rightAccessory?: Children; + showPasswordLabel?: never; + hidePasswordLabel?: never; +}; + +export function BaseTextInput(props: Props) { + const config = useBentoConfig().input; + const { defaultMessages } = useDefaultMessages(); + + const { observe: rightAccessoryRef, width: rightAccessoryWidth } = useDimensions({ + // This is needed to include the padding in the width + useBorderBoxSize: true, + }); + + const [showPassword, setShowPassword] = useState(false); + const passwordIcon = showPassword ? config.passwordHideIcon : config.passwordShowIcon; + const passwordIconLabel = showPassword + ? props.hidePasswordLabel ?? defaultMessages.TextField.hidePasswordLabel + : props.showPasswordLabel ?? defaultMessages.TextField.showPasswordLabel; + + const type = match(props.type ?? "text") + .with("password", () => (showPassword ? "text" : "password")) + .with("text", "email", "url", () => props.type) + .exhaustive(); + + const rightAccessory = match(props.type ?? "text") + .with("password", () => ( + // if we have both a rightAccessory and type='password', display the accessory on the left of the password toggle field + + setShowPassword((prevValue) => !prevValue)} + kind="transparent" + hierarchy="secondary" + label={passwordIconLabel} + /> + {props.rightAccessory} + + )) + .with("email", "text", "url", () => props.rightAccessory) + .exhaustive(); + + return ( + + + {rightAccessory && ( + + {rightAccessory} + + )} + + ); +} diff --git a/packages/bento-design-system/src/TextField/TextField.tsx b/packages/bento-design-system/src/TextField/TextField.tsx index 2a4cc245e..6a3d45b29 100644 --- a/packages/bento-design-system/src/TextField/TextField.tsx +++ b/packages/bento-design-system/src/TextField/TextField.tsx @@ -1,15 +1,9 @@ import { useTextField } from "@react-aria/textfield"; -import { useRef, useState } from "react"; -import { Box, IconButton, Field, Children, Columns, useDefaultMessages } from ".."; +import { useRef } from "react"; +import { Field, Children } from ".."; import { LocalizedString } from "../util/LocalizedString"; -import { inputRecipe } from "../Field/Field.css"; import { FieldProps } from "../Field/FieldProps"; -import { bodyRecipe } from "../Typography/Body/Body.css"; -import useDimensions from "react-cool-dimensions"; -import { useBentoConfig } from "../BentoConfigContext"; -import { match } from "ts-pattern"; -import { getReadOnlyBackgroundStyle } from "../Field/utils"; -import { getRadiusPropsFromConfig } from "../util/BorderRadiusConfig"; +import { BaseTextInput } from "./BaseTextInput"; type Props = FieldProps & { placeholder?: LocalizedString; @@ -21,14 +15,7 @@ type Props = FieldProps & { } & Pick, "onKeyDown" | "onKeyUp">; export function TextField(props: Props) { - const config = useBentoConfig().input; const inputRef = useRef(null); - const { defaultMessages } = useDefaultMessages(); - - const { observe: rightAccessoryRef, width: rightAccessoryWidth } = useDimensions({ - // This is needed to include the padding in the width - useBorderBoxSize: true, - }); const validationState = props.isReadOnly ? undefined : props.issues ? "invalid" : "valid"; @@ -43,35 +30,6 @@ export function TextField(props: Props) { inputRef ); - const [showPassword, setShowPassword] = useState(false); - const passwordIcon = showPassword ? config.passwordHideIcon : config.passwordShowIcon; - const passwordIconLabel = showPassword - ? props.hidePasswordLabel ?? defaultMessages.TextField.hidePasswordLabel - : props.showPasswordLabel ?? defaultMessages.TextField.showPasswordLabel; - - const type = match(props.type ?? "text") - .with("password", () => (showPassword ? "text" : "password")) - .with("text", "email", "url", () => props.type) - .exhaustive(); - - const rightAccessory = match(props.type ?? "text") - .with("password", () => ( - // if we have both a rightAccessory and type='password', display the accessory on the left of the password toggle field - - setShowPassword((prevValue) => !prevValue)} - kind="transparent" - hierarchy="secondary" - label={passwordIconLabel} - /> - {props.rightAccessory} - - )) - .with("email", "text", "url", () => props.rightAccessory) - .exhaustive(); - return ( - - - {rightAccessory && ( - - {rightAccessory} - - )} - + ); } diff --git a/packages/bento-design-system/src/TextField/TextInput.tsx b/packages/bento-design-system/src/TextField/TextInput.tsx new file mode 100644 index 000000000..eb889df49 --- /dev/null +++ b/packages/bento-design-system/src/TextField/TextInput.tsx @@ -0,0 +1,40 @@ +import { HTMLAttributes, useRef } from "react"; +import { AtLeast } from "../util/AtLeast"; +import { FieldProps } from "../Field/FieldProps"; +import { BaseTextInput } from "./BaseTextInput"; +import { useTextField } from "@react-aria/textfield"; +import { LocalizedString } from "../util/LocalizedString"; +import { Children } from "../util/Children"; + +type Props = AtLeast, "aria-label" | "aria-labelledby">> & + Pick, "autoFocus" | "disabled" | "onBlur" | "onChange" | "value"> & { + validationState: "valid" | "invalid"; + placeholder?: LocalizedString; + isReadOnly?: boolean; + type?: "text" | "email" | "url" | "password"; + rightAccessory?: Children; + showPasswordLabel?: never; + hidePasswordLabel?: never; + }; + +/** + * Standalone text input component. + * + * Since it has no label, users must pass either `aria-label` or `aria-labelledby` in order to + * preserve accessibility. + */ +export function TextInput(props: Props) { + const inputRef = useRef(null); + const { inputProps } = useTextField( + { + ...props, + validationState: props.validationState, + isDisabled: props.disabled, + }, + inputRef + ); + + return ; +} + +export type { Props as TextInputProps }; diff --git a/packages/bento-design-system/src/index.ts b/packages/bento-design-system/src/index.ts index 291981d49..8fcf755e9 100644 --- a/packages/bento-design-system/src/index.ts +++ b/packages/bento-design-system/src/index.ts @@ -52,8 +52,9 @@ export * from "./List/ListItem"; export * from "./Menu/Menu"; export * from "./Modal/Modal"; export * from "./Navigation/Navigation"; +export * from "./NumberField/BaseNumberInput"; export * from "./NumberField/NumberField"; -export * from "./NumberInput/NumberInput"; +export * from "./NumberField/NumberInput"; export * from "./Pagination/Pagination"; export * from "./Placeholder/Placeholder"; export * from "./Popover/Popover"; @@ -61,7 +62,9 @@ export * from "./ProgressBar/ProgressBar"; export * from "./RadioGroupField/RadioGroupField"; export * from "./ReadOnlyField/ReadOnlyField"; export * from "./SearchBar/SearchBar"; +export * from "./SelectField/BaseSelect"; export * from "./SelectField/SelectField"; +export * from "./SelectField/SelectInput"; export * from "./Slider/Slider"; export * from "./SliderField/SliderField"; export * from "./Stepper/Stepper"; @@ -70,7 +73,9 @@ export * from "./Table/Table"; export * from "./Tabs/Tabs"; export { bentoSprinkles } from "./internal/sprinkles.css"; export * from "./TextArea/TextArea"; +export * from "./TextField/BaseTextInput"; export * from "./TextField/TextField"; +export * from "./TextField/TextInput"; export * from "./TimeField/TimeField"; export type { TypeOverrides, diff --git a/packages/bento-design-system/stories/Components/Inputs/NumberInput.stories.tsx b/packages/bento-design-system/stories/Components/Inputs/NumberInput.stories.tsx new file mode 100644 index 000000000..2a602a895 --- /dev/null +++ b/packages/bento-design-system/stories/Components/Inputs/NumberInput.stories.tsx @@ -0,0 +1,79 @@ +import { NumberInput } from "../.."; +import { Meta, StoryObj } from "@storybook/react"; + +const meta = { + component: NumberInput, + args: { + value: undefined, + "aria-label": "number-input", + validationState: "valid", + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Default = {} satisfies Story; + +export const Disabled = { + args: { + value: 0, + disabled: true, + }, +} satisfies Story; + +export const Invalid = { + args: { + value: 0, + validationState: "invalid", + }, +} satisfies Story; + +export const Currency = { + args: { + value: 0, + kind: "currency", + currency: "EUR", + }, +} satisfies Story; + +export const Percentage = { + args: { + value: 0, + kind: "percentage", + }, +} satisfies Story; + +export const ReadOnly = { + args: { + value: 50, + kind: "percentage", + isReadOnly: true, + }, +} satisfies Story; + +export const MinMaxAndStep = { + args: { + value: 5, + minValue: 0.1, + maxValue: 10, + step: 0.5, + }, +} satisfies Story; + +export const RightAccessory = { + args: { + value: 0, + rightAccessory: "👍", + }, +} satisfies Story; + +export const KindAndRightAccessory = { + args: { + value: 0, + rightAccessory: "💰", + kind: "currency", + currency: "EUR", + }, +} satisfies Story; diff --git a/packages/bento-design-system/stories/Components/Inputs/SelectInput.stories.tsx b/packages/bento-design-system/stories/Components/Inputs/SelectInput.stories.tsx new file mode 100644 index 000000000..25364d9ca --- /dev/null +++ b/packages/bento-design-system/stories/Components/Inputs/SelectInput.stories.tsx @@ -0,0 +1,189 @@ +import { StoryFn, Meta, StoryObj } from "@storybook/react"; +import { + IconLightbulb, + IconUser, + IconPlaceholder, + Modal, + SelectInput, + BentoConfigProvider, + SelectFieldProps, +} from "../.."; + +const meta = { + component: SelectInput, + args: { + value: undefined, + "aria-label": "select-input", + validationState: "valid", + menuSize: "large", + options: [ + { + value: 1, + label: "Red", + kind: "two-line", + secondLine: "prova", + icon: IconPlaceholder, + }, + { + value: 2, + label: "Blue", + kind: "two-line", + secondLine: "prova", + icon: IconPlaceholder, + }, + { + value: 3, + label: "Green", + kind: "two-line", + secondLine: "prova", + icon: IconPlaceholder, + disabled: true, + }, + { + value: 4, + label: ` + Very very very very very very very very long label. Did I say this label is very long? Well let me say it again, it's loooooong, very looooooooong. Maybe we should say it again, let's go! Very very very very very very very very long label. + Very very very very very very very very long label. Did I say this label is very long? Well let me say it again, it's loooooong, very looooooooong. Maybe we should say it again, let's go! Very very very very very very very very long label.`, + kind: "single-line", + }, + ], + noOptionsMessage: "No options", + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const LargeMenu = {} satisfies Story; + +export const MediumMenu = { + args: { + menuSize: "medium", + }, +} satisfies Story; + +export const Disabled = { + args: { + disabled: true, + }, +} satisfies Story; + +export const Invalid = { + args: { + validationState: "invalid", + }, +} satisfies Story; + +export const InModal = { + decorators: [ + (Story: StoryFn) => ( + {}} closeButtonLabel="Close"> + + + ), + ], +} satisfies Story; + +export const MultiSelectOneOptionSelected = { + args: { + value: [1], + isMulti: true, + multiValueMessage: (numberOfSelectedOptions: number) => + `${numberOfSelectedOptions} options selected`, + showMultiSelectBulkActions: true, + }, +} satisfies Story; + +export const MultiSelectMultipleOptionsSelected = { + args: { + value: [1, 2], + isMulti: true, + multiValueMessage: (numberOfSelectedOptions: number) => + `${numberOfSelectedOptions} options selected`, + }, +} satisfies Story; + +const manyColors = [ + "red", + "green", + "blue", + "yellow", + "orange", + "purple", + "pink", + "brown", + "black", + "white", + "gray", + "cyan", + "magenta", + "lime", + "maroon", + "navy", + "olive", + "teal", + "aqua", + "fuchsia", +]; + +export const MultiSelectModeChipsSelected = { + args: { + value: manyColors, + isMulti: true, + multiSelectMode: "chips", + showMultiSelectBulkActions: true, + options: manyColors.map((color) => ({ + value: color, + label: color, + kind: "single-line", + })), + }, +} satisfies Story; + +export const WithIconSelected = { + args: { + value: 1, + options: [ + { value: 1, label: "Idea", icon: IconLightbulb }, + { value: 2, label: "User", icon: IconUser }, + ], + }, +} satisfies Story; + +export const ReadOnly = { + args: { + value: 1, + options: [ + { value: 1, label: "Idea", icon: IconLightbulb }, + { value: 2, label: "User", icon: IconUser }, + ], + isReadOnly: true, + }, +} satisfies Story; + +// This story tests that we can configure List specifically for SelectInput +export const CustomListConfig = { + args: { + autoFocus: true, + }, + decorators: [ + (Story: StoryFn) => ( + + {}}> + + + + ), + ], +} satisfies Story; diff --git a/packages/bento-design-system/stories/Components/Inputs/TextInput.stories.tsx b/packages/bento-design-system/stories/Components/Inputs/TextInput.stories.tsx new file mode 100644 index 000000000..d4c8abea0 --- /dev/null +++ b/packages/bento-design-system/stories/Components/Inputs/TextInput.stories.tsx @@ -0,0 +1,50 @@ +import { TextInput } from "../.."; +import { StoryObj, Meta } from "@storybook/react"; + +const meta = { + component: TextInput, + args: { + value: "", + validationState: "valid", + "aria-label": "text-input", + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Default = {} satisfies Story; + +export const Invalid = { + args: { + validationState: "invalid", + }, +} satisfies Story; + +export const Disabled = { + args: { + disabled: true, + }, +} satisfies Story; + +export const ReadOnly = { + args: { + value: "Read only", + isReadOnly: true, + }, +} satisfies Story; + +export const CustomAccessory = { + args: { + value: "With a custom accessory", + rightAccessory: "👍", + }, +} satisfies Story; + +export const Password = { + args: { + value: "password", + type: "password", + }, +} satisfies Story; diff --git a/packages/configuration-builder/src/ColorEditor/CounterField.tsx b/packages/configuration-builder/src/ColorEditor/CounterField.tsx index 8b0423b3c..35f000c03 100644 --- a/packages/configuration-builder/src/ColorEditor/CounterField.tsx +++ b/packages/configuration-builder/src/ColorEditor/CounterField.tsx @@ -6,7 +6,7 @@ import { IconMinus, IconPlus, NumberFieldProps, - NumberInput, + BaseNumberInput, } from "@buildo/bento-design-system"; import { useRef } from "react"; import { useTranslation } from "react-i18next"; @@ -61,7 +61,7 @@ export function CounterField(props: Omit) }} /> -