Skip to content

Commit

Permalink
Merge pull request #814 from buildo/add_text_and_select_input
Browse files Browse the repository at this point in the history
Add text and select input
  • Loading branch information
federico-ercoles committed Nov 30, 2023
2 parents 2a4d336 + fa3ddc8 commit f9dc56e
Show file tree
Hide file tree
Showing 20 changed files with 830 additions and 333 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLInputElement>;
inputRef: React.Ref<HTMLInputElement>;
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();

Expand Down Expand Up @@ -132,5 +129,3 @@ export function NumberInput(props: Props) {
</Box>
);
}

export type { Props as NumberInputProps };
17 changes: 7 additions & 10 deletions packages/bento-design-system/src/NumberField/NumberField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<number | undefined, number> & {
placeholder?: LocalizedString;
isReadOnly?: boolean;
rightAccessory?: Children;
} & FormatProps &
type Props = FieldProps<number | undefined, number> &
BaseNumberProps &
FormatProps &
Pick<NumberFieldStateOptions, "minValue" | "maxValue" | "step">;

export function NumberField(props: Props) {
Expand Down Expand Up @@ -45,7 +42,7 @@ export function NumberField(props: Props) {
assistiveTextProps={descriptionProps}
errorMessageProps={errorMessageProps}
>
<NumberInput
<BaseNumberInput
inputProps={inputProps}
inputRef={inputRef}
validationState={validationState}
Expand Down
40 changes: 40 additions & 0 deletions packages/bento-design-system/src/NumberField/NumberInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { NumberFieldStateOptions, useNumberFieldState } from "@react-stately/numberfield";
import { FieldProps } from "../Field/FieldProps";
import { BaseNumberProps, FormatProps } from "./types";
import { useLocale } from "@react-aria/i18n";
import { useFormatOptions } from "./formatOptions";
import { HTMLAttributes, useRef } from "react";
import { useNumberField } from "@react-aria/numberfield";
import { BaseNumberInput } from "./BaseNumberInput";
import { AtLeast } from "../util/AtLeast";

type Props = AtLeast<Pick<HTMLAttributes<HTMLInputElement>, "aria-label" | "aria-labelledby">> &
Pick<
FieldProps<number | undefined, number>,
"autoFocus" | "disabled" | "name" | "onBlur" | "onChange" | "value"
> &
BaseNumberProps & {
validationState: "valid" | "invalid";
} & FormatProps &
Pick<NumberFieldStateOptions, "minValue" | "maxValue" | "step">;

export function NumberInput(props: Props) {
const { locale } = useLocale();
const formatOptions = useFormatOptions(props);
const state = useNumberFieldState({ ...props, locale, formatOptions });
const inputRef = useRef<HTMLInputElement>(null);

const { inputProps } = useNumberField(
{
...props,
isDisabled: props.disabled,
formatOptions,
},
state,
inputRef
);

return <BaseNumberInput inputProps={inputProps} inputRef={inputRef} {...props} />;
}

export type { Props as NumberInputProps };
Original file line number Diff line number Diff line change
@@ -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:
Expand Down
20 changes: 20 additions & 0 deletions packages/bento-design-system/src/NumberField/types.ts
Original file line number Diff line number Diff line change
@@ -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";
};
11 changes: 0 additions & 11 deletions packages/bento-design-system/src/NumberInput/FormatProps.ts

This file was deleted.

136 changes: 136 additions & 0 deletions packages/bento-design-system/src/SelectField/BaseSelect.tsx
Original file line number Diff line number Diff line change
@@ -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<A> = BaseMultiProps &
Pick<FieldProps<A[]>, "autoFocus" | "disabled" | "name" | "onBlur" | "onChange" | "value">;

type SingleProps<A> = BaseSingleProps &
Pick<
FieldProps<A | undefined>,
"autoFocus" | "disabled" | "name" | "onBlur" | "onChange" | "value"
>;

type Props<A> = BaseSelectProps<A> & {
fieldProps: AriaLabelingProps & DOMProps;
validationState: "valid" | "invalid";
} & (SingleProps<A> | MultiProps<A>);

export function BaseSelect<A>(props: Props<A>) {
const dropdownConfig = useBentoConfig().dropdown;
const { defaultMessages } = useDefaultMessages();

const menuPortalTarget = useRef<HTMLDivElement>();
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
<BentoConfigProvider value={{ list: dropdownConfig.list }}>
<Select
id={fieldProps.id}
name={name}
aria-label={fieldProps["aria-label"]}
aria-labelledby={fieldProps["aria-labelledby"]}
isDisabled={disabled}
isReadOnly={isReadOnly || false}
autoFocus={autoFocus}
value={
isMulti
? options.filter((o) => ((value ?? []) as readonly A[]).includes(o.value))
: options.find((o) => o.value === value)
}
onChange={(o) => {
if (isMulti) {
const multiValue = o as MultiValueT<SelectOption<A>>;
onChange(multiValue.map((a) => a.value));
} else {
const singleValue = o as SingleValueT<SelectOption<A>>;
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<A>) => 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}
openMenuOnFocus
styles={selectComponents.styles<SelectOption<A>>()}
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}
/>
</BentoConfigProvider>
);
}
Loading

0 comments on commit f9dc56e

Please sign in to comment.