@mindlogic-ai/logician-ui 3.0.0-alpha.34 → 3.0.0-alpha.35
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/Pagination/Pagination.d.ts.map +1 -1
- package/dist/components/Pagination/Pagination.js +7 -20
- package/dist/components/Pagination/Pagination.js.map +1 -1
- package/dist/components/Pagination/Pagination.mjs +7 -20
- package/dist/components/Pagination/Pagination.mjs.map +1 -1
- package/dist/components/Select/Combobox.d.ts +28 -0
- package/dist/components/Select/Combobox.d.ts.map +1 -0
- package/dist/components/Select/Combobox.js +47 -0
- package/dist/components/Select/Combobox.js.map +1 -0
- package/dist/components/Select/Combobox.mjs +45 -0
- package/dist/components/Select/Combobox.mjs.map +1 -0
- package/dist/components/Select/ComboboxField.d.ts +12 -0
- package/dist/components/Select/ComboboxField.d.ts.map +1 -0
- package/dist/components/Select/ComboboxField.js +34 -0
- package/dist/components/Select/ComboboxField.js.map +1 -0
- package/dist/components/Select/ComboboxField.mjs +32 -0
- package/dist/components/Select/ComboboxField.mjs.map +1 -0
- package/dist/components/Select/Select.d.ts +29 -3
- package/dist/components/Select/Select.d.ts.map +1 -1
- package/dist/components/Select/Select.js +41 -156
- package/dist/components/Select/Select.js.map +1 -1
- package/dist/components/Select/Select.mjs +43 -158
- package/dist/components/Select/Select.mjs.map +1 -1
- package/dist/components/Select/Select.styles.d.ts +91 -62
- package/dist/components/Select/Select.styles.d.ts.map +1 -1
- package/dist/components/Select/Select.styles.js +67 -69
- package/dist/components/Select/Select.styles.js.map +1 -1
- package/dist/components/Select/Select.styles.mjs +63 -66
- package/dist/components/Select/Select.styles.mjs.map +1 -1
- package/dist/components/Select/Select.types.d.ts +31 -8
- package/dist/components/Select/Select.types.d.ts.map +1 -1
- package/dist/components/Select/SelectField.d.ts +11 -0
- package/dist/components/Select/SelectField.d.ts.map +1 -0
- package/dist/components/Select/SelectField.js +32 -0
- package/dist/components/Select/SelectField.js.map +1 -0
- package/dist/components/Select/SelectField.mjs +30 -0
- package/dist/components/Select/SelectField.mjs.map +1 -0
- package/dist/components/Select/index.d.ts +4 -0
- package/dist/components/Select/index.d.ts.map +1 -1
- package/dist/index.js +27 -9
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +4 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -3
- package/src/components/FormIntegration/FormIntegration.stories.tsx +7 -13
- package/src/components/FormIntegration/README.md +1 -1
- package/src/components/Pagination/Pagination.tsx +7 -26
- package/src/components/Select/Combobox.stories.tsx +172 -0
- package/src/components/Select/Combobox.tsx +69 -0
- package/src/components/Select/ComboboxField.tsx +86 -0
- package/src/components/Select/Comparison.stories.tsx +38 -321
- package/src/components/Select/Select.stories.tsx +691 -404
- package/src/components/Select/Select.styles.ts +64 -76
- package/src/components/Select/Select.tsx +76 -202
- package/src/components/Select/Select.types.ts +33 -13
- package/src/components/Select/SelectField.tsx +87 -0
- package/src/components/Select/index.ts +8 -0
- package/dist/components/Select/MenuList/MenuList.d.ts +0 -4
- package/dist/components/Select/MenuList/MenuList.d.ts.map +0 -1
- package/dist/components/Select/MenuList/MenuList.types.d.ts +0 -9
- package/dist/components/Select/MenuList/MenuList.types.d.ts.map +0 -1
- package/dist/components/Select/MenuList/VirtualizedMenuList.d.ts +0 -4
- package/dist/components/Select/MenuList/VirtualizedMenuList.d.ts.map +0 -1
- package/dist/components/Select/MenuList/VirtualizedMenuListContext.d.ts +0 -9
- package/dist/components/Select/MenuList/VirtualizedMenuListContext.d.ts.map +0 -1
- package/dist/components/Select/MenuList/index.d.ts +0 -4
- package/dist/components/Select/MenuList/index.d.ts.map +0 -1
- package/dist/components/Select/_utils/resolveStyle.d.ts +0 -5
- package/dist/components/Select/_utils/resolveStyle.d.ts.map +0 -1
- package/src/components/Select/MenuList/MenuList.tsx +0 -87
- package/src/components/Select/MenuList/MenuList.types.ts +0 -21
- package/src/components/Select/MenuList/VirtualizedMenuList.tsx +0 -136
- package/src/components/Select/MenuList/VirtualizedMenuListContext.tsx +0 -35
- package/src/components/Select/MenuList/index.ts +0 -3
- package/src/components/Select/_utils/resolveStyle.ts +0 -21
|
@@ -8,7 +8,7 @@ import { Button } from '../Button';
|
|
|
8
8
|
import { FormLabel } from '../FormLabel';
|
|
9
9
|
import { Input } from '../Input';
|
|
10
10
|
import { Textarea } from '../Textarea';
|
|
11
|
-
import {
|
|
11
|
+
import { SelectField } from '../Select';
|
|
12
12
|
import { FileInput } from '../FileInput';
|
|
13
13
|
import { Radio } from '../Radio';
|
|
14
14
|
import { RadioGroup } from '../Radio/RadioGroup';
|
|
@@ -34,7 +34,7 @@ interface FormValues {
|
|
|
34
34
|
name: string;
|
|
35
35
|
email: string;
|
|
36
36
|
bio: string;
|
|
37
|
-
country:
|
|
37
|
+
country: string | null;
|
|
38
38
|
gender: string;
|
|
39
39
|
experience: number;
|
|
40
40
|
files: FileList | null;
|
|
@@ -60,13 +60,7 @@ const validationSchema = Yup.object({
|
|
|
60
60
|
.min(10, '자기소개는 최소 10글자 이상이어야 합니다')
|
|
61
61
|
.max(200, '자기소개는 최대 200글자까지 가능합니다')
|
|
62
62
|
.required('자기소개는 필수입니다'),
|
|
63
|
-
country: Yup.
|
|
64
|
-
.shape({
|
|
65
|
-
value: Yup.string().required(),
|
|
66
|
-
label: Yup.string().required(),
|
|
67
|
-
})
|
|
68
|
-
.nullable()
|
|
69
|
-
.required('국가를 선택해주세요'),
|
|
63
|
+
country: Yup.string().nullable().required('국가를 선택해주세요'),
|
|
70
64
|
gender: Yup.string()
|
|
71
65
|
.oneOf(['male', 'female', 'other'], '성별을 선택해주세요')
|
|
72
66
|
.required('성별은 필수입니다'),
|
|
@@ -115,7 +109,7 @@ export const FormikFormExample: StoryObj = {
|
|
|
115
109
|
console.log('👤 이름:', values.name);
|
|
116
110
|
console.log('📧 이메일:', values.email);
|
|
117
111
|
console.log('📝 자기소개:', values.bio);
|
|
118
|
-
console.log('🌍 국가:', values.country
|
|
112
|
+
console.log('🌍 국가:', values.country);
|
|
119
113
|
console.log('👫 성별:', values.gender);
|
|
120
114
|
console.log('💼 경력:', `${values.experience}년`);
|
|
121
115
|
console.log(
|
|
@@ -231,7 +225,7 @@ export const FormikFormExample: StoryObj = {
|
|
|
231
225
|
<FormLabel>
|
|
232
226
|
국가 <Field.RequiredIndicator />
|
|
233
227
|
</FormLabel>
|
|
234
|
-
<
|
|
228
|
+
<SelectField
|
|
235
229
|
name="country"
|
|
236
230
|
options={countryOptions}
|
|
237
231
|
value={values.country}
|
|
@@ -445,7 +439,7 @@ export const RegularFormExample: StoryObj = {
|
|
|
445
439
|
console.log('👤 이름:', formData.name);
|
|
446
440
|
console.log('📧 이메일:', formData.email);
|
|
447
441
|
console.log('📝 자기소개:', formData.bio);
|
|
448
|
-
console.log('🌍 국가:', formData.country
|
|
442
|
+
console.log('🌍 국가:', formData.country);
|
|
449
443
|
console.log('👫 성별:', formData.gender);
|
|
450
444
|
console.log('💼 경력:', `${formData.experience}년`);
|
|
451
445
|
console.log(
|
|
@@ -561,7 +555,7 @@ export const RegularFormExample: StoryObj = {
|
|
|
561
555
|
<FormLabel>
|
|
562
556
|
국가 <Field.RequiredIndicator />
|
|
563
557
|
</FormLabel>
|
|
564
|
-
<
|
|
558
|
+
<SelectField
|
|
565
559
|
name="country"
|
|
566
560
|
options={countryOptions}
|
|
567
561
|
value={formData.country}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { useMemo, useState } from 'react';
|
|
2
|
-
import { SingleValue } from 'react-select';
|
|
3
2
|
import { Flex } from '@chakra-ui/react';
|
|
4
3
|
|
|
5
4
|
import { IoChevronForward, IoIosArrowBack } from '@/components/Icon';
|
|
@@ -7,8 +6,7 @@ import { IconButton } from '@/components/IconButton';
|
|
|
7
6
|
import { Subtext, Subtitle } from '@/components/Typography';
|
|
8
7
|
import { useTranslate } from '@/hooks/useTranslate';
|
|
9
8
|
|
|
10
|
-
import {
|
|
11
|
-
import { SelectOption } from '../Select/Select.types';
|
|
9
|
+
import { SelectField } from '../Select';
|
|
12
10
|
import { PaginationProps } from './Pagination.types';
|
|
13
11
|
|
|
14
12
|
/**
|
|
@@ -61,11 +59,8 @@ export const Pagination = ({
|
|
|
61
59
|
}
|
|
62
60
|
};
|
|
63
61
|
|
|
64
|
-
const handleItemsPerPageOptionChange = (
|
|
65
|
-
|
|
66
|
-
) => {
|
|
67
|
-
if (newValue) {
|
|
68
|
-
const newItemsPerPage = newValue.value;
|
|
62
|
+
const handleItemsPerPageOptionChange = (newItemsPerPage: number | null) => {
|
|
63
|
+
if (newItemsPerPage != null) {
|
|
69
64
|
onItemsPerPageOptionChange?.(newItemsPerPage);
|
|
70
65
|
|
|
71
66
|
const newMaxPage = Math.ceil(numTotalItems / newItemsPerPage);
|
|
@@ -83,28 +78,14 @@ export const Pagination = ({
|
|
|
83
78
|
<Flex align="center" gap={2}>
|
|
84
79
|
{itemsPerPageOptions && (
|
|
85
80
|
<Flex align="center" gap={2}>
|
|
86
|
-
<
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
control: (base, _state) => ({
|
|
90
|
-
...base,
|
|
91
|
-
fontSize: '0.875rem',
|
|
92
|
-
padding: 0,
|
|
93
|
-
minHeight: '28px',
|
|
94
|
-
}),
|
|
95
|
-
dropdownIndicator: (base, _props) => ({
|
|
96
|
-
...base,
|
|
97
|
-
padding: '0 4px',
|
|
98
|
-
}),
|
|
99
|
-
}}
|
|
81
|
+
<SelectField<number>
|
|
82
|
+
size="sm"
|
|
83
|
+
width="5.5rem"
|
|
100
84
|
options={itemsPerPageOptions.map((option) => ({
|
|
101
85
|
label: option.toString(),
|
|
102
86
|
value: option,
|
|
103
87
|
}))}
|
|
104
|
-
value={
|
|
105
|
-
label: itemsPerPage.toString(),
|
|
106
|
-
value: itemsPerPage,
|
|
107
|
-
}}
|
|
88
|
+
value={itemsPerPage}
|
|
108
89
|
onChange={handleItemsPerPageOptionChange}
|
|
109
90
|
/>
|
|
110
91
|
<Subtext whiteSpace="nowrap">
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Box,
|
|
4
|
+
Portal,
|
|
5
|
+
Span,
|
|
6
|
+
Stack,
|
|
7
|
+
useFilter,
|
|
8
|
+
useListCollection,
|
|
9
|
+
} from '@chakra-ui/react';
|
|
10
|
+
import { Meta } from '@storybook/react';
|
|
11
|
+
|
|
12
|
+
import { Combobox } from './Combobox';
|
|
13
|
+
import { ComboboxField } from './ComboboxField';
|
|
14
|
+
|
|
15
|
+
const meta = {
|
|
16
|
+
title: 'Components/Combobox',
|
|
17
|
+
} satisfies Meta;
|
|
18
|
+
|
|
19
|
+
export default meta;
|
|
20
|
+
|
|
21
|
+
const fruitOptions = [
|
|
22
|
+
{ label: 'Apple', value: 'apple' },
|
|
23
|
+
{ label: 'Banana', value: 'banana' },
|
|
24
|
+
{ label: 'Blueberry', value: 'blueberry' },
|
|
25
|
+
{ label: 'Cherry', value: 'cherry' },
|
|
26
|
+
{ label: 'Grape', value: 'grape' },
|
|
27
|
+
{ label: 'Mango', value: 'mango' },
|
|
28
|
+
{ label: 'Orange', value: 'orange' },
|
|
29
|
+
{ label: 'Strawberry', value: 'strawberry', disabled: true },
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Type to filter the option list. Clear text to see every option again,
|
|
34
|
+
* type something with no match to see the empty state.
|
|
35
|
+
*/
|
|
36
|
+
export const Basic = () => {
|
|
37
|
+
const [value, setValue] = useState<string | null>(null);
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<Box maxW="320px">
|
|
41
|
+
<ComboboxField
|
|
42
|
+
options={fruitOptions}
|
|
43
|
+
value={value}
|
|
44
|
+
onChange={setValue}
|
|
45
|
+
placeholder="Search a fruit"
|
|
46
|
+
/>
|
|
47
|
+
</Box>
|
|
48
|
+
);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export const WithLabel = () => (
|
|
52
|
+
<Box maxW="320px">
|
|
53
|
+
<ComboboxField
|
|
54
|
+
label="Favorite fruit"
|
|
55
|
+
options={fruitOptions}
|
|
56
|
+
placeholder="Search a fruit"
|
|
57
|
+
/>
|
|
58
|
+
</Box>
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
export const Sizes = () => (
|
|
62
|
+
<Stack maxW="320px" gap={4}>
|
|
63
|
+
{(['xs', 'sm', 'md', 'lg'] as const).map((size) => (
|
|
64
|
+
<ComboboxField
|
|
65
|
+
key={size}
|
|
66
|
+
size={size}
|
|
67
|
+
options={fruitOptions}
|
|
68
|
+
placeholder={`Size: ${size}`}
|
|
69
|
+
/>
|
|
70
|
+
))}
|
|
71
|
+
</Stack>
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
export const Invalid = () => (
|
|
75
|
+
<Box maxW="320px">
|
|
76
|
+
<ComboboxField
|
|
77
|
+
options={fruitOptions}
|
|
78
|
+
placeholder="Invalid combobox"
|
|
79
|
+
invalid
|
|
80
|
+
/>
|
|
81
|
+
</Box>
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
export const Disabled = () => (
|
|
85
|
+
<Box maxW="320px">
|
|
86
|
+
<ComboboxField options={fruitOptions} defaultValue="apple" disabled />
|
|
87
|
+
</Box>
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
export const CustomEmptyText = () => (
|
|
91
|
+
<Box maxW="320px">
|
|
92
|
+
<ComboboxField
|
|
93
|
+
options={fruitOptions}
|
|
94
|
+
placeholder="Type 'xyz' to see the empty state"
|
|
95
|
+
emptyText="해당하는 항목이 없습니다"
|
|
96
|
+
/>
|
|
97
|
+
</Box>
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
export const ManyOptions = () => {
|
|
101
|
+
const manyOptions = Array.from({ length: 100 }, (_, i) => ({
|
|
102
|
+
label: `Item ${i + 1}`,
|
|
103
|
+
value: `item${i + 1}`,
|
|
104
|
+
}));
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<Box maxW="320px">
|
|
108
|
+
<ComboboxField options={manyOptions} placeholder="Search 100 items" />
|
|
109
|
+
</Box>
|
|
110
|
+
);
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
/* -------------------------------------------------------------------------
|
|
114
|
+
* Custom children
|
|
115
|
+
*
|
|
116
|
+
* `ComboboxField` takes a flat `options` array; for richer option content
|
|
117
|
+
* compose the `Combobox` primitives directly with `useListCollection` and
|
|
118
|
+
* `useFilter`. Keep `Combobox.ItemText` so filtering and the selected
|
|
119
|
+
* input value still resolve the option label.
|
|
120
|
+
* ---------------------------------------------------------------------- */
|
|
121
|
+
|
|
122
|
+
const teamMembers = [
|
|
123
|
+
{ label: 'Alice Kim', value: 'alice', role: 'Product Designer' },
|
|
124
|
+
{ label: 'Brian Lee', value: 'brian', role: 'Frontend Engineer' },
|
|
125
|
+
{ label: 'Chloe Park', value: 'chloe', role: 'Backend Engineer' },
|
|
126
|
+
{ label: 'David Choi', value: 'david', role: 'Engineering Manager' },
|
|
127
|
+
];
|
|
128
|
+
|
|
129
|
+
/** Two-line options inside a filterable combobox. */
|
|
130
|
+
export const CustomItems = () => {
|
|
131
|
+
const { contains } = useFilter({ sensitivity: 'base' });
|
|
132
|
+
const { collection, filter } = useListCollection({
|
|
133
|
+
initialItems: teamMembers,
|
|
134
|
+
filter: contains,
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
return (
|
|
138
|
+
<Box maxW="320px">
|
|
139
|
+
<Combobox.Root
|
|
140
|
+
collection={collection}
|
|
141
|
+
onInputValueChange={(details) => filter(details.inputValue)}
|
|
142
|
+
>
|
|
143
|
+
<Combobox.Label>Assignee</Combobox.Label>
|
|
144
|
+
<Combobox.Control>
|
|
145
|
+
<Combobox.Input placeholder="Search team members" />
|
|
146
|
+
<Combobox.IndicatorGroup>
|
|
147
|
+
<Combobox.ClearTrigger />
|
|
148
|
+
<Combobox.Trigger />
|
|
149
|
+
</Combobox.IndicatorGroup>
|
|
150
|
+
</Combobox.Control>
|
|
151
|
+
<Portal>
|
|
152
|
+
<Combobox.Positioner>
|
|
153
|
+
<Combobox.Content>
|
|
154
|
+
<Combobox.Empty>No members found</Combobox.Empty>
|
|
155
|
+
{collection.items.map((item) => (
|
|
156
|
+
<Combobox.Item item={item} key={item.value}>
|
|
157
|
+
<Stack gap={0}>
|
|
158
|
+
<Combobox.ItemText>{item.label}</Combobox.ItemText>
|
|
159
|
+
<Span fontSize="xs" color="gray.600">
|
|
160
|
+
{item.role}
|
|
161
|
+
</Span>
|
|
162
|
+
</Stack>
|
|
163
|
+
<Combobox.ItemIndicator />
|
|
164
|
+
</Combobox.Item>
|
|
165
|
+
))}
|
|
166
|
+
</Combobox.Content>
|
|
167
|
+
</Combobox.Positioner>
|
|
168
|
+
</Portal>
|
|
169
|
+
</Combobox.Root>
|
|
170
|
+
</Box>
|
|
171
|
+
);
|
|
172
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { forwardRef } from 'react';
|
|
2
|
+
import { Combobox as ChakraCombobox } from '@chakra-ui/react';
|
|
3
|
+
|
|
4
|
+
import { mergeCss } from '@/utils/mergeCss';
|
|
5
|
+
|
|
6
|
+
import { contentStyles, inputStyles, itemStyles } from './Select.styles';
|
|
7
|
+
|
|
8
|
+
const Input = forwardRef<HTMLInputElement, ChakraCombobox.InputProps>(
|
|
9
|
+
function ComboboxInput({ css, ...props }, ref) {
|
|
10
|
+
return (
|
|
11
|
+
<ChakraCombobox.Input
|
|
12
|
+
ref={ref}
|
|
13
|
+
{...props}
|
|
14
|
+
css={mergeCss(inputStyles, css)}
|
|
15
|
+
/>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
const Content = forwardRef<HTMLDivElement, ChakraCombobox.ContentProps>(
|
|
21
|
+
function ComboboxContent({ css, ...props }, ref) {
|
|
22
|
+
return (
|
|
23
|
+
<ChakraCombobox.Content
|
|
24
|
+
ref={ref}
|
|
25
|
+
{...props}
|
|
26
|
+
css={mergeCss(contentStyles, css)}
|
|
27
|
+
/>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
const Item = forwardRef<HTMLDivElement, ChakraCombobox.ItemProps>(
|
|
33
|
+
function ComboboxItem({ css, ...props }, ref) {
|
|
34
|
+
return (
|
|
35
|
+
<ChakraCombobox.Item
|
|
36
|
+
ref={ref}
|
|
37
|
+
{...props}
|
|
38
|
+
css={mergeCss(itemStyles, css)}
|
|
39
|
+
/>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Compound Combobox primitives — Chakra's `Combobox` namespace with the
|
|
46
|
+
* Logician design-system styling baked into the visual parts (`Input`,
|
|
47
|
+
* `Content`, `Item`). Use these directly for full compositional control;
|
|
48
|
+
* use `ComboboxField` for the common single-select case.
|
|
49
|
+
*/
|
|
50
|
+
export const Combobox = {
|
|
51
|
+
Root: ChakraCombobox.Root,
|
|
52
|
+
PropsProvider: ChakraCombobox.PropsProvider,
|
|
53
|
+
Context: ChakraCombobox.Context,
|
|
54
|
+
Label: ChakraCombobox.Label,
|
|
55
|
+
Control: ChakraCombobox.Control,
|
|
56
|
+
Input,
|
|
57
|
+
Trigger: ChakraCombobox.Trigger,
|
|
58
|
+
IndicatorGroup: ChakraCombobox.IndicatorGroup,
|
|
59
|
+
ClearTrigger: ChakraCombobox.ClearTrigger,
|
|
60
|
+
Positioner: ChakraCombobox.Positioner,
|
|
61
|
+
Content,
|
|
62
|
+
Empty: ChakraCombobox.Empty,
|
|
63
|
+
ItemGroup: ChakraCombobox.ItemGroup,
|
|
64
|
+
ItemGroupLabel: ChakraCombobox.ItemGroupLabel,
|
|
65
|
+
Item,
|
|
66
|
+
ItemText: ChakraCombobox.ItemText,
|
|
67
|
+
ItemIndicator: ChakraCombobox.ItemIndicator,
|
|
68
|
+
ItemContext: ChakraCombobox.ItemContext,
|
|
69
|
+
};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { Portal, useFilter, useListCollection } from '@chakra-ui/react';
|
|
2
|
+
|
|
3
|
+
import { Combobox } from './Combobox';
|
|
4
|
+
import { ComboboxFieldProps, SelectOption } from './Select.types';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Single-select combobox — a Select with a typeable, filterable input.
|
|
8
|
+
* The ergonomic counterpart to `Input` for searchable option lists; pass
|
|
9
|
+
* `options`, `value` and `onChange` and it renders the full composition
|
|
10
|
+
* internally.
|
|
11
|
+
*
|
|
12
|
+
* For grouped options or custom layouts, compose the `Combobox` primitives
|
|
13
|
+
* directly.
|
|
14
|
+
*/
|
|
15
|
+
export function ComboboxField<T = string>({
|
|
16
|
+
options,
|
|
17
|
+
value,
|
|
18
|
+
defaultValue,
|
|
19
|
+
onChange,
|
|
20
|
+
placeholder,
|
|
21
|
+
label,
|
|
22
|
+
size = 'md',
|
|
23
|
+
invalid,
|
|
24
|
+
disabled,
|
|
25
|
+
readOnly,
|
|
26
|
+
required,
|
|
27
|
+
name,
|
|
28
|
+
width = 'full',
|
|
29
|
+
emptyText = 'No results found',
|
|
30
|
+
}: ComboboxFieldProps<T>) {
|
|
31
|
+
const { contains } = useFilter({ sensitivity: 'base' });
|
|
32
|
+
const { collection, filter } = useListCollection<SelectOption<T>>({
|
|
33
|
+
initialItems: options,
|
|
34
|
+
filter: contains,
|
|
35
|
+
itemToValue: (item) => String(item.value),
|
|
36
|
+
itemToString: (item) => item.label,
|
|
37
|
+
isItemDisabled: (item) => Boolean(item.disabled),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const toKeys = (val: T | null | undefined) =>
|
|
41
|
+
val === null || val === undefined ? [] : [String(val)];
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<Combobox.Root
|
|
45
|
+
collection={collection}
|
|
46
|
+
size={size}
|
|
47
|
+
width={width}
|
|
48
|
+
invalid={invalid}
|
|
49
|
+
disabled={disabled}
|
|
50
|
+
readOnly={readOnly}
|
|
51
|
+
required={required}
|
|
52
|
+
name={name}
|
|
53
|
+
value={value === undefined ? undefined : toKeys(value)}
|
|
54
|
+
defaultValue={
|
|
55
|
+
defaultValue === undefined ? undefined : toKeys(defaultValue)
|
|
56
|
+
}
|
|
57
|
+
onValueChange={(details) => {
|
|
58
|
+
const selected = details.items[0];
|
|
59
|
+
onChange?.(selected ? selected.value : null);
|
|
60
|
+
}}
|
|
61
|
+
onInputValueChange={(details) => filter(details.inputValue)}
|
|
62
|
+
>
|
|
63
|
+
{label != null && <Combobox.Label>{label}</Combobox.Label>}
|
|
64
|
+
<Combobox.Control>
|
|
65
|
+
<Combobox.Input placeholder={placeholder} />
|
|
66
|
+
<Combobox.IndicatorGroup>
|
|
67
|
+
<Combobox.ClearTrigger />
|
|
68
|
+
<Combobox.Trigger />
|
|
69
|
+
</Combobox.IndicatorGroup>
|
|
70
|
+
</Combobox.Control>
|
|
71
|
+
<Portal>
|
|
72
|
+
<Combobox.Positioner>
|
|
73
|
+
<Combobox.Content>
|
|
74
|
+
<Combobox.Empty>{emptyText}</Combobox.Empty>
|
|
75
|
+
{collection.items.map((item) => (
|
|
76
|
+
<Combobox.Item item={item} key={String(item.value)}>
|
|
77
|
+
<Combobox.ItemText>{item.label}</Combobox.ItemText>
|
|
78
|
+
<Combobox.ItemIndicator />
|
|
79
|
+
</Combobox.Item>
|
|
80
|
+
))}
|
|
81
|
+
</Combobox.Content>
|
|
82
|
+
</Combobox.Positioner>
|
|
83
|
+
</Portal>
|
|
84
|
+
</Combobox.Root>
|
|
85
|
+
);
|
|
86
|
+
}
|