Skip to content

Commit f9dc56e

Browse files
Merge pull request #814 from buildo/add_text_and_select_input
Add text and select input
2 parents 2a4d336 + fa3ddc8 commit f9dc56e

20 files changed

+830
-333
lines changed

packages/bento-design-system/src/NumberInput/NumberInput.tsx packages/bento-design-system/src/NumberField/BaseNumberInput.tsx

+3-8
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,20 @@ import useDimensions from "react-cool-dimensions";
44
import { Label, LocalizedString, Box, Children, Columns } from "..";
55
import { inputRecipe } from "../Field/Field.css";
66
import { bodyRecipe } from "../Typography/Body/Body.css";
7-
import { FormatProps } from "./FormatProps";
7+
import { BaseNumberProps, FormatProps } from "./types";
88
import { useBentoConfig } from "../BentoConfigContext";
99
import { match, not, __ } from "ts-pattern";
1010
import { getReadOnlyBackgroundStyle } from "../Field/utils";
1111
import { getRadiusPropsFromConfig } from "../util/BorderRadiusConfig";
1212

13-
type Props = {
13+
type Props = BaseNumberProps & {
1414
inputProps: React.InputHTMLAttributes<HTMLInputElement>;
1515
inputRef: React.Ref<HTMLInputElement>;
16-
placeholder?: LocalizedString;
1716
validationState: "valid" | "invalid";
1817
disabled?: boolean;
19-
isReadOnly?: boolean;
20-
rightAccessory?: Children;
2118
} & FormatProps;
2219

23-
export function NumberInput(props: Props) {
20+
export function BaseNumberInput(props: Props) {
2421
const config = useBentoConfig().input;
2522
const { locale } = useLocale();
2623

@@ -132,5 +129,3 @@ export function NumberInput(props: Props) {
132129
</Box>
133130
);
134131
}
135-
136-
export type { Props as NumberInputProps };

packages/bento-design-system/src/NumberField/NumberField.tsx

+7-10
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,15 @@ import { useLocale } from "@react-aria/i18n";
22
import { useNumberField } from "@react-aria/numberfield";
33
import { NumberFieldStateOptions, useNumberFieldState } from "@react-stately/numberfield";
44
import { useRef } from "react";
5-
import { Children, LocalizedString } from "..";
65
import { FieldProps } from "../Field/FieldProps";
7-
import { FormatProps } from "../NumberInput/FormatProps";
8-
import { useFormatOptions } from "../NumberInput/formatOptions";
6+
import { BaseNumberProps, FormatProps } from "./types";
7+
import { useFormatOptions } from "./formatOptions";
98
import { Field } from "../Field/Field";
10-
import { NumberInput } from "../NumberInput/NumberInput";
9+
import { BaseNumberInput } from "./BaseNumberInput";
1110

12-
type Props = FieldProps<number | undefined, number> & {
13-
placeholder?: LocalizedString;
14-
isReadOnly?: boolean;
15-
rightAccessory?: Children;
16-
} & FormatProps &
11+
type Props = FieldProps<number | undefined, number> &
12+
BaseNumberProps &
13+
FormatProps &
1714
Pick<NumberFieldStateOptions, "minValue" | "maxValue" | "step">;
1815

1916
export function NumberField(props: Props) {
@@ -45,7 +42,7 @@ export function NumberField(props: Props) {
4542
assistiveTextProps={descriptionProps}
4643
errorMessageProps={errorMessageProps}
4744
>
48-
<NumberInput
45+
<BaseNumberInput
4946
inputProps={inputProps}
5047
inputRef={inputRef}
5148
validationState={validationState}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { NumberFieldStateOptions, useNumberFieldState } from "@react-stately/numberfield";
2+
import { FieldProps } from "../Field/FieldProps";
3+
import { BaseNumberProps, FormatProps } from "./types";
4+
import { useLocale } from "@react-aria/i18n";
5+
import { useFormatOptions } from "./formatOptions";
6+
import { HTMLAttributes, useRef } from "react";
7+
import { useNumberField } from "@react-aria/numberfield";
8+
import { BaseNumberInput } from "./BaseNumberInput";
9+
import { AtLeast } from "../util/AtLeast";
10+
11+
type Props = AtLeast<Pick<HTMLAttributes<HTMLInputElement>, "aria-label" | "aria-labelledby">> &
12+
Pick<
13+
FieldProps<number | undefined, number>,
14+
"autoFocus" | "disabled" | "name" | "onBlur" | "onChange" | "value"
15+
> &
16+
BaseNumberProps & {
17+
validationState: "valid" | "invalid";
18+
} & FormatProps &
19+
Pick<NumberFieldStateOptions, "minValue" | "maxValue" | "step">;
20+
21+
export function NumberInput(props: Props) {
22+
const { locale } = useLocale();
23+
const formatOptions = useFormatOptions(props);
24+
const state = useNumberFieldState({ ...props, locale, formatOptions });
25+
const inputRef = useRef<HTMLInputElement>(null);
26+
27+
const { inputProps } = useNumberField(
28+
{
29+
...props,
30+
isDisabled: props.disabled,
31+
formatOptions,
32+
},
33+
state,
34+
inputRef
35+
);
36+
37+
return <BaseNumberInput inputProps={inputProps} inputRef={inputRef} {...props} />;
38+
}
39+
40+
export type { Props as NumberInputProps };

packages/bento-design-system/src/NumberInput/formatOptions.ts packages/bento-design-system/src/NumberField/formatOptions.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useMemo } from "react";
2-
import { FormatProps } from "./FormatProps";
2+
import { FormatProps } from "./types";
33

44
export function useFormatOptions({ kind }: FormatProps) {
55
// This function must be memoized, see this relevant issue:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { Children } from "../util/Children";
2+
import { LocalizedString } from "../util/LocalizedString";
3+
4+
export type BaseNumberProps = {
5+
placeholder?: LocalizedString;
6+
isReadOnly?: boolean;
7+
rightAccessory?: Children;
8+
};
9+
10+
export type FormatProps =
11+
| {
12+
kind: "currency";
13+
currency: string;
14+
}
15+
| {
16+
kind: "percentage";
17+
}
18+
| {
19+
kind?: "decimal";
20+
};

packages/bento-design-system/src/NumberInput/FormatProps.ts

-11
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import Select, { MultiValue as MultiValueT, SingleValue as SingleValueT } from "react-select";
2+
import { useDefaultMessages } from "..";
3+
import { FieldProps } from "../Field/FieldProps";
4+
import { BentoConfigProvider, useBentoConfig } from "../BentoConfigContext";
5+
import { AriaLabelingProps, DOMProps } from "@react-types/shared";
6+
import * as selectComponents from "./components";
7+
import { useEffect, useRef } from "react";
8+
import { BaseMultiProps, BaseSelectProps, BaseSingleProps, SelectOption } from "./types";
9+
10+
type MultiProps<A> = BaseMultiProps &
11+
Pick<FieldProps<A[]>, "autoFocus" | "disabled" | "name" | "onBlur" | "onChange" | "value">;
12+
13+
type SingleProps<A> = BaseSingleProps &
14+
Pick<
15+
FieldProps<A | undefined>,
16+
"autoFocus" | "disabled" | "name" | "onBlur" | "onChange" | "value"
17+
>;
18+
19+
type Props<A> = BaseSelectProps<A> & {
20+
fieldProps: AriaLabelingProps & DOMProps;
21+
validationState: "valid" | "invalid";
22+
} & (SingleProps<A> | MultiProps<A>);
23+
24+
export function BaseSelect<A>(props: Props<A>) {
25+
const dropdownConfig = useBentoConfig().dropdown;
26+
const { defaultMessages } = useDefaultMessages();
27+
28+
const menuPortalTarget = useRef<HTMLDivElement>();
29+
useEffect(() => {
30+
if (!menuPortalTarget.current) {
31+
menuPortalTarget.current = document.createElement("div");
32+
}
33+
document.body.appendChild(menuPortalTarget.current);
34+
35+
return () => {
36+
if (document.body.contains(menuPortalTarget.current!)) {
37+
document.body.removeChild(menuPortalTarget.current!);
38+
}
39+
};
40+
}, [menuPortalTarget]);
41+
42+
const {
43+
fieldProps,
44+
validationState,
45+
value,
46+
onChange,
47+
options,
48+
onBlur,
49+
name,
50+
placeholder,
51+
disabled,
52+
isReadOnly,
53+
isMulti,
54+
noOptionsMessage,
55+
autoFocus,
56+
menuSize = dropdownConfig.defaultMenuSize,
57+
searchable,
58+
} = props;
59+
60+
return (
61+
// NOTE(gabro): SelectField has its own config for List, so we override it here using BentoConfigProvider
62+
<BentoConfigProvider value={{ list: dropdownConfig.list }}>
63+
<Select
64+
id={fieldProps.id}
65+
name={name}
66+
aria-label={fieldProps["aria-label"]}
67+
aria-labelledby={fieldProps["aria-labelledby"]}
68+
isDisabled={disabled}
69+
isReadOnly={isReadOnly || false}
70+
autoFocus={autoFocus}
71+
value={
72+
isMulti
73+
? options.filter((o) => ((value ?? []) as readonly A[]).includes(o.value))
74+
: options.find((o) => o.value === value)
75+
}
76+
onChange={(o) => {
77+
if (isMulti) {
78+
const multiValue = o as MultiValueT<SelectOption<A>>;
79+
onChange(multiValue.map((a) => a.value));
80+
} else {
81+
const singleValue = o as SingleValueT<SelectOption<A>>;
82+
onChange(singleValue == null ? undefined : singleValue.value);
83+
}
84+
}}
85+
onBlur={onBlur}
86+
options={options
87+
.slice() // avoid mutating the original array
88+
.sort((a, b) => {
89+
// In case of multi-select, we display the selected options first
90+
if (isMulti) {
91+
const selectedValues = (value ?? []) as readonly A[];
92+
const isSelected = (a: SelectOption<A>) => selectedValues.includes(a.value);
93+
if (isSelected(a) && !isSelected(b)) {
94+
return -1;
95+
}
96+
if (!isSelected(a) && isSelected(b)) {
97+
return 1;
98+
}
99+
}
100+
return 0;
101+
})}
102+
placeholder={placeholder}
103+
menuPortalTarget={menuPortalTarget.current}
104+
components={selectComponents}
105+
openMenuOnFocus
106+
styles={selectComponents.styles<SelectOption<A>>()}
107+
validationState={validationState}
108+
isMulti={isMulti}
109+
isClearable={false}
110+
noOptionsMessage={() => noOptionsMessage ?? defaultMessages.SelectField.noOptionsMessage}
111+
multiValueMessage={
112+
props.isMulti && (!props.multiSelectMode || props.multiSelectMode === "summary")
113+
? props.multiValueMessage ?? defaultMessages.SelectField.multiOptionsSelected
114+
: undefined
115+
}
116+
closeMenuOnSelect={!isMulti}
117+
hideSelectedOptions={false}
118+
menuSize={menuSize}
119+
menuIsOpen={isReadOnly ? false : undefined}
120+
isSearchable={isReadOnly ? false : searchable ?? true}
121+
showMultiSelectBulkActions={isMulti ? props.showMultiSelectBulkActions : false}
122+
clearAllButtonLabel={
123+
isMulti
124+
? props.clearAllButtonLabel ?? defaultMessages.SelectField.clearAllButtonLabel
125+
: undefined
126+
}
127+
selectAllButtonLabel={
128+
isMulti
129+
? props.selectAllButtonLabel ?? defaultMessages.SelectField.selectAllButtonLabel
130+
: undefined
131+
}
132+
multiSelectMode={isMulti ? props.multiSelectMode : undefined}
133+
/>
134+
</BentoConfigProvider>
135+
);
136+
}

0 commit comments

Comments
 (0)