@sproutsocial/seeds-react-menu 1.11.1 → 1.11.3
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/.turbo/turbo-build.log +10 -10
- package/CHANGELOG.md +12 -0
- package/dist/esm/v3/index.js +1413 -3
- package/dist/esm/v3/index.js.map +1 -1
- package/dist/v3/index.d.mts +16 -2
- package/dist/v3/index.d.ts +16 -2
- package/dist/v3/index.js +1418 -2
- package/dist/v3/index.js.map +1 -1
- package/package.json +1 -1
- package/src/v3/Common/ComboboxItem.tsx +4 -0
- package/src/v3/Common/ComboboxStyles.tsx +28 -0
- package/src/v3/Common/SelectItem.tsx +8 -1
- package/src/v3/Common/SelectStyles.tsx +5 -0
- package/src/v3/Common/types.ts +1 -0
- package/src/v3/ModalStories.stories.tsx +6 -6
- package/src/v3/MultiCombobox.stories.tsx +26 -2
- package/src/v3/MultiCombobox.tsx +6 -1
- package/src/v3/MultiSearchableSelect.stories.tsx +27 -2
- package/src/v3/MultiSearchableSelect.tsx +6 -1
- package/src/v3/MultiSelect.stories.tsx +63 -40
- package/src/v3/MultiSelect.tsx +5 -222
- package/src/v3/SingleCombobox.stories.tsx +26 -2
- package/src/v3/SingleCombobox.tsx +5 -1
- package/src/v3/SingleSearchableSelect.stories.tsx +47 -2
- package/src/v3/SingleSearchableSelect.tsx +31 -13
- package/src/v3/SingleSelect.stories.tsx +44 -2
- package/src/v3/SingleSelect.tsx +5 -2
- package/src/v3/storyData.tsx +5 -0
package/src/v3/MultiSelect.tsx
CHANGED
|
@@ -42,6 +42,7 @@ const Placeholder = styled.div`
|
|
|
42
42
|
interface MultiSelectBaseProps {
|
|
43
43
|
id?: string;
|
|
44
44
|
placeholder?: string;
|
|
45
|
+
disabled?: boolean;
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
interface MultiSelectDataProps<T extends DataWithId>
|
|
@@ -79,234 +80,13 @@ interface MultiSelectChildrenProps extends MultiSelectBaseProps {
|
|
|
79
80
|
removeSelectedItems?: never;
|
|
80
81
|
}
|
|
81
82
|
|
|
82
|
-
/*
|
|
83
|
-
'use client';
|
|
84
|
-
import * as React from 'react';
|
|
85
|
-
import { Combobox } from '@base-ui/react/combobox';
|
|
86
|
-
|
|
87
|
-
export default function ExampleAsyncMultipleCombobox() {
|
|
88
|
-
const id = React.useId();
|
|
89
|
-
|
|
90
|
-
const [searchResults, setSearchResults] = React.useState<DirectoryUser[]>([]);
|
|
91
|
-
const [selectedValues, setSelectedValues] = React.useState<DirectoryUser[]>([]);
|
|
92
|
-
const [searchValue, setSearchValue] = React.useState('');
|
|
93
|
-
const [error, setError] = React.useState<string | null>(null);
|
|
94
|
-
const [blockStartStatus, setBlockStartStatus] = React.useState(false);
|
|
95
|
-
|
|
96
|
-
const [isPending, startTransition] = React.useTransition();
|
|
97
|
-
|
|
98
|
-
const { contains } = Combobox.useFilter();
|
|
99
|
-
|
|
100
|
-
const abortControllerRef = React.useRef<AbortController | null>(null);
|
|
101
|
-
const selectedValuesRef = React.useRef<DirectoryUser[]>([]);
|
|
102
|
-
|
|
103
|
-
const trimmedSearchValue = searchValue.trim();
|
|
104
|
-
|
|
105
|
-
const items = React.useMemo(() => {
|
|
106
|
-
if (selectedValues.length === 0) {
|
|
107
|
-
return searchResults;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const merged = [...searchResults];
|
|
111
|
-
|
|
112
|
-
selectedValues.forEach((user) => {
|
|
113
|
-
if (!searchResults.some((result) => result.id === user.id)) {
|
|
114
|
-
merged.push(user);
|
|
115
|
-
}
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
return merged;
|
|
119
|
-
}, [searchResults, selectedValues]);
|
|
120
|
-
|
|
121
|
-
function getStatus() {
|
|
122
|
-
if (isPending) {
|
|
123
|
-
return (
|
|
124
|
-
<React.Fragment>
|
|
125
|
-
<span
|
|
126
|
-
aria-hidden
|
|
127
|
-
className="inline-block size-3 animate-[spin_0.75s_linear_infinite] rounded-full border border-current border-r-transparent rtl:border-r-current rtl:border-l-transparent"
|
|
128
|
-
/>
|
|
129
|
-
Searching…
|
|
130
|
-
</React.Fragment>
|
|
131
|
-
);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
if (error) {
|
|
135
|
-
return error;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
if (trimmedSearchValue === '' && !blockStartStatus) {
|
|
139
|
-
return selectedValues.length > 0 ? null : 'Start typing to search people…';
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
if (searchResults.length === 0 && !blockStartStatus) {
|
|
143
|
-
return `No matches for "${trimmedSearchValue}".`;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
return null;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
function getEmptyMessage() {
|
|
150
|
-
if (trimmedSearchValue === '' || isPending || searchResults.length > 0 || error) {
|
|
151
|
-
return null;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
return 'Try a different search term.';
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
const status = getStatus();
|
|
158
|
-
const emptyMessage = getEmptyMessage();
|
|
159
|
-
|
|
160
|
-
return (
|
|
161
|
-
<Combobox.Root
|
|
162
|
-
items={items}
|
|
163
|
-
itemToStringLabel={(user: DirectoryUser) => user.name}
|
|
164
|
-
multiple
|
|
165
|
-
filter={null}
|
|
166
|
-
onOpenChangeComplete={(open) => {
|
|
167
|
-
if (!open) {
|
|
168
|
-
setSearchResults(selectedValuesRef.current);
|
|
169
|
-
setBlockStartStatus(false);
|
|
170
|
-
}
|
|
171
|
-
}}
|
|
172
|
-
onValueChange={(nextSelectedValues) => {
|
|
173
|
-
selectedValuesRef.current = nextSelectedValues;
|
|
174
|
-
setSelectedValues(nextSelectedValues);
|
|
175
|
-
setSearchValue('');
|
|
176
|
-
setError(null);
|
|
177
|
-
|
|
178
|
-
if (nextSelectedValues.length === 0) {
|
|
179
|
-
setSearchResults([]);
|
|
180
|
-
setBlockStartStatus(false);
|
|
181
|
-
} else {
|
|
182
|
-
setBlockStartStatus(true);
|
|
183
|
-
}
|
|
184
|
-
}}
|
|
185
|
-
onInputValueChange={(nextSearchValue, { reason }) => {
|
|
186
|
-
setSearchValue(nextSearchValue);
|
|
187
|
-
|
|
188
|
-
const controller = new AbortController();
|
|
189
|
-
abortControllerRef.current?.abort();
|
|
190
|
-
abortControllerRef.current = controller;
|
|
191
|
-
|
|
192
|
-
if (nextSearchValue === '') {
|
|
193
|
-
setSearchResults(selectedValuesRef.current);
|
|
194
|
-
setError(null);
|
|
195
|
-
setBlockStartStatus(false);
|
|
196
|
-
return;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
if (reason === 'item-press') {
|
|
200
|
-
return;
|
|
201
|
-
}
|
|
202
|
-
startTransition(async () => {
|
|
203
|
-
setError(null);
|
|
204
|
-
|
|
205
|
-
const result = await searchUsers(nextSearchValue, contains);
|
|
206
|
-
|
|
207
|
-
if (controller.signal.aborted) {
|
|
208
|
-
return;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
startTransition(() => {
|
|
212
|
-
setSearchResults(result.users);
|
|
213
|
-
setError(result.error);
|
|
214
|
-
});
|
|
215
|
-
});
|
|
216
|
-
}}
|
|
217
|
-
>
|
|
218
|
-
<div className="flex flex-col gap-1 text-sm text-gray-900">
|
|
219
|
-
<label className="inline-flex text-inherit font-bold" htmlFor={id}>
|
|
220
|
-
Assign reviewers
|
|
221
|
-
</label>
|
|
222
|
-
<Combobox.InputGroup className="relative flex min-h-10 w-[16rem] cursor-text rounded-md border border-gray-200 bg-[canvas] px-1.5 py-1 focus-within:outline-2 focus-within:-outline-offset-1 focus-within:outline-blue-800 md:w-[20rem]">
|
|
223
|
-
<Combobox.Chips className="flex w-full flex-wrap items-center gap-1">
|
|
224
|
-
<Combobox.Value>
|
|
225
|
-
{(value: DirectoryUser[]) => (
|
|
226
|
-
<React.Fragment>
|
|
227
|
-
{value.map((user) => (
|
|
228
|
-
<Combobox.Chip
|
|
229
|
-
key={user.id}
|
|
230
|
-
className="flex cursor-default items-center gap-1 rounded-md bg-gray-100 py-1 pl-2 pr-1 text-sm text-gray-900 outline-none focus-within:bg-blue-800 focus-within:text-gray-50 [@media(hover:hover)]:[&[data-highlighted]]:bg-blue-800 [@media(hover:hover)]:[&[data-highlighted]]:text-gray-50"
|
|
231
|
-
aria-label={user.name}
|
|
232
|
-
>
|
|
233
|
-
{user.name}
|
|
234
|
-
<Combobox.ChipRemove
|
|
235
|
-
className="inline-flex items-center justify-center rounded-md border-none bg-transparent p-[0.2rem] text-inherit hover:bg-gray-200"
|
|
236
|
-
aria-label={`Remove ${user.name}`}
|
|
237
|
-
>
|
|
238
|
-
<XIcon />
|
|
239
|
-
</Combobox.ChipRemove>
|
|
240
|
-
</Combobox.Chip>
|
|
241
|
-
))}
|
|
242
|
-
<Combobox.Input
|
|
243
|
-
id={id}
|
|
244
|
-
placeholder={value.length > 0 ? '' : 'e.g. Michael'}
|
|
245
|
-
className="h-8 min-w-24 flex-1 rounded-md border-0 bg-transparent pl-2 text-base font-normal text-gray-900 outline-none placeholder:font-normal"
|
|
246
|
-
/>
|
|
247
|
-
</React.Fragment>
|
|
248
|
-
)}
|
|
249
|
-
</Combobox.Value>
|
|
250
|
-
</Combobox.Chips>
|
|
251
|
-
</Combobox.InputGroup>
|
|
252
|
-
</div>
|
|
253
|
-
|
|
254
|
-
<Combobox.Portal>
|
|
255
|
-
<Combobox.Positioner className="outline-none" sideOffset={4}>
|
|
256
|
-
<Combobox.Popup
|
|
257
|
-
className="box-border w-[var(--anchor-width)] max-h-[min(var(--available-height),23rem)] max-w-[var(--available-width)] origin-[var(--transform-origin)] overflow-y-auto scroll-pb-2 scroll-pt-2 overscroll-contain rounded-md bg-[canvas] py-2 text-gray-900 shadow-[0_10px_15px_-3px_var(--color-gray-200),0_4px_6px_-4px_var(--color-gray-200)] outline outline-1 outline-gray-200 transition-[opacity,transform,scale] duration-100 data-[ending-style]:transition-none data-[starting-style]:scale-95 data-[starting-style]:opacity-0 dark:-outline-offset-1 dark:shadow-none dark:outline-gray-300"
|
|
258
|
-
aria-busy={isPending || undefined}
|
|
259
|
-
>
|
|
260
|
-
<Combobox.Status>
|
|
261
|
-
{status ? (
|
|
262
|
-
<div className="flex items-center gap-2 py-1 pl-4 pr-5 text-sm text-gray-600">
|
|
263
|
-
{status}
|
|
264
|
-
</div>
|
|
265
|
-
) : null}
|
|
266
|
-
</Combobox.Status>
|
|
267
|
-
<Combobox.Empty>
|
|
268
|
-
{emptyMessage ? (
|
|
269
|
-
<div className="box-border px-4 py-2 text-sm leading-4 text-gray-600">
|
|
270
|
-
{emptyMessage}
|
|
271
|
-
</div>
|
|
272
|
-
) : null}
|
|
273
|
-
</Combobox.Empty>
|
|
274
|
-
<Combobox.List>
|
|
275
|
-
{(user: DirectoryUser) => (
|
|
276
|
-
<Combobox.Item
|
|
277
|
-
key={user.id}
|
|
278
|
-
value={user}
|
|
279
|
-
className="grid cursor-default select-none grid-cols-[0.75rem_1fr] items-start gap-2 py-2 pl-4 pr-5 text-base leading-[1.2rem] outline-none [@media(hover:hover)]:[&[data-highlighted]]:relative [@media(hover:hover)]:[&[data-highlighted]]:z-0 [@media(hover:hover)]:[&[data-highlighted]]:text-gray-900 [@media(hover:hover)]:[&[data-highlighted]]:before:absolute [@media(hover:hover)]:[&[data-highlighted]]:before:inset-y-0 [@media(hover:hover)]:[&[data-highlighted]]:before:inset-x-2 [@media(hover:hover)]:[&[data-highlighted]]:before:z-[-1] [@media(hover:hover)]:[&[data-highlighted]]:before:rounded [@media(hover:hover)]:[&[data-highlighted]]:before:bg-gray-100 [@media(hover:hover)]:[&[data-highlighted]]:before:content-['']"
|
|
280
|
-
>
|
|
281
|
-
<Combobox.ItemIndicator className="col-start-1 mt-1">
|
|
282
|
-
<CheckIcon className="size-3" />
|
|
283
|
-
</Combobox.ItemIndicator>
|
|
284
|
-
<span className="col-start-2 flex flex-col gap-1">
|
|
285
|
-
<span className="text-[0.95rem] font-bold">{user.name}</span>
|
|
286
|
-
<span className="flex flex-wrap gap-2 text-[0.8125rem] text-gray-600">
|
|
287
|
-
<span className="opacity-80">@{user.username}</span>
|
|
288
|
-
<span>{user.title}</span>
|
|
289
|
-
</span>
|
|
290
|
-
<span className="text-xs text-gray-500">{user.email}</span>
|
|
291
|
-
</span>
|
|
292
|
-
</Combobox.Item>
|
|
293
|
-
)}
|
|
294
|
-
</Combobox.List>
|
|
295
|
-
</Combobox.Popup>
|
|
296
|
-
</Combobox.Positioner>
|
|
297
|
-
</Combobox.Portal>
|
|
298
|
-
</Combobox.Root>
|
|
299
|
-
*/
|
|
300
|
-
|
|
301
83
|
export type MultiSelectProps<T extends DataWithId> =
|
|
302
84
|
| MultiSelectDataProps<T>
|
|
303
85
|
| MultiSelectChildrenProps;
|
|
304
86
|
|
|
305
87
|
// ─── Component ────────────────────────────────────────────────────────────────
|
|
306
88
|
|
|
307
|
-
export
|
|
308
|
-
props: MultiSelectProps<T>
|
|
309
|
-
) {
|
|
89
|
+
export function MultiSelect<T extends DataWithId>(props: MultiSelectProps<T>) {
|
|
310
90
|
const { id, placeholder = "Select..." } = props;
|
|
311
91
|
|
|
312
92
|
const isChildrenMode = "children" in props && props.children != null;
|
|
@@ -396,6 +176,7 @@ export default function MultiSelect<T extends DataWithId>(
|
|
|
396
176
|
items={items}
|
|
397
177
|
value={value}
|
|
398
178
|
onValueChange={handleValueChange}
|
|
179
|
+
disabled={props.disabled}
|
|
399
180
|
id={id}
|
|
400
181
|
>
|
|
401
182
|
<SelectTrigger>
|
|
@@ -457,6 +238,7 @@ export default function MultiSelect<T extends DataWithId>(
|
|
|
457
238
|
key={item.id}
|
|
458
239
|
value={String(item.id)}
|
|
459
240
|
label={itemToString(item)}
|
|
241
|
+
disabled={item.disabled}
|
|
460
242
|
inputType={inputType}
|
|
461
243
|
hidden={
|
|
462
244
|
removeSelectedItems &&
|
|
@@ -476,6 +258,7 @@ export default function MultiSelect<T extends DataWithId>(
|
|
|
476
258
|
key={item.id}
|
|
477
259
|
value={String(item.id)}
|
|
478
260
|
label={itemToString(item)}
|
|
261
|
+
disabled={item.disabled}
|
|
479
262
|
inputType={inputType}
|
|
480
263
|
hidden={
|
|
481
264
|
removeSelectedItems && value.includes(String(item.id))
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import type { Meta, StoryObj } from "@storybook/react";
|
|
3
3
|
import { FormField } from "@sproutsocial/seeds-react-form-field";
|
|
4
|
-
import SingleCombobox from "./SingleCombobox";
|
|
5
|
-
import {
|
|
4
|
+
import { SingleCombobox } from "./SingleCombobox";
|
|
5
|
+
import {
|
|
6
|
+
books,
|
|
7
|
+
booksWithDisabled,
|
|
8
|
+
largeBookSet,
|
|
9
|
+
itemToString,
|
|
10
|
+
renderItem,
|
|
11
|
+
} from "./storyData";
|
|
6
12
|
|
|
7
13
|
const meta: Meta<typeof SingleCombobox> = {
|
|
8
14
|
title: "Really Under Development/V3/Single Combobox",
|
|
@@ -148,6 +154,24 @@ export const Controlled: Story = {
|
|
|
148
154
|
},
|
|
149
155
|
};
|
|
150
156
|
|
|
157
|
+
export const DisabledItems: Story = {
|
|
158
|
+
name: "Disabled items",
|
|
159
|
+
render: () => (
|
|
160
|
+
<div style={{ width: "300px" }}>
|
|
161
|
+
<FormField label="Pick a book">
|
|
162
|
+
{({ id }) => (
|
|
163
|
+
<SingleCombobox
|
|
164
|
+
id={id}
|
|
165
|
+
data={booksWithDisabled}
|
|
166
|
+
itemToString={itemToString}
|
|
167
|
+
placeholder="Search books..."
|
|
168
|
+
/>
|
|
169
|
+
)}
|
|
170
|
+
</FormField>
|
|
171
|
+
</div>
|
|
172
|
+
),
|
|
173
|
+
};
|
|
174
|
+
|
|
151
175
|
export const Virtualized: Story = {
|
|
152
176
|
name: "Virtualized",
|
|
153
177
|
render: () => (
|
|
@@ -29,6 +29,7 @@ interface SingleComboboxBaseProps {
|
|
|
29
29
|
id?: string;
|
|
30
30
|
placeholder?: string;
|
|
31
31
|
emptyText?: string;
|
|
32
|
+
disabled?: boolean;
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
interface SingleComboboxDataBaseProps<T extends DataWithId>
|
|
@@ -78,7 +79,7 @@ export type SingleComboboxProps<T extends DataWithId> =
|
|
|
78
79
|
|
|
79
80
|
// ─── Component ────────────────────────────────────────────────────────────────
|
|
80
81
|
|
|
81
|
-
export
|
|
82
|
+
export function SingleCombobox<T extends DataWithId>(
|
|
82
83
|
props: SingleComboboxProps<T>
|
|
83
84
|
) {
|
|
84
85
|
const {
|
|
@@ -175,6 +176,7 @@ export default function SingleCombobox<T extends DataWithId>(
|
|
|
175
176
|
isVirtualized ? data : groups ?? (isChildrenMode ? undefined : data)
|
|
176
177
|
}
|
|
177
178
|
virtualized={isVirtualized}
|
|
179
|
+
disabled={props.disabled}
|
|
178
180
|
open={open}
|
|
179
181
|
onOpenChange={setOpen}
|
|
180
182
|
onItemHighlighted={
|
|
@@ -265,6 +267,7 @@ export default function SingleCombobox<T extends DataWithId>(
|
|
|
265
267
|
value={item}
|
|
266
268
|
itemId={String(item.id)}
|
|
267
269
|
label={itemToString(item)}
|
|
270
|
+
disabled={item.disabled}
|
|
268
271
|
inputType={inputType}
|
|
269
272
|
renderItem={
|
|
270
273
|
renderItem
|
|
@@ -285,6 +288,7 @@ export default function SingleCombobox<T extends DataWithId>(
|
|
|
285
288
|
value={item}
|
|
286
289
|
itemId={String(item.id)}
|
|
287
290
|
label={itemToString(item)}
|
|
291
|
+
disabled={item.disabled}
|
|
288
292
|
inputType={inputType}
|
|
289
293
|
renderItem={
|
|
290
294
|
renderItem
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import type { Meta, StoryObj } from "@storybook/react";
|
|
3
3
|
import { FormField } from "@sproutsocial/seeds-react-form-field";
|
|
4
|
-
import SingleSearchableSelect from "./SingleSearchableSelect";
|
|
5
|
-
import {
|
|
4
|
+
import { SingleSearchableSelect } from "./SingleSearchableSelect";
|
|
5
|
+
import {
|
|
6
|
+
books,
|
|
7
|
+
booksWithDisabled,
|
|
8
|
+
largeBookSet,
|
|
9
|
+
itemToString,
|
|
10
|
+
renderItem,
|
|
11
|
+
} from "./storyData";
|
|
6
12
|
|
|
7
13
|
const meta: Meta<typeof SingleSearchableSelect> = {
|
|
8
14
|
title: "Really Under Development/V3/Single Searchable Select",
|
|
@@ -163,6 +169,45 @@ export const CustomSelection: Story = {
|
|
|
163
169
|
),
|
|
164
170
|
};
|
|
165
171
|
|
|
172
|
+
export const DisabledItems: Story = {
|
|
173
|
+
name: "Disabled items",
|
|
174
|
+
render: () => (
|
|
175
|
+
<div style={{ width: "300px" }}>
|
|
176
|
+
<FormField label="Pick a book">
|
|
177
|
+
{({ id }) => (
|
|
178
|
+
<SingleSearchableSelect
|
|
179
|
+
id={id}
|
|
180
|
+
data={booksWithDisabled}
|
|
181
|
+
itemToString={itemToString}
|
|
182
|
+
placeholder="Select a book"
|
|
183
|
+
searchPlaceholder="Search books..."
|
|
184
|
+
/>
|
|
185
|
+
)}
|
|
186
|
+
</FormField>
|
|
187
|
+
</div>
|
|
188
|
+
),
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
export const IncludePlaceholderItem: Story = {
|
|
192
|
+
name: "Include placeholder item",
|
|
193
|
+
render: () => (
|
|
194
|
+
<div style={{ width: "300px" }}>
|
|
195
|
+
<FormField label="Pick a book">
|
|
196
|
+
{({ id }) => (
|
|
197
|
+
<SingleSearchableSelect
|
|
198
|
+
id={id}
|
|
199
|
+
data={books}
|
|
200
|
+
itemToString={itemToString}
|
|
201
|
+
placeholder="Select a book"
|
|
202
|
+
searchPlaceholder="Search books..."
|
|
203
|
+
includePlaceholderItem
|
|
204
|
+
/>
|
|
205
|
+
)}
|
|
206
|
+
</FormField>
|
|
207
|
+
</div>
|
|
208
|
+
),
|
|
209
|
+
};
|
|
210
|
+
|
|
166
211
|
export const Virtualized: Story = {
|
|
167
212
|
name: "Virtualized",
|
|
168
213
|
render: () => (
|
|
@@ -29,10 +29,12 @@ import { StyledChevron, ChevronIcon } from "./Common/SelectIcons";
|
|
|
29
29
|
interface SingleSearchableSelectBaseProps {
|
|
30
30
|
id?: string;
|
|
31
31
|
placeholder?: string;
|
|
32
|
+
includePlaceholderItem?: boolean;
|
|
32
33
|
searchPlaceholder?: string;
|
|
33
34
|
emptyText?: string;
|
|
34
35
|
isLoading?: boolean;
|
|
35
36
|
loadingText?: string;
|
|
37
|
+
disabled?: boolean;
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
interface SingleSearchableSelectDataBaseProps<T extends DataWithId>
|
|
@@ -85,12 +87,13 @@ export type SingleSearchableSelectProps<T extends DataWithId> =
|
|
|
85
87
|
|
|
86
88
|
// ─── Component ────────────────────────────────────────────────────────────────
|
|
87
89
|
|
|
88
|
-
export
|
|
90
|
+
export function SingleSearchableSelect<T extends DataWithId>(
|
|
89
91
|
props: SingleSearchableSelectProps<T>
|
|
90
92
|
) {
|
|
91
93
|
const {
|
|
92
94
|
id,
|
|
93
95
|
placeholder = "Select...",
|
|
96
|
+
includePlaceholderItem,
|
|
94
97
|
searchPlaceholder = "Search...",
|
|
95
98
|
emptyText = "No results found.",
|
|
96
99
|
isLoading = false,
|
|
@@ -179,6 +182,7 @@ export default function SingleSearchableSelect<T extends DataWithId>(
|
|
|
179
182
|
<Combobox.Root
|
|
180
183
|
id={id}
|
|
181
184
|
value={value}
|
|
185
|
+
disabled={props.disabled}
|
|
182
186
|
onValueChange={handleValueChange}
|
|
183
187
|
itemToStringLabel={itemToString}
|
|
184
188
|
isItemEqualToValue={(a, b) => a != null && b != null && a.id === b.id}
|
|
@@ -282,6 +286,7 @@ export default function SingleSearchableSelect<T extends DataWithId>(
|
|
|
282
286
|
value={item}
|
|
283
287
|
itemId={String(item.id)}
|
|
284
288
|
label={itemToString(item)}
|
|
289
|
+
disabled={item.disabled}
|
|
285
290
|
inputType={inputType}
|
|
286
291
|
renderItem={
|
|
287
292
|
renderItem
|
|
@@ -296,18 +301,31 @@ export default function SingleSearchableSelect<T extends DataWithId>(
|
|
|
296
301
|
</React.Fragment>
|
|
297
302
|
)
|
|
298
303
|
) : (
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
304
|
+
<>
|
|
305
|
+
{includePlaceholderItem && (
|
|
306
|
+
<ComboboxItem
|
|
307
|
+
key="placeholder"
|
|
308
|
+
value={null as unknown as T}
|
|
309
|
+
itemId="placeholder"
|
|
310
|
+
label={placeholder}
|
|
311
|
+
/>
|
|
312
|
+
)}
|
|
313
|
+
<Combobox.Collection>
|
|
314
|
+
{(item: T) => (
|
|
315
|
+
<ComboboxItem
|
|
316
|
+
key={item.id}
|
|
317
|
+
value={item}
|
|
318
|
+
itemId={String(item.id)}
|
|
319
|
+
label={itemToString(item)}
|
|
320
|
+
disabled={item.disabled}
|
|
321
|
+
inputType={inputType}
|
|
322
|
+
renderItem={
|
|
323
|
+
renderItem ? () => renderItem(item) : undefined
|
|
324
|
+
}
|
|
325
|
+
/>
|
|
326
|
+
)}
|
|
327
|
+
</Combobox.Collection>
|
|
328
|
+
</>
|
|
311
329
|
)}
|
|
312
330
|
</SearchableSelectList>
|
|
313
331
|
</SearchableSelectPopup>
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import type { Meta, StoryObj } from "@storybook/react";
|
|
3
3
|
import { FormField } from "@sproutsocial/seeds-react-form-field";
|
|
4
|
-
import SingleSelect from "./SingleSelect";
|
|
4
|
+
import { SingleSelect } from "./SingleSelect";
|
|
5
5
|
import SelectItem from "./Common/SelectItem";
|
|
6
6
|
import SelectGroup from "./Common/SelectGroup";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
books,
|
|
9
|
+
booksWithDisabled,
|
|
10
|
+
itemToString,
|
|
11
|
+
renderItem,
|
|
12
|
+
} from "./storyData";
|
|
8
13
|
|
|
9
14
|
const meta: Meta<typeof SingleSelect> = {
|
|
10
15
|
title: "Really Under Development/V3/Single Select",
|
|
@@ -32,6 +37,25 @@ export const Basic: Story = {
|
|
|
32
37
|
),
|
|
33
38
|
};
|
|
34
39
|
|
|
40
|
+
export const Disabled: Story = {
|
|
41
|
+
name: "Disabled",
|
|
42
|
+
render: () => (
|
|
43
|
+
<div style={{ width: "300px" }}>
|
|
44
|
+
<FormField label="Pick a book">
|
|
45
|
+
{({ id }) => (
|
|
46
|
+
<SingleSelect
|
|
47
|
+
id={id}
|
|
48
|
+
data={books}
|
|
49
|
+
itemToString={itemToString}
|
|
50
|
+
placeholder="Select a book"
|
|
51
|
+
disabled
|
|
52
|
+
/>
|
|
53
|
+
)}
|
|
54
|
+
</FormField>
|
|
55
|
+
</div>
|
|
56
|
+
),
|
|
57
|
+
};
|
|
58
|
+
|
|
35
59
|
export const IncludePlaceholderItem: Story = {
|
|
36
60
|
name: "Include placeholder item",
|
|
37
61
|
render: () => (
|
|
@@ -198,6 +222,24 @@ export const CustomSelection: Story = {
|
|
|
198
222
|
),
|
|
199
223
|
};
|
|
200
224
|
|
|
225
|
+
export const DisabledItems: Story = {
|
|
226
|
+
name: "Disabled items",
|
|
227
|
+
render: () => (
|
|
228
|
+
<div style={{ width: "300px" }}>
|
|
229
|
+
<FormField label="Pick a book">
|
|
230
|
+
{({ id }) => (
|
|
231
|
+
<SingleSelect
|
|
232
|
+
id={id}
|
|
233
|
+
data={booksWithDisabled}
|
|
234
|
+
itemToString={itemToString}
|
|
235
|
+
placeholder="Select a book"
|
|
236
|
+
/>
|
|
237
|
+
)}
|
|
238
|
+
</FormField>
|
|
239
|
+
</div>
|
|
240
|
+
),
|
|
241
|
+
};
|
|
242
|
+
|
|
201
243
|
export const Children: Story = {
|
|
202
244
|
name: "Children API",
|
|
203
245
|
render: () => (
|
package/src/v3/SingleSelect.tsx
CHANGED
|
@@ -38,6 +38,7 @@ interface SingleSelectBaseProps {
|
|
|
38
38
|
id?: string;
|
|
39
39
|
placeholder?: string;
|
|
40
40
|
includePlaceholderItem?: boolean;
|
|
41
|
+
disabled?: boolean;
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
interface SingleSelectDataProps<T extends DataWithId>
|
|
@@ -75,7 +76,7 @@ export type SingleSelectProps<T extends DataWithId> =
|
|
|
75
76
|
|
|
76
77
|
// ─── Component ────────────────────────────────────────────────────────────────
|
|
77
78
|
|
|
78
|
-
export
|
|
79
|
+
export function SingleSelect<T extends DataWithId>(
|
|
79
80
|
props: SingleSelectProps<T>
|
|
80
81
|
) {
|
|
81
82
|
const { id, placeholder, includePlaceholderItem } = props;
|
|
@@ -161,6 +162,7 @@ export default function SingleSelect<T extends DataWithId>(
|
|
|
161
162
|
<Select.Root
|
|
162
163
|
items={items}
|
|
163
164
|
value={value}
|
|
165
|
+
disabled={props.disabled}
|
|
164
166
|
onValueChange={handleValueChange}
|
|
165
167
|
id={id}
|
|
166
168
|
>
|
|
@@ -197,6 +199,7 @@ export default function SingleSelect<T extends DataWithId>(
|
|
|
197
199
|
key={item.id}
|
|
198
200
|
value={String(item.id)}
|
|
199
201
|
label={itemToString(item)}
|
|
202
|
+
disabled={item.disabled}
|
|
200
203
|
inputType={inputType}
|
|
201
204
|
renderItem={
|
|
202
205
|
renderItem
|
|
@@ -210,7 +213,6 @@ export default function SingleSelect<T extends DataWithId>(
|
|
|
210
213
|
</React.Fragment>
|
|
211
214
|
))
|
|
212
215
|
) : (
|
|
213
|
-
// TODO: need to add a placeholder item when includePlaceholderItem is true
|
|
214
216
|
<>
|
|
215
217
|
{includePlaceholderItem && (
|
|
216
218
|
<SelectItem
|
|
@@ -224,6 +226,7 @@ export default function SingleSelect<T extends DataWithId>(
|
|
|
224
226
|
key={item.id}
|
|
225
227
|
value={String(item.id)}
|
|
226
228
|
label={itemToString(item)}
|
|
229
|
+
disabled={item.disabled}
|
|
227
230
|
inputType={inputType}
|
|
228
231
|
renderItem={
|
|
229
232
|
renderItem
|
package/src/v3/storyData.tsx
CHANGED
|
@@ -105,6 +105,11 @@ const generateBooks = (count: number): Book[] => {
|
|
|
105
105
|
|
|
106
106
|
export const largeBookSet = generateBooks(1000);
|
|
107
107
|
|
|
108
|
+
export const booksWithDisabled: Book[] = books.map((book) => ({
|
|
109
|
+
...book,
|
|
110
|
+
disabled: book.id === "book-3" || book.id === "book-7",
|
|
111
|
+
}));
|
|
112
|
+
|
|
108
113
|
export const itemToString = (item: Book | null) => item?.title ?? "";
|
|
109
114
|
|
|
110
115
|
export const renderItem = (item: Book) => (
|