@sproutsocial/seeds-react-modal 2.4.8 → 2.5.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.
- package/.turbo/turbo-build.log +28 -28
- package/CHANGELOG.md +70 -0
- package/dist/{Modal-ki8oiGbC.d.mts → Modal-DTeKLfEI.d.mts} +1 -1
- package/dist/{Modal-ki8oiGbC.d.ts → Modal-DTeKLfEI.d.ts} +1 -1
- package/dist/{ModalAction-BHG3Zbd9.d.mts → ModalExternalTrigger-BnbJk9zY.d.mts} +85 -3
- package/dist/{ModalAction-BHG3Zbd9.d.ts → ModalExternalTrigger-BnbJk9zY.d.ts} +85 -3
- package/dist/esm/{chunk-ZY6VJ7XT.js → chunk-62MRZAJV.js} +36 -10
- package/dist/esm/chunk-62MRZAJV.js.map +1 -0
- package/dist/esm/{chunk-IYDY4OPB.js → chunk-72GBDCA2.js} +17 -1
- package/dist/esm/chunk-72GBDCA2.js.map +1 -0
- package/dist/esm/index.js +10 -4
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/v1/index.js +1 -1
- package/dist/esm/v2/index.js +1 -1
- package/dist/index.d.mts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +85 -11
- package/dist/index.js.map +1 -1
- package/dist/v1/index.d.mts +2 -2
- package/dist/v1/index.d.ts +2 -2
- package/dist/v1/index.js +16 -0
- package/dist/v1/index.js.map +1 -1
- package/dist/v2/index.d.mts +4 -77
- package/dist/v2/index.d.ts +4 -77
- package/dist/v2/index.js +35 -9
- package/dist/v2/index.js.map +1 -1
- package/package.json +7 -7
- package/src/Modal.stories.tsx +64 -0
- package/src/__tests__/v1/Modal.test.tsx +146 -1
- package/src/__tests__/v2/Modal.test.tsx +182 -0
- package/src/index.ts +4 -0
- package/src/v1/Modal.tsx +30 -0
- package/src/v2/Modal.tsx +8 -0
- package/src/v2/ModalTypes.ts +9 -2
- package/src/v2/ModalV2.stories.tsx +476 -49
- package/src/v2/components/ModalFooter.tsx +15 -4
- package/dist/esm/chunk-IYDY4OPB.js.map +0 -1
- package/dist/esm/chunk-ZY6VJ7XT.js.map +0 -1
|
@@ -3,9 +3,14 @@ import type { Meta, StoryObj } from "@storybook/react";
|
|
|
3
3
|
import { Box } from "@sproutsocial/seeds-react-box";
|
|
4
4
|
import { Button } from "@sproutsocial/seeds-react-button";
|
|
5
5
|
import Text from "@sproutsocial/seeds-react-text";
|
|
6
|
+
import { Popout } from "@sproutsocial/seeds-react-popout";
|
|
7
|
+
import { Popout as PopoutV2 } from "@sproutsocial/seeds-react-popout/v2";
|
|
8
|
+
import ModalV1 from "../v1/Modal";
|
|
6
9
|
import { FormField } from "@sproutsocial/seeds-react-form-field";
|
|
7
10
|
import {
|
|
11
|
+
ActionMenu,
|
|
8
12
|
MenuContent,
|
|
13
|
+
MenuGroup,
|
|
9
14
|
MenuItem,
|
|
10
15
|
MenuToggleButton,
|
|
11
16
|
SingleSelectMenu,
|
|
@@ -847,6 +852,61 @@ export const FooterWithLeftAction: Story = {
|
|
|
847
852
|
},
|
|
848
853
|
};
|
|
849
854
|
|
|
855
|
+
export const AsyncPrimaryAction: Story = {
|
|
856
|
+
render: () => {
|
|
857
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
858
|
+
const [isSaving, setIsSaving] = useState(false);
|
|
859
|
+
|
|
860
|
+
const handleSave = async () => {
|
|
861
|
+
setIsSaving(true);
|
|
862
|
+
// Simulate async operation
|
|
863
|
+
await new Promise((resolve) => setTimeout(resolve, 1500));
|
|
864
|
+
setIsSaving(false);
|
|
865
|
+
setIsOpen(false);
|
|
866
|
+
};
|
|
867
|
+
|
|
868
|
+
return (
|
|
869
|
+
<>
|
|
870
|
+
<Button appearance="primary" onClick={() => setIsOpen(true)}>
|
|
871
|
+
Open Async Save Modal
|
|
872
|
+
</Button>
|
|
873
|
+
<Modal
|
|
874
|
+
open={isOpen}
|
|
875
|
+
onOpenChange={setIsOpen}
|
|
876
|
+
aria-label="Async Save Modal"
|
|
877
|
+
closeButtonAriaLabel="Close"
|
|
878
|
+
>
|
|
879
|
+
<ModalHeader
|
|
880
|
+
title="Save Changes"
|
|
881
|
+
subtitle="The modal stays open until the save completes."
|
|
882
|
+
/>
|
|
883
|
+
<ModalBody>
|
|
884
|
+
<Text>
|
|
885
|
+
This modal uses <code>closeOnPrimaryAction=false</code> so the
|
|
886
|
+
primary button does not auto-close the modal. Instead, the
|
|
887
|
+
consumer controls when to close after the async operation
|
|
888
|
+
completes.
|
|
889
|
+
</Text>
|
|
890
|
+
</ModalBody>
|
|
891
|
+
<ModalFooter
|
|
892
|
+
closeOnPrimaryAction={false}
|
|
893
|
+
cancelButton={<Button>Cancel</Button>}
|
|
894
|
+
primaryButton={
|
|
895
|
+
<Button
|
|
896
|
+
appearance="primary"
|
|
897
|
+
onClick={handleSave}
|
|
898
|
+
disabled={isSaving}
|
|
899
|
+
>
|
|
900
|
+
{isSaving ? "Saving..." : "Save"}
|
|
901
|
+
</Button>
|
|
902
|
+
}
|
|
903
|
+
/>
|
|
904
|
+
</Modal>
|
|
905
|
+
</>
|
|
906
|
+
);
|
|
907
|
+
},
|
|
908
|
+
};
|
|
909
|
+
|
|
850
910
|
export const CustomFooterOverride: Story = {
|
|
851
911
|
render: () => {
|
|
852
912
|
return (
|
|
@@ -974,16 +1034,15 @@ export const PortaledComponentsInModal: Story = {
|
|
|
974
1034
|
};
|
|
975
1035
|
|
|
976
1036
|
/**
|
|
977
|
-
*
|
|
978
|
-
*
|
|
979
|
-
* ⚠️ NOT RECOMMENDED - This is a last resort pattern. Prefer using modalTrigger prop.
|
|
1037
|
+
* The recommended pattern for controlled modals where the trigger lives outside
|
|
1038
|
+
* the Modal component tree (e.g., a button in a table row, page header, or list item).
|
|
980
1039
|
*
|
|
981
|
-
*
|
|
982
|
-
* - triggerRef: Ref to attach to your trigger element
|
|
983
|
-
* - triggerProps: Function
|
|
984
|
-
* - onCloseAutoFocus:
|
|
1040
|
+
* useModalExternalTrigger provides:
|
|
1041
|
+
* - triggerRef: Ref to attach to your trigger element for focus restoration
|
|
1042
|
+
* - triggerProps: Function returning ARIA attributes (aria-haspopup, aria-expanded, aria-controls)
|
|
1043
|
+
* - onCloseAutoFocus: Pass to Modal to automatically restore focus to the trigger on close
|
|
985
1044
|
*
|
|
986
|
-
* This
|
|
1045
|
+
* This is the standard pattern for most real-world controlled modals.
|
|
987
1046
|
*/
|
|
988
1047
|
export const ExternalTriggerWithHook: Story = {
|
|
989
1048
|
render: () => {
|
|
@@ -994,29 +1053,31 @@ export const ExternalTriggerWithHook: Story = {
|
|
|
994
1053
|
return (
|
|
995
1054
|
<Box>
|
|
996
1055
|
<Text mb={400} fontWeight="semibold">
|
|
997
|
-
|
|
1056
|
+
Controlled Modal: useModalExternalTrigger Hook
|
|
998
1057
|
</Text>
|
|
999
1058
|
<Text mb={400}>
|
|
1000
|
-
|
|
1001
|
-
|
|
1059
|
+
Use this pattern when the trigger and modal live in different parts of
|
|
1060
|
+
the component tree — for example, a button in a data table row, a list
|
|
1061
|
+
item, or a page header.
|
|
1002
1062
|
</Text>
|
|
1003
1063
|
|
|
1004
1064
|
<Text mb={400} fontWeight="semibold">
|
|
1005
|
-
|
|
1065
|
+
What this hook provides:
|
|
1006
1066
|
</Text>
|
|
1007
1067
|
<Box as="ul" pl={400} mb={400}>
|
|
1008
1068
|
<li>
|
|
1009
1069
|
<Text>
|
|
1010
|
-
Works with any trigger element (Button, custom
|
|
1070
|
+
Works with any trigger element (Button, link, custom component)
|
|
1011
1071
|
</Text>
|
|
1012
1072
|
</li>
|
|
1013
1073
|
<li>
|
|
1014
1074
|
<Text>
|
|
1015
|
-
|
|
1075
|
+
Correct ARIA attributes on the trigger (aria-haspopup,
|
|
1076
|
+
aria-expanded, aria-controls)
|
|
1016
1077
|
</Text>
|
|
1017
1078
|
</li>
|
|
1018
1079
|
<li>
|
|
1019
|
-
<Text>
|
|
1080
|
+
<Text>Automatic focus restoration when the modal closes</Text>
|
|
1020
1081
|
</li>
|
|
1021
1082
|
</Box>
|
|
1022
1083
|
|
|
@@ -1037,31 +1098,34 @@ export const ExternalTriggerWithHook: Story = {
|
|
|
1037
1098
|
closeButtonAriaLabel="Close Modal"
|
|
1038
1099
|
>
|
|
1039
1100
|
<ModalHeader
|
|
1040
|
-
title="
|
|
1041
|
-
subtitle="
|
|
1101
|
+
title="Controlled Modal"
|
|
1102
|
+
subtitle="Trigger lives outside the Modal component tree"
|
|
1042
1103
|
/>
|
|
1043
1104
|
<ModalBody>
|
|
1044
1105
|
<Text mb={400}>
|
|
1045
|
-
The
|
|
1046
|
-
|
|
1106
|
+
The trigger button used to open this modal is rendered separately
|
|
1107
|
+
from the Modal component. The useModalExternalTrigger hook
|
|
1108
|
+
handles:
|
|
1047
1109
|
</Text>
|
|
1048
1110
|
<Box as="ul" pl={400}>
|
|
1049
1111
|
<li>
|
|
1050
|
-
<Text>
|
|
1112
|
+
<Text>
|
|
1113
|
+
A ref on the trigger for focus restoration when the modal
|
|
1114
|
+
closes
|
|
1115
|
+
</Text>
|
|
1051
1116
|
</li>
|
|
1052
1117
|
<li>
|
|
1053
|
-
<Text>
|
|
1118
|
+
<Text>
|
|
1119
|
+
ARIA attributes on the trigger (aria-haspopup, aria-expanded,
|
|
1120
|
+
aria-controls)
|
|
1121
|
+
</Text>
|
|
1054
1122
|
</li>
|
|
1055
1123
|
<li>
|
|
1056
1124
|
<Text>
|
|
1057
|
-
|
|
1125
|
+
The onCloseAutoFocus callback to return focus to the trigger
|
|
1058
1126
|
</Text>
|
|
1059
1127
|
</li>
|
|
1060
1128
|
</Box>
|
|
1061
|
-
<Text mt={400}>
|
|
1062
|
-
This eliminates the boilerplate of manual ref management and
|
|
1063
|
-
custom onCloseAutoFocus implementation.
|
|
1064
|
-
</Text>
|
|
1065
1129
|
</ModalBody>
|
|
1066
1130
|
<ModalFooter
|
|
1067
1131
|
cancelButton={
|
|
@@ -1082,14 +1146,15 @@ export const ExternalTriggerWithHook: Story = {
|
|
|
1082
1146
|
};
|
|
1083
1147
|
|
|
1084
1148
|
/**
|
|
1085
|
-
*
|
|
1086
|
-
*
|
|
1087
|
-
* ⚠️ NOT RECOMMENDED - This is a last resort pattern. Prefer using modalTrigger prop.
|
|
1149
|
+
* Alternative to useModalExternalTrigger when the trigger is specifically a Seeds Button.
|
|
1088
1150
|
*
|
|
1089
|
-
* ModalExternalTrigger is a Button
|
|
1090
|
-
*
|
|
1151
|
+
* ModalExternalTrigger is a pre-configured Button component that automatically applies
|
|
1152
|
+
* the correct ARIA attributes (aria-haspopup, aria-expanded, aria-controls) for a modal
|
|
1153
|
+
* trigger. Use it when the trigger lives outside the Modal component tree and you want
|
|
1154
|
+
* to avoid spreading hook props manually.
|
|
1091
1155
|
*
|
|
1092
|
-
* Note:
|
|
1156
|
+
* Note: Unlike useModalExternalTrigger, focus restoration still requires passing an
|
|
1157
|
+
* onCloseAutoFocus callback to the Modal manually.
|
|
1093
1158
|
*/
|
|
1094
1159
|
export const ExternalTriggerComponent: Story = {
|
|
1095
1160
|
render: () => {
|
|
@@ -1099,37 +1164,41 @@ export const ExternalTriggerComponent: Story = {
|
|
|
1099
1164
|
return (
|
|
1100
1165
|
<Box>
|
|
1101
1166
|
<Text mb={400} fontWeight="semibold">
|
|
1102
|
-
|
|
1167
|
+
Controlled Modal: ModalExternalTrigger Component
|
|
1103
1168
|
</Text>
|
|
1104
1169
|
<Text mb={400}>
|
|
1105
|
-
|
|
1106
|
-
|
|
1170
|
+
Use this component when the trigger is a Seeds Button and lives
|
|
1171
|
+
outside the Modal component tree. It applies the correct ARIA
|
|
1172
|
+
attributes automatically without needing to spread hook props.
|
|
1107
1173
|
</Text>
|
|
1108
1174
|
|
|
1109
1175
|
<Text mb={400} fontWeight="semibold">
|
|
1110
|
-
|
|
1176
|
+
What this component provides:
|
|
1111
1177
|
</Text>
|
|
1112
1178
|
<Box as="ul" pl={400} mb={400}>
|
|
1113
1179
|
<li>
|
|
1114
1180
|
<Text>
|
|
1115
|
-
Automatic ARIA attributes (aria-haspopup, aria-expanded,
|
|
1181
|
+
Automatic ARIA attributes (aria-haspopup, aria-expanded,
|
|
1182
|
+
aria-controls)
|
|
1116
1183
|
</Text>
|
|
1117
1184
|
</li>
|
|
1118
1185
|
<li>
|
|
1119
1186
|
<Text>
|
|
1120
|
-
All Button props supported (appearance, size, disabled,
|
|
1187
|
+
All Seeds Button props supported (appearance, size, disabled,
|
|
1188
|
+
etc.)
|
|
1121
1189
|
</Text>
|
|
1122
1190
|
</li>
|
|
1123
1191
|
<li>
|
|
1124
|
-
<Text>Cleaner than hook
|
|
1192
|
+
<Text>Cleaner than the hook when using a Button trigger</Text>
|
|
1125
1193
|
</li>
|
|
1126
1194
|
</Box>
|
|
1127
1195
|
|
|
1128
1196
|
<Text mb={400} fontWeight="semibold">
|
|
1129
|
-
|
|
1197
|
+
Note:
|
|
1130
1198
|
</Text>
|
|
1131
1199
|
<Text mb={400}>
|
|
1132
|
-
|
|
1200
|
+
Pass a ref and implement onCloseAutoFocus on the Modal to restore
|
|
1201
|
+
focus when the modal closes.
|
|
1133
1202
|
</Text>
|
|
1134
1203
|
|
|
1135
1204
|
<ModalExternalTrigger
|
|
@@ -1153,28 +1222,31 @@ export const ExternalTriggerComponent: Story = {
|
|
|
1153
1222
|
closeButtonAriaLabel="Close Modal"
|
|
1154
1223
|
>
|
|
1155
1224
|
<ModalHeader
|
|
1156
|
-
title="
|
|
1157
|
-
subtitle="Using ModalExternalTrigger"
|
|
1225
|
+
title="Controlled Modal"
|
|
1226
|
+
subtitle="Using ModalExternalTrigger component"
|
|
1158
1227
|
/>
|
|
1159
1228
|
<ModalBody>
|
|
1160
1229
|
<Text mb={400}>
|
|
1161
|
-
|
|
1162
|
-
|
|
1230
|
+
The trigger button for this modal is a ModalExternalTrigger
|
|
1231
|
+
component rendered outside the Modal tree. It automatically
|
|
1232
|
+
applies:
|
|
1163
1233
|
</Text>
|
|
1164
1234
|
<Box as="ul" pl={400}>
|
|
1165
1235
|
<li>
|
|
1166
1236
|
<Text>aria-haspopup="dialog"</Text>
|
|
1167
1237
|
</li>
|
|
1168
1238
|
<li>
|
|
1169
|
-
<Text>aria-expanded
|
|
1239
|
+
<Text>aria-expanded reflecting the current open state</Text>
|
|
1170
1240
|
</li>
|
|
1171
1241
|
<li>
|
|
1172
|
-
<Text>
|
|
1242
|
+
<Text>
|
|
1243
|
+
aria-controls linking to the modal (if modalId provided)
|
|
1244
|
+
</Text>
|
|
1173
1245
|
</li>
|
|
1174
1246
|
</Box>
|
|
1175
1247
|
<Text mt={400}>
|
|
1176
|
-
|
|
1177
|
-
|
|
1248
|
+
Focus returns to the trigger on close via the onCloseAutoFocus
|
|
1249
|
+
callback passed to the Modal.
|
|
1178
1250
|
</Text>
|
|
1179
1251
|
</ModalBody>
|
|
1180
1252
|
<ModalFooter
|
|
@@ -1194,3 +1266,358 @@ export const ExternalTriggerComponent: Story = {
|
|
|
1194
1266
|
);
|
|
1195
1267
|
},
|
|
1196
1268
|
};
|
|
1269
|
+
|
|
1270
|
+
export const WithFormAndMenus: Story = {
|
|
1271
|
+
render: () => {
|
|
1272
|
+
const [selectedItem, setSelectedItem] =
|
|
1273
|
+
useState<TypeSingleSelectMenuProps["selectedItem"]>(null);
|
|
1274
|
+
const [name, setName] = useState("");
|
|
1275
|
+
const [submitCount, setSubmitCount] = useState(0);
|
|
1276
|
+
|
|
1277
|
+
const handleSubmit = (e: React.FormEvent) => {
|
|
1278
|
+
e.preventDefault();
|
|
1279
|
+
setSubmitCount((c) => c + 1);
|
|
1280
|
+
};
|
|
1281
|
+
|
|
1282
|
+
return (
|
|
1283
|
+
<Modal
|
|
1284
|
+
modalTrigger={
|
|
1285
|
+
<Button appearance="primary">Open Modal with Form</Button>
|
|
1286
|
+
}
|
|
1287
|
+
aria-label="Form Auto-Submit Bug Reproduction"
|
|
1288
|
+
closeButtonProps={{ "aria-label": "Close" }}
|
|
1289
|
+
>
|
|
1290
|
+
<ModalHeader
|
|
1291
|
+
title="Assign User Role"
|
|
1292
|
+
subtitle="Selecting a role should not submit the form"
|
|
1293
|
+
/>
|
|
1294
|
+
<ModalBody>
|
|
1295
|
+
<form onSubmit={handleSubmit}>
|
|
1296
|
+
<Box p={400} display="flex" flexDirection="column" gap={400}>
|
|
1297
|
+
<FormField
|
|
1298
|
+
label="Name"
|
|
1299
|
+
error={null}
|
|
1300
|
+
helperText={null}
|
|
1301
|
+
id="name-field"
|
|
1302
|
+
qa={{}}
|
|
1303
|
+
required={false}
|
|
1304
|
+
>
|
|
1305
|
+
{(props) => (
|
|
1306
|
+
<input
|
|
1307
|
+
{...props}
|
|
1308
|
+
type="text"
|
|
1309
|
+
placeholder="Enter a name"
|
|
1310
|
+
value={name}
|
|
1311
|
+
onChange={(e) => setName(e.target.value)}
|
|
1312
|
+
style={{
|
|
1313
|
+
padding: "8px 12px",
|
|
1314
|
+
borderRadius: "4px",
|
|
1315
|
+
border: "1px solid #ccc",
|
|
1316
|
+
width: "100%",
|
|
1317
|
+
boxSizing: "border-box",
|
|
1318
|
+
fontSize: "14px",
|
|
1319
|
+
}}
|
|
1320
|
+
/>
|
|
1321
|
+
)}
|
|
1322
|
+
</FormField>
|
|
1323
|
+
|
|
1324
|
+
<FormField
|
|
1325
|
+
label="Role"
|
|
1326
|
+
error={null}
|
|
1327
|
+
helperText={null}
|
|
1328
|
+
id="role-field"
|
|
1329
|
+
qa={{}}
|
|
1330
|
+
required={false}
|
|
1331
|
+
>
|
|
1332
|
+
{() => (
|
|
1333
|
+
<SingleSelectMenu
|
|
1334
|
+
selectedItem={selectedItem}
|
|
1335
|
+
onSelectedItemChange={({ selectedItem: item }) =>
|
|
1336
|
+
setSelectedItem(item)
|
|
1337
|
+
}
|
|
1338
|
+
menuToggleElement={
|
|
1339
|
+
<MenuToggleButton>
|
|
1340
|
+
{selectedItem?.id ?? "Select a role..."}
|
|
1341
|
+
</MenuToggleButton>
|
|
1342
|
+
}
|
|
1343
|
+
>
|
|
1344
|
+
<MenuContent>
|
|
1345
|
+
<MenuItem id="admin">Admin</MenuItem>
|
|
1346
|
+
<MenuItem id="editor">Editor</MenuItem>
|
|
1347
|
+
<MenuItem id="viewer">Viewer</MenuItem>
|
|
1348
|
+
</MenuContent>
|
|
1349
|
+
</SingleSelectMenu>
|
|
1350
|
+
)}
|
|
1351
|
+
</FormField>
|
|
1352
|
+
|
|
1353
|
+
{submitCount > 0 && (
|
|
1354
|
+
<Box
|
|
1355
|
+
p={400}
|
|
1356
|
+
bg="container.background.destructive"
|
|
1357
|
+
borderRadius="6px"
|
|
1358
|
+
border="1px solid"
|
|
1359
|
+
borderColor="container.border.destructive"
|
|
1360
|
+
>
|
|
1361
|
+
<Text fontWeight="bold">
|
|
1362
|
+
Form submitted {submitCount} time(s)!
|
|
1363
|
+
</Text>
|
|
1364
|
+
<Text fontSize={200}>
|
|
1365
|
+
If you only clicked a menu item, this is a bug.
|
|
1366
|
+
</Text>
|
|
1367
|
+
</Box>
|
|
1368
|
+
)}
|
|
1369
|
+
</Box>
|
|
1370
|
+
|
|
1371
|
+
<ModalFooter
|
|
1372
|
+
cancelButton={<Button>Cancel</Button>}
|
|
1373
|
+
primaryButton={
|
|
1374
|
+
<Button type="submit" appearance="primary">
|
|
1375
|
+
Save
|
|
1376
|
+
</Button>
|
|
1377
|
+
}
|
|
1378
|
+
/>
|
|
1379
|
+
</form>
|
|
1380
|
+
</ModalBody>
|
|
1381
|
+
</Modal>
|
|
1382
|
+
);
|
|
1383
|
+
},
|
|
1384
|
+
};
|
|
1385
|
+
|
|
1386
|
+
export const WithPopout: Story = {
|
|
1387
|
+
render: (args) => {
|
|
1388
|
+
const [selectedItem, setSelectedItem] =
|
|
1389
|
+
useState<TypeSingleSelectMenuProps["selectedItem"]>(null);
|
|
1390
|
+
|
|
1391
|
+
return (
|
|
1392
|
+
<Modal
|
|
1393
|
+
{...args}
|
|
1394
|
+
closeButtonAriaLabel="Close Modal"
|
|
1395
|
+
modalTrigger={<Button appearance="primary">Open Modal</Button>}
|
|
1396
|
+
>
|
|
1397
|
+
<ModalHeader
|
|
1398
|
+
title="Popout Components"
|
|
1399
|
+
subtitle="Various popout and menu components inside a modal"
|
|
1400
|
+
/>
|
|
1401
|
+
<ModalBody>
|
|
1402
|
+
<Box p={400} display="flex" flexDirection="column" gap={400}>
|
|
1403
|
+
<Box>
|
|
1404
|
+
<Text fontSize={200} fontWeight="semibold" mb={200}>
|
|
1405
|
+
Popout V1
|
|
1406
|
+
</Text>
|
|
1407
|
+
<Popout
|
|
1408
|
+
content={
|
|
1409
|
+
<Popout.Content>
|
|
1410
|
+
Look this is some popout content
|
|
1411
|
+
</Popout.Content>
|
|
1412
|
+
}
|
|
1413
|
+
>
|
|
1414
|
+
<Button>Open Popout</Button>
|
|
1415
|
+
</Popout>
|
|
1416
|
+
</Box>
|
|
1417
|
+
<Box>
|
|
1418
|
+
<Text fontSize={200} fontWeight="semibold" mb={200}>
|
|
1419
|
+
Popout V2
|
|
1420
|
+
</Text>
|
|
1421
|
+
<PopoutV2 content="Look this is some popout content">
|
|
1422
|
+
<Button>Open Popout V2</Button>
|
|
1423
|
+
</PopoutV2>
|
|
1424
|
+
</Box>
|
|
1425
|
+
<Box>
|
|
1426
|
+
<Text fontSize={200} fontWeight="semibold" mb={200}>
|
|
1427
|
+
Action Menu
|
|
1428
|
+
</Text>
|
|
1429
|
+
<ActionMenu
|
|
1430
|
+
menuToggleElement={<MenuToggleButton>Actions</MenuToggleButton>}
|
|
1431
|
+
>
|
|
1432
|
+
<MenuContent>
|
|
1433
|
+
<MenuGroup id="actions">
|
|
1434
|
+
<MenuItem id="edit">Edit</MenuItem>
|
|
1435
|
+
<MenuItem id="duplicate">Duplicate</MenuItem>
|
|
1436
|
+
<MenuItem id="delete">Delete</MenuItem>
|
|
1437
|
+
</MenuGroup>
|
|
1438
|
+
</MenuContent>
|
|
1439
|
+
</ActionMenu>
|
|
1440
|
+
</Box>
|
|
1441
|
+
<Box>
|
|
1442
|
+
<Text fontSize={200} fontWeight="semibold" mb={200}>
|
|
1443
|
+
Single Select Menu
|
|
1444
|
+
</Text>
|
|
1445
|
+
<SingleSelectMenu
|
|
1446
|
+
selectedItem={selectedItem}
|
|
1447
|
+
onSelectedItemChange={({ selectedItem: item }) =>
|
|
1448
|
+
setSelectedItem(item)
|
|
1449
|
+
}
|
|
1450
|
+
menuToggleElement={
|
|
1451
|
+
<MenuToggleButton>
|
|
1452
|
+
{selectedItem?.id ?? "Select..."}
|
|
1453
|
+
</MenuToggleButton>
|
|
1454
|
+
}
|
|
1455
|
+
>
|
|
1456
|
+
<MenuContent>
|
|
1457
|
+
<MenuItem id="option-1">Option 1</MenuItem>
|
|
1458
|
+
<MenuItem id="option-2">Option 2</MenuItem>
|
|
1459
|
+
<MenuItem id="option-3">Option 3</MenuItem>
|
|
1460
|
+
</MenuContent>
|
|
1461
|
+
</SingleSelectMenu>
|
|
1462
|
+
</Box>
|
|
1463
|
+
</Box>
|
|
1464
|
+
</ModalBody>
|
|
1465
|
+
<ModalFooter
|
|
1466
|
+
cancelButton={<Button>Cancel</Button>}
|
|
1467
|
+
primaryButton={<Button appearance="primary">Confirm</Button>}
|
|
1468
|
+
/>
|
|
1469
|
+
</Modal>
|
|
1470
|
+
);
|
|
1471
|
+
},
|
|
1472
|
+
};
|
|
1473
|
+
|
|
1474
|
+
export const WithModalV1: Story = {
|
|
1475
|
+
render: () => {
|
|
1476
|
+
const [v1Open, setV1Open] = useState(false);
|
|
1477
|
+
return (
|
|
1478
|
+
<Modal
|
|
1479
|
+
closeButtonAriaLabel="Close Modal V2"
|
|
1480
|
+
modalTrigger={<Button appearance="primary">Open Modal V2</Button>}
|
|
1481
|
+
>
|
|
1482
|
+
<ModalHeader title="Modal V2" subtitle="Open a Modal V1 from inside" />
|
|
1483
|
+
<ModalBody>
|
|
1484
|
+
<Text as="p" mb={400}>
|
|
1485
|
+
Click the button below to open a Modal V1 on top of this Modal V2.
|
|
1486
|
+
The V1 modal should be fully interactive and closing it should not
|
|
1487
|
+
dismiss the V2 modal.
|
|
1488
|
+
</Text>
|
|
1489
|
+
<Button appearance="secondary" onClick={() => setV1Open(true)}>
|
|
1490
|
+
Open Modal V1
|
|
1491
|
+
</Button>
|
|
1492
|
+
<ModalV1
|
|
1493
|
+
isOpen={v1Open}
|
|
1494
|
+
onClose={() => setV1Open(false)}
|
|
1495
|
+
label="Modal V1 on top of V2"
|
|
1496
|
+
closeButtonLabel="Close Modal V1"
|
|
1497
|
+
zIndex={100}
|
|
1498
|
+
>
|
|
1499
|
+
<ModalV1.Header title="Modal V1" />
|
|
1500
|
+
<ModalV1.Content>
|
|
1501
|
+
<Box p={400}>
|
|
1502
|
+
<Text as="p">
|
|
1503
|
+
This Modal V1 is rendered on top of a Modal V2. It should be
|
|
1504
|
+
clickable and closing it should not dismiss the V2 behind it.
|
|
1505
|
+
</Text>
|
|
1506
|
+
</Box>
|
|
1507
|
+
</ModalV1.Content>
|
|
1508
|
+
</ModalV1>
|
|
1509
|
+
</ModalBody>
|
|
1510
|
+
<ModalFooter
|
|
1511
|
+
cancelButton={<Button>Cancel</Button>}
|
|
1512
|
+
primaryButton={<Button appearance="primary">Confirm</Button>}
|
|
1513
|
+
/>
|
|
1514
|
+
</Modal>
|
|
1515
|
+
);
|
|
1516
|
+
},
|
|
1517
|
+
};
|
|
1518
|
+
|
|
1519
|
+
/**
|
|
1520
|
+
* Two Modal V2s stacked — a primary modal that opens a secondary confirmation modal.
|
|
1521
|
+
*
|
|
1522
|
+
* The secondary modal uses zIndex={8} so its overlay (8) and content (9) render
|
|
1523
|
+
* above the primary modal's overlay (6) and content (7). Without this, the second
|
|
1524
|
+
* modal's overlay would sit behind the first modal's content.
|
|
1525
|
+
*/
|
|
1526
|
+
export const NestedModals: Story = {
|
|
1527
|
+
render: () => {
|
|
1528
|
+
const [primaryOpen, setPrimaryOpen] = useState(false);
|
|
1529
|
+
const [secondaryOpen, setSecondaryOpen] = useState(false);
|
|
1530
|
+
const secondaryTriggerRef = React.useRef<HTMLButtonElement>(null);
|
|
1531
|
+
const {
|
|
1532
|
+
triggerRef: primaryTriggerRef,
|
|
1533
|
+
triggerProps: primaryTriggerProps,
|
|
1534
|
+
onCloseAutoFocus: primaryOnCloseAutoFocus,
|
|
1535
|
+
} = useModalExternalTrigger();
|
|
1536
|
+
|
|
1537
|
+
return (
|
|
1538
|
+
<Box>
|
|
1539
|
+
<Button
|
|
1540
|
+
ref={primaryTriggerRef}
|
|
1541
|
+
{...primaryTriggerProps(primaryOpen)}
|
|
1542
|
+
appearance="primary"
|
|
1543
|
+
onClick={() => setPrimaryOpen(true)}
|
|
1544
|
+
>
|
|
1545
|
+
Open Primary Modal
|
|
1546
|
+
</Button>
|
|
1547
|
+
|
|
1548
|
+
{/* Primary modal — default z-index (overlay: 6, content: 7) */}
|
|
1549
|
+
<Modal
|
|
1550
|
+
open={primaryOpen}
|
|
1551
|
+
onOpenChange={setPrimaryOpen}
|
|
1552
|
+
onCloseAutoFocus={primaryOnCloseAutoFocus}
|
|
1553
|
+
aria-label="Primary Modal"
|
|
1554
|
+
closeButtonAriaLabel="Close Primary Modal"
|
|
1555
|
+
>
|
|
1556
|
+
<ModalHeader
|
|
1557
|
+
title="Primary Modal"
|
|
1558
|
+
subtitle="This is the first modal"
|
|
1559
|
+
/>
|
|
1560
|
+
<ModalBody>
|
|
1561
|
+
<Text mb={400}>
|
|
1562
|
+
This is the primary modal. Click the button below to open a
|
|
1563
|
+
secondary confirmation modal on top of this one.
|
|
1564
|
+
</Text>
|
|
1565
|
+
<Button
|
|
1566
|
+
ref={secondaryTriggerRef}
|
|
1567
|
+
aria-haspopup="dialog"
|
|
1568
|
+
aria-expanded={secondaryOpen}
|
|
1569
|
+
appearance="destructive"
|
|
1570
|
+
onClick={() => setSecondaryOpen(true)}
|
|
1571
|
+
>
|
|
1572
|
+
Delete item
|
|
1573
|
+
</Button>
|
|
1574
|
+
</ModalBody>
|
|
1575
|
+
<ModalFooter
|
|
1576
|
+
cancelButton={<Button>Cancel</Button>}
|
|
1577
|
+
primaryButton={<Button appearance="primary">Save</Button>}
|
|
1578
|
+
/>
|
|
1579
|
+
</Modal>
|
|
1580
|
+
|
|
1581
|
+
{/* Secondary modal — elevated z-index (overlay: 8, content: 9) */}
|
|
1582
|
+
<Modal
|
|
1583
|
+
open={secondaryOpen}
|
|
1584
|
+
onOpenChange={setSecondaryOpen}
|
|
1585
|
+
onCloseAutoFocus={(e) => {
|
|
1586
|
+
e.preventDefault();
|
|
1587
|
+
secondaryTriggerRef.current?.focus();
|
|
1588
|
+
}}
|
|
1589
|
+
zIndex={8}
|
|
1590
|
+
aria-label="Confirm Deletion"
|
|
1591
|
+
closeButtonAriaLabel="Close Confirmation"
|
|
1592
|
+
>
|
|
1593
|
+
<ModalHeader
|
|
1594
|
+
title="Are you sure?"
|
|
1595
|
+
subtitle="This action can't be undone."
|
|
1596
|
+
/>
|
|
1597
|
+
<ModalBody>
|
|
1598
|
+
<Text>
|
|
1599
|
+
Deleting this item will remove it permanently. The primary modal
|
|
1600
|
+
behind this one remains open.
|
|
1601
|
+
</Text>
|
|
1602
|
+
</ModalBody>
|
|
1603
|
+
<ModalFooter
|
|
1604
|
+
cancelButton={
|
|
1605
|
+
<Button onClick={() => setSecondaryOpen(false)}>Cancel</Button>
|
|
1606
|
+
}
|
|
1607
|
+
primaryButton={
|
|
1608
|
+
<Button
|
|
1609
|
+
appearance="destructive"
|
|
1610
|
+
onClick={() => {
|
|
1611
|
+
setSecondaryOpen(false);
|
|
1612
|
+
setPrimaryOpen(false);
|
|
1613
|
+
}}
|
|
1614
|
+
>
|
|
1615
|
+
Delete
|
|
1616
|
+
</Button>
|
|
1617
|
+
}
|
|
1618
|
+
/>
|
|
1619
|
+
</Modal>
|
|
1620
|
+
</Box>
|
|
1621
|
+
);
|
|
1622
|
+
},
|
|
1623
|
+
};
|