@khanacademy/wonder-blocks-modal 2.3.5 → 2.3.7
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 +19 -0
- package/dist/es/index.js +18 -69
- package/dist/index.js +180 -164
- package/package.json +8 -8
- package/src/__tests__/__snapshots__/generated-snapshot.test.js.snap +208 -208
- package/src/components/__docs__/modal-dialog.stories.js +308 -0
- package/src/components/__docs__/modal-footer.stories.js +337 -0
- package/src/components/__docs__/modal-header.argtypes.js +76 -0
- package/src/components/__docs__/modal-header.stories.js +294 -0
- package/src/components/__docs__/modal-launcher.argtypes.js +78 -0
- package/src/components/__docs__/modal-launcher.stories.js +512 -0
- package/src/components/__docs__/modal-panel.stories.js +414 -0
- package/src/components/__docs__/one-pane-dialog.argtypes.js +102 -0
- package/src/components/__docs__/one-pane-dialog.stories.js +582 -0
- package/src/components/__tests__/focus-trap.test.js +101 -0
- package/src/components/focus-trap.js +47 -98
- package/src/components/modal-footer.js +8 -0
- package/src/components/one-pane-dialog.js +26 -1
- package/src/components/one-pane-dialog.stories.js +0 -248
|
@@ -6,6 +6,13 @@ import {View} from "@khanacademy/wonder-blocks-core";
|
|
|
6
6
|
|
|
7
7
|
import type {StyleType} from "@khanacademy/wonder-blocks-core";
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* List of elements that can be focused
|
|
11
|
+
* @see https://www.w3.org/TR/html5/editing.html#can-be-focused
|
|
12
|
+
*/
|
|
13
|
+
const FOCUSABLE_ELEMENTS =
|
|
14
|
+
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
|
|
15
|
+
|
|
9
16
|
/**
|
|
10
17
|
* This component ensures that focus stays within itself. If the user uses Tab
|
|
11
18
|
* at the end of the modal, or Shift-Tab at the start of the modal, then this
|
|
@@ -35,35 +42,11 @@ type Props = {|
|
|
|
35
42
|
|};
|
|
36
43
|
|
|
37
44
|
export default class FocusTrap extends React.Component<Props> {
|
|
38
|
-
/** The most recent node _inside this component_ to receive focus. */
|
|
39
|
-
lastNodeFocusedInModal: ?Node;
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Whether we're currently applying programmatic focus, and should therefore
|
|
43
|
-
* ignore focus change events.
|
|
44
|
-
*/
|
|
45
|
-
ignoreFocusChanges: boolean;
|
|
46
|
-
|
|
47
45
|
/**
|
|
48
46
|
* Tabbing is restricted to descendents of this element.
|
|
49
47
|
*/
|
|
50
48
|
modalRoot: ?Node;
|
|
51
49
|
|
|
52
|
-
constructor(props: Props) {
|
|
53
|
-
super(props);
|
|
54
|
-
|
|
55
|
-
this.lastNodeFocusedInModal = null;
|
|
56
|
-
this.ignoreFocusChanges = false;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
componentDidMount() {
|
|
60
|
-
window.addEventListener("focus", this.handleGlobalFocus, true);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
componentWillUnmount() {
|
|
64
|
-
window.removeEventListener("focus", this.handleGlobalFocus, true);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
50
|
getModalRoot: (node: any) => void = (node) => {
|
|
68
51
|
if (!node) {
|
|
69
52
|
// The component is being umounted
|
|
@@ -79,98 +62,54 @@ export default class FocusTrap extends React.Component<Props> {
|
|
|
79
62
|
this.modalRoot = modalRoot;
|
|
80
63
|
};
|
|
81
64
|
|
|
82
|
-
/**
|
|
65
|
+
/**
|
|
66
|
+
* Try to focus the given node. Return true if successful.
|
|
67
|
+
*/
|
|
83
68
|
tryToFocus(node: Node): ?boolean {
|
|
84
69
|
if (node instanceof HTMLElement) {
|
|
85
|
-
this.ignoreFocusChanges = true;
|
|
86
70
|
try {
|
|
87
71
|
node.focus();
|
|
88
72
|
} catch (e) {
|
|
89
73
|
// ignore error
|
|
90
74
|
}
|
|
91
|
-
this.ignoreFocusChanges = false;
|
|
92
75
|
|
|
93
76
|
return document.activeElement === node;
|
|
94
77
|
}
|
|
95
78
|
}
|
|
96
79
|
|
|
97
80
|
/**
|
|
98
|
-
* Focus the
|
|
81
|
+
* Focus the next available focusable element within the modal root.
|
|
99
82
|
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
83
|
+
* @param {boolean} isLast Used to determine the next available item. true =
|
|
84
|
+
* First element within the modal, false = Last element within the modal.
|
|
102
85
|
*/
|
|
103
|
-
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
86
|
+
focusElementIn(isLast: boolean) {
|
|
87
|
+
const modalRootAsHtmlEl = ((this.modalRoot: any): HTMLElement);
|
|
88
|
+
// Get the list of available focusable elements within the modal.
|
|
89
|
+
const focusableNodes = Array.from(
|
|
90
|
+
modalRootAsHtmlEl.querySelectorAll(FOCUSABLE_ELEMENTS),
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
const nodeIndex = !isLast ? focusableNodes.length - 1 : 0;
|
|
94
|
+
|
|
95
|
+
const focusableNode = focusableNodes[nodeIndex];
|
|
96
|
+
this.tryToFocus(focusableNode);
|
|
112
97
|
}
|
|
113
98
|
|
|
114
99
|
/**
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
* Return true if we succeed. Or, if the given node has no focusable
|
|
118
|
-
* descendants, return false.
|
|
100
|
+
* Triggered when the focus is set to the first sentinel. This way, the
|
|
101
|
+
* focus will be redirected to the last element inside the modal dialog.
|
|
119
102
|
*/
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
const child = children[i];
|
|
124
|
-
if (this.tryToFocus(child) || this.focusLastElementIn(child)) {
|
|
125
|
-
return true;
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
return false;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/** This method is called when any node on the page is focused. */
|
|
132
|
-
handleGlobalFocus: (e: FocusEvent) => void = (e) => {
|
|
133
|
-
// If we're busy applying our own programmatic focus, we ignore focus
|
|
134
|
-
// changes, to avoid an infinite loop.
|
|
135
|
-
if (this.ignoreFocusChanges) {
|
|
136
|
-
return;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
const target = e.target;
|
|
140
|
-
if (!(target instanceof Node)) {
|
|
141
|
-
// Sometimes focus events trigger on the document itself. Ignore!
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
const modalRoot = this.modalRoot;
|
|
146
|
-
if (!modalRoot) {
|
|
147
|
-
return;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
if (modalRoot.contains(target)) {
|
|
151
|
-
// If the newly focused node is inside the modal, we just keep track
|
|
152
|
-
// of that.
|
|
153
|
-
this.lastNodeFocusedInModal = target;
|
|
154
|
-
} else {
|
|
155
|
-
// If the newly focused node is outside the modal, we try refocusing
|
|
156
|
-
// the first focusable node of the modal. (This could be the user
|
|
157
|
-
// pressing Tab on the last node of the modal, or focus escaping in
|
|
158
|
-
// some other way.)
|
|
159
|
-
this.focusFirstElementIn(modalRoot);
|
|
160
|
-
|
|
161
|
-
// But, if it turns out that the first focusable node of the modal
|
|
162
|
-
// was what we were previously focusing, then this is probably the
|
|
163
|
-
// user pressing Shift-Tab on the first node, wanting to go to the
|
|
164
|
-
// end. So, we instead try focusing the last focusable node of the
|
|
165
|
-
// modal.
|
|
166
|
-
if (document.activeElement === this.lastNodeFocusedInModal) {
|
|
167
|
-
this.focusLastElementIn(modalRoot);
|
|
168
|
-
}
|
|
103
|
+
handleFocusMoveToLast: () => void = () => {
|
|
104
|
+
this.focusElementIn(false);
|
|
105
|
+
};
|
|
169
106
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
107
|
+
/**
|
|
108
|
+
* Triggered when the focus is set to the last sentinel. This way, the focus
|
|
109
|
+
* will be redirected to the first element inside the modal dialog.
|
|
110
|
+
*/
|
|
111
|
+
handleFocusMoveToFirst: () => void = () => {
|
|
112
|
+
this.focusElementIn(true);
|
|
174
113
|
};
|
|
175
114
|
|
|
176
115
|
render(): React.Node {
|
|
@@ -190,11 +129,21 @@ export default class FocusTrap extends React.Component<Props> {
|
|
|
190
129
|
* We set the sentinels to be position: fixed to make sure
|
|
191
130
|
* they're always in view, this prevents page scrolling when
|
|
192
131
|
* tabbing. */}
|
|
193
|
-
<div
|
|
132
|
+
<div
|
|
133
|
+
tabIndex="0"
|
|
134
|
+
className="modal-focus-trap-first"
|
|
135
|
+
onFocus={this.handleFocusMoveToLast}
|
|
136
|
+
style={{position: "fixed"}}
|
|
137
|
+
/>
|
|
194
138
|
<View style={style} ref={this.getModalRoot}>
|
|
195
139
|
{this.props.children}
|
|
196
140
|
</View>
|
|
197
|
-
<div
|
|
141
|
+
<div
|
|
142
|
+
tabIndex="0"
|
|
143
|
+
className="modal-focus-trap-last"
|
|
144
|
+
onFocus={this.handleFocusMoveToFirst}
|
|
145
|
+
style={{position: "fixed"}}
|
|
146
|
+
/>
|
|
198
147
|
</React.Fragment>
|
|
199
148
|
);
|
|
200
149
|
}
|
|
@@ -17,6 +17,14 @@ type Props = {|
|
|
|
17
17
|
* If you are creating a custom Dialog, make sure to follow these guidelines:
|
|
18
18
|
* - Make sure to include it as part of [ModalPanel](/#modalpanel) by using the `footer` prop.
|
|
19
19
|
* - The footer is completely flexible. Meaning the developer needs to add its own custom layout to match design specs.
|
|
20
|
+
*
|
|
21
|
+
* **Usage**
|
|
22
|
+
*
|
|
23
|
+
* ```js
|
|
24
|
+
* <ModalFooter>
|
|
25
|
+
* <Button onClick={() => {}}>Submit</Button>
|
|
26
|
+
* </ModalFooter>
|
|
27
|
+
* ```
|
|
20
28
|
*/
|
|
21
29
|
export default class ModalFooter extends React.Component<Props> {
|
|
22
30
|
static isClassOf(instance: any): boolean {
|
|
@@ -114,7 +114,32 @@ type DefaultProps = {|
|
|
|
114
114
|
* This is the standard layout for most straightforward modal experiences.
|
|
115
115
|
*
|
|
116
116
|
* The ModalHeader is required, but the ModalFooter is optional.
|
|
117
|
-
* The content of the dialog itself is fully customizable, but the
|
|
117
|
+
* The content of the dialog itself is fully customizable, but the
|
|
118
|
+
* left/right/top/bottom padding is fixed.
|
|
119
|
+
*
|
|
120
|
+
* ### Usage
|
|
121
|
+
*
|
|
122
|
+
* ```jsx
|
|
123
|
+
* import {OnePaneDialog} from "@khanacademy/wonder-blocks-modal";
|
|
124
|
+
* import {Body} from "@khanacademy/wonder-blocks-typography";
|
|
125
|
+
*
|
|
126
|
+
* <OnePaneDialog
|
|
127
|
+
* title="Some title"
|
|
128
|
+
* content={
|
|
129
|
+
* <Body>
|
|
130
|
+
* {`Lorem ipsum dolor sit amet, consectetur adipiscing
|
|
131
|
+
* elit, sed do eiusmod tempor incididunt ut labore et
|
|
132
|
+
* dolore magna aliqua. Ut enim ad minim veniam,
|
|
133
|
+
* quis nostrud exercitation ullamco laboris nisi ut
|
|
134
|
+
* aliquip ex ea commodo consequat. Duis aute irure
|
|
135
|
+
* dolor in reprehenderit in voluptate velit esse
|
|
136
|
+
* cillum dolore eu fugiat nulla pariatur. Excepteur
|
|
137
|
+
* sint occaecat cupidatat non proident, sunt in culpa
|
|
138
|
+
* qui officia deserunt mollit anim id est.`}
|
|
139
|
+
* </Body>
|
|
140
|
+
* }
|
|
141
|
+
* />
|
|
142
|
+
* ```
|
|
118
143
|
*/
|
|
119
144
|
export default class OnePaneDialog extends React.Component<Props> {
|
|
120
145
|
static defaultProps: DefaultProps = {
|
|
@@ -1,248 +0,0 @@
|
|
|
1
|
-
/* eslint-disable no-alert */
|
|
2
|
-
// @flow
|
|
3
|
-
import * as React from "react";
|
|
4
|
-
import {StyleSheet} from "aphrodite";
|
|
5
|
-
|
|
6
|
-
import {View} from "@khanacademy/wonder-blocks-core";
|
|
7
|
-
import {Body} from "@khanacademy/wonder-blocks-typography";
|
|
8
|
-
import Button from "@khanacademy/wonder-blocks-button";
|
|
9
|
-
import {ActionMenu, ActionItem} from "@khanacademy/wonder-blocks-dropdown";
|
|
10
|
-
|
|
11
|
-
import type {StoryComponentType} from "@storybook/react";
|
|
12
|
-
import OnePaneDialog from "./one-pane-dialog.js";
|
|
13
|
-
import ModalLauncher from "./modal-launcher.js";
|
|
14
|
-
|
|
15
|
-
import type {ModalElement} from "../util/types.js";
|
|
16
|
-
|
|
17
|
-
const customViewports = {
|
|
18
|
-
phone: {
|
|
19
|
-
name: "phone",
|
|
20
|
-
styles: {
|
|
21
|
-
width: "320px",
|
|
22
|
-
height: "568px",
|
|
23
|
-
},
|
|
24
|
-
},
|
|
25
|
-
tablet: {
|
|
26
|
-
name: "tablet",
|
|
27
|
-
styles: {
|
|
28
|
-
width: "640px",
|
|
29
|
-
height: "960px",
|
|
30
|
-
},
|
|
31
|
-
},
|
|
32
|
-
desktop: {
|
|
33
|
-
name: "desktop",
|
|
34
|
-
styles: {
|
|
35
|
-
width: "1024px",
|
|
36
|
-
height: "768px",
|
|
37
|
-
},
|
|
38
|
-
},
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
export default {
|
|
42
|
-
title: "Floating/Modal/OnePaneDialog",
|
|
43
|
-
parameters: {
|
|
44
|
-
viewport: {
|
|
45
|
-
viewports: customViewports,
|
|
46
|
-
defaultViewport: "desktop",
|
|
47
|
-
},
|
|
48
|
-
chromatic: {
|
|
49
|
-
viewports: [320, 640, 1024],
|
|
50
|
-
},
|
|
51
|
-
},
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
export const Simple: StoryComponentType = () => {
|
|
55
|
-
const modal = (
|
|
56
|
-
<OnePaneDialog
|
|
57
|
-
testId="one-pane-dialog-above"
|
|
58
|
-
title="Hello, world!"
|
|
59
|
-
content={
|
|
60
|
-
<View>
|
|
61
|
-
<Body>
|
|
62
|
-
{
|
|
63
|
-
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est."
|
|
64
|
-
}
|
|
65
|
-
</Body>
|
|
66
|
-
</View>
|
|
67
|
-
}
|
|
68
|
-
closeButtonVisible={true}
|
|
69
|
-
/>
|
|
70
|
-
);
|
|
71
|
-
|
|
72
|
-
return (
|
|
73
|
-
<ModalLauncher
|
|
74
|
-
modal={modal}
|
|
75
|
-
testId="modal-launcher-default-example"
|
|
76
|
-
opened={true}
|
|
77
|
-
onClose={() => alert("This would close the modal.")}
|
|
78
|
-
/>
|
|
79
|
-
);
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
const styles = StyleSheet.create({
|
|
83
|
-
above: {
|
|
84
|
-
background: "url(./modal-above.png)",
|
|
85
|
-
width: 874,
|
|
86
|
-
height: 551,
|
|
87
|
-
position: "absolute",
|
|
88
|
-
top: 40,
|
|
89
|
-
left: -140,
|
|
90
|
-
},
|
|
91
|
-
|
|
92
|
-
below: {
|
|
93
|
-
background: "url(./modal-below.png)",
|
|
94
|
-
width: 868,
|
|
95
|
-
height: 521,
|
|
96
|
-
position: "absolute",
|
|
97
|
-
top: -100,
|
|
98
|
-
left: -300,
|
|
99
|
-
},
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
export const KitchenSink: StoryComponentType = () => {
|
|
103
|
-
const modal = (
|
|
104
|
-
<OnePaneDialog
|
|
105
|
-
title="Single-line title"
|
|
106
|
-
subtitle="Subtitle that provides additional context to the title"
|
|
107
|
-
testId="one-pane-dialog-above"
|
|
108
|
-
content={
|
|
109
|
-
<View>
|
|
110
|
-
<Body>
|
|
111
|
-
{
|
|
112
|
-
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est."
|
|
113
|
-
}
|
|
114
|
-
</Body>
|
|
115
|
-
<br />
|
|
116
|
-
<Body>
|
|
117
|
-
{
|
|
118
|
-
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est."
|
|
119
|
-
}
|
|
120
|
-
</Body>
|
|
121
|
-
<br />
|
|
122
|
-
<Body>
|
|
123
|
-
{
|
|
124
|
-
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est."
|
|
125
|
-
}
|
|
126
|
-
</Body>
|
|
127
|
-
</View>
|
|
128
|
-
}
|
|
129
|
-
footer={<Button onClick={() => {}}>Button (no-op)</Button>}
|
|
130
|
-
closeButtonVisible={true}
|
|
131
|
-
above={<View style={styles.above} />}
|
|
132
|
-
below={<View style={styles.below} />}
|
|
133
|
-
/>
|
|
134
|
-
);
|
|
135
|
-
|
|
136
|
-
return (
|
|
137
|
-
<ModalLauncher
|
|
138
|
-
modal={modal}
|
|
139
|
-
testId="modal-launcher-default-example"
|
|
140
|
-
opened={true}
|
|
141
|
-
onClose={() => alert("This would close the modal.")}
|
|
142
|
-
/>
|
|
143
|
-
);
|
|
144
|
-
};
|
|
145
|
-
|
|
146
|
-
export const WithOpener: StoryComponentType = () => {
|
|
147
|
-
type MyModalProps = {|
|
|
148
|
-
closeModal: () => void,
|
|
149
|
-
|};
|
|
150
|
-
|
|
151
|
-
const MyModal = ({closeModal}: MyModalProps): ModalElement => (
|
|
152
|
-
<OnePaneDialog
|
|
153
|
-
title="Single-line title"
|
|
154
|
-
subtitle="Subtitle that provides additional context to the title"
|
|
155
|
-
testId="one-pane-dialog-above"
|
|
156
|
-
content={
|
|
157
|
-
<View>
|
|
158
|
-
<Body>
|
|
159
|
-
{
|
|
160
|
-
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est."
|
|
161
|
-
}
|
|
162
|
-
</Body>
|
|
163
|
-
<br />
|
|
164
|
-
<Body>
|
|
165
|
-
{
|
|
166
|
-
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est."
|
|
167
|
-
}
|
|
168
|
-
</Body>
|
|
169
|
-
<br />
|
|
170
|
-
<Body>
|
|
171
|
-
{
|
|
172
|
-
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est."
|
|
173
|
-
}
|
|
174
|
-
</Body>
|
|
175
|
-
</View>
|
|
176
|
-
}
|
|
177
|
-
footer={<Button onClick={() => {}}>Button (no-op)</Button>}
|
|
178
|
-
closeButtonVisible={true}
|
|
179
|
-
above={<View style={styles.above} />}
|
|
180
|
-
below={<View style={styles.below} />}
|
|
181
|
-
/>
|
|
182
|
-
);
|
|
183
|
-
|
|
184
|
-
return (
|
|
185
|
-
<ModalLauncher
|
|
186
|
-
modal={MyModal}
|
|
187
|
-
testId="modal-launcher-default-example"
|
|
188
|
-
onClose={() => alert("This would close the modal.")}
|
|
189
|
-
>
|
|
190
|
-
{({openModal}) => <Button onClick={openModal}>Click me</Button>}
|
|
191
|
-
</ModalLauncher>
|
|
192
|
-
);
|
|
193
|
-
};
|
|
194
|
-
|
|
195
|
-
WithOpener.parameters = {
|
|
196
|
-
viewport: {
|
|
197
|
-
defaultViewport: null,
|
|
198
|
-
},
|
|
199
|
-
chromatic: {
|
|
200
|
-
// Don't take screenshots of this story since it would only show a button.
|
|
201
|
-
disableSnapshot: true,
|
|
202
|
-
},
|
|
203
|
-
};
|
|
204
|
-
|
|
205
|
-
export const WithClosedFocusId: StoryComponentType = () => {
|
|
206
|
-
const [opened, setOpened] = React.useState(false);
|
|
207
|
-
|
|
208
|
-
const handleOpen = () => {
|
|
209
|
-
setOpened(true);
|
|
210
|
-
};
|
|
211
|
-
|
|
212
|
-
const handleClose = () => {
|
|
213
|
-
setOpened(false);
|
|
214
|
-
};
|
|
215
|
-
|
|
216
|
-
return (
|
|
217
|
-
<View style={{gap: 20}}>
|
|
218
|
-
<Button>Top of page (should not receive focus)</Button>
|
|
219
|
-
<Button id="button-to-focus-on">Focus here after close</Button>
|
|
220
|
-
<ActionMenu menuText="actions">
|
|
221
|
-
<ActionItem label="Open modal" onClick={() => handleOpen()} />
|
|
222
|
-
</ActionMenu>
|
|
223
|
-
<ModalLauncher
|
|
224
|
-
onClose={() => handleClose()}
|
|
225
|
-
opened={opened}
|
|
226
|
-
closedFocusId="button-to-focus-on"
|
|
227
|
-
modal={({closeModal}) => (
|
|
228
|
-
<OnePaneDialog
|
|
229
|
-
title="Triggered from action menu"
|
|
230
|
-
content={<View>Hello World</View>}
|
|
231
|
-
footer={
|
|
232
|
-
<Button onClick={closeModal}>Close Modal</Button>
|
|
233
|
-
}
|
|
234
|
-
/>
|
|
235
|
-
)}
|
|
236
|
-
/>
|
|
237
|
-
</View>
|
|
238
|
-
);
|
|
239
|
-
};
|
|
240
|
-
|
|
241
|
-
WithClosedFocusId.parameters = {
|
|
242
|
-
chromatic: {
|
|
243
|
-
// Don't take screenshots of this story since the case we want
|
|
244
|
-
// to test doesn't appear on first render - it occurs after
|
|
245
|
-
// we complete a series of steps.
|
|
246
|
-
disableSnapshot: true,
|
|
247
|
-
},
|
|
248
|
-
};
|