@sproutsocial/seeds-react-menu 1.18.3 → 1.18.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 +8 -8
- package/CHANGELOG.md +21 -0
- package/dist/esm/index.js +41 -11
- package/dist/esm/index.js.map +1 -1
- package/dist/index.js +33 -3
- package/dist/index.js.map +1 -1
- package/package.json +13 -13
- package/src/NestedMenu/NestedMenuPopout.tsx +57 -4
- package/src/v3/ModalStories.stories.tsx +5 -125
- package/src/v3/MultiCombobox.stories.tsx +5 -9
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sproutsocial/seeds-react-menu",
|
|
3
|
-
"version": "1.18.
|
|
3
|
+
"version": "1.18.4",
|
|
4
4
|
"description": "Seeds React Menu",
|
|
5
5
|
"author": "Sprout Social, Inc.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -35,18 +35,18 @@
|
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"@radix-ui/react-popover": "^1.1.2",
|
|
38
|
-
"@sproutsocial/seeds-react-box": "^1.1.
|
|
39
|
-
"@sproutsocial/seeds-react-button": "^2.3.
|
|
40
|
-
"@sproutsocial/seeds-react-checkbox": "^1.3.
|
|
41
|
-
"@sproutsocial/seeds-react-icon": "^2.5.
|
|
42
|
-
"@sproutsocial/seeds-react-input": "^1.5.
|
|
43
|
-
"@sproutsocial/seeds-react-label": "^1.0.
|
|
44
|
-
"@sproutsocial/seeds-react-popout": "^2.5.
|
|
45
|
-
"@sproutsocial/seeds-react-radio": "^1.3.
|
|
46
|
-
"@sproutsocial/seeds-react-switch": "^1.2.
|
|
38
|
+
"@sproutsocial/seeds-react-box": "^1.1.34",
|
|
39
|
+
"@sproutsocial/seeds-react-button": "^2.3.11",
|
|
40
|
+
"@sproutsocial/seeds-react-checkbox": "^1.3.57",
|
|
41
|
+
"@sproutsocial/seeds-react-icon": "^2.5.4",
|
|
42
|
+
"@sproutsocial/seeds-react-input": "^1.5.45",
|
|
43
|
+
"@sproutsocial/seeds-react-label": "^1.0.36",
|
|
44
|
+
"@sproutsocial/seeds-react-popout": "^2.5.26",
|
|
45
|
+
"@sproutsocial/seeds-react-radio": "^1.3.37",
|
|
46
|
+
"@sproutsocial/seeds-react-switch": "^1.2.54",
|
|
47
47
|
"@sproutsocial/seeds-react-system-props": "^3.2.1",
|
|
48
|
-
"@sproutsocial/seeds-react-theme": "^4.
|
|
49
|
-
"@sproutsocial/seeds-react-token-input": "^1.4.
|
|
48
|
+
"@sproutsocial/seeds-react-theme": "^4.7.0",
|
|
49
|
+
"@sproutsocial/seeds-react-token-input": "^1.4.67",
|
|
50
50
|
"@sproutsocial/seeds-react-utilities": "^4.3.0",
|
|
51
51
|
"@base-ui/react": "^1.3.0",
|
|
52
52
|
"@tanstack/react-virtual": "^3.13.12",
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"styled-system": "^5.1.5"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
|
-
"@sproutsocial/seeds-react-tooltip": "^1.1.
|
|
59
|
+
"@sproutsocial/seeds-react-tooltip": "^1.1.41",
|
|
60
60
|
"@sproutsocial/eslint-config-seeds": "*",
|
|
61
61
|
"@sproutsocial/seeds-react-testing-library": "*",
|
|
62
62
|
"@sproutsocial/seeds-testing": "*",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type RefObject } from "react";
|
|
1
|
+
import { type RefObject, useEffect, useRef } from "react";
|
|
2
2
|
import { useNestedMenuContext } from "./NestedMenuContext";
|
|
3
3
|
import {
|
|
4
4
|
MenuPopout,
|
|
@@ -14,6 +14,13 @@ const StyledDiv = styled(motion.div)`
|
|
|
14
14
|
overflow: hidden;
|
|
15
15
|
`;
|
|
16
16
|
|
|
17
|
+
/** How long to wait after closing before returning focus to the toggle. */
|
|
18
|
+
const RETURN_FOCUS_DELAY = 200;
|
|
19
|
+
/** How long to wait before retrying an attempt that was undone. */
|
|
20
|
+
const RETURN_FOCUS_RETRY_DELAY = 50;
|
|
21
|
+
/** Upper bound on retrying, so an unfocusable toggle cannot retry forever. */
|
|
22
|
+
const RETURN_FOCUS_BUDGET = 1000;
|
|
23
|
+
|
|
17
24
|
export const NestedMenuPopout = ({
|
|
18
25
|
content,
|
|
19
26
|
menuToggleElement,
|
|
@@ -29,6 +36,52 @@ export const NestedMenuPopout = ({
|
|
|
29
36
|
const { activeMenuContext, rootMenuContextProps, resetSelectedMenuPath } =
|
|
30
37
|
useNestedMenuContext();
|
|
31
38
|
|
|
39
|
+
const closeTimerRef = useRef<ReturnType<typeof setTimeout>>();
|
|
40
|
+
const retryTimerRef = useRef<ReturnType<typeof setTimeout>>();
|
|
41
|
+
|
|
42
|
+
useEffect(
|
|
43
|
+
() => () => {
|
|
44
|
+
clearTimeout(closeTimerRef.current);
|
|
45
|
+
clearTimeout(retryTimerRef.current);
|
|
46
|
+
},
|
|
47
|
+
[]
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Move focus back to the toggle, retrying until it takes effect.
|
|
52
|
+
*
|
|
53
|
+
* The popout's focus lock stays mounted for the length of the close
|
|
54
|
+
* animation, and while it is mounted it traps focus: a focus() call landing
|
|
55
|
+
* outside the menu is immediately pulled back in. On a slow machine the
|
|
56
|
+
* first attempt can fall inside that window, and once the lock unmounts
|
|
57
|
+
* focus is left on <body> with nothing to restore it. Retrying covers that
|
|
58
|
+
* instead of lengthening the initial delay, which can only ever guess at how
|
|
59
|
+
* long the animation took.
|
|
60
|
+
*
|
|
61
|
+
* An attempt that takes effect stops the retries, so this never keeps
|
|
62
|
+
* pulling focus back from somewhere the user deliberately moved it.
|
|
63
|
+
*/
|
|
64
|
+
const attemptReturnFocus = (deadline: number) => {
|
|
65
|
+
const toggleElement = toggleElementRef.current;
|
|
66
|
+
|
|
67
|
+
if (toggleElement) {
|
|
68
|
+
if (document.activeElement === toggleElement) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
toggleElement.focus();
|
|
72
|
+
if (document.activeElement === toggleElement) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (Date.now() < deadline) {
|
|
78
|
+
retryTimerRef.current = setTimeout(
|
|
79
|
+
() => attemptReturnFocus(deadline),
|
|
80
|
+
RETURN_FOCUS_RETRY_DELAY
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
32
85
|
return (
|
|
33
86
|
<MenuPopout
|
|
34
87
|
lockPlacement
|
|
@@ -36,10 +89,10 @@ export const NestedMenuPopout = ({
|
|
|
36
89
|
onClose={() => {
|
|
37
90
|
onClose?.();
|
|
38
91
|
// Manually focus the toggle element and then reset state
|
|
39
|
-
setTimeout(() => {
|
|
40
|
-
|
|
92
|
+
closeTimerRef.current = setTimeout(() => {
|
|
93
|
+
attemptReturnFocus(Date.now() + RETURN_FOCUS_BUDGET);
|
|
41
94
|
resetSelectedMenuPath?.();
|
|
42
|
-
},
|
|
95
|
+
}, RETURN_FOCUS_DELAY);
|
|
43
96
|
}}
|
|
44
97
|
isOpen={!!rootMenuContextProps.isOpen}
|
|
45
98
|
openMenu={rootMenuContextProps.openMenu}
|
|
@@ -5,12 +5,7 @@ import { FormField } from "@sproutsocial/seeds-react-form-field";
|
|
|
5
5
|
import { Button } from "@sproutsocial/seeds-react-button";
|
|
6
6
|
import { Input } from "@sproutsocial/seeds-react-input";
|
|
7
7
|
import { TokenInput } from "@sproutsocial/seeds-react-token-input";
|
|
8
|
-
import {
|
|
9
|
-
Modal as ModalV1,
|
|
10
|
-
ModalV2,
|
|
11
|
-
ModalBody,
|
|
12
|
-
ModalFooter,
|
|
13
|
-
} from "@sproutsocial/seeds-react-modal";
|
|
8
|
+
import { Modal, ModalBody, ModalFooter } from "@sproutsocial/seeds-react-modal";
|
|
14
9
|
import {
|
|
15
10
|
PeekIn,
|
|
16
11
|
PeekInHeader,
|
|
@@ -146,40 +141,11 @@ type Story = StoryObj;
|
|
|
146
141
|
|
|
147
142
|
// ─── Stories ──────────────────────────────────────────────────────────────────
|
|
148
143
|
|
|
149
|
-
export const V1Modal: Story = {
|
|
150
|
-
name: "V1 Modal",
|
|
151
|
-
render: () => {
|
|
152
|
-
const [isOpen, setIsOpen] = React.useState(false);
|
|
153
|
-
return (
|
|
154
|
-
<>
|
|
155
|
-
<Button appearance="primary" onClick={() => setIsOpen(true)}>
|
|
156
|
-
Open V1 Modal
|
|
157
|
-
</Button>
|
|
158
|
-
<ModalV1
|
|
159
|
-
appElementSelector="#root"
|
|
160
|
-
isOpen={isOpen}
|
|
161
|
-
onClose={() => setIsOpen(false)}
|
|
162
|
-
closeButtonLabel="Close"
|
|
163
|
-
label="V1 Modal"
|
|
164
|
-
>
|
|
165
|
-
<ModalV1.Header title="" subtitle="" bordered>
|
|
166
|
-
V1 modal
|
|
167
|
-
</ModalV1.Header>
|
|
168
|
-
<ModalV1.Content>
|
|
169
|
-
<AllComponents />
|
|
170
|
-
</ModalV1.Content>
|
|
171
|
-
<ModalV1.Footer>Footer</ModalV1.Footer>
|
|
172
|
-
</ModalV1>
|
|
173
|
-
</>
|
|
174
|
-
);
|
|
175
|
-
},
|
|
176
|
-
};
|
|
177
|
-
|
|
178
144
|
export const V2Modal: Story = {
|
|
179
145
|
name: "V2 Modal",
|
|
180
146
|
render: () => (
|
|
181
147
|
<>
|
|
182
|
-
<
|
|
148
|
+
<Modal
|
|
183
149
|
title="Pick some books"
|
|
184
150
|
modalTrigger={<Button appearance="primary">Open V2 Modal</Button>}
|
|
185
151
|
closeButtonAriaLabel="Close"
|
|
@@ -188,7 +154,7 @@ export const V2Modal: Story = {
|
|
|
188
154
|
<AllComponents />
|
|
189
155
|
</ModalBody>
|
|
190
156
|
<ModalFooter cancelButton={<Button>Cancel</Button>} />
|
|
191
|
-
</
|
|
157
|
+
</Modal>
|
|
192
158
|
<AllComponents />
|
|
193
159
|
</>
|
|
194
160
|
),
|
|
@@ -196,92 +162,6 @@ export const V2Modal: Story = {
|
|
|
196
162
|
|
|
197
163
|
// ─── Action Menu in Modal ─────────────────────────────────────────────────────
|
|
198
164
|
|
|
199
|
-
export const ActionMenuInV1Modal: Story = {
|
|
200
|
-
name: "Action Menu in V1 Modal",
|
|
201
|
-
render: () => {
|
|
202
|
-
const [isOpen, setIsOpen] = React.useState(false);
|
|
203
|
-
return (
|
|
204
|
-
<>
|
|
205
|
-
<Button appearance="primary" onClick={() => setIsOpen(true)}>
|
|
206
|
-
Open V1 Modal
|
|
207
|
-
</Button>
|
|
208
|
-
<ModalV1
|
|
209
|
-
appElementSelector="#root"
|
|
210
|
-
isOpen={isOpen}
|
|
211
|
-
onClose={() => setIsOpen(false)}
|
|
212
|
-
closeButtonLabel="Close"
|
|
213
|
-
label="Action Menu in V1 Modal"
|
|
214
|
-
>
|
|
215
|
-
<ModalV1.Header title="" subtitle="" bordered>
|
|
216
|
-
Actions
|
|
217
|
-
</ModalV1.Header>
|
|
218
|
-
<ModalV1.Content>
|
|
219
|
-
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
|
220
|
-
<ActionMenu
|
|
221
|
-
trigger={<Button appearance="secondary">Basic Menu</Button>}
|
|
222
|
-
>
|
|
223
|
-
<MenuItem onClick={action("Compose")}>Compose</MenuItem>
|
|
224
|
-
<MenuItem onClick={action("View Messages")}>
|
|
225
|
-
View Messages
|
|
226
|
-
</MenuItem>
|
|
227
|
-
<MenuItem onClick={action("Analyze post")}>
|
|
228
|
-
Analyze post
|
|
229
|
-
</MenuItem>
|
|
230
|
-
</ActionMenu>
|
|
231
|
-
|
|
232
|
-
<ActionMenu
|
|
233
|
-
trigger={
|
|
234
|
-
<Button appearance="secondary">Menu with Groups</Button>
|
|
235
|
-
}
|
|
236
|
-
>
|
|
237
|
-
<MenuGroup label="Actions" showSeparator>
|
|
238
|
-
<MenuItem onClick={action("Edit")}>Edit</MenuItem>
|
|
239
|
-
<MenuItem onClick={action("Duplicate")}>Duplicate</MenuItem>
|
|
240
|
-
</MenuGroup>
|
|
241
|
-
<MenuGroup label="Danger">
|
|
242
|
-
<MenuItem onClick={action("Delete")} disabled>
|
|
243
|
-
Delete
|
|
244
|
-
</MenuItem>
|
|
245
|
-
</MenuGroup>
|
|
246
|
-
</ActionMenu>
|
|
247
|
-
|
|
248
|
-
<ActionMenu
|
|
249
|
-
trigger={
|
|
250
|
-
<Button appearance="secondary">Menu with Submenu</Button>
|
|
251
|
-
}
|
|
252
|
-
>
|
|
253
|
-
<MenuItem onClick={action("Compose")}>Compose</MenuItem>
|
|
254
|
-
<MenuSub trigger={<MenuSubTrigger>Share</MenuSubTrigger>}>
|
|
255
|
-
<MenuItem onClick={action("Share to Twitter")}>
|
|
256
|
-
Twitter
|
|
257
|
-
</MenuItem>
|
|
258
|
-
<MenuItem onClick={action("Share to LinkedIn")}>
|
|
259
|
-
LinkedIn
|
|
260
|
-
</MenuItem>
|
|
261
|
-
<MenuSub
|
|
262
|
-
trigger={<MenuSubTrigger>More platforms</MenuSubTrigger>}
|
|
263
|
-
>
|
|
264
|
-
<MenuItem onClick={action("Share to Facebook")}>
|
|
265
|
-
Facebook
|
|
266
|
-
</MenuItem>
|
|
267
|
-
<MenuItem onClick={action("Share to Instagram")}>
|
|
268
|
-
Instagram
|
|
269
|
-
</MenuItem>
|
|
270
|
-
</MenuSub>
|
|
271
|
-
</MenuSub>
|
|
272
|
-
<MenuItem onClick={action("Delete")} disabled>
|
|
273
|
-
Delete
|
|
274
|
-
</MenuItem>
|
|
275
|
-
</ActionMenu>
|
|
276
|
-
</div>
|
|
277
|
-
</ModalV1.Content>
|
|
278
|
-
<ModalV1.Footer>Footer</ModalV1.Footer>
|
|
279
|
-
</ModalV1>
|
|
280
|
-
</>
|
|
281
|
-
);
|
|
282
|
-
},
|
|
283
|
-
};
|
|
284
|
-
|
|
285
165
|
export const ActionMenuInPeekIn: Story = {
|
|
286
166
|
name: "Action Menu in PeekIn",
|
|
287
167
|
render: () => {
|
|
@@ -391,7 +271,7 @@ export const ActionMenuInPeekIn: Story = {
|
|
|
391
271
|
export const ActionMenuInModal: Story = {
|
|
392
272
|
name: "Action Menu in V2 Modal",
|
|
393
273
|
render: () => (
|
|
394
|
-
<
|
|
274
|
+
<Modal
|
|
395
275
|
title="Actions"
|
|
396
276
|
modalTrigger={<Button appearance="primary">Open Modal</Button>}
|
|
397
277
|
closeButtonAriaLabel="Close"
|
|
@@ -447,6 +327,6 @@ export const ActionMenuInModal: Story = {
|
|
|
447
327
|
</div>
|
|
448
328
|
</ModalBody>
|
|
449
329
|
<ModalFooter cancelButton={<Button>Close</Button>} />
|
|
450
|
-
</
|
|
330
|
+
</Modal>
|
|
451
331
|
),
|
|
452
332
|
};
|
|
@@ -2,11 +2,7 @@ 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 { Button } from "@sproutsocial/seeds-react-button";
|
|
5
|
-
import {
|
|
6
|
-
ModalV2,
|
|
7
|
-
ModalBody,
|
|
8
|
-
ModalFooter,
|
|
9
|
-
} from "@sproutsocial/seeds-react-modal";
|
|
5
|
+
import { Modal, ModalBody, ModalFooter } from "@sproutsocial/seeds-react-modal";
|
|
10
6
|
import { MultiCombobox } from "./MultiCombobox";
|
|
11
7
|
import {
|
|
12
8
|
books,
|
|
@@ -570,7 +566,7 @@ export const Required: Story = {
|
|
|
570
566
|
export const LargeBookSetWithMaxTokensInModal: Story = {
|
|
571
567
|
name: "Large book set with max tokens (in modal)",
|
|
572
568
|
render: () => (
|
|
573
|
-
<
|
|
569
|
+
<Modal
|
|
574
570
|
title="Pick some books"
|
|
575
571
|
modalTrigger={<Button appearance="primary">Open Modal</Button>}
|
|
576
572
|
closeButtonAriaLabel="Close"
|
|
@@ -594,7 +590,7 @@ export const LargeBookSetWithMaxTokensInModal: Story = {
|
|
|
594
590
|
</div>
|
|
595
591
|
</ModalBody>
|
|
596
592
|
<ModalFooter cancelButton={<Button>Cancel</Button>} />
|
|
597
|
-
</
|
|
593
|
+
</Modal>
|
|
598
594
|
),
|
|
599
595
|
};
|
|
600
596
|
|
|
@@ -605,7 +601,7 @@ export const LargeBookSetWithMaxTokensInModal: Story = {
|
|
|
605
601
|
export const EscapeKeyInModal: Story = {
|
|
606
602
|
name: "Escape Key in Modal (a11y)",
|
|
607
603
|
render: () => (
|
|
608
|
-
<
|
|
604
|
+
<Modal
|
|
609
605
|
title="Pick some books"
|
|
610
606
|
modalTrigger={<Button appearance="primary">Open Modal</Button>}
|
|
611
607
|
closeButtonAriaLabel="Close"
|
|
@@ -631,6 +627,6 @@ export const EscapeKeyInModal: Story = {
|
|
|
631
627
|
</div>
|
|
632
628
|
</ModalBody>
|
|
633
629
|
<ModalFooter cancelButton={<Button>Cancel</Button>} />
|
|
634
|
-
</
|
|
630
|
+
</Modal>
|
|
635
631
|
),
|
|
636
632
|
};
|