@sproutsocial/seeds-react-menu 1.11.3 → 1.11.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sproutsocial/seeds-react-menu",
3
- "version": "1.11.3",
3
+ "version": "1.11.5",
4
4
  "description": "Seeds React Menu",
5
5
  "author": "Sprout Social, Inc.",
6
6
  "license": "MIT",
@@ -37,17 +37,17 @@
37
37
  "dependencies": {
38
38
  "@radix-ui/react-popover": "^1.1.2",
39
39
  "@sproutsocial/seeds-react-box": "^1.1.16",
40
- "@sproutsocial/seeds-react-button": "^2.0.1",
41
- "@sproutsocial/seeds-react-checkbox": "^1.3.29",
42
- "@sproutsocial/seeds-react-icon": "^2.3.3",
43
- "@sproutsocial/seeds-react-input": "^1.5.12",
40
+ "@sproutsocial/seeds-react-button": "^2.0.2",
41
+ "@sproutsocial/seeds-react-checkbox": "^1.3.30",
42
+ "@sproutsocial/seeds-react-icon": "^2.3.4",
43
+ "@sproutsocial/seeds-react-input": "^1.5.13",
44
44
  "@sproutsocial/seeds-react-label": "^1.0.16",
45
- "@sproutsocial/seeds-react-popout": "^2.4.34",
45
+ "@sproutsocial/seeds-react-popout": "^2.4.35",
46
46
  "@sproutsocial/seeds-react-radio": "^1.3.16",
47
- "@sproutsocial/seeds-react-switch": "^1.2.28",
47
+ "@sproutsocial/seeds-react-switch": "^1.2.29",
48
48
  "@sproutsocial/seeds-react-system-props": "^3.0.2",
49
49
  "@sproutsocial/seeds-react-theme": "^3.6.2",
50
- "@sproutsocial/seeds-react-token-input": "^1.4.34",
50
+ "@sproutsocial/seeds-react-token-input": "^1.4.35",
51
51
  "@sproutsocial/seeds-react-utilities": "^4.3.0",
52
52
  "@base-ui/react": "^1.3.0",
53
53
  "@tanstack/react-virtual": "^3.13.12",
@@ -57,7 +57,7 @@
57
57
  "styled-system": "^5.1.5"
58
58
  },
59
59
  "devDependencies": {
60
- "@sproutsocial/seeds-react-tooltip": "^1.1.16",
60
+ "@sproutsocial/seeds-react-tooltip": "^1.1.17",
61
61
  "@sproutsocial/eslint-config-seeds": "*",
62
62
  "@sproutsocial/seeds-react-testing-library": "*",
63
63
  "@sproutsocial/seeds-testing": "*",
@@ -6,6 +6,7 @@ export const Field = styled.div`
6
6
  display: flex;
7
7
  flex-direction: column;
8
8
  gap: ${({ theme }) => theme.space[200]};
9
+ width: 100%;
9
10
  `;
10
11
 
11
12
  export const SelectLabel = styled(Select.Label)`
@@ -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 (!row) return null;
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
+ }
@@ -208,6 +208,27 @@ export const IncludePlaceholderItem: Story = {
208
208
  ),
209
209
  };
210
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
+
211
232
  export const Virtualized: Story = {
212
233
  name: "Virtualized",
213
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,
@@ -164,10 +169,14 @@ export function SingleSearchableSelect<T extends DataWithId>(
164
169
  ? idToItem(selectedItemId as T["id"] | null)
165
170
  : internalValue;
166
171
 
167
- function handleValueChange(newItem: T | null) {
168
- if (!isControlled) setInternalValue(newItem);
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);
169
178
  if (onSelectedItemIdChange) {
170
- onSelectedItemIdChange(newItem ? newItem.id : null);
179
+ onSelectedItemIdChange(realItem ? realItem.id : null);
171
180
  }
172
181
  }
173
182
 
@@ -183,11 +192,22 @@ export function SingleSearchableSelect<T extends DataWithId>(
183
192
  id={id}
184
193
  value={value}
185
194
  disabled={props.disabled}
186
- onValueChange={handleValueChange}
187
- itemToStringLabel={itemToString}
188
- isItemEqualToValue={(a, b) => a != null && b != null && a.id === b.id}
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
+ }}
189
205
  items={
190
- isVirtualized ? data : groups ?? (isChildrenMode ? undefined : data)
206
+ isVirtualized
207
+ ? includePlaceholderItem
208
+ ? [makePlaceholderSentinel(placeholder) as unknown as T, ...data]
209
+ : data
210
+ : groups ?? (isChildrenMode ? undefined : data)
191
211
  }
192
212
  virtualized={isVirtualized}
193
213
  open={open}
@@ -220,7 +240,7 @@ export function SingleSearchableSelect<T extends DataWithId>(
220
240
  ? renderSelection
221
241
  ? renderSelection(value)
222
242
  : itemToString(value)
223
- : null}
243
+ : placeholder}
224
244
  </Combobox.Value>
225
245
  </SearchableSelectPlaceholder>
226
246
  <SearchableSelectTriggerIcon>
@@ -305,9 +325,12 @@ export function SingleSearchableSelect<T extends DataWithId>(
305
325
  {includePlaceholderItem && (
306
326
  <ComboboxItem
307
327
  key="placeholder"
308
- value={null as unknown as T}
328
+ value={
329
+ makePlaceholderSentinel(placeholder) as unknown as T
330
+ }
309
331
  itemId="placeholder"
310
332
  label={placeholder}
333
+ inputType={inputType}
311
334
  />
312
335
  )}
313
336
  <Combobox.Collection>