@sproutsocial/seeds-react-menu 1.11.2 → 1.11.4
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 +16 -16
- package/CHANGELOG.md +12 -0
- package/dist/esm/v3/index.js +104 -42
- package/dist/esm/v3/index.js.map +1 -1
- package/dist/v3/index.d.mts +2 -0
- package/dist/v3/index.d.ts +2 -0
- package/dist/v3/index.js +103 -41
- package/dist/v3/index.js.map +1 -1
- package/package.json +1 -1
- package/src/v3/Common/ComboboxItem.tsx +3 -0
- package/src/v3/Common/SelectItem.tsx +3 -0
- package/src/v3/Common/SelectStyles.tsx +1 -0
- package/src/v3/Common/VirtualizedComboboxList.tsx +23 -1
- package/src/v3/Common/sentinels.ts +17 -0
- package/src/v3/Common/types.ts +1 -0
- package/src/v3/MultiCombobox.stories.tsx +25 -1
- package/src/v3/MultiCombobox.tsx +3 -0
- package/src/v3/MultiSearchableSelect.stories.tsx +26 -1
- package/src/v3/MultiSearchableSelect.tsx +3 -0
- package/src/v3/MultiSelect.stories.tsx +24 -1
- package/src/v3/MultiSelect.tsx +2 -0
- package/src/v3/SingleCombobox.stories.tsx +25 -1
- package/src/v3/SingleCombobox.tsx +2 -0
- package/src/v3/SingleSearchableSelect.stories.tsx +67 -1
- package/src/v3/SingleSearchableSelect.tsx +59 -20
- package/src/v3/SingleSelect.stories.tsx +24 -1
- package/src/v3/SingleSelect.tsx +2 -1
- package/src/v3/storyData.tsx +5 -0
package/package.json
CHANGED
|
@@ -11,6 +11,7 @@ interface ComboboxItemProps<T> {
|
|
|
11
11
|
value: T;
|
|
12
12
|
itemId: string;
|
|
13
13
|
label: string;
|
|
14
|
+
disabled?: boolean;
|
|
14
15
|
inputType?: "icon" | "radio" | "checkbox" | "none";
|
|
15
16
|
renderItem?: () => React.ReactNode;
|
|
16
17
|
index?: number;
|
|
@@ -23,6 +24,7 @@ function ComboboxItemInner<T>(
|
|
|
23
24
|
value,
|
|
24
25
|
itemId,
|
|
25
26
|
label,
|
|
27
|
+
disabled,
|
|
26
28
|
inputType = "icon",
|
|
27
29
|
renderItem,
|
|
28
30
|
index,
|
|
@@ -38,6 +40,7 @@ function ComboboxItemInner<T>(
|
|
|
38
40
|
data-index={dataIndex}
|
|
39
41
|
style={style}
|
|
40
42
|
ref={ref}
|
|
43
|
+
disabled={disabled}
|
|
41
44
|
data-qa-menu-item={label}
|
|
42
45
|
>
|
|
43
46
|
{inputType === "radio" ? (
|
|
@@ -57,6 +57,7 @@ function CheckIcon() {
|
|
|
57
57
|
interface SelectItemProps {
|
|
58
58
|
value: string | null;
|
|
59
59
|
label: string;
|
|
60
|
+
disabled?: boolean;
|
|
60
61
|
inputType?: "icon" | "radio" | "checkbox" | "none";
|
|
61
62
|
renderItem?: () => React.ReactNode;
|
|
62
63
|
hidden?: boolean;
|
|
@@ -65,6 +66,7 @@ interface SelectItemProps {
|
|
|
65
66
|
export default function SelectItem({
|
|
66
67
|
value,
|
|
67
68
|
label,
|
|
69
|
+
disabled,
|
|
68
70
|
inputType = "icon",
|
|
69
71
|
renderItem,
|
|
70
72
|
hidden,
|
|
@@ -72,6 +74,7 @@ export default function SelectItem({
|
|
|
72
74
|
return (
|
|
73
75
|
<StyledItem
|
|
74
76
|
value={value}
|
|
77
|
+
disabled={disabled}
|
|
75
78
|
style={hidden ? { display: "none" } : undefined}
|
|
76
79
|
data-qa-menu-item={label}
|
|
77
80
|
>
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
ComboboxSelectAllItem,
|
|
9
9
|
} from "./ComboboxStyles";
|
|
10
10
|
import type { DataWithId } from "./types";
|
|
11
|
+
import { isPlaceholderSentinel } from "./sentinels";
|
|
11
12
|
|
|
12
13
|
export const ITEM_HEIGHT = 32;
|
|
13
14
|
|
|
@@ -37,6 +38,10 @@ interface VirtualizedComboboxListProps<T extends DataWithId> {
|
|
|
37
38
|
> | null>;
|
|
38
39
|
value: T | T[] | null;
|
|
39
40
|
groups: Array<{ value: string; items: T[] }> | null;
|
|
41
|
+
// `data` contains only real items (no sentinels). Sentinel rows (placeholder,
|
|
42
|
+
// group headers, select-all) come exclusively from `filteredItems` via
|
|
43
|
+
// Combobox.useFilteredItems. This means `data.length` is the correct count
|
|
44
|
+
// of selectable items for select-all logic.
|
|
40
45
|
data: T[];
|
|
41
46
|
itemToString: (item: T) => string;
|
|
42
47
|
inputType: "icon" | "radio" | "checkbox" | "none";
|
|
@@ -109,7 +114,7 @@ export default function VirtualizedComboboxList<T extends DataWithId>({
|
|
|
109
114
|
>
|
|
110
115
|
{virtualizer.getVirtualItems().map((virtualItem) => {
|
|
111
116
|
const row = filteredItems[virtualItem.index];
|
|
112
|
-
if (
|
|
117
|
+
if (row == null) return null;
|
|
113
118
|
|
|
114
119
|
const baseStyle: React.CSSProperties = {
|
|
115
120
|
position: "absolute",
|
|
@@ -120,6 +125,22 @@ export default function VirtualizedComboboxList<T extends DataWithId>({
|
|
|
120
125
|
boxSizing: "border-box",
|
|
121
126
|
};
|
|
122
127
|
|
|
128
|
+
if (isPlaceholderSentinel(row)) {
|
|
129
|
+
return (
|
|
130
|
+
<ComboboxItem
|
|
131
|
+
key="placeholder"
|
|
132
|
+
index={virtualItem.index}
|
|
133
|
+
data-index={virtualItem.index}
|
|
134
|
+
value={row as unknown as T}
|
|
135
|
+
itemId="placeholder"
|
|
136
|
+
label={row.label}
|
|
137
|
+
inputType={inputType}
|
|
138
|
+
style={baseStyle}
|
|
139
|
+
ref={virtualizer.measureElement}
|
|
140
|
+
/>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
123
144
|
if (isSelectAllSentinel(row)) {
|
|
124
145
|
const selectedCount = selectedArray.length;
|
|
125
146
|
const totalCount = data.length;
|
|
@@ -185,6 +206,7 @@ export default function VirtualizedComboboxList<T extends DataWithId>({
|
|
|
185
206
|
value={item}
|
|
186
207
|
itemId={String(item.id)}
|
|
187
208
|
label={itemToString(item)}
|
|
209
|
+
disabled={item.disabled}
|
|
188
210
|
inputType={inputType}
|
|
189
211
|
renderItem={
|
|
190
212
|
renderItem ? () => renderItem(item) : () => itemToString(item)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// ─── Shared sentinel types & predicates ────────────────────────────────────
|
|
2
|
+
// Sentinels are non-data rows injected into virtualized lists for special UI
|
|
3
|
+
// items (placeholder, group headers, select-all). They carry no `.id` field
|
|
4
|
+
// and must never be counted toward real data totals.
|
|
5
|
+
|
|
6
|
+
export interface PlaceholderSentinel {
|
|
7
|
+
__placeholder: true;
|
|
8
|
+
label: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function makePlaceholderSentinel(label: string): PlaceholderSentinel {
|
|
12
|
+
return { __placeholder: true, label };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function isPlaceholderSentinel(v: unknown): v is PlaceholderSentinel {
|
|
16
|
+
return typeof v === "object" && v !== null && "__placeholder" in v;
|
|
17
|
+
}
|
package/src/v3/Common/types.ts
CHANGED
|
@@ -2,7 +2,13 @@ import React from "react";
|
|
|
2
2
|
import type { Meta, StoryObj } from "@storybook/react";
|
|
3
3
|
import { FormField } from "@sproutsocial/seeds-react-form-field";
|
|
4
4
|
import { MultiCombobox } from "./MultiCombobox";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
books,
|
|
7
|
+
booksWithDisabled,
|
|
8
|
+
largeBookSet,
|
|
9
|
+
itemToString,
|
|
10
|
+
renderItem,
|
|
11
|
+
} from "./storyData";
|
|
6
12
|
|
|
7
13
|
const meta: Meta<typeof MultiCombobox> = {
|
|
8
14
|
title: "Really Under Development/V3/Multi Combobox",
|
|
@@ -135,6 +141,24 @@ export const Controlled: Story = {
|
|
|
135
141
|
},
|
|
136
142
|
};
|
|
137
143
|
|
|
144
|
+
export const DisabledItems: Story = {
|
|
145
|
+
name: "Disabled items",
|
|
146
|
+
render: () => (
|
|
147
|
+
<div style={{ width: "300px" }}>
|
|
148
|
+
<FormField label="Pick books">
|
|
149
|
+
{({ id }) => (
|
|
150
|
+
<MultiCombobox
|
|
151
|
+
id={id}
|
|
152
|
+
data={booksWithDisabled}
|
|
153
|
+
itemToString={itemToString}
|
|
154
|
+
placeholder="Search books..."
|
|
155
|
+
/>
|
|
156
|
+
)}
|
|
157
|
+
</FormField>
|
|
158
|
+
</div>
|
|
159
|
+
),
|
|
160
|
+
};
|
|
161
|
+
|
|
138
162
|
export const SelectableHeadings: Story = {
|
|
139
163
|
name: "Selectable headings",
|
|
140
164
|
render: () => (
|
package/src/v3/MultiCombobox.tsx
CHANGED
|
@@ -343,6 +343,7 @@ export function MultiCombobox<T extends DataWithId>(
|
|
|
343
343
|
value={item}
|
|
344
344
|
itemId={String(item.id)}
|
|
345
345
|
label={itemToString(item)}
|
|
346
|
+
disabled={item.disabled}
|
|
346
347
|
inputType={inputType}
|
|
347
348
|
renderItem={
|
|
348
349
|
renderItem ? () => renderItem(item) : () => itemToString(item)
|
|
@@ -525,6 +526,7 @@ export function MultiCombobox<T extends DataWithId>(
|
|
|
525
526
|
value={item}
|
|
526
527
|
itemId={String(item.id)}
|
|
527
528
|
label={itemToString(item)}
|
|
529
|
+
disabled={item.disabled}
|
|
528
530
|
inputType={inputType}
|
|
529
531
|
renderItem={
|
|
530
532
|
renderItem
|
|
@@ -545,6 +547,7 @@ export function MultiCombobox<T extends DataWithId>(
|
|
|
545
547
|
value={item}
|
|
546
548
|
itemId={String(item.id)}
|
|
547
549
|
label={itemToString(item)}
|
|
550
|
+
disabled={item.disabled}
|
|
548
551
|
inputType={inputType}
|
|
549
552
|
renderItem={
|
|
550
553
|
renderItem
|
|
@@ -2,7 +2,13 @@ import React from "react";
|
|
|
2
2
|
import type { Meta, StoryObj } from "@storybook/react";
|
|
3
3
|
import { FormField } from "@sproutsocial/seeds-react-form-field";
|
|
4
4
|
import { MultiSearchableSelect } from "./MultiSearchableSelect";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
books,
|
|
7
|
+
booksWithDisabled,
|
|
8
|
+
largeBookSet,
|
|
9
|
+
itemToString,
|
|
10
|
+
renderItem,
|
|
11
|
+
} from "./storyData";
|
|
6
12
|
|
|
7
13
|
const meta: Meta<typeof MultiSearchableSelect> = {
|
|
8
14
|
title: "Really Under Development/V3/Multi Searchable Select",
|
|
@@ -193,6 +199,25 @@ export const CustomRenderSelections: Story = {
|
|
|
193
199
|
),
|
|
194
200
|
};
|
|
195
201
|
|
|
202
|
+
export const DisabledItems: Story = {
|
|
203
|
+
name: "Disabled items",
|
|
204
|
+
render: () => (
|
|
205
|
+
<div style={{ width: "300px" }}>
|
|
206
|
+
<FormField label="Pick books">
|
|
207
|
+
{({ id }) => (
|
|
208
|
+
<MultiSearchableSelect
|
|
209
|
+
id={id}
|
|
210
|
+
data={booksWithDisabled}
|
|
211
|
+
itemToString={itemToString}
|
|
212
|
+
placeholder="Select books"
|
|
213
|
+
searchPlaceholder="Search books..."
|
|
214
|
+
/>
|
|
215
|
+
)}
|
|
216
|
+
</FormField>
|
|
217
|
+
</div>
|
|
218
|
+
),
|
|
219
|
+
};
|
|
220
|
+
|
|
196
221
|
export const SelectableHeadings: Story = {
|
|
197
222
|
name: "Selectable headings",
|
|
198
223
|
render: () => (
|
|
@@ -342,6 +342,7 @@ export function MultiSearchableSelect<T extends DataWithId>(
|
|
|
342
342
|
value={item}
|
|
343
343
|
itemId={String(item.id)}
|
|
344
344
|
label={itemToString(item)}
|
|
345
|
+
disabled={item.disabled}
|
|
345
346
|
inputType={inputType}
|
|
346
347
|
renderItem={
|
|
347
348
|
renderItem ? () => renderItem(item) : () => itemToString(item)
|
|
@@ -508,6 +509,7 @@ export function MultiSearchableSelect<T extends DataWithId>(
|
|
|
508
509
|
value={item}
|
|
509
510
|
itemId={String(item.id)}
|
|
510
511
|
label={itemToString(item)}
|
|
512
|
+
disabled={item.disabled}
|
|
511
513
|
inputType={inputType}
|
|
512
514
|
renderItem={
|
|
513
515
|
renderItem
|
|
@@ -528,6 +530,7 @@ export function MultiSearchableSelect<T extends DataWithId>(
|
|
|
528
530
|
value={item}
|
|
529
531
|
itemId={String(item.id)}
|
|
530
532
|
label={itemToString(item)}
|
|
533
|
+
disabled={item.disabled}
|
|
531
534
|
inputType={inputType}
|
|
532
535
|
renderItem={
|
|
533
536
|
renderItem
|
|
@@ -4,7 +4,12 @@ import { FormField } from "@sproutsocial/seeds-react-form-field";
|
|
|
4
4
|
import { MultiSelect } from "./MultiSelect";
|
|
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 MultiSelect> = {
|
|
10
15
|
title: "Really Under Development/V3/Multi Select",
|
|
@@ -223,6 +228,24 @@ export const CustomSelection: Story = {
|
|
|
223
228
|
// ),
|
|
224
229
|
// };
|
|
225
230
|
|
|
231
|
+
export const DisabledItems: Story = {
|
|
232
|
+
name: "Disabled items",
|
|
233
|
+
render: () => (
|
|
234
|
+
<div style={{ width: "300px" }}>
|
|
235
|
+
<FormField label="Pick books">
|
|
236
|
+
{({ id }) => (
|
|
237
|
+
<MultiSelect
|
|
238
|
+
id={id}
|
|
239
|
+
data={booksWithDisabled}
|
|
240
|
+
itemToString={itemToString}
|
|
241
|
+
placeholder="Select books..."
|
|
242
|
+
/>
|
|
243
|
+
)}
|
|
244
|
+
</FormField>
|
|
245
|
+
</div>
|
|
246
|
+
),
|
|
247
|
+
};
|
|
248
|
+
|
|
226
249
|
export const RemoveSelectedItems: Story = {
|
|
227
250
|
name: "Remove selected items",
|
|
228
251
|
render: () => (
|
package/src/v3/MultiSelect.tsx
CHANGED
|
@@ -238,6 +238,7 @@ export function MultiSelect<T extends DataWithId>(props: MultiSelectProps<T>) {
|
|
|
238
238
|
key={item.id}
|
|
239
239
|
value={String(item.id)}
|
|
240
240
|
label={itemToString(item)}
|
|
241
|
+
disabled={item.disabled}
|
|
241
242
|
inputType={inputType}
|
|
242
243
|
hidden={
|
|
243
244
|
removeSelectedItems &&
|
|
@@ -257,6 +258,7 @@ export function MultiSelect<T extends DataWithId>(props: MultiSelectProps<T>) {
|
|
|
257
258
|
key={item.id}
|
|
258
259
|
value={String(item.id)}
|
|
259
260
|
label={itemToString(item)}
|
|
261
|
+
disabled={item.disabled}
|
|
260
262
|
inputType={inputType}
|
|
261
263
|
hidden={
|
|
262
264
|
removeSelectedItems && value.includes(String(item.id))
|
|
@@ -2,7 +2,13 @@ import React from "react";
|
|
|
2
2
|
import type { Meta, StoryObj } from "@storybook/react";
|
|
3
3
|
import { FormField } from "@sproutsocial/seeds-react-form-field";
|
|
4
4
|
import { SingleCombobox } from "./SingleCombobox";
|
|
5
|
-
import {
|
|
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: () => (
|
|
@@ -267,6 +267,7 @@ export function SingleCombobox<T extends DataWithId>(
|
|
|
267
267
|
value={item}
|
|
268
268
|
itemId={String(item.id)}
|
|
269
269
|
label={itemToString(item)}
|
|
270
|
+
disabled={item.disabled}
|
|
270
271
|
inputType={inputType}
|
|
271
272
|
renderItem={
|
|
272
273
|
renderItem
|
|
@@ -287,6 +288,7 @@ export function SingleCombobox<T extends DataWithId>(
|
|
|
287
288
|
value={item}
|
|
288
289
|
itemId={String(item.id)}
|
|
289
290
|
label={itemToString(item)}
|
|
291
|
+
disabled={item.disabled}
|
|
290
292
|
inputType={inputType}
|
|
291
293
|
renderItem={
|
|
292
294
|
renderItem
|
|
@@ -2,7 +2,13 @@ import React from "react";
|
|
|
2
2
|
import type { Meta, StoryObj } from "@storybook/react";
|
|
3
3
|
import { FormField } from "@sproutsocial/seeds-react-form-field";
|
|
4
4
|
import { SingleSearchableSelect } from "./SingleSearchableSelect";
|
|
5
|
-
import {
|
|
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,66 @@ 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
|
+
|
|
211
|
+
export const IncludePlaceholderItemVirtualized: Story = {
|
|
212
|
+
name: "Include placeholder item (virtualized)",
|
|
213
|
+
render: () => (
|
|
214
|
+
<div style={{ width: "300px" }}>
|
|
215
|
+
<FormField label="Pick a book">
|
|
216
|
+
{({ id }) => (
|
|
217
|
+
<SingleSearchableSelect
|
|
218
|
+
id={id}
|
|
219
|
+
data={largeBookSet}
|
|
220
|
+
itemToString={itemToString}
|
|
221
|
+
placeholder="Select a book"
|
|
222
|
+
searchPlaceholder="Search books..."
|
|
223
|
+
includePlaceholderItem
|
|
224
|
+
isVirtualized
|
|
225
|
+
/>
|
|
226
|
+
)}
|
|
227
|
+
</FormField>
|
|
228
|
+
</div>
|
|
229
|
+
),
|
|
230
|
+
};
|
|
231
|
+
|
|
166
232
|
export const Virtualized: Story = {
|
|
167
233
|
name: "Virtualized",
|
|
168
234
|
render: () => (
|
|
@@ -6,6 +6,11 @@ import ComboboxItem from "./Common/ComboboxItem";
|
|
|
6
6
|
import VirtualizedComboboxList from "./Common/VirtualizedComboboxList";
|
|
7
7
|
import type { DataWithId } from "./Common/types";
|
|
8
8
|
import { groupItems } from "./Common/groupItems";
|
|
9
|
+
import type { PlaceholderSentinel } from "./Common/sentinels";
|
|
10
|
+
import {
|
|
11
|
+
makePlaceholderSentinel,
|
|
12
|
+
isPlaceholderSentinel,
|
|
13
|
+
} from "./Common/sentinels";
|
|
9
14
|
import { Field } from "./Common/SelectStyles";
|
|
10
15
|
import {
|
|
11
16
|
ComboboxGroup,
|
|
@@ -29,6 +34,7 @@ import { StyledChevron, ChevronIcon } from "./Common/SelectIcons";
|
|
|
29
34
|
interface SingleSearchableSelectBaseProps {
|
|
30
35
|
id?: string;
|
|
31
36
|
placeholder?: string;
|
|
37
|
+
includePlaceholderItem?: boolean;
|
|
32
38
|
searchPlaceholder?: string;
|
|
33
39
|
emptyText?: string;
|
|
34
40
|
isLoading?: boolean;
|
|
@@ -92,6 +98,7 @@ export function SingleSearchableSelect<T extends DataWithId>(
|
|
|
92
98
|
const {
|
|
93
99
|
id,
|
|
94
100
|
placeholder = "Select...",
|
|
101
|
+
includePlaceholderItem,
|
|
95
102
|
searchPlaceholder = "Search...",
|
|
96
103
|
emptyText = "No results found.",
|
|
97
104
|
isLoading = false,
|
|
@@ -162,10 +169,14 @@ export function SingleSearchableSelect<T extends DataWithId>(
|
|
|
162
169
|
? idToItem(selectedItemId as T["id"] | null)
|
|
163
170
|
: internalValue;
|
|
164
171
|
|
|
165
|
-
|
|
166
|
-
|
|
172
|
+
type ItemOrSentinel = T | PlaceholderSentinel;
|
|
173
|
+
|
|
174
|
+
function handleValueChange(newItem: ItemOrSentinel | null) {
|
|
175
|
+
const realItem =
|
|
176
|
+
newItem == null || isPlaceholderSentinel(newItem) ? null : newItem;
|
|
177
|
+
if (!isControlled) setInternalValue(realItem);
|
|
167
178
|
if (onSelectedItemIdChange) {
|
|
168
|
-
onSelectedItemIdChange(
|
|
179
|
+
onSelectedItemIdChange(realItem ? realItem.id : null);
|
|
169
180
|
}
|
|
170
181
|
}
|
|
171
182
|
|
|
@@ -181,11 +192,22 @@ export function SingleSearchableSelect<T extends DataWithId>(
|
|
|
181
192
|
id={id}
|
|
182
193
|
value={value}
|
|
183
194
|
disabled={props.disabled}
|
|
184
|
-
onValueChange={handleValueChange}
|
|
185
|
-
itemToStringLabel={
|
|
186
|
-
|
|
195
|
+
onValueChange={handleValueChange as (item: T | null) => void}
|
|
196
|
+
itemToStringLabel={(item: T | null) =>
|
|
197
|
+
isPlaceholderSentinel(item) ? item.label : itemToString(item)
|
|
198
|
+
}
|
|
199
|
+
isItemEqualToValue={(a, b) => {
|
|
200
|
+
if (isPlaceholderSentinel(a) && isPlaceholderSentinel(b)) return true;
|
|
201
|
+
if (isPlaceholderSentinel(a)) return b == null;
|
|
202
|
+
if (isPlaceholderSentinel(b)) return a == null;
|
|
203
|
+
return a != null && b != null && a.id === b.id;
|
|
204
|
+
}}
|
|
187
205
|
items={
|
|
188
|
-
isVirtualized
|
|
206
|
+
isVirtualized
|
|
207
|
+
? includePlaceholderItem
|
|
208
|
+
? [makePlaceholderSentinel(placeholder) as unknown as T, ...data]
|
|
209
|
+
: data
|
|
210
|
+
: groups ?? (isChildrenMode ? undefined : data)
|
|
189
211
|
}
|
|
190
212
|
virtualized={isVirtualized}
|
|
191
213
|
open={open}
|
|
@@ -218,7 +240,7 @@ export function SingleSearchableSelect<T extends DataWithId>(
|
|
|
218
240
|
? renderSelection
|
|
219
241
|
? renderSelection(value)
|
|
220
242
|
: itemToString(value)
|
|
221
|
-
:
|
|
243
|
+
: placeholder}
|
|
222
244
|
</Combobox.Value>
|
|
223
245
|
</SearchableSelectPlaceholder>
|
|
224
246
|
<SearchableSelectTriggerIcon>
|
|
@@ -284,6 +306,7 @@ export function SingleSearchableSelect<T extends DataWithId>(
|
|
|
284
306
|
value={item}
|
|
285
307
|
itemId={String(item.id)}
|
|
286
308
|
label={itemToString(item)}
|
|
309
|
+
disabled={item.disabled}
|
|
287
310
|
inputType={inputType}
|
|
288
311
|
renderItem={
|
|
289
312
|
renderItem
|
|
@@ -298,18 +321,34 @@ export function SingleSearchableSelect<T extends DataWithId>(
|
|
|
298
321
|
</React.Fragment>
|
|
299
322
|
)
|
|
300
323
|
) : (
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
324
|
+
<>
|
|
325
|
+
{includePlaceholderItem && (
|
|
326
|
+
<ComboboxItem
|
|
327
|
+
key="placeholder"
|
|
328
|
+
value={
|
|
329
|
+
makePlaceholderSentinel(placeholder) as unknown as T
|
|
330
|
+
}
|
|
331
|
+
itemId="placeholder"
|
|
332
|
+
label={placeholder}
|
|
333
|
+
inputType={inputType}
|
|
334
|
+
/>
|
|
335
|
+
)}
|
|
336
|
+
<Combobox.Collection>
|
|
337
|
+
{(item: T) => (
|
|
338
|
+
<ComboboxItem
|
|
339
|
+
key={item.id}
|
|
340
|
+
value={item}
|
|
341
|
+
itemId={String(item.id)}
|
|
342
|
+
label={itemToString(item)}
|
|
343
|
+
disabled={item.disabled}
|
|
344
|
+
inputType={inputType}
|
|
345
|
+
renderItem={
|
|
346
|
+
renderItem ? () => renderItem(item) : undefined
|
|
347
|
+
}
|
|
348
|
+
/>
|
|
349
|
+
)}
|
|
350
|
+
</Combobox.Collection>
|
|
351
|
+
</>
|
|
313
352
|
)}
|
|
314
353
|
</SearchableSelectList>
|
|
315
354
|
</SearchableSelectPopup>
|
|
@@ -4,7 +4,12 @@ import { FormField } from "@sproutsocial/seeds-react-form-field";
|
|
|
4
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",
|
|
@@ -217,6 +222,24 @@ export const CustomSelection: Story = {
|
|
|
217
222
|
),
|
|
218
223
|
};
|
|
219
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
|
+
|
|
220
243
|
export const Children: Story = {
|
|
221
244
|
name: "Children API",
|
|
222
245
|
render: () => (
|
package/src/v3/SingleSelect.tsx
CHANGED
|
@@ -199,6 +199,7 @@ export function SingleSelect<T extends DataWithId>(
|
|
|
199
199
|
key={item.id}
|
|
200
200
|
value={String(item.id)}
|
|
201
201
|
label={itemToString(item)}
|
|
202
|
+
disabled={item.disabled}
|
|
202
203
|
inputType={inputType}
|
|
203
204
|
renderItem={
|
|
204
205
|
renderItem
|
|
@@ -212,7 +213,6 @@ export function SingleSelect<T extends DataWithId>(
|
|
|
212
213
|
</React.Fragment>
|
|
213
214
|
))
|
|
214
215
|
) : (
|
|
215
|
-
// TODO: need to add a placeholder item when includePlaceholderItem is true
|
|
216
216
|
<>
|
|
217
217
|
{includePlaceholderItem && (
|
|
218
218
|
<SelectItem
|
|
@@ -226,6 +226,7 @@ export function SingleSelect<T extends DataWithId>(
|
|
|
226
226
|
key={item.id}
|
|
227
227
|
value={String(item.id)}
|
|
228
228
|
label={itemToString(item)}
|
|
229
|
+
disabled={item.disabled}
|
|
229
230
|
inputType={inputType}
|
|
230
231
|
renderItem={
|
|
231
232
|
renderItem
|