@sproutsocial/seeds-react-tree 0.3.1

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.
@@ -0,0 +1,199 @@
1
+ import * as React from "react";
2
+ import { render, screen } from "@sproutsocial/seeds-react-testing-library";
3
+ import { Tree, TreeItem } from "..";
4
+
5
+ function renderBasicTree(
6
+ props: Partial<React.ComponentProps<typeof Tree>> = {}
7
+ ) {
8
+ return render(
9
+ <Tree aria-label="Files" defaultExpanded={["src"]} {...props}>
10
+ <TreeItem id="src" label="src">
11
+ <TreeItem id="Button.tsx" label="Button.tsx" />
12
+ <TreeItem id="Modal.tsx" label="Modal.tsx" />
13
+ </TreeItem>
14
+ <TreeItem id="package.json" label="package.json" />
15
+ </Tree>
16
+ );
17
+ }
18
+
19
+ describe("Tree", () => {
20
+ it("renders WAI-ARIA roles and aria-level on items", () => {
21
+ renderBasicTree();
22
+ expect(screen.getByRole("tree", { name: "Files" })).toBeInTheDocument();
23
+
24
+ const srcItem = screen.getByRole("treeitem", { name: /src/ });
25
+ expect(srcItem).toHaveAttribute("aria-level", "1");
26
+ expect(srcItem).toHaveAttribute("aria-expanded", "true");
27
+
28
+ const childItem = screen.getByRole("treeitem", { name: /Button\.tsx/ });
29
+ expect(childItem).toHaveAttribute("aria-level", "2");
30
+ expect(childItem).not.toHaveAttribute("aria-expanded");
31
+ });
32
+
33
+ it("hides children of collapsed branches", () => {
34
+ renderBasicTree({ defaultExpanded: [] });
35
+ const srcItem = screen.getByRole("treeitem", { name: /src/ });
36
+ expect(srcItem).toHaveAttribute("aria-expanded", "false");
37
+ // Group should be hidden — query without checking visibility filter.
38
+ const childItem = screen.queryByRole("treeitem", {
39
+ name: /Button\.tsx/,
40
+ hidden: true,
41
+ });
42
+ expect(childItem).toBeInTheDocument();
43
+ });
44
+
45
+ it("uses roving tabindex with the first item tabbable by default", async () => {
46
+ renderBasicTree();
47
+ // Wait for the mount-time effect that assigns initial roving target.
48
+ const srcItem = screen.getByRole("treeitem", { name: /src/ });
49
+ await new Promise((r) => setTimeout(r, 0));
50
+ expect(srcItem).toHaveAttribute("tabindex", "0");
51
+ expect(
52
+ screen.getByRole("treeitem", { name: /package\.json/ })
53
+ ).toHaveAttribute("tabindex", "-1");
54
+ });
55
+
56
+ it("ArrowRight expands a collapsed branch", async () => {
57
+ const { user } = renderBasicTree({ defaultExpanded: [] });
58
+ const srcItem = screen.getByRole("treeitem", { name: /src/ });
59
+ srcItem.focus();
60
+ await user.keyboard("{ArrowRight}");
61
+ expect(srcItem).toHaveAttribute("aria-expanded", "true");
62
+ });
63
+
64
+ it("ArrowLeft collapses an expanded branch", async () => {
65
+ const { user } = renderBasicTree({ defaultExpanded: ["src"] });
66
+ const srcItem = screen.getByRole("treeitem", { name: /src/ });
67
+ srcItem.focus();
68
+ await user.keyboard("{ArrowLeft}");
69
+ expect(srcItem).toHaveAttribute("aria-expanded", "false");
70
+ });
71
+
72
+ it("ArrowDown moves focus to the next visible item", async () => {
73
+ const { user } = renderBasicTree();
74
+ const srcItem = screen.getByRole("treeitem", { name: /src/ });
75
+ srcItem.focus();
76
+ await user.keyboard("{ArrowDown}");
77
+ expect(screen.getByRole("treeitem", { name: /Button\.tsx/ })).toHaveFocus();
78
+ });
79
+
80
+ it("Home/End jump to first/last visible item", async () => {
81
+ const { user } = renderBasicTree();
82
+ const last = screen.getByRole("treeitem", { name: /package\.json/ });
83
+ last.focus();
84
+ await user.keyboard("{Home}");
85
+ expect(screen.getByRole("treeitem", { name: /src/ })).toHaveFocus();
86
+ await user.keyboard("{End}");
87
+ expect(last).toHaveFocus();
88
+ });
89
+
90
+ it("Enter toggles selection in single-select mode", async () => {
91
+ const onChange = jest.fn();
92
+ const { user } = render(
93
+ <Tree
94
+ aria-label="Files"
95
+ selectionMode="single"
96
+ onSelectionChange={onChange}
97
+ >
98
+ <TreeItem id="a" label="A" />
99
+ <TreeItem id="b" label="B" />
100
+ </Tree>
101
+ );
102
+ const a = screen.getByRole("treeitem", { name: "A" });
103
+ a.focus();
104
+ await user.keyboard("{Enter}");
105
+ expect(onChange).toHaveBeenCalledWith(["a"]);
106
+ expect(a).toHaveAttribute("aria-selected", "true");
107
+ });
108
+
109
+ it("multi-select uses aria-checked and accumulates selections", async () => {
110
+ const { user } = render(
111
+ <Tree aria-label="Files" selectionMode="multiple">
112
+ <TreeItem id="a" label="A" />
113
+ <TreeItem id="b" label="B" />
114
+ </Tree>
115
+ );
116
+ const a = screen.getByRole("treeitem", { name: "A" });
117
+ const b = screen.getByRole("treeitem", { name: "B" });
118
+ a.focus();
119
+ await user.keyboard("{Enter}");
120
+ b.focus();
121
+ await user.keyboard(" ");
122
+ expect(a).toHaveAttribute("aria-checked", "true");
123
+ expect(b).toHaveAttribute("aria-checked", "true");
124
+ });
125
+
126
+ it('selectableNodes="leaves" hides the selection indicator on parents', () => {
127
+ render(
128
+ <Tree
129
+ aria-label="Files"
130
+ selectionMode="single"
131
+ selectableNodes="leaves"
132
+ defaultExpanded={["src"]}
133
+ renderSelectionIndicator={() => (
134
+ <span data-testid="indicator">indicator</span>
135
+ )}
136
+ >
137
+ <TreeItem id="src" label="src">
138
+ <TreeItem id="leaf" label="Leaf" />
139
+ </TreeItem>
140
+ </Tree>
141
+ );
142
+ // Only the leaf gets an indicator; the parent doesn't.
143
+ const indicators = screen.getAllByTestId("indicator");
144
+ expect(indicators).toHaveLength(1);
145
+ const leaf = screen.getByRole("treeitem", { name: /Leaf/ });
146
+ expect(leaf).toContainElement(indicators[0]!);
147
+ });
148
+
149
+ it('selectableNodes="leaves" blocks parent selection', async () => {
150
+ const onChange = jest.fn();
151
+ const { user } = render(
152
+ <Tree
153
+ aria-label="Files"
154
+ selectionMode="single"
155
+ selectableNodes="leaves"
156
+ defaultExpanded={["src"]}
157
+ onSelectionChange={onChange}
158
+ >
159
+ <TreeItem id="src" label="src">
160
+ <TreeItem id="leaf" label="Leaf" />
161
+ </TreeItem>
162
+ </Tree>
163
+ );
164
+ const branch = screen.getByRole("treeitem", { name: /src/ });
165
+ branch.focus();
166
+ await user.keyboard("{Enter}");
167
+ expect(onChange).not.toHaveBeenCalled();
168
+
169
+ const leaf = screen.getByRole("treeitem", { name: /Leaf/ });
170
+ leaf.focus();
171
+ await user.keyboard("{Enter}");
172
+ expect(onChange).toHaveBeenCalledWith(["leaf"]);
173
+ });
174
+
175
+ it("disabled items are skipped and not selectable", async () => {
176
+ const onChange = jest.fn();
177
+ const { user } = render(
178
+ <Tree
179
+ aria-label="Files"
180
+ selectionMode="single"
181
+ onSelectionChange={onChange}
182
+ >
183
+ <TreeItem id="a" label="A" />
184
+ <TreeItem id="b" label="B" disabled />
185
+ <TreeItem id="c" label="C" />
186
+ </Tree>
187
+ );
188
+ const a = screen.getByRole("treeitem", { name: "A" });
189
+ a.focus();
190
+ await user.keyboard("{ArrowDown}");
191
+ // B is disabled, so focus skips to C.
192
+ expect(screen.getByRole("treeitem", { name: "C" })).toHaveFocus();
193
+ });
194
+
195
+ it("passes axe a11y check", async () => {
196
+ const { runA11yCheck } = renderBasicTree({ selectionMode: "single" });
197
+ await runA11yCheck();
198
+ });
199
+ });
@@ -0,0 +1,230 @@
1
+ import * as React from "react";
2
+ import { render, screen } from "@sproutsocial/seeds-react-testing-library";
3
+ import { TreeCombobox } from "../TreeCombobox";
4
+ import type { TreeItemData } from "../Common/types";
5
+
6
+ const data: TreeItemData[] = [
7
+ {
8
+ id: "src",
9
+ label: "src",
10
+ children: [
11
+ { id: "Button.tsx", label: "Button.tsx" },
12
+ { id: "Modal.tsx", label: "Modal.tsx" },
13
+ ],
14
+ },
15
+ {
16
+ id: "tests",
17
+ label: "tests",
18
+ children: [{ id: "Button.test.tsx", label: "Button.test.tsx" }],
19
+ },
20
+ ];
21
+
22
+ describe("TreeCombobox", () => {
23
+ it("renders the combobox role with the WAI-ARIA combobox wiring", () => {
24
+ render(<TreeCombobox aria-label="Files" items={data} />);
25
+ const input = screen.getByRole("combobox", { name: "Files" });
26
+ expect(input).toHaveAttribute("aria-autocomplete", "list");
27
+ expect(input).toHaveAttribute("aria-haspopup", "tree");
28
+ expect(input).toHaveAttribute("aria-expanded", "true");
29
+
30
+ const tree = screen.getByRole("tree", { name: "Files" });
31
+ expect(input.getAttribute("aria-controls")).toBe(tree.id);
32
+ });
33
+
34
+ it("renders all items when query is empty", () => {
35
+ render(<TreeCombobox aria-label="Files" items={data} />);
36
+ expect(screen.getByRole("treeitem", { name: /src/ })).toBeInTheDocument();
37
+ expect(screen.getByRole("treeitem", { name: /tests/ })).toBeInTheDocument();
38
+ });
39
+
40
+ it("filters items by label and auto-expands ancestors of matches", async () => {
41
+ const { user } = render(<TreeCombobox aria-label="Files" items={data} />);
42
+ const input = screen.getByRole("combobox", { name: "Files" });
43
+ await user.type(input, "Modal");
44
+
45
+ expect(
46
+ screen.getByRole("treeitem", { name: /Modal\.tsx/ })
47
+ ).toBeInTheDocument();
48
+ expect(
49
+ screen.queryByRole("treeitem", { name: /^Button\.tsx$/ })
50
+ ).not.toBeInTheDocument();
51
+
52
+ const src = screen.getByRole("treeitem", { name: /src/ });
53
+ expect(src).toHaveAttribute("aria-expanded", "true");
54
+ expect(
55
+ screen.queryByRole("treeitem", { name: /^tests$/ })
56
+ ).not.toBeInTheDocument();
57
+ });
58
+
59
+ it("restores user expansion state when query clears", async () => {
60
+ const { user } = render(
61
+ <TreeCombobox aria-label="Files" items={data} defaultExpanded={[]} />
62
+ );
63
+ const input = screen.getByRole("combobox", { name: "Files" });
64
+ await user.type(input, "Modal");
65
+ expect(screen.getByRole("treeitem", { name: /src/ })).toHaveAttribute(
66
+ "aria-expanded",
67
+ "true"
68
+ );
69
+
70
+ await user.clear(input);
71
+ expect(screen.getByRole("treeitem", { name: /src/ })).toHaveAttribute(
72
+ "aria-expanded",
73
+ "false"
74
+ );
75
+ });
76
+
77
+ it("shows the empty state and sets aria-expanded=false when no items match", async () => {
78
+ const { user } = render(
79
+ <TreeCombobox aria-label="Files" items={data} emptyText="Nothing here" />
80
+ );
81
+ const input = screen.getByRole("combobox", { name: "Files" });
82
+ await user.type(input, "zzzzz");
83
+ expect(screen.getByText("Nothing here")).toBeInTheDocument();
84
+ expect(input).toHaveAttribute("aria-expanded", "false");
85
+ });
86
+
87
+ it("Escape clears a non-empty query and the active descendant", async () => {
88
+ const { user } = render(<TreeCombobox aria-label="Files" items={data} />);
89
+ const input = screen.getByRole("combobox", { name: "Files" });
90
+ await user.click(input);
91
+ await user.type(input, "Mod");
92
+ await user.keyboard("{ArrowDown}");
93
+ expect(input).toHaveAttribute("aria-activedescendant");
94
+
95
+ await user.keyboard("{Escape}");
96
+ expect(input).toHaveValue("");
97
+ expect(input).not.toHaveAttribute("aria-activedescendant");
98
+ });
99
+
100
+ it("ArrowDown sets aria-activedescendant to the first visible item; subsequent arrows navigate", async () => {
101
+ const { user } = render(
102
+ <TreeCombobox aria-label="Files" items={data} defaultExpanded={["src"]} />
103
+ );
104
+ const input = screen.getByRole("combobox", { name: "Files" });
105
+ await user.click(input);
106
+ await user.keyboard("{ArrowDown}");
107
+ expect(input).toHaveAttribute("aria-activedescendant", "src__item");
108
+
109
+ await user.keyboard("{ArrowDown}");
110
+ expect(input).toHaveAttribute("aria-activedescendant", "Button.tsx__item");
111
+
112
+ await user.keyboard("{ArrowUp}");
113
+ expect(input).toHaveAttribute("aria-activedescendant", "src__item");
114
+ });
115
+
116
+ it("ArrowRight expands a collapsed branch; ArrowLeft collapses an expanded branch", async () => {
117
+ const { user } = render(
118
+ <TreeCombobox aria-label="Files" items={data} defaultExpanded={[]} />
119
+ );
120
+ const input = screen.getByRole("combobox", { name: "Files" });
121
+ await user.click(input);
122
+ await user.keyboard("{ArrowDown}");
123
+ expect(screen.getByRole("treeitem", { name: /src/ })).toHaveAttribute(
124
+ "aria-expanded",
125
+ "false"
126
+ );
127
+
128
+ await user.keyboard("{ArrowRight}");
129
+ expect(screen.getByRole("treeitem", { name: /src/ })).toHaveAttribute(
130
+ "aria-expanded",
131
+ "true"
132
+ );
133
+
134
+ await user.keyboard("{ArrowLeft}");
135
+ expect(screen.getByRole("treeitem", { name: /src/ })).toHaveAttribute(
136
+ "aria-expanded",
137
+ "false"
138
+ );
139
+ });
140
+
141
+ it("DOM focus stays on the input across arrow keys", async () => {
142
+ const { user } = render(<TreeCombobox aria-label="Files" items={data} />);
143
+ const input = screen.getByRole("combobox", { name: "Files" });
144
+ await user.click(input);
145
+ await user.keyboard("{ArrowDown}{ArrowDown}{ArrowUp}");
146
+ expect(input).toHaveFocus();
147
+ });
148
+
149
+ it("treeitems are not in the tab order in combobox mode", () => {
150
+ render(
151
+ <TreeCombobox aria-label="Files" items={data} defaultExpanded={["src"]} />
152
+ );
153
+ const items = screen.getAllByRole("treeitem");
154
+ items.forEach((el) => {
155
+ expect(el).toHaveAttribute("tabindex", "-1");
156
+ });
157
+ });
158
+
159
+ it("Enter on the active item toggles single-select and fires onSelectionChange", async () => {
160
+ const onChange = jest.fn();
161
+ const { user } = render(
162
+ <TreeCombobox
163
+ aria-label="Files"
164
+ items={data}
165
+ defaultExpanded={["src"]}
166
+ selectionMode="single"
167
+ onSelectionChange={onChange}
168
+ />
169
+ );
170
+ const input = screen.getByRole("combobox", { name: "Files" });
171
+ await user.click(input);
172
+ await user.keyboard("{ArrowDown}{ArrowDown}"); // src → Button.tsx
173
+ await user.keyboard("{Enter}");
174
+ expect(onChange).toHaveBeenLastCalledWith(["Button.tsx"]);
175
+ });
176
+
177
+ it("Enter on multi-select accumulates selections without closing or clearing the query", async () => {
178
+ const onChange = jest.fn();
179
+ const { user } = render(
180
+ <TreeCombobox
181
+ aria-label="Files"
182
+ items={data}
183
+ defaultExpanded={["src"]}
184
+ selectionMode="multiple"
185
+ onSelectionChange={onChange}
186
+ />
187
+ );
188
+ const input = screen.getByRole("combobox", { name: "Files" });
189
+ await user.click(input);
190
+ await user.type(input, "tsx");
191
+ await user.keyboard("{ArrowDown}{ArrowDown}"); // src → Button.tsx
192
+ await user.keyboard("{Enter}");
193
+ expect(onChange).toHaveBeenLastCalledWith(["Button.tsx"]);
194
+
195
+ await user.keyboard("{ArrowDown}"); // Modal.tsx
196
+ await user.keyboard("{Enter}");
197
+ expect(onChange).toHaveBeenLastCalledWith(
198
+ expect.arrayContaining(["Button.tsx", "Modal.tsx"])
199
+ );
200
+ // Query and active descendant persist after Enter.
201
+ expect(input).toHaveValue("tsx");
202
+ expect(input).toHaveAttribute("aria-activedescendant", "Modal.tsx__item");
203
+ });
204
+
205
+ it("clicking a treeitem keeps focus on the input", async () => {
206
+ const onChange = jest.fn();
207
+ const { user } = render(
208
+ <TreeCombobox
209
+ aria-label="Files"
210
+ items={data}
211
+ defaultExpanded={["src"]}
212
+ selectionMode="single"
213
+ onSelectionChange={onChange}
214
+ />
215
+ );
216
+ const input = screen.getByRole("combobox", { name: "Files" });
217
+ await user.click(input);
218
+ const button = screen.getByRole("treeitem", { name: /Button\.tsx/ });
219
+ await user.click(button);
220
+ expect(input).toHaveFocus();
221
+ expect(onChange).toHaveBeenLastCalledWith(["Button.tsx"]);
222
+ });
223
+
224
+ it("passes axe a11y check", async () => {
225
+ const { runA11yCheck } = render(
226
+ <TreeCombobox aria-label="Files" items={data} />
227
+ );
228
+ await runA11yCheck();
229
+ });
230
+ });
package/src/index.ts ADDED
@@ -0,0 +1,10 @@
1
+ export { Tree, type TreeProps } from "./Tree";
2
+ export { TreeItem, type TreeItemProps } from "./TreeItem";
3
+ export { TreeCombobox, type TreeComboboxProps } from "./TreeCombobox";
4
+ export type {
5
+ TreeItemData,
6
+ TreeSelectionMode,
7
+ TreeSelectableNodes,
8
+ TreeSelectionIndicator,
9
+ TreeSelectionIndicatorState,
10
+ } from "./Common/types";
@@ -0,0 +1,166 @@
1
+ import styled from "styled-components";
2
+ import { Icon } from "@sproutsocial/seeds-react-icon";
3
+ import type { TreeItemData, TreeSelectionIndicator } from "./Common/types";
4
+
5
+ export const collections: TreeItemData[] = [
6
+ {
7
+ id: "customer-care",
8
+ label: "Customer care",
9
+ icon: <Icon name="headset-outline" size="small" />,
10
+ children: [
11
+ { id: "questions", label: "Questions & How-To", icon: <span>❓</span> },
12
+ { id: "bug-reports", label: "Bug Reports", icon: <span>🐛</span> },
13
+ {
14
+ id: "feature-requests",
15
+ label: "Feature Requests",
16
+ icon: <span>💡</span>,
17
+ },
18
+ {
19
+ id: "complaints",
20
+ label: "Complaints & Escalations",
21
+ icon: <span>😞</span>,
22
+ },
23
+ ],
24
+ },
25
+ {
26
+ id: "collection-b",
27
+ label: "Collection B",
28
+ icon: <Icon name="headset-outline" size="small" />,
29
+ children: [
30
+ { id: "b-1", label: "Topic One", icon: <span>📌</span> },
31
+ { id: "b-2", label: "Topic Two", icon: <span>📎</span> },
32
+ ],
33
+ },
34
+ {
35
+ id: "collection-c",
36
+ label: "Collection C",
37
+ icon: <Icon name="headset-outline" size="small" />,
38
+ children: [
39
+ { id: "c-1", label: "Alpha", icon: <span>🔤</span> },
40
+ { id: "c-2", label: "Beta", icon: <span>🔣</span> },
41
+ ],
42
+ },
43
+ ];
44
+
45
+ export const fileTree: TreeItemData[] = [
46
+ {
47
+ id: "src",
48
+ label: "src",
49
+ children: [
50
+ {
51
+ id: "components",
52
+ label: "components",
53
+ children: [
54
+ { id: "Button.tsx", label: "Button.tsx" },
55
+ { id: "Modal.tsx", label: "Modal.tsx" },
56
+ { id: "Tree.tsx", label: "Tree.tsx" },
57
+ ],
58
+ },
59
+ {
60
+ id: "hooks",
61
+ label: "hooks",
62
+ children: [
63
+ { id: "useTree.ts", label: "useTree.ts" },
64
+ { id: "useKey.ts", label: "useKey.ts" },
65
+ ],
66
+ },
67
+ { id: "index.ts", label: "index.ts" },
68
+ ],
69
+ },
70
+ {
71
+ id: "package.json",
72
+ label: "package.json",
73
+ },
74
+ {
75
+ id: "tsconfig.json",
76
+ label: "tsconfig.json",
77
+ },
78
+ ];
79
+
80
+ const RadioOuter = styled.span<{ $selected: boolean; $disabled: boolean }>`
81
+ display: inline-flex;
82
+ align-items: center;
83
+ justify-content: center;
84
+ flex-shrink: 0;
85
+ width: 18px;
86
+ height: 18px;
87
+ border-radius: 50%;
88
+ border: 2px solid
89
+ ${({ theme, $selected }) =>
90
+ $selected
91
+ ? theme.colors.form.border.selected
92
+ : theme.colors.form.border.base};
93
+ background: ${({ theme }) => theme.colors.form.background.base};
94
+ opacity: ${({ $disabled }) => ($disabled ? 0.4 : 1)};
95
+ `;
96
+
97
+ const RadioInner = styled.span`
98
+ width: 10px;
99
+ height: 10px;
100
+ border-radius: 50%;
101
+ background: ${({ theme }) => theme.colors.form.border.selected};
102
+ `;
103
+
104
+ export const radioIndicator: TreeSelectionIndicator = ({
105
+ selected,
106
+ disabled,
107
+ }) => (
108
+ <RadioOuter $selected={selected} $disabled={disabled}>
109
+ {selected ? <RadioInner /> : null}
110
+ </RadioOuter>
111
+ );
112
+
113
+ const CheckboxOuter = styled.span<{ $selected: boolean; $disabled: boolean }>`
114
+ display: inline-flex;
115
+ align-items: center;
116
+ justify-content: center;
117
+ flex-shrink: 0;
118
+ width: 18px;
119
+ height: 18px;
120
+ border-radius: 4px;
121
+ border: 2px solid
122
+ ${({ theme, $selected }) =>
123
+ $selected
124
+ ? theme.colors.form.border.selected
125
+ : theme.colors.form.border.base};
126
+ background: ${({ theme, $selected }) =>
127
+ $selected
128
+ ? theme.colors.form.border.selected
129
+ : theme.colors.form.background.base};
130
+ color: ${({ theme }) => theme.colors.text.inverse};
131
+ opacity: ${({ $disabled }) => ($disabled ? 0.4 : 1)};
132
+ `;
133
+
134
+ export const checkboxIndicator: TreeSelectionIndicator = ({
135
+ selected,
136
+ disabled,
137
+ }) => (
138
+ <CheckboxOuter $selected={selected} $disabled={disabled}>
139
+ {selected ? <Icon name="check-outline" size="small" /> : null}
140
+ </CheckboxOuter>
141
+ );
142
+
143
+ const ReadoutPanel = styled.div`
144
+ margin-top: ${({ theme }) => theme.space[400]};
145
+ padding: ${({ theme }) => theme.space[300]} ${({ theme }) => theme.space[400]};
146
+ border-radius: ${({ theme }) => theme.radii[400]};
147
+ background: ${({ theme }) => theme.colors.container.background.base};
148
+ border: 1px solid ${({ theme }) => theme.colors.container.border.base};
149
+ font-family: ${({ theme }) => theme.fontFamily};
150
+ ${({ theme }) => theme.typography[200]}
151
+ color: ${({ theme }) => theme.colors.text.body};
152
+ `;
153
+
154
+ const ReadoutLabel = styled.span`
155
+ font-weight: ${({ theme }) => theme.fontWeights.semibold};
156
+ margin-right: ${({ theme }) => theme.space[200]};
157
+ `;
158
+
159
+ export function SelectionReadout({ selected }: { selected: string[] }) {
160
+ return (
161
+ <ReadoutPanel aria-live="polite">
162
+ <ReadoutLabel>Selected:</ReadoutLabel>
163
+ {selected.length === 0 ? "(none)" : selected.join(", ")}
164
+ </ReadoutPanel>
165
+ );
166
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,12 @@
1
+ {
2
+ "extends": "@sproutsocial/seeds-tsconfig/react-library.json",
3
+ "compilerOptions": {
4
+ "noImplicitAny": false,
5
+ "noUncheckedIndexedAccess": false,
6
+ "baseUrl": ".",
7
+ "types": ["@testing-library/jest-dom"],
8
+ "verbatimModuleSyntax": true
9
+ },
10
+ "include": ["src/**/*"],
11
+ "exclude": ["node_modules", "dist", "**/*.stories.tsx", "**/*.stories.ts"]
12
+ }
package/tsup.config.ts ADDED
@@ -0,0 +1,12 @@
1
+ import { defineConfig } from "tsup";
2
+
3
+ export default defineConfig((options) => ({
4
+ entry: ["src/index.ts"],
5
+ format: ["cjs", "esm"],
6
+ clean: true,
7
+ legacyOutput: true,
8
+ dts: options.dts,
9
+ sourcemap: true,
10
+ external: ["react", "styled-components", /^@sproutsocial\//],
11
+ metafile: options.metafile,
12
+ }));