@khanacademy/wonder-blocks-modal 4.1.0 → 4.2.1

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.
@@ -1,10 +1,17 @@
1
1
  import * as React from "react";
2
- import {StyleSheet} from "aphrodite";
3
2
  import {View} from "@khanacademy/wonder-blocks-core";
4
- import {MediaLayout} from "@khanacademy/wonder-blocks-layout";
5
3
  import Spacing from "@khanacademy/wonder-blocks-spacing";
6
4
 
7
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";
8
15
 
9
16
  type Props = {
10
17
  /** Should the content scroll on overflow, or just expand. */
@@ -15,68 +22,60 @@ type Props = {
15
22
  style?: StyleType;
16
23
  };
17
24
 
18
- type DefaultProps = {
19
- scrollOverflow: Props["scrollOverflow"];
20
- };
21
-
22
25
  /**
23
26
  * The Modal content included after the header
24
27
  */
25
- export default class ModalContent extends React.Component<Props> {
26
- static isClassOf(instance: any): boolean {
27
- return instance && instance.type && instance.type.__IS_MODAL_CONTENT__;
28
- }
29
- static defaultProps: DefaultProps = {
30
- scrollOverflow: true,
31
- };
28
+ function ModalContent(props: Props) {
29
+ const {scrollOverflow, style, children} = props;
30
+ const {theme} = useScopedTheme(ModalDialogThemeContext);
31
+ const styles = useStyles(themedStylesFn, theme);
32
32
 
33
- static __IS_MODAL_CONTENT__ = true;
33
+ return (
34
+ <View style={[styles.wrapper, scrollOverflow && styles.scrollOverflow]}>
35
+ <View style={[styles.content, style]}>{children}</View>
36
+ </View>
37
+ );
38
+ }
34
39
 
35
- render(): React.ReactNode {
36
- const {scrollOverflow, style, children} = this.props;
40
+ ModalContent.__IS_MODAL_CONTENT__ = true;
37
41
 
38
- return (
39
- <MediaLayout styleSheets={styleSheets}>
40
- {({styles}) => (
41
- <View
42
- style={[
43
- styles.wrapper,
44
- scrollOverflow && styles.scrollOverflow,
45
- ]}
46
- >
47
- <View style={[styles.content, style]}>{children}</View>
48
- </View>
49
- )}
50
- </MediaLayout>
51
- );
52
- }
53
- }
42
+ ModalContent.isComponentOf = (instance: any): boolean => {
43
+ return instance && instance.type && instance.type.__IS_MODAL_CONTENT__;
44
+ };
54
45
 
55
- const styleSheets = {
56
- all: StyleSheet.create({
57
- wrapper: {
58
- flex: 1,
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)";
59
52
 
60
- // This helps to ensure that the paddingBottom is preserved when
61
- // the contents start to overflow, this goes away on display: flex
62
- display: "block",
63
- },
53
+ const themedStylesFn: ThemedStylesFn<ModalDialogThemeContract> = (theme) => ({
54
+ wrapper: {
55
+ flex: 1,
64
56
 
65
- scrollOverflow: {
66
- overflow: "auto",
67
- },
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
+ },
68
61
 
69
- content: {
70
- flex: 1,
71
- minHeight: "100%",
72
- padding: Spacing.xLarge_32,
73
- boxSizing: "border-box",
74
- },
75
- }),
62
+ scrollOverflow: {
63
+ overflow: "auto",
64
+ },
76
65
 
77
- small: StyleSheet.create({
78
- content: {
66
+ content: {
67
+ flex: 1,
68
+ minHeight: "100%",
69
+ padding: Spacing.xLarge_32,
70
+ boxSizing: "border-box",
71
+ [small]: {
79
72
  padding: `${Spacing.xLarge_32}px ${Spacing.medium_16}px`,
80
73
  },
81
- }),
82
- } as const;
74
+ },
75
+ });
76
+
77
+ ModalContent.defaultProps = {
78
+ scrollOverflow: true,
79
+ };
80
+
81
+ export default ModalContent;
@@ -25,18 +25,16 @@ type Props = {
25
25
  * </ModalFooter>
26
26
  * ```
27
27
  */
28
- export default class ModalFooter extends React.Component<Props> {
29
- static isClassOf(instance: any): boolean {
30
- return instance && instance.type && instance.type.__IS_MODAL_FOOTER__;
31
- }
32
- static __IS_MODAL_FOOTER__ = true;
33
-
34
- render(): React.ReactNode {
35
- const {children} = this.props;
36
- return <View style={styles.footer}>{children}</View>;
37
- }
28
+ export default function ModalFooter({children}: Props) {
29
+ return <View style={styles.footer}>{children}</View>;
38
30
  }
39
31
 
32
+ ModalFooter.__IS_MODAL_FOOTER__ = true;
33
+
34
+ ModalFooter.isComponentOf = (instance: any): boolean => {
35
+ return instance && instance.type && instance.type.__IS_MODAL_FOOTER__;
36
+ };
37
+
40
38
  const styles = StyleSheet.create({
41
39
  footer: {
42
40
  flex: "0 0 auto",
@@ -1,11 +1,16 @@
1
1
  import * as React from "react";
2
- import {StyleSheet} from "aphrodite";
3
2
  import {Breadcrumbs} from "@khanacademy/wonder-blocks-breadcrumbs";
4
- import Color from "@khanacademy/wonder-blocks-color";
5
3
  import {View} from "@khanacademy/wonder-blocks-core";
6
- import {MediaLayout} from "@khanacademy/wonder-blocks-layout";
7
- import Spacing from "@khanacademy/wonder-blocks-spacing";
8
4
  import {HeadingMedium, LabelSmall} from "@khanacademy/wonder-blocks-typography";
5
+ import {
6
+ ThemedStylesFn,
7
+ useScopedTheme,
8
+ useStyles,
9
+ } from "@khanacademy/wonder-blocks-theming";
10
+ import {
11
+ ModalDialogThemeContext,
12
+ ModalDialogThemeContract,
13
+ } from "../themes/themed-modal-dialog";
9
14
 
10
15
  type Common = {
11
16
  /**
@@ -51,10 +56,6 @@ type WithBreadcrumbs = Common & {
51
56
 
52
57
  type Props = Common | WithSubtitle | WithBreadcrumbs;
53
58
 
54
- type DefaultProps = {
55
- light: Props["light"];
56
- };
57
-
58
59
  /**
59
60
  * This is a helper component that is never rendered by itself. It is always
60
61
  * pinned to the top of the dialog, is responsive using the same behavior as its
@@ -98,104 +99,96 @@ type DefaultProps = {
98
99
  * />
99
100
  * ```
100
101
  */
101
- export default class ModalHeader extends React.Component<Props> {
102
- static defaultProps: DefaultProps = {
103
- light: true,
104
- };
105
-
106
- render(): React.ReactNode {
107
- const {
108
- // @ts-expect-error [FEI-5019] - TS2339 - Property 'breadcrumbs' does not exist on type 'Readonly<Props> & Readonly<{ children?: ReactNode; }>'.
109
- breadcrumbs = undefined,
110
- light,
111
- // @ts-expect-error [FEI-5019] - TS2339 - Property 'subtitle' does not exist on type 'Readonly<Props> & Readonly<{ children?: ReactNode; }>'.
112
- subtitle = undefined,
113
- testId,
114
- title,
115
- titleId,
116
- } = this.props;
117
-
118
- if (subtitle && breadcrumbs) {
119
- throw new Error(
120
- "'subtitle' and 'breadcrumbs' can't be used together",
121
- );
122
- }
123
-
124
- return (
125
- <MediaLayout styleSheets={styleSheets}>
126
- {({styles}) => (
127
- <View
128
- style={[styles.header, !light && styles.dark]}
129
- testId={testId}
130
- >
131
- {breadcrumbs && (
132
- <View style={styles.breadcrumbs}>
133
- {breadcrumbs}
134
- </View>
135
- )}
136
- <HeadingMedium
137
- style={styles.title}
138
- id={titleId}
139
- testId={testId && `${testId}-title`}
140
- >
141
- {title}
142
- </HeadingMedium>
143
- {subtitle && (
144
- <LabelSmall
145
- style={light && styles.subtitle}
146
- testId={testId && `${testId}-subtitle`}
147
- >
148
- {subtitle}
149
- </LabelSmall>
150
- )}
151
- </View>
152
- )}
153
- </MediaLayout>
154
- );
102
+ export default function ModalHeader(props: Props) {
103
+ const {
104
+ // @ts-expect-error [FEI-5019] - TS2339 - Property 'breadcrumbs' does not exist on type 'Readonly<Props> & Readonly<{ children?: ReactNode; }>'.
105
+ breadcrumbs = undefined,
106
+ light,
107
+ // @ts-expect-error [FEI-5019] - TS2339 - Property 'subtitle' does not exist on type 'Readonly<Props> & Readonly<{ children?: ReactNode; }>'.
108
+ subtitle = undefined,
109
+ testId,
110
+ title,
111
+ titleId,
112
+ } = props;
113
+
114
+ if (subtitle && breadcrumbs) {
115
+ throw new Error("'subtitle' and 'breadcrumbs' can't be used together");
155
116
  }
156
- }
157
-
158
- const styleSheets = {
159
- all: StyleSheet.create({
160
- header: {
161
- boxShadow: `0px 1px 0px ${Color.offBlack16}`,
162
- display: "flex",
163
- flexDirection: "column",
164
- minHeight: 66,
165
- padding: `${Spacing.large_24}px ${Spacing.xLarge_32}px`,
166
- position: "relative",
167
- width: "100%",
168
- },
169
-
170
- dark: {
171
- background: Color.darkBlue,
172
- color: Color.white,
173
- },
174
117
 
175
- breadcrumbs: {
176
- color: Color.offBlack64,
177
- marginBottom: Spacing.xSmall_8,
178
- },
118
+ const {theme} = useScopedTheme(ModalDialogThemeContext);
119
+ const styles = useStyles(themedStylesFn, theme);
120
+
121
+ return (
122
+ <View style={[styles.header, !light && styles.dark]} testId={testId}>
123
+ {breadcrumbs && (
124
+ <View style={styles.breadcrumbs}>{breadcrumbs}</View>
125
+ )}
126
+ <HeadingMedium
127
+ style={styles.title}
128
+ id={titleId}
129
+ testId={testId && `${testId}-title`}
130
+ >
131
+ {title}
132
+ </HeadingMedium>
133
+ {subtitle && (
134
+ <LabelSmall
135
+ style={light && styles.subtitle}
136
+ testId={testId && `${testId}-subtitle`}
137
+ >
138
+ {subtitle}
139
+ </LabelSmall>
140
+ )}
141
+ </View>
142
+ );
143
+ }
179
144
 
180
- title: {
181
- // Prevent title from overlapping the close button
182
- paddingRight: Spacing.medium_16,
145
+ /**
146
+ * Media query for small screens.
147
+ * TODO(WB-1655): Change this to use the theme instead (inside themedStylesFn).
148
+ * e.g. `[theme.breakpoints.small]: {...}`
149
+ */
150
+ const small = "@media (max-width: 767px)";
151
+
152
+ const themedStylesFn: ThemedStylesFn<ModalDialogThemeContract> = (theme) => ({
153
+ header: {
154
+ boxShadow: `0px 1px 0px ${theme.color.shadow.default}`,
155
+ display: "flex",
156
+ flexDirection: "column",
157
+ minHeight: 66,
158
+ padding: `${theme.spacing.header.medium}px ${theme.spacing.header.large}px`,
159
+ position: "relative",
160
+ width: "100%",
161
+
162
+ [small]: {
163
+ paddingLeft: theme.spacing.header.small,
164
+ paddingRight: theme.spacing.header.small,
183
165
  },
184
-
185
- subtitle: {
186
- color: Color.offBlack64,
187
- marginTop: Spacing.xSmall_8,
166
+ },
167
+
168
+ dark: {
169
+ background: theme.color.bg.inverse,
170
+ color: theme.color.text.inverse,
171
+ },
172
+
173
+ breadcrumbs: {
174
+ color: theme.color.text.secondary,
175
+ marginBottom: theme.spacing.header.xsmall,
176
+ },
177
+
178
+ title: {
179
+ // Prevent title from overlapping the close button
180
+ paddingRight: theme.spacing.header.small,
181
+ [small]: {
182
+ paddingRight: theme.spacing.header.large,
188
183
  },
189
- }),
184
+ },
190
185
 
191
- small: StyleSheet.create({
192
- header: {
193
- paddingLeft: Spacing.medium_16,
194
- paddingRight: Spacing.medium_16,
195
- },
186
+ subtitle: {
187
+ color: theme.color.text.secondary,
188
+ marginTop: theme.spacing.header.xsmall,
189
+ },
190
+ });
196
191
 
197
- title: {
198
- paddingRight: Spacing.xLarge_32,
199
- },
200
- }),
201
- } as const;
192
+ ModalHeader.defaultProps = {
193
+ light: true,
194
+ };
@@ -1,14 +1,20 @@
1
1
  import * as React from "react";
2
- import {StyleSheet} from "aphrodite";
3
- import Color from "@khanacademy/wonder-blocks-color";
4
- import {View} from "@khanacademy/wonder-blocks-core";
5
- import Spacing from "@khanacademy/wonder-blocks-spacing";
2
+ import {PropsFor, View} from "@khanacademy/wonder-blocks-core";
6
3
  import type {StyleType} from "@khanacademy/wonder-blocks-core";
7
4
 
5
+ import {
6
+ ThemedStylesFn,
7
+ useScopedTheme,
8
+ useStyles,
9
+ } from "@khanacademy/wonder-blocks-theming";
8
10
  import ModalContent from "./modal-content";
9
11
  import ModalHeader from "./modal-header";
10
12
  import ModalFooter from "./modal-footer";
11
13
  import CloseButton from "./close-button";
14
+ import {
15
+ ModalDialogThemeContext,
16
+ ModalDialogThemeContract,
17
+ } from "../themes/themed-modal-dialog";
12
18
 
13
19
  type Props = {
14
20
  /**
@@ -16,20 +22,16 @@ type Props = {
16
22
  * are positioned around it.
17
23
  */
18
24
  content:
19
- | React.ReactElement<React.ComponentProps<typeof ModalContent>>
25
+ | React.ReactElement<PropsFor<typeof ModalContent>>
20
26
  | React.ReactNode;
21
27
  /**
22
28
  * The modal header to show at the top of the panel.
23
29
  */
24
- header?:
25
- | React.ReactElement<React.ComponentProps<typeof ModalHeader>>
26
- | React.ReactNode;
30
+ header?: React.ReactElement<PropsFor<typeof ModalHeader>> | React.ReactNode;
27
31
  /**
28
32
  * A footer to show beneath the contents.
29
33
  */
30
- footer?:
31
- | React.ReactElement<React.ComponentProps<typeof ModalFooter>>
32
- | React.ReactNode;
34
+ footer?: React.ReactElement<PropsFor<typeof ModalFooter>> | React.ReactNode;
33
35
  /**
34
36
  * When true, the close button is shown; otherwise, the close button is not shown.
35
37
  */
@@ -65,14 +67,8 @@ type Props = {
65
67
  testId?: string;
66
68
  };
67
69
 
68
- type DefaultProps = {
69
- closeButtonVisible: Props["closeButtonVisible"];
70
- scrollOverflow: Props["scrollOverflow"];
71
- light: Props["light"];
72
- };
73
-
74
70
  /**
75
- * ModalPanel is the content container.
71
+ * ModalPanel is the content container.
76
72
  *
77
73
  * **Implementation notes:**
78
74
  *
@@ -91,20 +87,23 @@ type DefaultProps = {
91
87
  * </ModalDialog>
92
88
  * ```
93
89
  */
94
- export default class ModalPanel extends React.Component<Props> {
95
- static defaultProps: DefaultProps = {
96
- closeButtonVisible: true,
97
- scrollOverflow: true,
98
- light: true,
99
- };
100
-
101
- renderMainContent(): React.ReactNode {
102
- const {content, footer, scrollOverflow} = this.props;
90
+ export default function ModalPanel({
91
+ closeButtonVisible = true,
92
+ scrollOverflow = true,
93
+ light = true,
94
+ content,
95
+ footer,
96
+ header,
97
+ onClose,
98
+ style,
99
+ testId,
100
+ }: Props) {
101
+ const {theme} = useScopedTheme(ModalDialogThemeContext);
102
+ const styles = useStyles(themedStylesFn, theme);
103
103
 
104
- const mainContent = ModalContent.isClassOf(content) ? (
105
- (content as React.ReactElement<
106
- React.ComponentProps<typeof ModalContent>
107
- >)
104
+ const renderMainContent = React.useCallback((): React.ReactNode => {
105
+ const mainContent = ModalContent.isComponentOf(content) ? (
106
+ (content as React.ReactElement<PropsFor<typeof ModalContent>>)
108
107
  ) : (
109
108
  <ModalContent>{content}</ModalContent>
110
109
  );
@@ -122,47 +121,41 @@ export default class ModalPanel extends React.Component<Props> {
122
121
  // know about things being positioned around it.
123
122
  style: [!!footer && styles.hasFooter, mainContent.props.style],
124
123
  });
125
- }
124
+ }, [content, footer, scrollOverflow, styles.hasFooter]);
126
125
 
127
- render(): React.ReactNode {
128
- const {
129
- closeButtonVisible,
130
- footer,
131
- header,
132
- light,
133
- onClose,
134
- style,
135
- testId,
136
- } = this.props;
126
+ const mainContent = renderMainContent();
137
127
 
138
- const mainContent = this.renderMainContent();
139
-
140
- return (
141
- <View
142
- style={[styles.wrapper, !light && styles.dark, style]}
143
- testId={testId && `${testId}-panel`}
144
- >
145
- {closeButtonVisible && (
146
- <CloseButton
147
- light={!light}
148
- onClick={onClose}
149
- style={styles.closeButton}
150
- testId={testId && `${testId}-close`}
151
- />
152
- )}
153
- {header}
154
- {mainContent}
155
- {!footer || ModalFooter.isClassOf(footer) ? (
156
- footer
157
- ) : (
158
- <ModalFooter>{footer}</ModalFooter>
159
- )}
160
- </View>
161
- );
162
- }
128
+ return (
129
+ <View
130
+ style={[styles.wrapper, !light && styles.dark, style]}
131
+ testId={testId && `${testId}-panel`}
132
+ >
133
+ {closeButtonVisible && (
134
+ <CloseButton
135
+ light={!light}
136
+ onClick={onClose}
137
+ style={styles.closeButton}
138
+ testId={testId && `${testId}-close`}
139
+ />
140
+ )}
141
+ {header}
142
+ {mainContent}
143
+ {!footer || ModalFooter.isComponentOf(footer) ? (
144
+ footer
145
+ ) : (
146
+ <ModalFooter>{footer}</ModalFooter>
147
+ )}
148
+ </View>
149
+ );
163
150
  }
164
151
 
165
- const styles = StyleSheet.create({
152
+ ModalPanel.defaultProps = {
153
+ closeButtonVisible: true,
154
+ scrollOverflow: true,
155
+ light: true,
156
+ };
157
+
158
+ const themedStylesFn: ThemedStylesFn<ModalDialogThemeContract> = (theme) => ({
166
159
  wrapper: {
167
160
  flex: "1 1 auto",
168
161
  position: "relative",
@@ -177,19 +170,19 @@ const styles = StyleSheet.create({
177
170
 
178
171
  closeButton: {
179
172
  position: "absolute",
180
- right: Spacing.medium_16,
181
- top: Spacing.medium_16,
173
+ right: theme.spacing.panel.closeButton,
174
+ top: theme.spacing.panel.closeButton,
182
175
  // This is to allow the button to be tab-ordered before the modal
183
176
  // content but still be above the header and content.
184
177
  zIndex: 1,
185
178
  },
186
179
 
187
180
  dark: {
188
- background: Color.darkBlue,
189
- color: Color.white,
181
+ background: theme.color.bg.inverse,
182
+ color: theme.color.text.inverse,
190
183
  },
191
184
 
192
185
  hasFooter: {
193
- paddingBottom: Spacing.xLarge_32,
186
+ paddingBottom: theme.spacing.panel.footer,
194
187
  },
195
188
  });
@@ -1,6 +1,18 @@
1
1
  import {tokens} from "@khanacademy/wonder-blocks-theming";
2
2
 
3
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
+ },
4
16
  border: {
5
17
  radius: tokens.border.radius.medium_4,
6
18
  },
@@ -8,6 +20,16 @@ const theme = {
8
20
  dialog: {
9
21
  small: tokens.spacing.medium_16,
10
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
+ },
11
33
  },
12
34
  };
13
35
 
@@ -0,0 +1,15 @@
1
+ import {mergeTheme, tokens} from "@khanacademy/wonder-blocks-theming";
2
+ import defaultTheme from "./default";
3
+
4
+ /**
5
+ * The overrides for the Khanmigo theme.
6
+ */
7
+ const theme = mergeTheme(defaultTheme, {
8
+ color: {
9
+ bg: {
10
+ inverse: tokens.color.eggplant,
11
+ },
12
+ },
13
+ });
14
+
15
+ export default theme;
@@ -6,6 +6,7 @@ import {
6
6
  } from "@khanacademy/wonder-blocks-theming";
7
7
 
8
8
  import defaultTheme from "./default";
9
+ import khanmigoTheme from "./khanmigo";
9
10
 
10
11
  type Props = {
11
12
  children: React.ReactNode;
@@ -18,7 +19,7 @@ export type ModalDialogThemeContract = typeof defaultTheme;
18
19
  */
19
20
  const themes: Themes<ModalDialogThemeContract> = {
20
21
  default: defaultTheme,
21
- // khanmigo: khanmigoTheme,
22
+ khanmigo: khanmigoTheme,
22
23
  };
23
24
 
24
25
  /**