@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,148 +0,0 @@
|
|
|
1
|
-
import * as React from "react";
|
|
2
|
-
import * as ReactDOM from "react-dom";
|
|
3
|
-
|
|
4
|
-
import {View} from "@khanacademy/wonder-blocks-core";
|
|
5
|
-
|
|
6
|
-
import type {StyleType} from "@khanacademy/wonder-blocks-core";
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* List of elements that can be focused
|
|
10
|
-
* @see https://www.w3.org/TR/html5/editing.html#can-be-focused
|
|
11
|
-
*/
|
|
12
|
-
const FOCUSABLE_ELEMENTS =
|
|
13
|
-
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* This component ensures that focus stays within itself. If the user uses Tab
|
|
17
|
-
* at the end of the modal, or Shift-Tab at the start of the modal, then this
|
|
18
|
-
* component wraps focus to the start/end respectively.
|
|
19
|
-
*
|
|
20
|
-
* We use this in `ModalBackdrop` to ensure that focus stays within the launched
|
|
21
|
-
* modal.
|
|
22
|
-
*
|
|
23
|
-
* Adapted from the WAI-ARIA dialog behavior example.
|
|
24
|
-
* https://www.w3.org/TR/2017/NOTE-wai-aria-practices-1.1-20171214/examples/dialog-modal/dialog.html
|
|
25
|
-
*
|
|
26
|
-
* NOTE(mdr): This component frequently references the "modal" and the "modal
|
|
27
|
-
* root", to aid readability in this package. But this component isn't
|
|
28
|
-
* actually coupled to the modal, and these could be renamed "children"
|
|
29
|
-
* instead if we were to generalize!
|
|
30
|
-
*/
|
|
31
|
-
|
|
32
|
-
type Props = {
|
|
33
|
-
children: React.ReactNode;
|
|
34
|
-
/**
|
|
35
|
-
* Style applied to the View containing children.
|
|
36
|
-
* TODO(kevinb): only allow z-index to be specified. We'll be able to remove
|
|
37
|
-
* this prop once we remove all uses of z-indexes from webapp.
|
|
38
|
-
*/
|
|
39
|
-
style?: StyleType;
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
export default class FocusTrap extends React.Component<Props> {
|
|
43
|
-
/**
|
|
44
|
-
* Tabbing is restricted to descendents of this element.
|
|
45
|
-
*/
|
|
46
|
-
modalRoot: Node | null | undefined;
|
|
47
|
-
|
|
48
|
-
getModalRoot: (node?: any) => void = (node) => {
|
|
49
|
-
if (!node) {
|
|
50
|
-
// The component is being umounted
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const modalRoot = ReactDOM.findDOMNode(node);
|
|
55
|
-
if (!modalRoot) {
|
|
56
|
-
throw new Error(
|
|
57
|
-
"Assertion error: modal root should exist after mount",
|
|
58
|
-
);
|
|
59
|
-
}
|
|
60
|
-
this.modalRoot = modalRoot;
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Try to focus the given node. Return true if successful.
|
|
65
|
-
*/
|
|
66
|
-
tryToFocus(node: Node): boolean | null | undefined {
|
|
67
|
-
if (node instanceof HTMLElement) {
|
|
68
|
-
try {
|
|
69
|
-
node.focus();
|
|
70
|
-
} catch (e: any) {
|
|
71
|
-
// ignore error
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
return document.activeElement === node;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Focus the next available focusable element within the modal root.
|
|
80
|
-
*
|
|
81
|
-
* @param {boolean} isLast Used to determine the next available item. true =
|
|
82
|
-
* First element within the modal, false = Last element within the modal.
|
|
83
|
-
*/
|
|
84
|
-
focusElementIn(isLast: boolean) {
|
|
85
|
-
const modalRootAsHtmlEl = this.modalRoot as HTMLElement;
|
|
86
|
-
// Get the list of available focusable elements within the modal.
|
|
87
|
-
const focusableNodes = Array.from(
|
|
88
|
-
modalRootAsHtmlEl.querySelectorAll(FOCUSABLE_ELEMENTS),
|
|
89
|
-
);
|
|
90
|
-
|
|
91
|
-
const nodeIndex = !isLast ? focusableNodes.length - 1 : 0;
|
|
92
|
-
|
|
93
|
-
const focusableNode = focusableNodes[nodeIndex];
|
|
94
|
-
this.tryToFocus(focusableNode);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Triggered when the focus is set to the first sentinel. This way, the
|
|
99
|
-
* focus will be redirected to the last element inside the modal dialog.
|
|
100
|
-
*/
|
|
101
|
-
handleFocusMoveToLast: () => void = () => {
|
|
102
|
-
this.focusElementIn(false);
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Triggered when the focus is set to the last sentinel. This way, the focus
|
|
107
|
-
* will be redirected to the first element inside the modal dialog.
|
|
108
|
-
*/
|
|
109
|
-
handleFocusMoveToFirst: () => void = () => {
|
|
110
|
-
this.focusElementIn(true);
|
|
111
|
-
};
|
|
112
|
-
|
|
113
|
-
render(): React.ReactNode {
|
|
114
|
-
const {style} = this.props;
|
|
115
|
-
|
|
116
|
-
return (
|
|
117
|
-
<React.Fragment>
|
|
118
|
-
{/* When you press Tab on the last focusable node of the
|
|
119
|
-
* document, some browsers will move your tab focus outside of
|
|
120
|
-
* the document. But we want to capture that as a focus event,
|
|
121
|
-
* and move focus back into the modal! So, we add focusable
|
|
122
|
-
* sentinel nodes. That way, tabbing out of the modal should
|
|
123
|
-
* take you to a sentinel node, rather than taking you out of
|
|
124
|
-
* the document. These sentinels aren't critical to focus
|
|
125
|
-
* wrapping, though; we're resilient to any kind of focus
|
|
126
|
-
* shift, whether it's to the sentinels or somewhere else!
|
|
127
|
-
* We set the sentinels to be position: fixed to make sure
|
|
128
|
-
* they're always in view, this prevents page scrolling when
|
|
129
|
-
* tabbing. */}
|
|
130
|
-
<div
|
|
131
|
-
tabIndex={0}
|
|
132
|
-
className="modal-focus-trap-first"
|
|
133
|
-
onFocus={this.handleFocusMoveToLast}
|
|
134
|
-
style={{position: "fixed"}}
|
|
135
|
-
/>
|
|
136
|
-
<View style={style} ref={this.getModalRoot}>
|
|
137
|
-
{this.props.children}
|
|
138
|
-
</View>
|
|
139
|
-
<div
|
|
140
|
-
tabIndex={0}
|
|
141
|
-
className="modal-focus-trap-last"
|
|
142
|
-
onFocus={this.handleFocusMoveToFirst}
|
|
143
|
-
style={{position: "fixed"}}
|
|
144
|
-
/>
|
|
145
|
-
</React.Fragment>
|
|
146
|
-
);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
@@ -1,172 +0,0 @@
|
|
|
1
|
-
import * as React from "react";
|
|
2
|
-
import * as ReactDOM from "react-dom";
|
|
3
|
-
import {StyleSheet} from "aphrodite";
|
|
4
|
-
|
|
5
|
-
import {color} from "@khanacademy/wonder-blocks-tokens";
|
|
6
|
-
import {View} from "@khanacademy/wonder-blocks-core";
|
|
7
|
-
import {ModalLauncherPortalAttributeName} from "../util/constants";
|
|
8
|
-
|
|
9
|
-
import {findFocusableNodes} from "../util/find-focusable-nodes";
|
|
10
|
-
|
|
11
|
-
import type {ModalElement} from "../util/types";
|
|
12
|
-
|
|
13
|
-
type Props = {
|
|
14
|
-
children: ModalElement;
|
|
15
|
-
onCloseModal: () => unknown;
|
|
16
|
-
/**
|
|
17
|
-
* The selector for the element that will be focused when the dialog shows.
|
|
18
|
-
* When not set, the first tabbable element within the dialog will be used.
|
|
19
|
-
*/
|
|
20
|
-
initialFocusId?: string;
|
|
21
|
-
/**
|
|
22
|
-
* Test ID used for e2e testing.
|
|
23
|
-
*/
|
|
24
|
-
testId?: string;
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* A private component used by ModalLauncher. This is the fixed-position
|
|
29
|
-
* container element that gets mounted outside the DOM. It overlays the modal
|
|
30
|
-
* content (provided as `children`) over the content, with a gray backdrop
|
|
31
|
-
* behind it.
|
|
32
|
-
*
|
|
33
|
-
* This component is also responsible for cloning the provided modal `children`,
|
|
34
|
-
* and adding an `onClose` prop that will call `onCloseModal`. If an
|
|
35
|
-
* `onClose` prop is already provided, the two are merged.
|
|
36
|
-
*/
|
|
37
|
-
export default class ModalBackdrop extends React.Component<Props> {
|
|
38
|
-
componentDidMount() {
|
|
39
|
-
const node: HTMLElement = ReactDOM.findDOMNode(this) as any;
|
|
40
|
-
if (!node) {
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const firstFocusableElement =
|
|
45
|
-
// 1. try to get element specified by the user
|
|
46
|
-
this._getInitialFocusElement(node) ||
|
|
47
|
-
// 2. get first occurence from list of focusable elements
|
|
48
|
-
this._getFirstFocusableElement(node) ||
|
|
49
|
-
// 3. get the dialog itself
|
|
50
|
-
this._getDialogElement(node);
|
|
51
|
-
|
|
52
|
-
// wait for styles to applied
|
|
53
|
-
setTimeout(() => {
|
|
54
|
-
firstFocusableElement.focus();
|
|
55
|
-
}, 0);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
_mousePressedOutside = false;
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Returns an element specified by the user
|
|
62
|
-
*/
|
|
63
|
-
_getInitialFocusElement(node: HTMLElement): HTMLElement | null {
|
|
64
|
-
const {initialFocusId} = this.props;
|
|
65
|
-
|
|
66
|
-
if (!initialFocusId) {
|
|
67
|
-
return null;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
return ReactDOM.findDOMNode(
|
|
71
|
-
node.querySelector(`#${initialFocusId}`),
|
|
72
|
-
) as any;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Returns the first focusable element found inside the Dialog
|
|
77
|
-
*/
|
|
78
|
-
_getFirstFocusableElement(node: HTMLElement): HTMLElement | null {
|
|
79
|
-
// get a collection of elements that can be focused
|
|
80
|
-
const focusableElements = findFocusableNodes(node);
|
|
81
|
-
|
|
82
|
-
if (!focusableElements) {
|
|
83
|
-
return null;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// if found, return the first focusable element
|
|
87
|
-
return focusableElements[0];
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Returns the dialog element
|
|
92
|
-
*/
|
|
93
|
-
_getDialogElement(node: HTMLElement): HTMLElement {
|
|
94
|
-
// If no focusable elements are found,
|
|
95
|
-
// the dialog content element itself will receive focus.
|
|
96
|
-
const dialogElement: HTMLElement = ReactDOM.findDOMNode(
|
|
97
|
-
node.querySelector('[role="dialog"]'),
|
|
98
|
-
) as any;
|
|
99
|
-
// add tabIndex to make the Dialog focusable
|
|
100
|
-
dialogElement.tabIndex = -1;
|
|
101
|
-
|
|
102
|
-
return dialogElement;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* When the user clicks on the gray backdrop area (i.e., the click came
|
|
107
|
-
* _directly_ from the positioner, not bubbled up from its children), close
|
|
108
|
-
* the modal.
|
|
109
|
-
*/
|
|
110
|
-
handleMouseDown: (e: React.SyntheticEvent) => void = (
|
|
111
|
-
e: React.SyntheticEvent,
|
|
112
|
-
) => {
|
|
113
|
-
// Confirm that it is the backdrop that is being clicked, not the child
|
|
114
|
-
this._mousePressedOutside = e.target === e.currentTarget;
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
handleMouseUp: (e: React.SyntheticEvent) => void = (
|
|
118
|
-
e: React.SyntheticEvent,
|
|
119
|
-
) => {
|
|
120
|
-
// Confirm that it is the backdrop that is being clicked, not the child
|
|
121
|
-
// and that the mouse was pressed in the backdrop first.
|
|
122
|
-
if (e.target === e.currentTarget && this._mousePressedOutside) {
|
|
123
|
-
this.props.onCloseModal();
|
|
124
|
-
}
|
|
125
|
-
this._mousePressedOutside = false;
|
|
126
|
-
};
|
|
127
|
-
|
|
128
|
-
render(): React.ReactNode {
|
|
129
|
-
const {children, testId} = this.props;
|
|
130
|
-
const backdropProps = {
|
|
131
|
-
[ModalLauncherPortalAttributeName]: true,
|
|
132
|
-
} as const;
|
|
133
|
-
|
|
134
|
-
return (
|
|
135
|
-
<View
|
|
136
|
-
style={styles.modalPositioner}
|
|
137
|
-
onMouseDown={this.handleMouseDown}
|
|
138
|
-
onMouseUp={this.handleMouseUp}
|
|
139
|
-
testId={testId}
|
|
140
|
-
{...backdropProps}
|
|
141
|
-
>
|
|
142
|
-
{children}
|
|
143
|
-
</View>
|
|
144
|
-
);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const styles = StyleSheet.create({
|
|
149
|
-
modalPositioner: {
|
|
150
|
-
position: "fixed",
|
|
151
|
-
left: 0,
|
|
152
|
-
top: 0,
|
|
153
|
-
|
|
154
|
-
width: "100%",
|
|
155
|
-
height: "100%",
|
|
156
|
-
|
|
157
|
-
alignItems: "center",
|
|
158
|
-
justifyContent: "center",
|
|
159
|
-
|
|
160
|
-
// If the modal ends up being too big for the viewport (e.g., the min
|
|
161
|
-
// height is triggered), add another scrollbar specifically for
|
|
162
|
-
// scrolling modal content.
|
|
163
|
-
//
|
|
164
|
-
// TODO(mdr): The specified behavior is that the modal should scroll
|
|
165
|
-
// with the rest of the page, rather than separately, if overflow
|
|
166
|
-
// turns out to be necessary. That sounds hard to do; punting for
|
|
167
|
-
// now!
|
|
168
|
-
overflow: "auto",
|
|
169
|
-
|
|
170
|
-
background: color.offBlack64,
|
|
171
|
-
},
|
|
172
|
-
});
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import * as React from "react";
|
|
2
|
-
import {View} from "@khanacademy/wonder-blocks-core";
|
|
3
|
-
import {spacing} from "@khanacademy/wonder-blocks-tokens";
|
|
4
|
-
|
|
5
|
-
import type {StyleType} from "@khanacademy/wonder-blocks-core";
|
|
6
|
-
import {
|
|
7
|
-
ThemedStylesFn,
|
|
8
|
-
useScopedTheme,
|
|
9
|
-
useStyles,
|
|
10
|
-
} from "@khanacademy/wonder-blocks-theming";
|
|
11
|
-
import {
|
|
12
|
-
ModalDialogThemeContext,
|
|
13
|
-
ModalDialogThemeContract,
|
|
14
|
-
} from "../themes/themed-modal-dialog";
|
|
15
|
-
|
|
16
|
-
type Props = {
|
|
17
|
-
/** Should the content scroll on overflow, or just expand. */
|
|
18
|
-
scrollOverflow: boolean;
|
|
19
|
-
/** The contents of the ModalContent */
|
|
20
|
-
children: React.ReactNode;
|
|
21
|
-
/** Optional styling to apply to the contents. */
|
|
22
|
-
style?: StyleType;
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* The Modal content included after the header
|
|
27
|
-
*/
|
|
28
|
-
function ModalContent(props: Props) {
|
|
29
|
-
const {scrollOverflow, style, children} = props;
|
|
30
|
-
const {theme} = useScopedTheme(ModalDialogThemeContext);
|
|
31
|
-
const styles = useStyles(themedStylesFn, theme);
|
|
32
|
-
|
|
33
|
-
return (
|
|
34
|
-
<View style={[styles.wrapper, scrollOverflow && styles.scrollOverflow]}>
|
|
35
|
-
<View style={[styles.content, style]}>{children}</View>
|
|
36
|
-
</View>
|
|
37
|
-
);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
ModalContent.__IS_MODAL_CONTENT__ = true;
|
|
41
|
-
|
|
42
|
-
ModalContent.isComponentOf = (instance: any): boolean => {
|
|
43
|
-
return instance && instance.type && instance.type.__IS_MODAL_CONTENT__;
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Media query for small screens.
|
|
48
|
-
* TODO(WB-1655): Change this to use the theme instead (inside themedStylesFn).
|
|
49
|
-
* e.g. `[theme.breakpoints.small]: {...}`
|
|
50
|
-
*/
|
|
51
|
-
const small = "@media (max-width: 767px)";
|
|
52
|
-
|
|
53
|
-
const themedStylesFn: ThemedStylesFn<ModalDialogThemeContract> = (theme) => ({
|
|
54
|
-
wrapper: {
|
|
55
|
-
flex: 1,
|
|
56
|
-
|
|
57
|
-
// This helps to ensure that the paddingBottom is preserved when
|
|
58
|
-
// the contents start to overflow, this goes away on display: flex
|
|
59
|
-
display: "block",
|
|
60
|
-
},
|
|
61
|
-
|
|
62
|
-
scrollOverflow: {
|
|
63
|
-
overflow: "auto",
|
|
64
|
-
},
|
|
65
|
-
|
|
66
|
-
content: {
|
|
67
|
-
flex: 1,
|
|
68
|
-
minHeight: "100%",
|
|
69
|
-
padding: spacing.xLarge_32,
|
|
70
|
-
boxSizing: "border-box",
|
|
71
|
-
[small]: {
|
|
72
|
-
padding: `${spacing.xLarge_32}px ${spacing.medium_16}px`,
|
|
73
|
-
},
|
|
74
|
-
},
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
ModalContent.defaultProps = {
|
|
78
|
-
scrollOverflow: true,
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
export default ModalContent;
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import * as React from "react";
|
|
2
|
-
|
|
3
|
-
type ContextType = {
|
|
4
|
-
closeModal?: () => unknown;
|
|
5
|
-
};
|
|
6
|
-
|
|
7
|
-
const defaultContext: ContextType = {
|
|
8
|
-
closeModal: undefined,
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
const ModalContext = React.createContext<ContextType>(
|
|
12
|
-
defaultContext,
|
|
13
|
-
) as React.Context<ContextType>;
|
|
14
|
-
ModalContext.displayName = "ModalContext";
|
|
15
|
-
|
|
16
|
-
export default ModalContext;
|
|
@@ -1,164 +0,0 @@
|
|
|
1
|
-
import * as React from "react";
|
|
2
|
-
import {View} from "@khanacademy/wonder-blocks-core";
|
|
3
|
-
import type {StyleType} from "@khanacademy/wonder-blocks-core";
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
ThemedStylesFn,
|
|
7
|
-
useScopedTheme,
|
|
8
|
-
useStyles,
|
|
9
|
-
} from "@khanacademy/wonder-blocks-theming";
|
|
10
|
-
import ThemeModalDialog, {
|
|
11
|
-
ModalDialogThemeContext,
|
|
12
|
-
ModalDialogThemeContract,
|
|
13
|
-
} from "../themes/themed-modal-dialog";
|
|
14
|
-
|
|
15
|
-
type Props = {
|
|
16
|
-
/**
|
|
17
|
-
* The dialog content
|
|
18
|
-
*/
|
|
19
|
-
children: React.ReactNode;
|
|
20
|
-
/**
|
|
21
|
-
* When set, provides a component that can render content above the top of the modal;
|
|
22
|
-
* when not set, no additional content is shown above the modal.
|
|
23
|
-
* This prop is passed down to the ModalDialog.
|
|
24
|
-
*/
|
|
25
|
-
above?: React.ReactNode;
|
|
26
|
-
/**
|
|
27
|
-
* When set, provides a component that will render content below the bottom of the modal;
|
|
28
|
-
* when not set, no additional content is shown below the modal.
|
|
29
|
-
* This prop is passed down to the ModalDialog.
|
|
30
|
-
*/
|
|
31
|
-
below?: React.ReactNode;
|
|
32
|
-
/**
|
|
33
|
-
* When set, overrides the default role value. Default role is "dialog"
|
|
34
|
-
* Roles other than dialog and alertdialog aren't appropriate for this
|
|
35
|
-
* component
|
|
36
|
-
*/
|
|
37
|
-
role?: "dialog" | "alertdialog";
|
|
38
|
-
/**
|
|
39
|
-
* Custom styles
|
|
40
|
-
*/
|
|
41
|
-
style?: StyleType;
|
|
42
|
-
/**
|
|
43
|
-
* Test ID used for e2e testing.
|
|
44
|
-
*/
|
|
45
|
-
testId?: string;
|
|
46
|
-
/**
|
|
47
|
-
* The ID of the title labelling this dialog. Required.
|
|
48
|
-
* See WCAG 2.1: 4.1.2 Name, Role, Value
|
|
49
|
-
*/
|
|
50
|
-
"aria-labelledby": string;
|
|
51
|
-
/**
|
|
52
|
-
* The ID of the content describing this dialog, if applicable.
|
|
53
|
-
*/
|
|
54
|
-
"aria-describedby"?: string;
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* `ModalDialog` is a component that contains these elements:
|
|
59
|
-
* - The visual dialog element itself (`<div role="dialog"/>`)
|
|
60
|
-
* - The custom contents below and/or above the Dialog itself (e.g. decorative graphics).
|
|
61
|
-
*
|
|
62
|
-
* **Accessibility notes:**
|
|
63
|
-
* - By default (e.g. using `OnePaneDialog`), `aria-labelledby` is populated automatically using the dialog title `id`.
|
|
64
|
-
* - If there is a custom Dialog implementation (e.g. `TwoPaneDialog`), the dialog element doesn’t have to have
|
|
65
|
-
* the `aria-labelledby` attribute however this is recommended. It should match the `id` of the dialog title.
|
|
66
|
-
*/
|
|
67
|
-
const ModalDialogCore = React.forwardRef(function ModalDialogCore(
|
|
68
|
-
props: Props,
|
|
69
|
-
ref: React.ForwardedRef<HTMLDivElement>,
|
|
70
|
-
) {
|
|
71
|
-
const {
|
|
72
|
-
above,
|
|
73
|
-
below,
|
|
74
|
-
role = "dialog",
|
|
75
|
-
style,
|
|
76
|
-
children,
|
|
77
|
-
testId,
|
|
78
|
-
"aria-labelledby": ariaLabelledBy,
|
|
79
|
-
"aria-describedby": ariaDescribedBy,
|
|
80
|
-
} = props;
|
|
81
|
-
|
|
82
|
-
const {theme} = useScopedTheme(ModalDialogThemeContext);
|
|
83
|
-
const styles = useStyles(themedStylesFn, theme);
|
|
84
|
-
|
|
85
|
-
return (
|
|
86
|
-
<View style={[styles.wrapper, style]}>
|
|
87
|
-
{below && <View style={styles.below}>{below}</View>}
|
|
88
|
-
<View
|
|
89
|
-
role={role}
|
|
90
|
-
aria-modal="true"
|
|
91
|
-
aria-labelledby={ariaLabelledBy}
|
|
92
|
-
aria-describedby={ariaDescribedBy}
|
|
93
|
-
ref={ref}
|
|
94
|
-
style={styles.dialog}
|
|
95
|
-
testId={testId}
|
|
96
|
-
>
|
|
97
|
-
{children}
|
|
98
|
-
</View>
|
|
99
|
-
{above && <View style={styles.above}>{above}</View>}
|
|
100
|
-
</View>
|
|
101
|
-
);
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
const ModalDialog = React.forwardRef(function ModalDialog(
|
|
105
|
-
props: Props,
|
|
106
|
-
ref: React.ForwardedRef<HTMLDivElement>,
|
|
107
|
-
) {
|
|
108
|
-
return (
|
|
109
|
-
<ThemeModalDialog>
|
|
110
|
-
<ModalDialogCore {...props} ref={ref} />
|
|
111
|
-
</ThemeModalDialog>
|
|
112
|
-
);
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
const small = "@media (max-width: 767px)";
|
|
116
|
-
|
|
117
|
-
const themedStylesFn: ThemedStylesFn<ModalDialogThemeContract> = (theme) => ({
|
|
118
|
-
wrapper: {
|
|
119
|
-
display: "flex",
|
|
120
|
-
flexDirection: "row",
|
|
121
|
-
alignItems: "stretch",
|
|
122
|
-
width: "100%",
|
|
123
|
-
height: "100%",
|
|
124
|
-
position: "relative",
|
|
125
|
-
[small]: {
|
|
126
|
-
padding: theme.spacing.dialog.small,
|
|
127
|
-
flexDirection: "column",
|
|
128
|
-
},
|
|
129
|
-
},
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Ensures the dialog container uses the container size
|
|
133
|
-
*/
|
|
134
|
-
dialog: {
|
|
135
|
-
width: "100%",
|
|
136
|
-
height: "100%",
|
|
137
|
-
borderRadius: theme.border.radius,
|
|
138
|
-
overflow: "hidden",
|
|
139
|
-
},
|
|
140
|
-
|
|
141
|
-
above: {
|
|
142
|
-
pointerEvents: "none",
|
|
143
|
-
position: "absolute",
|
|
144
|
-
top: 0,
|
|
145
|
-
left: 0,
|
|
146
|
-
bottom: 0,
|
|
147
|
-
right: 0,
|
|
148
|
-
zIndex: 1,
|
|
149
|
-
},
|
|
150
|
-
|
|
151
|
-
below: {
|
|
152
|
-
pointerEvents: "none",
|
|
153
|
-
position: "absolute",
|
|
154
|
-
top: 0,
|
|
155
|
-
left: 0,
|
|
156
|
-
bottom: 0,
|
|
157
|
-
right: 0,
|
|
158
|
-
zIndex: -1,
|
|
159
|
-
},
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
ModalDialog.displayName = "ModalDialog";
|
|
163
|
-
|
|
164
|
-
export default ModalDialog;
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import * as React from "react";
|
|
2
|
-
import {StyleSheet} from "aphrodite";
|
|
3
|
-
import {View} from "@khanacademy/wonder-blocks-core";
|
|
4
|
-
import {color, spacing} from "@khanacademy/wonder-blocks-tokens";
|
|
5
|
-
|
|
6
|
-
type Props = {
|
|
7
|
-
children: React.ReactNode;
|
|
8
|
-
};
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Modal footer included after the content.
|
|
12
|
-
*
|
|
13
|
-
* **Implementation notes**:
|
|
14
|
-
*
|
|
15
|
-
* If you are creating a custom Dialog, make sure to follow these guidelines:
|
|
16
|
-
* - Make sure to include it as part of [ModalPanel](/#modalpanel) by using the `footer` prop.
|
|
17
|
-
* - The footer is completely flexible. Meaning the developer needs to add its own custom layout to match design specs.
|
|
18
|
-
*
|
|
19
|
-
* **Usage**
|
|
20
|
-
*
|
|
21
|
-
* ```js
|
|
22
|
-
* <ModalFooter>
|
|
23
|
-
* <Button onClick={() => {}}>Submit</Button>
|
|
24
|
-
* </ModalFooter>
|
|
25
|
-
* ```
|
|
26
|
-
*/
|
|
27
|
-
export default function ModalFooter({children}: Props) {
|
|
28
|
-
return <View style={styles.footer}>{children}</View>;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
ModalFooter.__IS_MODAL_FOOTER__ = true;
|
|
32
|
-
|
|
33
|
-
ModalFooter.isComponentOf = (instance: any): boolean => {
|
|
34
|
-
return instance && instance.type && instance.type.__IS_MODAL_FOOTER__;
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
const styles = StyleSheet.create({
|
|
38
|
-
footer: {
|
|
39
|
-
flex: "0 0 auto",
|
|
40
|
-
boxSizing: "border-box",
|
|
41
|
-
minHeight: spacing.xxxLarge_64,
|
|
42
|
-
paddingLeft: spacing.medium_16,
|
|
43
|
-
paddingRight: spacing.medium_16,
|
|
44
|
-
paddingTop: spacing.xSmall_8,
|
|
45
|
-
paddingBottom: spacing.xSmall_8,
|
|
46
|
-
|
|
47
|
-
display: "flex",
|
|
48
|
-
flexDirection: "row",
|
|
49
|
-
alignItems: "center",
|
|
50
|
-
justifyContent: "flex-end",
|
|
51
|
-
|
|
52
|
-
boxShadow: `0px -1px 0px ${color.offBlack16}`,
|
|
53
|
-
},
|
|
54
|
-
});
|