@sproutsocial/seeds-react-menu 1.16.16 → 1.18.0

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.
Files changed (84) hide show
  1. package/.turbo/turbo-build.log +14 -22
  2. package/CHANGELOG.md +51 -0
  3. package/dist/esm/index.js +542 -229
  4. package/dist/esm/index.js.map +1 -1
  5. package/dist/esm/v3/index.js +870 -1081
  6. package/dist/esm/v3/index.js.map +1 -1
  7. package/dist/v3/index.d.mts +82 -38
  8. package/dist/v3/index.d.ts +82 -38
  9. package/dist/v3/index.js +887 -1098
  10. package/dist/v3/index.js.map +1 -1
  11. package/dist/v3/menu.css +947 -0
  12. package/package.json +20 -21
  13. package/src/ActionMenu/ActionMenu.stories.tsx +1 -1
  14. package/src/Autocomplete/Autocomplete.stories.tsx +1 -1
  15. package/src/MenuContent/MenuContent.stories.tsx +1 -1
  16. package/src/MenuFooter/MenuFooter.stories.tsx +1 -1
  17. package/src/MenuGroup/MenuGroup.stories.tsx +1 -1
  18. package/src/MenuHeader/MenuHeader.stories.tsx +1 -1
  19. package/src/MenuItem/MenuItem.stories.tsx +1 -1
  20. package/src/MenuToggleButton/MenuToggleButton.stories.tsx +1 -1
  21. package/src/MultiSelectMenu/MultiSelectMenu.stories.tsx +1 -1
  22. package/src/NestedMenu/NestedMenu.stories.tsx +1 -1
  23. package/src/SingleSelectMenu/SingleSelectMenu.stories.tsx +1 -1
  24. package/src/SubmenuItem/SubmenuItem.stories.tsx +1 -1
  25. package/src/v3/ActionMenu.stories.tsx +1 -1
  26. package/src/v3/ActionMenuStyles.tsx +169 -149
  27. package/src/v3/Common/ComboboxStyles.tsx +396 -511
  28. package/src/v3/Common/SelectIcons.tsx +17 -8
  29. package/src/v3/Common/SelectItem.tsx +25 -38
  30. package/src/v3/Common/SelectStyles.tsx +146 -144
  31. package/src/v3/Common/cn.ts +38 -0
  32. package/src/v3/MenuButton.stories.tsx +1 -1
  33. package/src/v3/MenuButtonStyles.tsx +55 -34
  34. package/src/v3/ModalStories.stories.tsx +1 -1
  35. package/src/v3/MultiCombobox.stories.tsx +1 -1
  36. package/src/v3/MultiCombobox.tsx +7 -0
  37. package/src/v3/MultiSearchableSelect.stories.tsx +1 -1
  38. package/src/v3/MultiSearchableSelect.tsx +15 -8
  39. package/src/v3/MultiSelect.stories.tsx +1 -1
  40. package/src/v3/MultiSelect.tsx +20 -17
  41. package/src/v3/SingleCombobox.stories.tsx +1 -1
  42. package/src/v3/SingleCombobox.tsx +7 -0
  43. package/src/v3/SingleSearchableSelect.stories.tsx +1 -1
  44. package/src/v3/SingleSearchableSelect.tsx +7 -0
  45. package/src/v3/SingleSelect.stories.tsx +1 -1
  46. package/src/v3/SingleSelect.tsx +22 -14
  47. package/src/v3/__tests__/ariaLabels.test.tsx +133 -0
  48. package/src/v3/menu.css +947 -0
  49. package/tsup.config.ts +0 -1
  50. package/dist/esm/chunk-ROQATIB7.js +0 -357
  51. package/dist/esm/chunk-ROQATIB7.js.map +0 -1
  52. package/dist/esm/v2/index.js +0 -2089
  53. package/dist/esm/v2/index.js.map +0 -1
  54. package/dist/v2/index.d.mts +0 -108
  55. package/dist/v2/index.d.ts +0 -108
  56. package/dist/v2/index.js +0 -2280
  57. package/dist/v2/index.js.map +0 -1
  58. package/src/v2/Autocomplete/Autocomplete.stories.tsx +0 -645
  59. package/src/v2/Autocomplete/MultiAutocomplete.tsx +0 -487
  60. package/src/v2/Autocomplete/SingleAutocomplete.tsx +0 -366
  61. package/src/v2/Autocomplete/index.ts +0 -2
  62. package/src/v2/Common-V2/ComboboxContent.tsx +0 -457
  63. package/src/v2/Common-V2/MenuGroup.tsx +0 -60
  64. package/src/v2/Common-V2/MenuGroupTypes.ts +0 -30
  65. package/src/v2/Common-V2/MenuHeading.tsx +0 -83
  66. package/src/v2/Common-V2/MenuItem.tsx +0 -138
  67. package/src/v2/Common-V2/Popout.tsx +0 -124
  68. package/src/v2/Common-V2/PopoutTypes.tsx +0 -109
  69. package/src/v2/Common-V2/SelectToggleButton.tsx +0 -99
  70. package/src/v2/Common-V2/index.ts +0 -11
  71. package/src/v2/Common-V2/props.ts +0 -84
  72. package/src/v2/Common-V2/styles.tsx +0 -41
  73. package/src/v2/Common-V2/types.ts +0 -16
  74. package/src/v2/Common-V2/useHighlightedIndex.ts +0 -62
  75. package/src/v2/Common-V2/utils.ts +0 -238
  76. package/src/v2/SearchableSelect/AutocompleteContent.tsx +0 -325
  77. package/src/v2/SearchableSelect/SearchableMultiSelect.tsx +0 -527
  78. package/src/v2/SearchableSelect/SearchableSelect.tsx +0 -395
  79. package/src/v2/SearchableSelect/index.ts +0 -12
  80. package/src/v2/Select/MultiSelect.tsx +0 -417
  81. package/src/v2/Select/Select.stories.tsx +0 -593
  82. package/src/v2/Select/SingleSelect.tsx +0 -285
  83. package/src/v2/Select/index.ts +0 -2
  84. package/src/v2/index.ts +0 -2
@@ -32,16 +32,16 @@ import {
32
32
  } from "./Common/ComboboxStyles";
33
33
  import { ClearIcon, StyledChevron, ChevronIcon } from "./Common/SelectIcons";
34
34
  import Icon from "@sproutsocial/seeds-react-icon";
35
- import styled from "styled-components";
35
+ import { cn } from "./Common/cn";
36
36
 
37
- // ─── Styled components ────────────────────────────────────────────────────────
37
+ // ─── CSS-class components (see menu.css) ────────────────────────────────────
38
38
 
39
- const SelectionText = styled.span`
40
- flex: 1;
41
- overflow: hidden;
42
- text-overflow: ellipsis;
43
- white-space: nowrap;
44
- `;
39
+ const SelectionText = ({
40
+ className,
41
+ ...rest
42
+ }: React.ComponentProps<"span">) => (
43
+ <span className={cn("seeds-select-selection-text", className)} {...rest} />
44
+ );
45
45
 
46
46
  // ─── Sentinels ────────────────────────────────────────────────────────────────
47
47
 
@@ -79,6 +79,9 @@ interface MultiSearchableSelectBaseProps {
79
79
  loadingText?: string;
80
80
  disabled?: boolean;
81
81
  filter?: ((item: unknown, query: string) => boolean) | null;
82
+ "aria-label"?: string;
83
+ /** Id of a visible label. Preferred over `aria-label`. */
84
+ "aria-labelledby"?: string;
82
85
  "aria-describedby"?: string;
83
86
  isInvalid?: boolean;
84
87
  required?: boolean;
@@ -151,6 +154,8 @@ export function MultiSearchableSelect<T extends DataWithId>(
151
154
  fullWidth = true,
152
155
  } = props;
153
156
  const ariaDescribedBy = props["aria-describedby"];
157
+ const ariaLabel = props["aria-label"];
158
+ const ariaLabelledBy = props["aria-labelledby"];
154
159
  const isInvalid = props.isInvalid;
155
160
 
156
161
  const isChildrenMode = "children" in props && props.children != null;
@@ -609,6 +614,8 @@ export function MultiSearchableSelect<T extends DataWithId>(
609
614
  <ComboboxStatus>{isLoading ? loadingText : null}</ComboboxStatus>
610
615
  <ComboboxEmpty>{isLoading ? null : emptyText}</ComboboxEmpty>
611
616
  <SearchableSelectList
617
+ aria-label={ariaLabel}
618
+ aria-labelledby={ariaLabelledBy}
612
619
  style={
613
620
  isVirtualized
614
621
  ? {
@@ -12,7 +12,7 @@ import {
12
12
  } from "./storyData";
13
13
 
14
14
  const meta: Meta<typeof MultiSelect> = {
15
- title: "Really Under Development/V3/Multi Select",
15
+ title: "Components/Menus/v3/Multi Select",
16
16
  component: MultiSelect,
17
17
  };
18
18
 
@@ -1,6 +1,5 @@
1
1
  import * as React from "react";
2
2
  import { Select } from "@base-ui/react/select";
3
- import styled from "styled-components";
4
3
  import { useSeedsPortalContainer } from "./SeedsPortal";
5
4
  import SelectItem from "./Common/SelectItem";
6
5
  import type { DataWithId } from "./Common/types";
@@ -17,25 +16,21 @@ import {
17
16
  SelectPopup,
18
17
  } from "./Common/SelectStyles";
19
18
  import { StyledChevron, ChevronIcon } from "./Common/SelectIcons";
19
+ import { cn } from "./Common/cn";
20
20
 
21
- // ─── Styled components ────────────────────────────────────────────────────────
21
+ // ─── CSS-class components (see menu.css) ────────────────────────────────────
22
22
 
23
- const SelectionText = styled.span`
24
- flex: 1;
25
- overflow: hidden;
26
- text-overflow: ellipsis;
27
- white-space: nowrap;
28
- `;
23
+ const SelectionText = ({
24
+ className,
25
+ ...rest
26
+ }: React.ComponentProps<"span">) => (
27
+ <span className={cn("seeds-select-selection-text", className)} {...rest} />
28
+ );
29
29
 
30
30
  // The styles in here need cleaned up, but in MultiSearchableSelect are correct
31
- const Placeholder = styled.div`
32
- color: ${({ theme }) => theme.colors.text.subtext};
33
- font-size: ${({ theme }) => theme.typography[200].fontSize};
34
- line-height: 16px;
35
- padding-top: 2px;
36
- padding-bottom: 2px;
37
- margin: 2px 8px 0px 0px;
38
- `;
31
+ const Placeholder = ({ className, ...rest }: React.ComponentProps<"div">) => (
32
+ <div className={cn("seeds-select-placeholder", className)} {...rest} />
33
+ );
39
34
 
40
35
  // ─── Props ────────────────────────────────────────────────────────────────────
41
36
 
@@ -43,6 +38,9 @@ interface MultiSelectBaseProps {
43
38
  id?: string;
44
39
  placeholder?: string;
45
40
  disabled?: boolean;
41
+ "aria-label"?: string;
42
+ /** Id of a visible label. Preferred over `aria-label`. */
43
+ "aria-labelledby"?: string;
46
44
  "aria-describedby"?: string;
47
45
  isInvalid?: boolean;
48
46
  required?: boolean;
@@ -93,6 +91,8 @@ export type MultiSelectProps<T extends DataWithId> =
93
91
  export function MultiSelect<T extends DataWithId>(props: MultiSelectProps<T>) {
94
92
  const { id, placeholder = "Select...", fullWidth = true } = props;
95
93
  const ariaDescribedBy = props["aria-describedby"];
94
+ const ariaLabel = props["aria-label"];
95
+ const ariaLabelledBy = props["aria-labelledby"];
96
96
  const isInvalid = props.isInvalid;
97
97
 
98
98
  const isChildrenMode = "children" in props && props.children != null;
@@ -232,7 +232,10 @@ export function MultiSelect<T extends DataWithId>(props: MultiSelectProps<T>) {
232
232
  <SelectPositioner sideOffset={8} alignItemWithTrigger={false}>
233
233
  <SelectPopup>
234
234
  <Select.ScrollUpArrow />
235
- <Select.List>
235
+ <Select.List
236
+ aria-label={ariaLabel}
237
+ aria-labelledby={ariaLabelledBy}
238
+ >
236
239
  {children
237
240
  ? children
238
241
  : groupBy
@@ -11,7 +11,7 @@ import {
11
11
  } from "./storyData";
12
12
 
13
13
  const meta: Meta<typeof SingleCombobox> = {
14
- title: "Really Under Development/V3/Single Combobox",
14
+ title: "Components/Menus/v3/Single Combobox",
15
15
  component: SingleCombobox,
16
16
  };
17
17
 
@@ -32,6 +32,9 @@ interface SingleComboboxBaseProps {
32
32
  emptyText?: string;
33
33
  disabled?: boolean;
34
34
  filter?: ((item: unknown, query: string) => boolean) | null;
35
+ "aria-label"?: string;
36
+ /** Id of a visible label. Preferred over `aria-label`. */
37
+ "aria-labelledby"?: string;
35
38
  "aria-describedby"?: string;
36
39
  isInvalid?: boolean;
37
40
  required?: boolean;
@@ -96,6 +99,8 @@ export function SingleCombobox<T extends DataWithId>(
96
99
  fullWidth = true,
97
100
  } = props;
98
101
  const ariaDescribedBy = props["aria-describedby"];
102
+ const ariaLabel = props["aria-label"];
103
+ const ariaLabelledBy = props["aria-labelledby"];
99
104
  const isInvalid = props.isInvalid;
100
105
 
101
106
  const isChildrenMode = "children" in props && props.children != null;
@@ -244,6 +249,8 @@ export function SingleCombobox<T extends DataWithId>(
244
249
  >
245
250
  <ComboboxEmpty>{emptyText}</ComboboxEmpty>
246
251
  <ComboboxList
252
+ aria-label={ariaLabel}
253
+ aria-labelledby={ariaLabelledBy}
247
254
  style={
248
255
  isVirtualized
249
256
  ? {
@@ -11,7 +11,7 @@ import {
11
11
  } from "./storyData";
12
12
 
13
13
  const meta: Meta<typeof SingleSearchableSelect> = {
14
- title: "Really Under Development/V3/Single Searchable Select",
14
+ title: "Components/Menus/v3/Single Searchable Select",
15
15
  component: SingleSearchableSelect,
16
16
  };
17
17
 
@@ -41,6 +41,9 @@ interface SingleSearchableSelectBaseProps {
41
41
  loadingText?: string;
42
42
  disabled?: boolean;
43
43
  filter?: ((item: unknown, query: string) => boolean) | null;
44
+ "aria-label"?: string;
45
+ /** Id of a visible label. Preferred over `aria-label`. */
46
+ "aria-labelledby"?: string;
44
47
  "aria-describedby"?: string;
45
48
  isInvalid?: boolean;
46
49
  required?: boolean;
@@ -112,6 +115,8 @@ export function SingleSearchableSelect<T extends DataWithId>(
112
115
  fullWidth = true,
113
116
  } = props;
114
117
  const ariaDescribedBy = props["aria-describedby"];
118
+ const ariaLabel = props["aria-label"];
119
+ const ariaLabelledBy = props["aria-labelledby"];
115
120
  const isInvalid = props.isInvalid;
116
121
 
117
122
  const isChildrenMode = "children" in props && props.children != null;
@@ -278,6 +283,8 @@ export function SingleSearchableSelect<T extends DataWithId>(
278
283
  <ComboboxStatus>{isLoading ? loadingText : null}</ComboboxStatus>
279
284
  <ComboboxEmpty>{isLoading ? null : emptyText}</ComboboxEmpty>
280
285
  <SearchableSelectList
286
+ aria-label={ariaLabel}
287
+ aria-labelledby={ariaLabelledBy}
281
288
  style={
282
289
  isVirtualized
283
290
  ? {
@@ -12,7 +12,7 @@ import {
12
12
  } from "./storyData";
13
13
 
14
14
  const meta: Meta<typeof SingleSelect> = {
15
- title: "Really Under Development/V3/Single Select",
15
+ title: "Components/Menus/v3/Single Select",
16
16
  component: SingleSelect,
17
17
  };
18
18
 
@@ -1,6 +1,5 @@
1
1
  import * as React from "react";
2
2
  import { Select } from "@base-ui/react/select";
3
- import styled from "styled-components";
4
3
  import { useSeedsPortalContainer } from "./SeedsPortal";
5
4
  import SelectItem from "./Common/SelectItem";
6
5
  import type { DataWithId } from "./Common/types";
@@ -16,21 +15,22 @@ import {
16
15
  SelectPopup,
17
16
  } from "./Common/SelectStyles";
18
17
  import { StyledChevron, ChevronIcon } from "./Common/SelectIcons";
18
+ import { cn, type WithStringClassName } from "./Common/cn";
19
19
 
20
20
  // <Listbox(?!\.|B)
21
- // ─── Styled components ────────────────────────────────────────────────────────
21
+ // ─── CSS-class components (see menu.css) ────────────────────────────────────
22
22
 
23
- const StyledValue = styled(Select.Value)`
24
- flex: 1;
25
- text-align: left;
26
- overflow: hidden;
27
- text-overflow: ellipsis;
28
- white-space: nowrap;
29
-
30
- &[data-placeholder] {
31
- color: ${({ theme }) => theme.colors.text.subtext};
32
- }
33
- `;
23
+ const StyledValue = React.forwardRef<
24
+ React.ElementRef<typeof Select.Value>,
25
+ WithStringClassName<React.ComponentProps<typeof Select.Value>>
26
+ >(({ className, ...rest }, ref) => (
27
+ <Select.Value
28
+ ref={ref}
29
+ className={cn("seeds-select-value-text", className)}
30
+ {...rest}
31
+ />
32
+ ));
33
+ StyledValue.displayName = "StyledValue";
34
34
 
35
35
  // ─── Props ────────────────────────────────────────────────────────────────────
36
36
 
@@ -39,6 +39,9 @@ interface SingleSelectBaseProps {
39
39
  placeholder?: string;
40
40
  includePlaceholderItem?: boolean;
41
41
  disabled?: boolean;
42
+ "aria-label"?: string;
43
+ /** Id of a visible label. Preferred over `aria-label`. */
44
+ "aria-labelledby"?: string;
42
45
  "aria-describedby"?: string;
43
46
  isInvalid?: boolean;
44
47
  required?: boolean;
@@ -85,6 +88,8 @@ export function SingleSelect<T extends DataWithId>(
85
88
  ) {
86
89
  const { id, placeholder, includePlaceholderItem, fullWidth = true } = props;
87
90
  const ariaDescribedBy = props["aria-describedby"];
91
+ const ariaLabel = props["aria-label"];
92
+ const ariaLabelledBy = props["aria-labelledby"];
88
93
  const isInvalid = props.isInvalid;
89
94
 
90
95
  const isChildrenMode = "children" in props && props.children != null;
@@ -193,7 +198,10 @@ export function SingleSelect<T extends DataWithId>(
193
198
  <SelectPositioner sideOffset={8} alignItemWithTrigger={false}>
194
199
  <SelectPopup>
195
200
  <Select.ScrollUpArrow />
196
- <Select.List>
201
+ <Select.List
202
+ aria-label={ariaLabel}
203
+ aria-labelledby={ariaLabelledBy}
204
+ >
197
205
  {children ? (
198
206
  children
199
207
  ) : groupBy ? (
@@ -0,0 +1,133 @@
1
+ import React from "react";
2
+ import {
3
+ fireEvent,
4
+ render,
5
+ screen,
6
+ } from "@sproutsocial/seeds-react-testing-library";
7
+ import { SingleSelect } from "../SingleSelect";
8
+ import { MultiSelect } from "../MultiSelect";
9
+ import { SingleCombobox } from "../SingleCombobox";
10
+ import { MultiCombobox } from "../MultiCombobox";
11
+ import { SingleSearchableSelect } from "../SingleSearchableSelect";
12
+ import { MultiSearchableSelect } from "../MultiSearchableSelect";
13
+ import { books, itemToString } from "../storyData";
14
+
15
+ // Base UI's Select.List and Combobox.List render role="listbox" with no name of
16
+ // their own. axe only reports that as aria-input-field-name once the trigger has
17
+ // dropped its aria-controls, which happens while the popup animates closed — a
18
+ // state jsdom does not reach. So these assert the property that fixes the rule:
19
+ // the listbox carries a name of its own, independent of the trigger.
20
+
21
+ const LABEL_ID = "book-label";
22
+ const VisibleLabel = () => <span id={LABEL_ID}>Book</span>;
23
+
24
+ const openSelectPopup = () => {
25
+ fireEvent.click(screen.getByRole("combobox"));
26
+ };
27
+
28
+ const openComboboxPopup = () => {
29
+ fireEvent.click(screen.getByRole("button", { name: "Open popup" }));
30
+ };
31
+
32
+ interface Labelling {
33
+ "aria-label"?: string;
34
+ "aria-labelledby"?: string;
35
+ }
36
+
37
+ interface Case {
38
+ name: string;
39
+ renderWith: (labelling: Labelling) => ReturnType<typeof render>;
40
+ openPopup: () => void;
41
+ }
42
+
43
+ const cases: Case[] = [
44
+ {
45
+ name: "SingleSelect",
46
+ renderWith: (labelling) =>
47
+ render(
48
+ <SingleSelect data={books} itemToString={itemToString} {...labelling} />
49
+ ),
50
+ openPopup: openSelectPopup,
51
+ },
52
+ {
53
+ name: "MultiSelect",
54
+ renderWith: (labelling) =>
55
+ render(
56
+ <MultiSelect data={books} itemToString={itemToString} {...labelling} />
57
+ ),
58
+ openPopup: openSelectPopup,
59
+ },
60
+ {
61
+ name: "SingleSearchableSelect",
62
+ renderWith: (labelling) =>
63
+ render(
64
+ <SingleSearchableSelect
65
+ data={books}
66
+ itemToString={itemToString}
67
+ {...labelling}
68
+ />
69
+ ),
70
+ openPopup: openSelectPopup,
71
+ },
72
+ {
73
+ name: "MultiSearchableSelect",
74
+ renderWith: (labelling) =>
75
+ render(
76
+ <MultiSearchableSelect
77
+ data={books}
78
+ itemToString={itemToString}
79
+ {...labelling}
80
+ />
81
+ ),
82
+ openPopup: openSelectPopup,
83
+ },
84
+ {
85
+ name: "SingleCombobox",
86
+ renderWith: (labelling) =>
87
+ render(
88
+ <SingleCombobox
89
+ data={books}
90
+ itemToString={itemToString}
91
+ {...labelling}
92
+ />
93
+ ),
94
+ openPopup: openComboboxPopup,
95
+ },
96
+ {
97
+ name: "MultiCombobox",
98
+ renderWith: (labelling) =>
99
+ render(
100
+ <MultiCombobox
101
+ data={books}
102
+ itemToString={itemToString}
103
+ {...labelling}
104
+ />
105
+ ),
106
+ openPopup: openComboboxPopup,
107
+ },
108
+ ];
109
+
110
+ describe.each(cases)("$name popup listbox", ({ renderWith, openPopup }) => {
111
+ test("is named by aria-label", () => {
112
+ renderWith({ "aria-label": "Book" });
113
+ openPopup();
114
+
115
+ expect(screen.getByRole("listbox", { name: "Book" })).toBeInTheDocument();
116
+ });
117
+
118
+ test("is named by aria-labelledby", () => {
119
+ render(<VisibleLabel />);
120
+ renderWith({ "aria-labelledby": LABEL_ID });
121
+ openPopup();
122
+
123
+ expect(screen.getByRole("listbox", { name: "Book" })).toBeInTheDocument();
124
+ });
125
+
126
+ test("is unnamed when neither is given", () => {
127
+ renderWith({});
128
+ openPopup();
129
+
130
+ expect(screen.getByRole("listbox")).not.toHaveAttribute("aria-label");
131
+ expect(screen.getByRole("listbox")).not.toHaveAttribute("aria-labelledby");
132
+ });
133
+ });