@khanacademy/wonder-blocks-modal 5.1.11 → 5.1.13
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/CHANGELOG.md +22 -0
- package/package.json +10 -10
- package/src/components/__tests__/close-button.test.tsx +0 -37
- package/src/components/__tests__/focus-trap.test.tsx +0 -100
- package/src/components/__tests__/modal-backdrop.test.tsx +0 -241
- package/src/components/__tests__/modal-dialog.test.tsx +0 -87
- package/src/components/__tests__/modal-header.test.tsx +0 -97
- package/src/components/__tests__/modal-launcher.test.tsx +0 -436
- package/src/components/__tests__/modal-panel.test.tsx +0 -42
- package/src/components/__tests__/one-pane-dialog.test.tsx +0 -87
- package/src/components/close-button.tsx +0 -64
- package/src/components/focus-trap.tsx +0 -148
- package/src/components/modal-backdrop.tsx +0 -172
- package/src/components/modal-content.tsx +0 -81
- package/src/components/modal-context.ts +0 -16
- package/src/components/modal-dialog.tsx +0 -164
- package/src/components/modal-footer.tsx +0 -54
- package/src/components/modal-header.tsx +0 -194
- package/src/components/modal-launcher.tsx +0 -297
- package/src/components/modal-panel.tsx +0 -188
- package/src/components/one-pane-dialog.tsx +0 -244
- package/src/components/scroll-disabler.ts +0 -95
- package/src/index.ts +0 -17
- package/src/themes/default.ts +0 -36
- package/src/themes/khanmigo.ts +0 -16
- package/src/themes/themed-modal-dialog.tsx +0 -44
- package/src/util/constants.ts +0 -6
- package/src/util/find-focusable-nodes.ts +0 -12
- package/src/util/maybe-get-portal-mounted-modal-host-element.test.tsx +0 -133
- package/src/util/maybe-get-portal-mounted-modal-host-element.ts +0 -35
- package/src/util/types.ts +0 -13
- package/tsconfig-build.json +0 -20
- package/tsconfig-build.tsbuildinfo +0 -1
|
@@ -1,244 +0,0 @@
|
|
|
1
|
-
import * as React from "react";
|
|
2
|
-
import {StyleSheet} from "aphrodite";
|
|
3
|
-
import {Breadcrumbs} from "@khanacademy/wonder-blocks-breadcrumbs";
|
|
4
|
-
import {MediaLayout} from "@khanacademy/wonder-blocks-layout";
|
|
5
|
-
import type {StyleType} from "@khanacademy/wonder-blocks-core";
|
|
6
|
-
|
|
7
|
-
import {IDProvider} from "@khanacademy/wonder-blocks-core";
|
|
8
|
-
import ModalDialog from "./modal-dialog";
|
|
9
|
-
import ModalPanel from "./modal-panel";
|
|
10
|
-
import ModalHeader from "./modal-header";
|
|
11
|
-
|
|
12
|
-
type Common = {
|
|
13
|
-
/**
|
|
14
|
-
* The content of the modal, appearing between the titlebar and footer.
|
|
15
|
-
*/
|
|
16
|
-
content: React.ReactNode;
|
|
17
|
-
/**
|
|
18
|
-
* The title of the modal, appearing in the titlebar.
|
|
19
|
-
*/
|
|
20
|
-
title: string;
|
|
21
|
-
/**
|
|
22
|
-
* The content of the modal's footer. A great place for buttons!
|
|
23
|
-
*
|
|
24
|
-
* Content is right-aligned by default. To control alignment yourself,
|
|
25
|
-
* provide a container element with 100% width.
|
|
26
|
-
*/
|
|
27
|
-
footer?: React.ReactNode;
|
|
28
|
-
/**
|
|
29
|
-
* Called when the close button is clicked.
|
|
30
|
-
*
|
|
31
|
-
* If you're using `ModalLauncher`, you probably shouldn't use this prop!
|
|
32
|
-
* Instead, to listen for when the modal closes, add an `onClose` handler
|
|
33
|
-
* to the `ModalLauncher`. Doing so will result in a console.warn().
|
|
34
|
-
*/
|
|
35
|
-
onClose?: () => unknown;
|
|
36
|
-
/**
|
|
37
|
-
* When true, the close button is shown; otherwise, the close button is not shown.
|
|
38
|
-
*/
|
|
39
|
-
closeButtonVisible?: boolean;
|
|
40
|
-
/**
|
|
41
|
-
* When set, provides a component that can render content above the top of the modal;
|
|
42
|
-
* when not set, no additional content is shown above the modal.
|
|
43
|
-
* This prop is passed down to the ModalDialog.
|
|
44
|
-
*/
|
|
45
|
-
above?: React.ReactNode;
|
|
46
|
-
/**
|
|
47
|
-
* When set, provides a component that will render content below the bottom of the modal;
|
|
48
|
-
* when not set, no additional content is shown below the modal.
|
|
49
|
-
* This prop is passed down to the ModalDialog.
|
|
50
|
-
*
|
|
51
|
-
* NOTE: Devs can customize this content by rendering the component assigned to this prop with custom styles,
|
|
52
|
-
* such as by wrapping it in a View.
|
|
53
|
-
*/
|
|
54
|
-
below?: React.ReactNode;
|
|
55
|
-
/**
|
|
56
|
-
* When set, overrides the default role value. Default role is "dialog"
|
|
57
|
-
* Roles other than dialog and alertdialog aren't appropriate for this
|
|
58
|
-
* component
|
|
59
|
-
*/
|
|
60
|
-
role?: "dialog" | "alertdialog";
|
|
61
|
-
/**
|
|
62
|
-
* Optional custom styles.
|
|
63
|
-
*/
|
|
64
|
-
style?: StyleType;
|
|
65
|
-
/**
|
|
66
|
-
* Test ID used for e2e testing. This ID will be passed down to the Dialog.
|
|
67
|
-
*/
|
|
68
|
-
testId?: string;
|
|
69
|
-
/**
|
|
70
|
-
* An optional id parameter for the title. If one is
|
|
71
|
-
* not provided, a unique id will be generated.
|
|
72
|
-
*/
|
|
73
|
-
titleId?: string;
|
|
74
|
-
/**
|
|
75
|
-
* The ID of the content describing this dialog, if applicable.
|
|
76
|
-
*/
|
|
77
|
-
"aria-describedby"?: string;
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
type WithSubtitle = Common & {
|
|
81
|
-
/**
|
|
82
|
-
* The subtitle of the modal, appearing in the titlebar, below the title.
|
|
83
|
-
*/
|
|
84
|
-
subtitle: string;
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
type WithBreadcrumbs = Common & {
|
|
88
|
-
/**
|
|
89
|
-
* Adds a breadcrumb-trail, appearing in the ModalHeader, above the title.
|
|
90
|
-
*/
|
|
91
|
-
breadcrumbs: React.ReactElement<React.ComponentProps<typeof Breadcrumbs>>;
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
type Props = Common | WithSubtitle | WithBreadcrumbs;
|
|
95
|
-
|
|
96
|
-
type DefaultProps = {
|
|
97
|
-
closeButtonVisible: Props["closeButtonVisible"];
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* This is the standard layout for most straightforward modal experiences.
|
|
102
|
-
*
|
|
103
|
-
* The ModalHeader is required, but the ModalFooter is optional.
|
|
104
|
-
* The content of the dialog itself is fully customizable, but the
|
|
105
|
-
* left/right/top/bottom padding is fixed.
|
|
106
|
-
*
|
|
107
|
-
* ### Usage
|
|
108
|
-
*
|
|
109
|
-
* ```jsx
|
|
110
|
-
* import {OnePaneDialog} from "@khanacademy/wonder-blocks-modal";
|
|
111
|
-
* import {Body} from "@khanacademy/wonder-blocks-typography";
|
|
112
|
-
*
|
|
113
|
-
* <OnePaneDialog
|
|
114
|
-
* title="Some title"
|
|
115
|
-
* content={
|
|
116
|
-
* <Body>
|
|
117
|
-
* {`Lorem ipsum dolor sit amet, consectetur adipiscing
|
|
118
|
-
* elit, sed do eiusmod tempor incididunt ut labore et
|
|
119
|
-
* dolore magna aliqua. Ut enim ad minim veniam,
|
|
120
|
-
* quis nostrud exercitation ullamco laboris nisi ut
|
|
121
|
-
* aliquip ex ea commodo consequat. Duis aute irure
|
|
122
|
-
* dolor in reprehenderit in voluptate velit esse
|
|
123
|
-
* cillum dolore eu fugiat nulla pariatur. Excepteur
|
|
124
|
-
* sint occaecat cupidatat non proident, sunt in culpa
|
|
125
|
-
* qui officia deserunt mollit anim id est.`}
|
|
126
|
-
* </Body>
|
|
127
|
-
* }
|
|
128
|
-
* />
|
|
129
|
-
* ```
|
|
130
|
-
*/
|
|
131
|
-
export default class OnePaneDialog extends React.Component<Props> {
|
|
132
|
-
static defaultProps: DefaultProps = {
|
|
133
|
-
closeButtonVisible: true,
|
|
134
|
-
};
|
|
135
|
-
|
|
136
|
-
renderHeader(
|
|
137
|
-
uniqueId: string,
|
|
138
|
-
): React.ReactElement<React.ComponentProps<typeof ModalHeader>> {
|
|
139
|
-
const {
|
|
140
|
-
title,
|
|
141
|
-
// @ts-expect-error [FEI-5019] - TS2339 - Property 'breadcrumbs' does not exist on type 'Readonly<Props> & Readonly<{ children?: ReactNode; }>'.
|
|
142
|
-
breadcrumbs = undefined,
|
|
143
|
-
// @ts-expect-error [FEI-5019] - TS2339 - Property 'subtitle' does not exist on type 'Readonly<Props> & Readonly<{ children?: ReactNode; }>'.
|
|
144
|
-
subtitle = undefined,
|
|
145
|
-
testId,
|
|
146
|
-
} = this.props;
|
|
147
|
-
|
|
148
|
-
if (breadcrumbs) {
|
|
149
|
-
return (
|
|
150
|
-
<ModalHeader
|
|
151
|
-
title={title}
|
|
152
|
-
breadcrumbs={
|
|
153
|
-
breadcrumbs as React.ReactElement<
|
|
154
|
-
React.ComponentProps<typeof Breadcrumbs>
|
|
155
|
-
>
|
|
156
|
-
}
|
|
157
|
-
titleId={uniqueId}
|
|
158
|
-
testId={testId && `${testId}-header`}
|
|
159
|
-
/>
|
|
160
|
-
);
|
|
161
|
-
} else if (subtitle) {
|
|
162
|
-
return (
|
|
163
|
-
<ModalHeader
|
|
164
|
-
title={title}
|
|
165
|
-
subtitle={subtitle as string}
|
|
166
|
-
titleId={uniqueId}
|
|
167
|
-
testId={testId && `${testId}-header`}
|
|
168
|
-
/>
|
|
169
|
-
);
|
|
170
|
-
} else {
|
|
171
|
-
return (
|
|
172
|
-
<ModalHeader
|
|
173
|
-
title={title}
|
|
174
|
-
titleId={uniqueId}
|
|
175
|
-
testId={testId && `${testId}-header`}
|
|
176
|
-
/>
|
|
177
|
-
);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
render(): React.ReactNode {
|
|
182
|
-
const {
|
|
183
|
-
onClose,
|
|
184
|
-
footer,
|
|
185
|
-
content,
|
|
186
|
-
above,
|
|
187
|
-
below,
|
|
188
|
-
style,
|
|
189
|
-
closeButtonVisible,
|
|
190
|
-
testId,
|
|
191
|
-
titleId,
|
|
192
|
-
role,
|
|
193
|
-
"aria-describedby": ariaDescribedBy,
|
|
194
|
-
} = this.props;
|
|
195
|
-
|
|
196
|
-
return (
|
|
197
|
-
<MediaLayout styleSheets={styleSheets}>
|
|
198
|
-
{({styles}) => (
|
|
199
|
-
<IDProvider id={titleId} scope="modal">
|
|
200
|
-
{(uniqueId) => (
|
|
201
|
-
<ModalDialog
|
|
202
|
-
style={[styles.dialog, style]}
|
|
203
|
-
above={above}
|
|
204
|
-
below={below}
|
|
205
|
-
testId={testId}
|
|
206
|
-
aria-labelledby={uniqueId}
|
|
207
|
-
aria-describedby={ariaDescribedBy}
|
|
208
|
-
role={role}
|
|
209
|
-
>
|
|
210
|
-
<ModalPanel
|
|
211
|
-
onClose={onClose}
|
|
212
|
-
header={this.renderHeader(uniqueId)}
|
|
213
|
-
content={content}
|
|
214
|
-
footer={footer}
|
|
215
|
-
closeButtonVisible={closeButtonVisible}
|
|
216
|
-
testId={testId}
|
|
217
|
-
/>
|
|
218
|
-
</ModalDialog>
|
|
219
|
-
)}
|
|
220
|
-
</IDProvider>
|
|
221
|
-
)}
|
|
222
|
-
</MediaLayout>
|
|
223
|
-
);
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
const styleSheets = {
|
|
228
|
-
small: StyleSheet.create({
|
|
229
|
-
dialog: {
|
|
230
|
-
width: "100%",
|
|
231
|
-
height: "100%",
|
|
232
|
-
overflow: "hidden",
|
|
233
|
-
},
|
|
234
|
-
}),
|
|
235
|
-
|
|
236
|
-
mdOrLarger: StyleSheet.create({
|
|
237
|
-
dialog: {
|
|
238
|
-
width: "93.75%",
|
|
239
|
-
maxWidth: 576,
|
|
240
|
-
height: "81.25%",
|
|
241
|
-
maxHeight: 624,
|
|
242
|
-
},
|
|
243
|
-
}),
|
|
244
|
-
} as const;
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A UI-less component that lets `ModalLauncher` disable page scroll.
|
|
3
|
-
*
|
|
4
|
-
* The positioning of the modal requires some global page state changed
|
|
5
|
-
* unfortunately, and this handles that in an encapsulated way.
|
|
6
|
-
*
|
|
7
|
-
* NOTE(mdr): This component was copied from webapp. Be wary of sync issues. It
|
|
8
|
-
* also doesn't have unit tests, and we haven't added any, since it's a
|
|
9
|
-
* relatively stable component that has now been stress-tested lots in prod.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import * as React from "react";
|
|
13
|
-
|
|
14
|
-
const needsHackyMobileSafariScrollDisabler = (() => {
|
|
15
|
-
if (typeof window === "undefined") {
|
|
16
|
-
return false;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const userAgent = window.navigator.userAgent;
|
|
20
|
-
return userAgent.indexOf("iPad") > -1 || userAgent.indexOf("iPhone") > -1;
|
|
21
|
-
})();
|
|
22
|
-
|
|
23
|
-
type Props = Record<any, any>;
|
|
24
|
-
|
|
25
|
-
class ScrollDisabler extends React.Component<Props> {
|
|
26
|
-
static oldOverflow: string;
|
|
27
|
-
static oldPosition: string;
|
|
28
|
-
static oldScrollY: number;
|
|
29
|
-
static oldWidth: string;
|
|
30
|
-
static oldTop: string;
|
|
31
|
-
|
|
32
|
-
componentDidMount() {
|
|
33
|
-
if (ScrollDisabler.numModalsOpened === 0) {
|
|
34
|
-
const body = document.body;
|
|
35
|
-
if (!body) {
|
|
36
|
-
throw new Error("couldn't find document.body");
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// Prevent scrolling of the background, the first time a modal is
|
|
40
|
-
// opened.
|
|
41
|
-
ScrollDisabler.oldOverflow = body.style.overflow;
|
|
42
|
-
ScrollDisabler.oldScrollY = window.scrollY;
|
|
43
|
-
|
|
44
|
-
// We need to grab all of the original style properties before we
|
|
45
|
-
// modified any of them.
|
|
46
|
-
if (needsHackyMobileSafariScrollDisabler) {
|
|
47
|
-
ScrollDisabler.oldPosition = body.style.position;
|
|
48
|
-
ScrollDisabler.oldWidth = body.style.width;
|
|
49
|
-
ScrollDisabler.oldTop = body.style.top;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
body.style.overflow = "hidden";
|
|
53
|
-
|
|
54
|
-
// On mobile Safari, overflow: hidden is not enough, position:
|
|
55
|
-
// fixed is also required. Setting style.top = -scollTop maintains
|
|
56
|
-
// the scroll position (without which we'd scroll to the top).
|
|
57
|
-
if (needsHackyMobileSafariScrollDisabler) {
|
|
58
|
-
body.style.position = "fixed";
|
|
59
|
-
body.style.width = "100%";
|
|
60
|
-
body.style.top = `${-ScrollDisabler.oldScrollY}px`;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
ScrollDisabler.numModalsOpened++;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
componentWillUnmount() {
|
|
67
|
-
ScrollDisabler.numModalsOpened--;
|
|
68
|
-
if (ScrollDisabler.numModalsOpened === 0) {
|
|
69
|
-
const body = document.body;
|
|
70
|
-
if (!body) {
|
|
71
|
-
throw new Error("couldn't find document.body");
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Reset all values on the closing of the final modal.
|
|
75
|
-
body.style.overflow = ScrollDisabler.oldOverflow;
|
|
76
|
-
if (needsHackyMobileSafariScrollDisabler) {
|
|
77
|
-
body.style.position = ScrollDisabler.oldPosition;
|
|
78
|
-
body.style.width = ScrollDisabler.oldWidth;
|
|
79
|
-
body.style.top = ScrollDisabler.oldTop;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
if (typeof window !== "undefined" && window.scrollTo) {
|
|
83
|
-
window.scrollTo(0, ScrollDisabler.oldScrollY);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
static numModalsOpened = 0;
|
|
89
|
-
|
|
90
|
-
render(): React.ReactElement | null {
|
|
91
|
-
return null;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
export default ScrollDisabler;
|
package/src/index.ts
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import ModalDialog from "./components/modal-dialog";
|
|
2
|
-
import ModalFooter from "./components/modal-footer";
|
|
3
|
-
import ModalHeader from "./components/modal-header";
|
|
4
|
-
import ModalLauncher from "./components/modal-launcher";
|
|
5
|
-
import ModalPanel from "./components/modal-panel";
|
|
6
|
-
import OnePaneDialog from "./components/one-pane-dialog";
|
|
7
|
-
import maybeGetPortalMountedModalHostElement from "./util/maybe-get-portal-mounted-modal-host-element";
|
|
8
|
-
|
|
9
|
-
export {
|
|
10
|
-
ModalHeader,
|
|
11
|
-
ModalFooter,
|
|
12
|
-
ModalDialog,
|
|
13
|
-
ModalPanel,
|
|
14
|
-
ModalLauncher,
|
|
15
|
-
OnePaneDialog,
|
|
16
|
-
maybeGetPortalMountedModalHostElement,
|
|
17
|
-
};
|
package/src/themes/default.ts
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import * as tokens from "@khanacademy/wonder-blocks-tokens";
|
|
2
|
-
|
|
3
|
-
const theme = {
|
|
4
|
-
color: {
|
|
5
|
-
bg: {
|
|
6
|
-
inverse: tokens.color.darkBlue,
|
|
7
|
-
},
|
|
8
|
-
text: {
|
|
9
|
-
inverse: tokens.color.white,
|
|
10
|
-
secondary: tokens.color.offBlack64,
|
|
11
|
-
},
|
|
12
|
-
shadow: {
|
|
13
|
-
default: tokens.color.offBlack16,
|
|
14
|
-
},
|
|
15
|
-
},
|
|
16
|
-
border: {
|
|
17
|
-
radius: tokens.border.radius.medium_4,
|
|
18
|
-
},
|
|
19
|
-
spacing: {
|
|
20
|
-
dialog: {
|
|
21
|
-
small: tokens.spacing.medium_16,
|
|
22
|
-
},
|
|
23
|
-
panel: {
|
|
24
|
-
closeButton: tokens.spacing.medium_16,
|
|
25
|
-
footer: tokens.spacing.xLarge_32,
|
|
26
|
-
},
|
|
27
|
-
header: {
|
|
28
|
-
xsmall: tokens.spacing.xSmall_8,
|
|
29
|
-
small: tokens.spacing.medium_16,
|
|
30
|
-
medium: tokens.spacing.large_24,
|
|
31
|
-
large: tokens.spacing.xLarge_32,
|
|
32
|
-
},
|
|
33
|
-
},
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
export default theme;
|
package/src/themes/khanmigo.ts
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import {mergeTheme} from "@khanacademy/wonder-blocks-theming";
|
|
2
|
-
import {color} from "@khanacademy/wonder-blocks-tokens";
|
|
3
|
-
import defaultTheme from "./default";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* The overrides for the Khanmigo theme.
|
|
7
|
-
*/
|
|
8
|
-
const theme = mergeTheme(defaultTheme, {
|
|
9
|
-
color: {
|
|
10
|
-
bg: {
|
|
11
|
-
inverse: color.eggplant,
|
|
12
|
-
},
|
|
13
|
-
},
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
export default theme;
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import * as React from "react";
|
|
2
|
-
import {
|
|
3
|
-
createThemeContext,
|
|
4
|
-
Themes,
|
|
5
|
-
ThemeSwitcherContext,
|
|
6
|
-
} from "@khanacademy/wonder-blocks-theming";
|
|
7
|
-
|
|
8
|
-
import defaultTheme from "./default";
|
|
9
|
-
import khanmigoTheme from "./khanmigo";
|
|
10
|
-
|
|
11
|
-
type Props = {
|
|
12
|
-
children: React.ReactNode;
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
export type ModalDialogThemeContract = typeof defaultTheme;
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* The themes available to the ModalDialog component.
|
|
19
|
-
*/
|
|
20
|
-
const themes: Themes<ModalDialogThemeContract> = {
|
|
21
|
-
default: defaultTheme,
|
|
22
|
-
khanmigo: khanmigoTheme,
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* The context that provides the theme to the ModalDialog component.
|
|
27
|
-
* This is generally consumed via the `useScopedTheme` hook.
|
|
28
|
-
*/
|
|
29
|
-
export const ModalDialogThemeContext = createThemeContext(defaultTheme);
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* ThemeModalDialog is a component that provides a theme to the <ModalDialog/>
|
|
33
|
-
* component.
|
|
34
|
-
*/
|
|
35
|
-
export default function ThemeModalDialog(props: Props) {
|
|
36
|
-
const currentTheme = React.useContext(ThemeSwitcherContext);
|
|
37
|
-
|
|
38
|
-
const theme = themes[currentTheme] || defaultTheme;
|
|
39
|
-
return (
|
|
40
|
-
<ModalDialogThemeContext.Provider value={theme}>
|
|
41
|
-
{props.children}
|
|
42
|
-
</ModalDialogThemeContext.Provider>
|
|
43
|
-
);
|
|
44
|
-
}
|
package/src/util/constants.ts
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* List of elements that can be focused
|
|
3
|
-
* @see https://www.w3.org/TR/html5/editing.html#can-be-focused
|
|
4
|
-
*/
|
|
5
|
-
const FOCUSABLE_ELEMENTS =
|
|
6
|
-
'a[href], details, input, textarea, select, button:not([aria-label^="Close"])';
|
|
7
|
-
|
|
8
|
-
export function findFocusableNodes(
|
|
9
|
-
root: HTMLElement | Document,
|
|
10
|
-
): Array<HTMLElement> {
|
|
11
|
-
return Array.from(root.querySelectorAll(FOCUSABLE_ELEMENTS));
|
|
12
|
-
}
|
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
import * as React from "react";
|
|
2
|
-
import * as ReactDOM from "react-dom";
|
|
3
|
-
import {render, screen} from "@testing-library/react";
|
|
4
|
-
import {userEvent} from "@testing-library/user-event";
|
|
5
|
-
|
|
6
|
-
import {ModalLauncherPortalAttributeName} from "./constants";
|
|
7
|
-
import maybeGetPortalMountedModalHostElement from "./maybe-get-portal-mounted-modal-host-element";
|
|
8
|
-
import ModalLauncher from "../components/modal-launcher";
|
|
9
|
-
import OnePaneDialog from "../components/one-pane-dialog";
|
|
10
|
-
|
|
11
|
-
describe("maybeGetPortalMountedModalHostElement", () => {
|
|
12
|
-
test("when candidate is null, returns null", async () => {
|
|
13
|
-
// Arrange
|
|
14
|
-
const candidateElement = null;
|
|
15
|
-
|
|
16
|
-
// Act
|
|
17
|
-
const result = maybeGetPortalMountedModalHostElement(candidateElement);
|
|
18
|
-
|
|
19
|
-
// Assert
|
|
20
|
-
expect(result).toBeFalsy();
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
test("when candidate is not hosted in a modal portal, returns null", async () => {
|
|
24
|
-
// Arrange
|
|
25
|
-
const nodes = (
|
|
26
|
-
<div>
|
|
27
|
-
<div>
|
|
28
|
-
<button>Button</button>
|
|
29
|
-
</div>
|
|
30
|
-
</div>
|
|
31
|
-
);
|
|
32
|
-
render(nodes);
|
|
33
|
-
const candidateElement = await screen.findByRole("button");
|
|
34
|
-
|
|
35
|
-
// Act
|
|
36
|
-
const result = maybeGetPortalMountedModalHostElement(candidateElement);
|
|
37
|
-
|
|
38
|
-
// Assert
|
|
39
|
-
expect(result).toBeFalsy();
|
|
40
|
-
expect(candidateElement).not.toBe(null);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
describe("hosted in a modal", () => {
|
|
44
|
-
test("modal is not mounted in a modal portal, returns null", async () => {
|
|
45
|
-
// Arrange
|
|
46
|
-
const modalContent = (
|
|
47
|
-
<div>
|
|
48
|
-
Fake modal things
|
|
49
|
-
<div>
|
|
50
|
-
<button>Candidate</button>
|
|
51
|
-
</div>
|
|
52
|
-
</div>
|
|
53
|
-
);
|
|
54
|
-
|
|
55
|
-
const modal = (
|
|
56
|
-
<OnePaneDialog
|
|
57
|
-
title="Testing"
|
|
58
|
-
footer="Footer"
|
|
59
|
-
content={modalContent}
|
|
60
|
-
/>
|
|
61
|
-
);
|
|
62
|
-
|
|
63
|
-
render(modal);
|
|
64
|
-
const candidateElement = await screen.findByRole("button", {
|
|
65
|
-
name: "Candidate",
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
// Act
|
|
69
|
-
const result =
|
|
70
|
-
maybeGetPortalMountedModalHostElement(candidateElement);
|
|
71
|
-
|
|
72
|
-
// Assert
|
|
73
|
-
expect(result).toBeFalsy();
|
|
74
|
-
expect(candidateElement).not.toBe(null);
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
test("modal is mounted in a modal portal, returns host", (done: any) => {
|
|
78
|
-
const arrange = async (checkDone: any) => {
|
|
79
|
-
// Arrange
|
|
80
|
-
const modalContent = (
|
|
81
|
-
<div>
|
|
82
|
-
Fake modal things
|
|
83
|
-
<div>
|
|
84
|
-
<span ref={checkDone}>Candidate</span>
|
|
85
|
-
</div>
|
|
86
|
-
</div>
|
|
87
|
-
);
|
|
88
|
-
const modal = (
|
|
89
|
-
<OnePaneDialog
|
|
90
|
-
title="Testing"
|
|
91
|
-
footer="Footer"
|
|
92
|
-
content={modalContent}
|
|
93
|
-
/>
|
|
94
|
-
);
|
|
95
|
-
const launcher = (
|
|
96
|
-
<ModalLauncher modal={modal}>
|
|
97
|
-
{({openModal}: any) => (
|
|
98
|
-
<button onClick={openModal}>Modal</button>
|
|
99
|
-
)}
|
|
100
|
-
</ModalLauncher>
|
|
101
|
-
);
|
|
102
|
-
render(launcher);
|
|
103
|
-
await userEvent.click(await screen.findByRole("button"));
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
const actAndAssert = (node: any) => {
|
|
107
|
-
if (node) {
|
|
108
|
-
// Act
|
|
109
|
-
const candidateElement = ReactDOM.findDOMNode(node);
|
|
110
|
-
const result =
|
|
111
|
-
maybeGetPortalMountedModalHostElement(candidateElement);
|
|
112
|
-
|
|
113
|
-
// Assert
|
|
114
|
-
expect(result).toBeTruthy();
|
|
115
|
-
|
|
116
|
-
const modalPortalElement = result;
|
|
117
|
-
expect(modalPortalElement).not.toBe(null);
|
|
118
|
-
|
|
119
|
-
const isModalPortal =
|
|
120
|
-
modalPortalElement &&
|
|
121
|
-
modalPortalElement.hasAttribute(
|
|
122
|
-
ModalLauncherPortalAttributeName,
|
|
123
|
-
);
|
|
124
|
-
expect(isModalPortal).toBeTruthy();
|
|
125
|
-
|
|
126
|
-
done();
|
|
127
|
-
}
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
arrange(actAndAssert);
|
|
131
|
-
});
|
|
132
|
-
});
|
|
133
|
-
});
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import {ModalLauncherPortalAttributeName} from "./constants";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* From a given element, finds its next ancestor that is a modal launcher portal
|
|
5
|
-
* element.
|
|
6
|
-
* @param {?(Element | Text)} element The element whose ancestors are to be
|
|
7
|
-
* walked.
|
|
8
|
-
* @returns {?Element} The nearest parent modal launcher portal.
|
|
9
|
-
*/
|
|
10
|
-
function maybeGetNextAncestorModalLauncherPortal(
|
|
11
|
-
element?: Element | Text | null,
|
|
12
|
-
) {
|
|
13
|
-
let candidateElement = element && element.parentElement;
|
|
14
|
-
while (
|
|
15
|
-
candidateElement &&
|
|
16
|
-
!candidateElement.hasAttribute(ModalLauncherPortalAttributeName)
|
|
17
|
-
) {
|
|
18
|
-
candidateElement = candidateElement.parentElement;
|
|
19
|
-
}
|
|
20
|
-
return candidateElement;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* From a given element, finds the next modal host that has been mounted in
|
|
25
|
-
* a modal portal.
|
|
26
|
-
* @param {?(Element | Text)} element The element whose ancestors are to be
|
|
27
|
-
* walked.
|
|
28
|
-
* @returns {?Element} The next portal-mounted modal host element.
|
|
29
|
-
* TODO(kevinb): look into getting rid of this
|
|
30
|
-
*/
|
|
31
|
-
export default function maybeGetPortalMountedModalHostElement(
|
|
32
|
-
element?: Element | Text | null,
|
|
33
|
-
): Element | null | undefined {
|
|
34
|
-
return maybeGetNextAncestorModalLauncherPortal(element);
|
|
35
|
-
}
|
package/src/util/types.ts
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import * as React from "react";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* A `ModalElement` is a React element that should either itself be a modal
|
|
5
|
-
* (OnePaneDialog), or wrap a modal.
|
|
6
|
-
*
|
|
7
|
-
* If it's a wrapper component, then its props must be passed along to the child
|
|
8
|
-
* modal, because we clone this element and add new props in order to capture
|
|
9
|
-
* `onClose` events.
|
|
10
|
-
*
|
|
11
|
-
* NOTE(kevinb): we include `| null` here because that's what React.FC<> returns.
|
|
12
|
-
*/
|
|
13
|
-
export type ModalElement = React.ReactElement | null;
|
package/tsconfig-build.json
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"exclude": ["dist"],
|
|
3
|
-
"extends": "../tsconfig-shared.json",
|
|
4
|
-
"compilerOptions": {
|
|
5
|
-
"outDir": "./dist",
|
|
6
|
-
"rootDir": "src",
|
|
7
|
-
},
|
|
8
|
-
"references": [
|
|
9
|
-
{"path": "../wonder-blocks-breadcrumbs/tsconfig-build.json"},
|
|
10
|
-
{"path": "../wonder-blocks-core/tsconfig-build.json"},
|
|
11
|
-
{"path": "../wonder-blocks-form/tsconfig-build.json"},
|
|
12
|
-
{"path": "../wonder-blocks-icon/tsconfig-build.json"},
|
|
13
|
-
{"path": "../wonder-blocks-icon-button/tsconfig-build.json"},
|
|
14
|
-
{"path": "../wonder-blocks-layout/tsconfig-build.json"},
|
|
15
|
-
{"path": "../wonder-blocks-theming/tsconfig-build.json"},
|
|
16
|
-
{"path": "../wonder-blocks-timing/tsconfig-build.json"},
|
|
17
|
-
{"path": "../wonder-blocks-tokens/tsconfig-build.json"},
|
|
18
|
-
{"path": "../wonder-blocks-typography/tsconfig-build.json"},
|
|
19
|
-
]
|
|
20
|
-
}
|