Skip to content

Commit c1d5ad5

Browse files
committed
Create tags widget with React Select
Update basic select widget to use react select
1 parent 115b215 commit c1d5ad5

File tree

6 files changed

+282
-19
lines changed

6 files changed

+282
-19
lines changed

package-lock.json

Lines changed: 77 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/data-widgets/lib/config/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { WidgetArray } from '../widgets/array';
99
import { WidgetArrayInput } from '../widgets/array-input';
1010
import { WidgetSelect } from '../widgets/select';
1111
import { WidgetJSON } from '../widgets/json';
12+
import { WidgetTags } from '../widgets/tags';
1213

1314
export const defaultPluginWidgetConfig = extendPluginConfig({
1415
'ui:widget': {
@@ -18,6 +19,7 @@ export const defaultPluginWidgetConfig = extendPluginConfig({
1819
radio: WidgetRadio,
1920
checkbox: WidgetCheckbox,
2021
select: WidgetSelect,
22+
tags: WidgetTags,
2123
array: WidgetArray,
2224
'array:string': WidgetArrayInput,
2325
json: WidgetJSON

packages/data-widgets/lib/utils/index.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,7 @@ export function toNumber(v: any) {
6161
const n = Number(v);
6262
return isNaN(n) ? null : n;
6363
}
64+
65+
export function castArray<T>(value: T | T[]): T[] {
66+
return Array.isArray(value) ? value : [value];
67+
}
Lines changed: 55 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,74 @@
1-
import React from 'react';
2-
import { Select, FormControl, FormLabel } from '@chakra-ui/react';
3-
import { SchemaFieldString, WidgetProps } from '@stac-manager/data-core';
4-
import { FastField } from 'formik';
1+
import React, { useMemo } from 'react';
2+
import { FormControl, FormLabel } from '@chakra-ui/react';
3+
import { useField } from 'formik';
4+
import ReactSelect from 'react-select';
5+
import {
6+
SchemaFieldArray,
7+
SchemaFieldString,
8+
WidgetProps
9+
} from '@stac-manager/data-core';
510

611
import { FieldLabel } from '../components/elements';
12+
import { castArray } from '../utils';
13+
14+
interface Option {
15+
readonly label: string;
16+
readonly value: string;
17+
}
718

819
export function WidgetSelect(props: WidgetProps) {
9-
const { pointer, isRequired } = props;
10-
const field = props.field as SchemaFieldString;
20+
const { pointer, isRequired, field } = props;
1121

12-
if (!field.enum?.length) {
22+
const isMulti = field.type === 'array';
23+
24+
if (
25+
isMulti
26+
? !(field as SchemaFieldArray<SchemaFieldString>).items?.enum?.length
27+
: !(field as SchemaFieldString).enum?.length
28+
) {
1329
throw new Error('WidgetSelect: enum is required');
1430
}
1531

32+
const options = useMemo(() => {
33+
const enums = isMulti
34+
? (field as SchemaFieldArray<SchemaFieldString>).items?.enum
35+
: (field as SchemaFieldString).enum;
36+
37+
return enums!.map<Option>(([value, label]) => ({
38+
value,
39+
label
40+
}));
41+
}, [field]);
42+
43+
const [{ value }, , { setValue }] = useField(pointer);
44+
45+
const selectedOpts = options.filter((option) =>
46+
castArray(value).includes(option.value)
47+
);
48+
1649
return (
1750
<FormControl isRequired={isRequired}>
1851
{field.label && (
1952
<FormLabel>
2053
<FieldLabel size='xs'>{field.label}</FieldLabel>
2154
</FormLabel>
2255
)}
23-
<FastField
24-
as={Select}
25-
type='select'
56+
<ReactSelect
2657
name={pointer}
27-
placeholder='Select option'
28-
size='sm'
29-
>
30-
{field.enum.map(([value, label]) => (
31-
<option key={value} value={value}>
32-
{label}
33-
</option>
34-
))}
35-
</FastField>
58+
options={options}
59+
isMulti={isMulti}
60+
isClearable={!isRequired}
61+
onChange={(option) => {
62+
if (option) {
63+
setValue(
64+
castArray(option as Option | Option[]).map((o) => o.value)
65+
);
66+
} else {
67+
setValue(isMulti ? [] : undefined);
68+
}
69+
}}
70+
value={selectedOpts}
71+
/>
3672
</FormControl>
3773
);
3874
}
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import React, { KeyboardEventHandler, useMemo, useState } from 'react';
2+
import { Kbd, FormControl, FormLabel } from '@chakra-ui/react';
3+
import { useField } from 'formik';
4+
import CreatableSelect from 'react-select/creatable';
5+
import {
6+
SchemaFieldArray,
7+
SchemaFieldString,
8+
WidgetProps
9+
} from '@stac-manager/data-core';
10+
11+
import { FieldLabel } from '../components/elements';
12+
13+
const components = {
14+
DropdownIndicator: null
15+
};
16+
17+
const createOption = (label: string): Option => ({
18+
label,
19+
value: label
20+
});
21+
22+
interface Option {
23+
readonly label: string;
24+
readonly value: string;
25+
}
26+
27+
export function WidgetTags(props: WidgetProps) {
28+
const { field } = props;
29+
30+
if (field.type !== 'array') {
31+
throw new Error('WidgetTags only supports array fields');
32+
}
33+
34+
if (field.items.type === 'string' && field.items.enum) {
35+
return <WidgetTagsWithOptions {...props} />;
36+
}
37+
38+
return <WidgetTagsNoOptions {...props} />;
39+
}
40+
41+
function WidgetTagsNoOptions(props: WidgetProps) {
42+
const { pointer, isRequired, field } = props;
43+
44+
const [inputValue, setInputValue] = useState('');
45+
46+
const [{ value }, , { setValue }] = useField(pointer);
47+
const selectedValues = Array.isArray(value) ? value.map(createOption) : [];
48+
49+
const handleKeyDown: KeyboardEventHandler = (event) => {
50+
if (!inputValue) return;
51+
switch (event.key) {
52+
case 'Enter':
53+
case 'Tab':
54+
if (!value.includes(inputValue)) {
55+
setValue([...value, inputValue]);
56+
}
57+
setInputValue('');
58+
event.preventDefault();
59+
}
60+
};
61+
62+
return (
63+
<FormControl isRequired={isRequired}>
64+
{field.label && (
65+
<FormLabel>
66+
<FieldLabel size='xs'>{field.label}</FieldLabel>
67+
</FormLabel>
68+
)}
69+
70+
<CreatableSelect
71+
components={components}
72+
inputValue={inputValue}
73+
isClearable
74+
isMulti
75+
menuIsOpen={false}
76+
onChange={(newValue) => {
77+
setValue(newValue.map((option) => option.value));
78+
}}
79+
onInputChange={(newValue) => setInputValue(newValue)}
80+
onKeyDown={handleKeyDown}
81+
placeholder={
82+
<React.Fragment>
83+
Type & <Kbd>enter</Kbd> to add options
84+
</React.Fragment>
85+
}
86+
value={selectedValues}
87+
/>
88+
</FormControl>
89+
);
90+
}
91+
92+
function WidgetTagsWithOptions(props: WidgetProps) {
93+
const { pointer, isRequired } = props;
94+
const field = props.field as SchemaFieldArray<SchemaFieldString>;
95+
96+
const [inputValue, setInputValue] = useState('');
97+
98+
const [{ value }, , { setValue }] = useField(pointer);
99+
const selectedValues = Array.isArray(value) ? value.map(createOption) : [];
100+
101+
const options = useMemo(() => {
102+
return field.items.enum!.map<Option>(([value, label]) => ({
103+
value,
104+
label
105+
}));
106+
}, [field]);
107+
108+
const handleKeyDown: KeyboardEventHandler = (event) => {
109+
if (!inputValue) return;
110+
switch (event.key) {
111+
case 'Enter':
112+
case 'Tab':
113+
if (value.includes(inputValue)) {
114+
setInputValue('');
115+
event.preventDefault();
116+
}
117+
}
118+
};
119+
120+
return (
121+
<FormControl isRequired={isRequired}>
122+
{field.label && (
123+
<FormLabel>
124+
<FieldLabel size='xs'>{field.label}</FieldLabel>
125+
</FormLabel>
126+
)}
127+
128+
<CreatableSelect
129+
inputValue={inputValue}
130+
isClearable
131+
isMulti
132+
onChange={(newValue) => {
133+
setValue(newValue.map((option) => option.value));
134+
}}
135+
onInputChange={(newValue) => setInputValue(newValue)}
136+
onKeyDown={handleKeyDown}
137+
options={options}
138+
placeholder='Select or type to add options'
139+
value={selectedValues}
140+
/>
141+
</FormControl>
142+
);
143+
}

packages/data-widgets/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
"jsoneditor": "^10.1.1",
3535
"react": "^18.3.1",
3636
"react-dom": "^18.3.1",
37+
"react-select": "^5.8.3",
3738
"uuid": "^11.0.3"
3839
},
3940
"devDependencies": {

0 commit comments

Comments
 (0)