@sproutsocial/seeds-react-menu 1.8.5 → 1.9.2

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.8.5",
3
+ "version": "1.9.2",
4
4
  "description": "Seeds React Menu",
5
5
  "author": "Sprout Social, Inc.",
6
6
  "license": "MIT",
@@ -31,18 +31,18 @@
31
31
  },
32
32
  "dependencies": {
33
33
  "@radix-ui/react-popover": "^1.1.2",
34
- "@sproutsocial/seeds-react-box": "^1.1.13",
35
- "@sproutsocial/seeds-react-button": "^1.3.18",
36
- "@sproutsocial/seeds-react-checkbox": "^1.3.22",
37
- "@sproutsocial/seeds-react-icon": "^2.2.4",
38
- "@sproutsocial/seeds-react-input": "^1.5.4",
39
- "@sproutsocial/seeds-react-label": "^1.0.13",
40
- "@sproutsocial/seeds-react-popout": "^2.4.25",
41
- "@sproutsocial/seeds-react-radio": "^1.3.12",
42
- "@sproutsocial/seeds-react-switch": "^1.2.21",
34
+ "@sproutsocial/seeds-react-box": "^1.1.14",
35
+ "@sproutsocial/seeds-react-button": "^1.4.0",
36
+ "@sproutsocial/seeds-react-checkbox": "^1.3.24",
37
+ "@sproutsocial/seeds-react-icon": "^2.2.5",
38
+ "@sproutsocial/seeds-react-input": "^1.5.7",
39
+ "@sproutsocial/seeds-react-label": "^1.0.14",
40
+ "@sproutsocial/seeds-react-popout": "^2.4.28",
41
+ "@sproutsocial/seeds-react-radio": "^1.3.14",
42
+ "@sproutsocial/seeds-react-switch": "^1.2.23",
43
43
  "@sproutsocial/seeds-react-system-props": "^3.0.2",
44
- "@sproutsocial/seeds-react-theme": "^3.5.1",
45
- "@sproutsocial/seeds-react-token-input": "^1.4.25",
44
+ "@sproutsocial/seeds-react-theme": "^3.6.0",
45
+ "@sproutsocial/seeds-react-token-input": "^1.4.28",
46
46
  "@sproutsocial/seeds-react-utilities": "^4.3.0",
47
47
  "@tanstack/react-virtual": "^3.13.12",
48
48
  "downshift": "9.0.4",
@@ -51,7 +51,7 @@
51
51
  "styled-system": "^5.1.5"
52
52
  },
53
53
  "devDependencies": {
54
- "@sproutsocial/seeds-react-tooltip": "^1.1.7",
54
+ "@sproutsocial/seeds-react-tooltip": "^1.1.10",
55
55
  "@sproutsocial/eslint-config-seeds": "*",
56
56
  "@sproutsocial/seeds-react-testing-library": "*",
57
57
  "@sproutsocial/seeds-testing": "*",
@@ -345,6 +345,8 @@ const TestVirtualizedMulti = () => {
345
345
  itemToSearchString={itemToSearchString}
346
346
  renderItem={renderItem}
347
347
  isVirtualized={true}
348
+ groupBy={(book) => book.author}
349
+ selectHeadings={true}
348
350
  EmptyState={defaultEmptyState}
349
351
  placeholder="Search for books"
350
352
  menuProps={{
@@ -607,3 +609,37 @@ export const CustomSearchString: Story = {
607
609
  return <TestCustomSearchString />;
608
610
  },
609
611
  };
612
+
613
+ // Popout Placement Top
614
+ // Positioned at the bottom of the viewport to demonstrate that the popout
615
+ // opens upward rather than downward when popoutPlacement="top" is set.
616
+ const TestPopoutPlacementTop = () => {
617
+ return (
618
+ <FormField label="Pick some books" width="100%">
619
+ {({ id }) => (
620
+ <MultiAutocomplete
621
+ id={id}
622
+ data={books}
623
+ itemToString={itemToString}
624
+ renderItem={renderItem}
625
+ onSelectedItemIdsChange={(itemIds) => console.log(itemIds)}
626
+ EmptyState={defaultEmptyState}
627
+ placeholder="Search for books"
628
+ popoutPlacement="top"
629
+ />
630
+ )}
631
+ </FormField>
632
+ );
633
+ };
634
+
635
+ export const PopoutPlacementTop: Story = {
636
+ name: "Popout Placement Top",
637
+ decorators: [
638
+ (Story) => (
639
+ <Box display="flex" height="100vh" alignItems="flex-end">
640
+ <Story />
641
+ </Box>
642
+ ),
643
+ ],
644
+ render: () => <TestPopoutPlacementTop />,
645
+ };
@@ -1,4 +1,4 @@
1
- import { useState, useMemo } from "react";
1
+ import { useState, useMemo, useRef } from "react";
2
2
  import { useCombobox, useMultipleSelection } from "downshift";
3
3
  import { TokenInput } from "@sproutsocial/seeds-react-token-input";
4
4
  // This will eventually be imported from seeds-react-popout
@@ -72,7 +72,12 @@ export function MultiAutocomplete<T extends DataWithId>({
72
72
  stateReducer = defaultReducer,
73
73
  menuProps,
74
74
  inputType = "checkbox",
75
+ onPointerDownOutside,
76
+ onInteractOutside,
77
+ popoutPlacement,
75
78
  }: MultiAutocompleteProps<T>) {
79
+ const mousedownInPopoverRef = useRef(false);
80
+
76
81
  const [uncontrolledInputValue, setUncontrolledInputValue] =
77
82
  useState<string>("");
78
83
  const [uncontrolledSelectedItemIds, setUncontrolledSelectedItemIds] =
@@ -116,6 +121,9 @@ export function MultiAutocomplete<T extends DataWithId>({
116
121
  };
117
122
 
118
123
  const updateIsOpen = (newIsOpen: boolean) => {
124
+ if (!newIsOpen) {
125
+ updateInputValue("");
126
+ }
119
127
  if (!isIsOpenControlled) {
120
128
  setUncontrolledIsOpen(newIsOpen);
121
129
  }
@@ -358,6 +366,13 @@ export function MultiAutocomplete<T extends DataWithId>({
358
366
  if (inputProps.onBlur) {
359
367
  inputProps.onBlur(e);
360
368
  }
369
+ // Don't close if the blur was caused by a mousedown inside the popover (e.g. clicking the scrollbar)
370
+ // Re-focus the input so future blur events can still close the menu
371
+ if (mousedownInPopoverRef.current) {
372
+ mousedownInPopoverRef.current = false;
373
+ e.target.focus();
374
+ return;
375
+ }
361
376
  // Need to figure why onblur is getting called when selecting select all item
362
377
  updateIsOpen(false);
363
378
  };
@@ -371,6 +386,7 @@ export function MultiAutocomplete<T extends DataWithId>({
371
386
  <Popout
372
387
  open={comboboxIsOpen}
373
388
  onOpenChange={updateIsOpen}
389
+ side={popoutPlacement}
374
390
  onOpenAutoFocus={(e) => {
375
391
  // Prevent default auto-focus behavior
376
392
  e.preventDefault();
@@ -379,7 +395,15 @@ export function MultiAutocomplete<T extends DataWithId>({
379
395
  // Prevent default auto-focus behavior
380
396
  e.preventDefault();
381
397
  }}
398
+ onPointerDownOutside={onPointerDownOutside}
399
+ onInteractOutside={onInteractOutside}
382
400
  animationConfig={menuAnimationConfig}
401
+ onMouseDown={() => {
402
+ mousedownInPopoverRef.current = true;
403
+ }}
404
+ onMouseUp={() => {
405
+ mousedownInPopoverRef.current = false;
406
+ }}
383
407
  content={
384
408
  <ComboboxContent
385
409
  items={items}
@@ -65,6 +65,9 @@ export function SingleAutocomplete<T extends DataWithId>({
65
65
  menuProps,
66
66
  inputType = "icon",
67
67
  includePlaceholderItem = false,
68
+ onPointerDownOutside,
69
+ onInteractOutside,
70
+ popoutPlacement,
68
71
  }: SingleAutocompleteProps<T>) {
69
72
  const [uncontrolledInputValue, setUncontrolledInputValue] =
70
73
  useState<string>("");
@@ -291,6 +294,7 @@ export function SingleAutocomplete<T extends DataWithId>({
291
294
  <Popout
292
295
  open={comboboxIsOpen}
293
296
  onOpenChange={updateIsOpen}
297
+ side={popoutPlacement}
294
298
  animationConfig={menuAnimationConfig}
295
299
  onOpenAutoFocus={(e) => {
296
300
  // Prevent default auto-focus behavior
@@ -300,6 +304,8 @@ export function SingleAutocomplete<T extends DataWithId>({
300
304
  // Prevent default auto-focus behavior
301
305
  e.preventDefault();
302
306
  }}
307
+ onPointerDownOutside={onPointerDownOutside}
308
+ onInteractOutside={onInteractOutside}
303
309
  content={
304
310
  <ComboboxContent
305
311
  items={items}
@@ -86,6 +86,7 @@ export const VirtualizedComboboxContent = <T extends DataWithId>({
86
86
  style={{
87
87
  height: `${rowVirtualizer.getTotalSize()}px`,
88
88
  position: "relative",
89
+ overflow: "hidden",
89
90
  }}
90
91
  >
91
92
  <div
@@ -77,12 +77,28 @@ export function Popout({
77
77
  // At the moment, if the popout content is the combobox the onclick gets overridden by the popout
78
78
  return (
79
79
  <Popover.Root open={popoverOpen} onOpenChange={popoverOnOpenChange}>
80
- <Popover.Trigger ref={radixRef} asChild>
81
- {children}
82
- </Popover.Trigger>
83
- <Popover.Anchor>
84
- <div style={{ height: "1px" }}>&nbsp;</div>
85
- </Popover.Anchor>
80
+ {/*
81
+ The Anchor provides the positioning reference for the popout content.
82
+ We can't rely on Popover.Trigger's ref for positioning because the children
83
+ (e.g. Input) are class components that don't support forwardRef — Radix's
84
+ asChild would connect the ref to the class instance rather than the DOM node.
85
+ The Anchor is absolutely positioned at the top or bottom edge of the wrapper
86
+ so the popout content appears correctly above or below the trigger.
87
+ */}
88
+ <div style={{ position: "relative" }}>
89
+ <Popover.Trigger ref={radixRef} asChild>
90
+ {children}
91
+ </Popover.Trigger>
92
+ <Popover.Anchor
93
+ style={{
94
+ position: "absolute",
95
+ top: side === "top" ? 0 : "100%",
96
+ left: 0,
97
+ right: 0,
98
+ height: "1px",
99
+ }}
100
+ />
101
+ </div>
86
102
  <Popover.Portal>
87
103
  <StyledContent
88
104
  side={side}
@@ -18,6 +18,15 @@ export interface BaseSelectProps<T extends DataWithId> {
18
18
  onIsOpenChange?: (isOpen: boolean) => void;
19
19
  stateReducer?: any;
20
20
  menuProps?: React.HTMLAttributes<HTMLDivElement>;
21
+ onPointerDownOutside?: (
22
+ event: CustomEvent<{ originalEvent: PointerEvent }>
23
+ ) => void;
24
+ onInteractOutside?: (event: CustomEvent<{ originalEvent: Event }>) => void;
25
+ /**
26
+ * Controls which side the popout appears relative to the trigger.
27
+ * @default "bottom"
28
+ */
29
+ popoutPlacement?: "top" | "bottom";
21
30
  }
22
31
  /**
23
32
  * Props common to all single-select components
@@ -76,6 +76,9 @@ export function SearchableMultiSelect<T extends DataWithId>({
76
76
  onIsOpenChange,
77
77
  stateReducer = defaultReducer,
78
78
  triggerButtonProps,
79
+ onPointerDownOutside,
80
+ onInteractOutside,
81
+ popoutPlacement,
79
82
  }: SearchableMultiSelectProps<T>) {
80
83
  const [uncontrolledInputValue, setUncontrolledInputValue] =
81
84
  useState<string>("");
@@ -365,6 +368,7 @@ export function SearchableMultiSelect<T extends DataWithId>({
365
368
  <Popout
366
369
  open={comboboxIsOpen}
367
370
  onOpenChange={updateIsOpen}
371
+ side={popoutPlacement}
368
372
  animationConfig={menuAnimationConfig}
369
373
  onOpenAutoFocus={(e) => {
370
374
  // Prevent default auto-focus behavior - we'll focus the input manually
@@ -383,6 +387,8 @@ export function SearchableMultiSelect<T extends DataWithId>({
383
387
  // Prevent default auto-focus behavior
384
388
  e.preventDefault();
385
389
  }}
390
+ onPointerDownOutside={onPointerDownOutside}
391
+ onInteractOutside={onInteractOutside}
386
392
  content={
387
393
  <div>
388
394
  {/* Input inside the Popout */}
@@ -72,6 +72,9 @@ export function SearchableSelect<T extends DataWithId>({
72
72
  onIsOpenChange,
73
73
  stateReducer = defaultReducer,
74
74
  triggerButtonProps,
75
+ onPointerDownOutside,
76
+ onInteractOutside,
77
+ popoutPlacement,
75
78
  }: SearchableSelectProps<T>) {
76
79
  const [uncontrolledInputValue, setUncontrolledInputValue] =
77
80
  useState<string>("");
@@ -255,6 +258,7 @@ export function SearchableSelect<T extends DataWithId>({
255
258
  <Popout
256
259
  open={comboboxIsOpen}
257
260
  onOpenChange={updateIsOpen}
261
+ side={popoutPlacement}
258
262
  animationConfig={menuAnimationConfig}
259
263
  onOpenAutoFocus={(e) => {
260
264
  // Prevent default auto-focus behavior - we'll focus the input manually
@@ -273,6 +277,8 @@ export function SearchableSelect<T extends DataWithId>({
273
277
  // Prevent default auto-focus behavior
274
278
  e.preventDefault();
275
279
  }}
280
+ onPointerDownOutside={onPointerDownOutside}
281
+ onInteractOutside={onInteractOutside}
276
282
  content={
277
283
  <div>
278
284
  {/* Input inside the Popout */}
@@ -77,6 +77,9 @@ export function MultiSelect<T extends DataWithId>({
77
77
  menuProps,
78
78
  inputType = "checkbox",
79
79
  shouldOverflow,
80
+ onPointerDownOutside,
81
+ onInteractOutside,
82
+ popoutPlacement,
80
83
  }: MultiSelectProps<T>) {
81
84
  const [uncontrolledSelectedItemIds, setUncontrolledSelectedItemIds] =
82
85
  useState<T["id"][]>([]);
@@ -333,6 +336,7 @@ export function MultiSelect<T extends DataWithId>({
333
336
  <Popout
334
337
  open={selectIsOpen}
335
338
  onOpenChange={updateIsOpen}
339
+ side={popoutPlacement}
336
340
  onOpenAutoFocus={(e) => {
337
341
  // Prevent default auto-focus behavior
338
342
  e.preventDefault();
@@ -341,6 +345,8 @@ export function MultiSelect<T extends DataWithId>({
341
345
  // Prevent default auto-focus behavior
342
346
  e.preventDefault();
343
347
  }}
348
+ onPointerDownOutside={onPointerDownOutside}
349
+ onInteractOutside={onInteractOutside}
344
350
  animationConfig={menuAnimationConfig}
345
351
  content={
346
352
  <ComboboxContent
@@ -64,6 +64,9 @@ export function SingleSelect<T extends DataWithId>({
64
64
  menuProps,
65
65
  inputType = "icon",
66
66
  removePlaceholderItem = false,
67
+ onPointerDownOutside,
68
+ onInteractOutside,
69
+ popoutPlacement,
67
70
  }: SingleSelectProps<T>) {
68
71
  const [uncontrolledSelectedItemId, setUncontrolledSelectedItemId] = useState<
69
72
  T["id"] | null
@@ -212,6 +215,7 @@ export function SingleSelect<T extends DataWithId>({
212
215
  <Popout
213
216
  open={selectIsOpen}
214
217
  onOpenChange={updateIsOpen}
218
+ side={popoutPlacement}
215
219
  animationConfig={menuAnimationConfig}
216
220
  onOpenAutoFocus={(e) => {
217
221
  // Prevent default auto-focus behavior
@@ -221,6 +225,8 @@ export function SingleSelect<T extends DataWithId>({
221
225
  // Prevent default auto-focus behavior
222
226
  e.preventDefault();
223
227
  }}
228
+ onPointerDownOutside={onPointerDownOutside}
229
+ onInteractOutside={onInteractOutside}
224
230
  content={
225
231
  <ComboboxContent
226
232
  items={items}