@khanacademy/wonder-blocks-layout 2.2.0 → 2.2.2
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 +16 -0
- package/package.json +3 -3
- package/src/components/__tests__/media-layout-context.test.tsx +0 -167
- package/src/components/__tests__/media-layout.test.tsx +0 -418
- package/src/components/media-layout-context.ts +0 -58
- package/src/components/media-layout.tsx +0 -298
- package/src/components/spring.tsx +0 -27
- package/src/components/strut.tsx +0 -32
- package/src/index.ts +0 -17
- package/src/util/specs.ts +0 -58
- package/src/util/test-util.test.ts +0 -274
- package/src/util/test-util.ts +0 -71
- package/src/util/types.ts +0 -22
- package/src/util/util.test.ts +0 -234
- package/src/util/util.ts +0 -39
- package/tsconfig-build.json +0 -12
- package/tsconfig-build.tsbuildinfo +0 -1
|
@@ -1,298 +0,0 @@
|
|
|
1
|
-
import * as React from "react";
|
|
2
|
-
import type {StyleDeclaration} from "aphrodite";
|
|
3
|
-
import type {StyleType} from "@khanacademy/wonder-blocks-core";
|
|
4
|
-
|
|
5
|
-
import {InitialFallback} from "@khanacademy/wonder-blocks-core";
|
|
6
|
-
import MediaLayoutContext from "./media-layout-context";
|
|
7
|
-
import type {MediaSize, MediaSpec} from "../util/types";
|
|
8
|
-
import type {Context} from "./media-layout-context";
|
|
9
|
-
import {
|
|
10
|
-
MEDIA_DEFAULT_SPEC,
|
|
11
|
-
MEDIA_INTERNAL_SPEC,
|
|
12
|
-
MEDIA_MODAL_SPEC,
|
|
13
|
-
} from "../util/specs";
|
|
14
|
-
|
|
15
|
-
const mediaQueryLists: {
|
|
16
|
-
[key: string]: MediaQueryList;
|
|
17
|
-
} = {};
|
|
18
|
-
|
|
19
|
-
export type MockStyleSheet = Record<string, StyleType>;
|
|
20
|
-
|
|
21
|
-
type Props = {
|
|
22
|
-
/**
|
|
23
|
-
* The contents to display. Alternatively, a function can be specified
|
|
24
|
-
* that takes three arguments and should return some nodes to display.
|
|
25
|
-
*
|
|
26
|
-
* - mediaSize: The current size of the viewport (small/medium/large)
|
|
27
|
-
* - mediaSpec: The current spec being used to manage the selection of
|
|
28
|
-
* the mediaSize.
|
|
29
|
-
* - styles: An Aphrodite stylesheet representing the current
|
|
30
|
-
* stylesheet for this mediaSize (as specified in the
|
|
31
|
-
* styleSheets prop).
|
|
32
|
-
*/
|
|
33
|
-
children: (arg1: {
|
|
34
|
-
mediaSize: MediaSize;
|
|
35
|
-
mediaSpec: MediaSpec;
|
|
36
|
-
styles: MockStyleSheet;
|
|
37
|
-
}) => React.ReactNode;
|
|
38
|
-
/**
|
|
39
|
-
* Aphrodite stylesheets to pass through to the styles prop. The
|
|
40
|
-
* stylesheets to render is based on the media size. "all" is always
|
|
41
|
-
* rendered.
|
|
42
|
-
*/
|
|
43
|
-
styleSheets?: {
|
|
44
|
-
all?: StyleDeclaration;
|
|
45
|
-
mdOrLarger?: StyleDeclaration;
|
|
46
|
-
mdOrSmaller?: StyleDeclaration;
|
|
47
|
-
small?: StyleDeclaration;
|
|
48
|
-
medium?: StyleDeclaration;
|
|
49
|
-
large?: StyleDeclaration;
|
|
50
|
-
};
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
type State = {
|
|
54
|
-
size?: MediaSize;
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
// If for some reason we're not able to resolve the current media size we
|
|
58
|
-
// fall back to this state.
|
|
59
|
-
const DEFAULT_SIZE = "large";
|
|
60
|
-
|
|
61
|
-
type CombinedProps = Props & Context;
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* `MediaLayout` is responsible for changing the rendering of contents at
|
|
65
|
-
* differently sized viewports. `MediaLayoutContext.Provider` can be used
|
|
66
|
-
* to specify different breakpoint configurations. By default it uses
|
|
67
|
-
* `MEDIA_DEFAULT_SPEC`. See media-layout-context.js for additiional options.
|
|
68
|
-
*/
|
|
69
|
-
class MediaLayoutInternal extends React.Component<CombinedProps, State> {
|
|
70
|
-
// A collection of thunks that's used to clean up event listeners
|
|
71
|
-
// when the component is unmounted.
|
|
72
|
-
cleanupThunks: Array<() => void>;
|
|
73
|
-
|
|
74
|
-
constructor(props: CombinedProps) {
|
|
75
|
-
super(props);
|
|
76
|
-
this.state = {
|
|
77
|
-
size: undefined,
|
|
78
|
-
};
|
|
79
|
-
this.cleanupThunks = [];
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
componentDidMount() {
|
|
83
|
-
// TODO(WB-534): handle changes to mediaSpec prop
|
|
84
|
-
const entries: Array<
|
|
85
|
-
[
|
|
86
|
-
MediaSize,
|
|
87
|
-
{
|
|
88
|
-
query: string;
|
|
89
|
-
},
|
|
90
|
-
]
|
|
91
|
-
> = Object.entries(this.props.mediaSpec) as any;
|
|
92
|
-
|
|
93
|
-
for (const [size, spec] of entries) {
|
|
94
|
-
const mql = mediaQueryLists[spec.query];
|
|
95
|
-
// during SSR there are no MediaQueryLists
|
|
96
|
-
if (!mql) {
|
|
97
|
-
continue;
|
|
98
|
-
}
|
|
99
|
-
const listener = (e: MediaQueryListEvent) => {
|
|
100
|
-
if (e.matches) {
|
|
101
|
-
this.setState({size});
|
|
102
|
-
}
|
|
103
|
-
};
|
|
104
|
-
mql.addListener(listener);
|
|
105
|
-
this.cleanupThunks.push(() => mql.removeListener(listener));
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
componentWillUnmount() {
|
|
110
|
-
// Remove our listeners.
|
|
111
|
-
this.cleanupThunks.forEach((cleaup) => cleaup());
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
getCurrentSize(spec: MediaSpec): MediaSize {
|
|
115
|
-
// If we have a state with the current size in it then we always want
|
|
116
|
-
// to use that. This will happen if the viewport changes sizes after
|
|
117
|
-
// we've already initialized.
|
|
118
|
-
if (this.state.size) {
|
|
119
|
-
return this.state.size;
|
|
120
|
-
} else {
|
|
121
|
-
const entries: Array<
|
|
122
|
-
[
|
|
123
|
-
MediaSize,
|
|
124
|
-
{
|
|
125
|
-
query: string;
|
|
126
|
-
},
|
|
127
|
-
]
|
|
128
|
-
> = Object.entries(this.props.mediaSpec) as any;
|
|
129
|
-
|
|
130
|
-
for (const [size, spec] of entries) {
|
|
131
|
-
const mql = mediaQueryLists[spec.query];
|
|
132
|
-
if (mql.matches) {
|
|
133
|
-
return size;
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
return DEFAULT_SIZE;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// We assume that we're running on an unsupported environment) if there is
|
|
142
|
-
// no window object or matchMedia function available.
|
|
143
|
-
isUnsupportedEnvironment() {
|
|
144
|
-
return typeof window === "undefined" || !window.matchMedia;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// Generate a mock Aphrodite StyleSheet based upon the current mediaSize
|
|
148
|
-
// We do this by looking at all of the stylesheets specified in the
|
|
149
|
-
// styleSheets prop and then all of the individual styles. We merge the
|
|
150
|
-
// styles together
|
|
151
|
-
// TODO(WB-533): move to util.js to make it easier to test
|
|
152
|
-
getMockStyleSheet(mediaSize: MediaSize) {
|
|
153
|
-
const {styleSheets} = this.props;
|
|
154
|
-
|
|
155
|
-
const mockStyleSheet: MockStyleSheet = {};
|
|
156
|
-
|
|
157
|
-
// If no stylesheets were specified then we just return an empty object
|
|
158
|
-
if (!styleSheets) {
|
|
159
|
-
return mockStyleSheet;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// Go through all of the stylesheets that were specified
|
|
163
|
-
for (const styleSize of Object.keys(styleSheets)) {
|
|
164
|
-
// @ts-expect-error [FEI-5019] - TS7053 - Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{ all?: StyleDeclaration | undefined; mdOrLarger?: StyleDeclaration | undefined; mdOrSmaller?: StyleDeclaration | undefined; }'.
|
|
165
|
-
const styleSheet = styleSheets[styleSize];
|
|
166
|
-
|
|
167
|
-
if (!styleSheet) {
|
|
168
|
-
continue;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// And then through each key of each stylesheet
|
|
172
|
-
for (const name of Object.keys(styleSheet)) {
|
|
173
|
-
if (
|
|
174
|
-
Object.prototype.hasOwnProperty.call(mockStyleSheet, name)
|
|
175
|
-
) {
|
|
176
|
-
continue;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// We create an entry that combines the values from all of
|
|
180
|
-
// the stylesheets together in least-specific to most-specific
|
|
181
|
-
// priority (thus small/medium/large styles will always have
|
|
182
|
-
// precedence over "all" or mdOrSmaller/mdOrLarger/etc.).
|
|
183
|
-
mockStyleSheet[name] = [
|
|
184
|
-
styleSheets.all && styleSheets.all[name],
|
|
185
|
-
mediaSize === "small" && [
|
|
186
|
-
styleSheets.mdOrSmaller &&
|
|
187
|
-
styleSheets.mdOrSmaller[name],
|
|
188
|
-
styleSheets.small && styleSheets.small[name],
|
|
189
|
-
],
|
|
190
|
-
mediaSize === "medium" && [
|
|
191
|
-
styleSheets.mdOrSmaller &&
|
|
192
|
-
styleSheets.mdOrSmaller[name],
|
|
193
|
-
styleSheets.mdOrLarger && styleSheets.mdOrLarger[name],
|
|
194
|
-
styleSheets.medium && styleSheets.medium[name],
|
|
195
|
-
],
|
|
196
|
-
mediaSize === "large" && [
|
|
197
|
-
styleSheets.mdOrLarger && styleSheets.mdOrLarger[name],
|
|
198
|
-
styleSheets.large && styleSheets.large[name],
|
|
199
|
-
],
|
|
200
|
-
];
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
return mockStyleSheet;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
renderContent(initialRender: boolean): React.ReactNode {
|
|
208
|
-
const {children, mediaSpec, ssrSize, overrideSize} = this.props;
|
|
209
|
-
|
|
210
|
-
const queries = [
|
|
211
|
-
...Object.values(MEDIA_DEFAULT_SPEC).map((spec: any) => spec.query),
|
|
212
|
-
...Object.values(MEDIA_INTERNAL_SPEC).map(
|
|
213
|
-
(spec: any) => spec.query,
|
|
214
|
-
),
|
|
215
|
-
...Object.values(MEDIA_MODAL_SPEC).map((spec: any) => spec.query),
|
|
216
|
-
...Object.values(mediaSpec).map((spec: any) => spec.query),
|
|
217
|
-
];
|
|
218
|
-
|
|
219
|
-
// We need to create the MediaQueryLists during the first render in order
|
|
220
|
-
// to query whether any of them match.
|
|
221
|
-
if (!initialRender) {
|
|
222
|
-
for (const query of queries.filter(
|
|
223
|
-
(query) => !mediaQueryLists[query],
|
|
224
|
-
)) {
|
|
225
|
-
mediaQueryLists[query] = window.matchMedia(query);
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// We need to figure out what the current media size is
|
|
230
|
-
// If an override has been specified, we use that.
|
|
231
|
-
// If we're rendering on the server then we use the default
|
|
232
|
-
// SSR rendering size.
|
|
233
|
-
// Otherwise we attempt to get the current size based on
|
|
234
|
-
// the current MediaSpec.
|
|
235
|
-
const mediaSize =
|
|
236
|
-
overrideSize ||
|
|
237
|
-
(initialRender && ssrSize) ||
|
|
238
|
-
this.getCurrentSize(mediaSpec);
|
|
239
|
-
|
|
240
|
-
// Generate a mock stylesheet
|
|
241
|
-
const styles = this.getMockStyleSheet(mediaSize);
|
|
242
|
-
|
|
243
|
-
return children({mediaSize, mediaSpec, styles});
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
render() {
|
|
247
|
-
return (
|
|
248
|
-
<InitialFallback fallback={() => this.renderContent(true)}>
|
|
249
|
-
{() => this.renderContent(this.isUnsupportedEnvironment())}
|
|
250
|
-
</InitialFallback>
|
|
251
|
-
);
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
/**
|
|
256
|
-
* ***NOTE: The MediaLayout component is being deprecated. Do not use this!!***
|
|
257
|
-
*
|
|
258
|
-
* MediaLayout is a container component that accepts a `styleSheets` object,
|
|
259
|
-
* whose keys are media sizes. It listens for changes to the current media
|
|
260
|
-
* size and passes the current `mediaSize`, `mediaSpec`, and `styles` to
|
|
261
|
-
* `children`, which is a render function taking those three values as an
|
|
262
|
-
* object.
|
|
263
|
-
*
|
|
264
|
-
* Valid keys for the `styleSheets` object are (in order of precedence):
|
|
265
|
-
* - `small`, `medium`, `large`
|
|
266
|
-
* - `mdOrSmaller`, `mdOrLarger`
|
|
267
|
-
* - `all`
|
|
268
|
-
*
|
|
269
|
-
* `MediaLayout` will merge style rules from multiple styles that match the
|
|
270
|
-
* current media query, e.g. `"(min-width: 1024px)"`.
|
|
271
|
-
*
|
|
272
|
-
* The `mediaSpec` is an object with one or more of the following keys:
|
|
273
|
-
* `small`, `medium`, or `large`. Each value contains the following data:
|
|
274
|
-
* - `query: string` e.g. "(min-width: 1024px)"
|
|
275
|
-
* - `totalColumns: number`
|
|
276
|
-
* - `gutterWidth: number`
|
|
277
|
-
* - `marginWidth: number`
|
|
278
|
-
* - `maxWidth: number`
|
|
279
|
-
*/
|
|
280
|
-
export default class MediaLayout extends React.Component<Props> {
|
|
281
|
-
render(): React.ReactNode {
|
|
282
|
-
// We listen to the MediaLayoutContext to see what defaults we're
|
|
283
|
-
// being given (this can be overriden by wrapping this component in
|
|
284
|
-
// a MediaLayoutContext.Consumer).
|
|
285
|
-
return (
|
|
286
|
-
<MediaLayoutContext.Consumer>
|
|
287
|
-
{({overrideSize, ssrSize, mediaSpec}) => (
|
|
288
|
-
<MediaLayoutInternal
|
|
289
|
-
{...this.props}
|
|
290
|
-
overrideSize={overrideSize}
|
|
291
|
-
ssrSize={ssrSize}
|
|
292
|
-
mediaSpec={mediaSpec}
|
|
293
|
-
/>
|
|
294
|
-
)}
|
|
295
|
-
</MediaLayoutContext.Consumer>
|
|
296
|
-
);
|
|
297
|
-
}
|
|
298
|
-
}
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import * as React from "react";
|
|
2
|
-
import {StyleSheet} from "aphrodite";
|
|
3
|
-
|
|
4
|
-
import {View} from "@khanacademy/wonder-blocks-core";
|
|
5
|
-
import type {StyleType} from "@khanacademy/wonder-blocks-core";
|
|
6
|
-
|
|
7
|
-
type Props = {
|
|
8
|
-
style?: StyleType;
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Expands to fill space between sibling components.
|
|
13
|
-
*
|
|
14
|
-
* Assumes parent is a View.
|
|
15
|
-
*/
|
|
16
|
-
export default class Spring extends React.Component<Props> {
|
|
17
|
-
render(): React.ReactNode {
|
|
18
|
-
const {style} = this.props;
|
|
19
|
-
return <View aria-hidden="true" style={[styles.grow, style]} />;
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const styles = StyleSheet.create({
|
|
24
|
-
grow: {
|
|
25
|
-
flexGrow: 1,
|
|
26
|
-
},
|
|
27
|
-
});
|
package/src/components/strut.tsx
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import * as React from "react";
|
|
2
|
-
|
|
3
|
-
import {View} from "@khanacademy/wonder-blocks-core";
|
|
4
|
-
import type {StyleType} from "@khanacademy/wonder-blocks-core";
|
|
5
|
-
|
|
6
|
-
type Props = {
|
|
7
|
-
size: number;
|
|
8
|
-
style?: StyleType;
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* A component for inserting fixed space between components.
|
|
13
|
-
*
|
|
14
|
-
* Assumes parent is a View.
|
|
15
|
-
*/
|
|
16
|
-
export default class Strut extends React.Component<Props> {
|
|
17
|
-
render(): React.ReactNode {
|
|
18
|
-
const {size, style} = this.props;
|
|
19
|
-
return <View aria-hidden="true" style={[strutStyle(size), style]} />;
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const strutStyle = (size: number) => {
|
|
24
|
-
return {
|
|
25
|
-
width: size,
|
|
26
|
-
MsFlexBasis: size,
|
|
27
|
-
MsFlexPreferredSize: size,
|
|
28
|
-
WebkitFlexBasis: size,
|
|
29
|
-
flexBasis: size,
|
|
30
|
-
flexShrink: 0,
|
|
31
|
-
};
|
|
32
|
-
};
|
package/src/index.ts
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import type {MediaQuery, MediaSize, MediaSpec} from "./util/types";
|
|
2
|
-
import type {Context} from "./components/media-layout-context";
|
|
3
|
-
import type {MockStyleSheet} from "./components/media-layout";
|
|
4
|
-
|
|
5
|
-
export {default as MediaLayout} from "./components/media-layout";
|
|
6
|
-
export {default as MediaLayoutContext} from "./components/media-layout-context";
|
|
7
|
-
export {default as Spring} from "./components/spring";
|
|
8
|
-
export {default as Strut} from "./components/strut";
|
|
9
|
-
export * from "./util/specs";
|
|
10
|
-
export {queryMatchesSize} from "./util/util";
|
|
11
|
-
export type {
|
|
12
|
-
MediaQuery,
|
|
13
|
-
MediaSize,
|
|
14
|
-
MediaSpec,
|
|
15
|
-
MockStyleSheet,
|
|
16
|
-
Context as MediaLayoutContextValue,
|
|
17
|
-
};
|
package/src/util/specs.ts
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import {spacing} from "@khanacademy/wonder-blocks-tokens";
|
|
2
|
-
|
|
3
|
-
import type {MediaSize, MediaSpec} from "./types";
|
|
4
|
-
|
|
5
|
-
// All possible valid media sizes
|
|
6
|
-
export const VALID_MEDIA_SIZES: Array<MediaSize> = ["small", "medium", "large"];
|
|
7
|
-
|
|
8
|
-
const mediaDefaultSpecLargeMarginWidth = spacing.large_24;
|
|
9
|
-
|
|
10
|
-
// The default spec for media layout, currently available in
|
|
11
|
-
// three different settings (roughly mobile, tablet, and desktop).
|
|
12
|
-
export const MEDIA_DEFAULT_SPEC: MediaSpec = {
|
|
13
|
-
small: {
|
|
14
|
-
query: "(max-width: 767px)",
|
|
15
|
-
totalColumns: 4,
|
|
16
|
-
gutterWidth: spacing.medium_16,
|
|
17
|
-
marginWidth: spacing.medium_16,
|
|
18
|
-
},
|
|
19
|
-
medium: {
|
|
20
|
-
query: "(min-width: 768px) and (max-width: 1023px)",
|
|
21
|
-
totalColumns: 8,
|
|
22
|
-
gutterWidth: spacing.xLarge_32,
|
|
23
|
-
marginWidth: spacing.large_24,
|
|
24
|
-
},
|
|
25
|
-
large: {
|
|
26
|
-
query: "(min-width: 1024px)",
|
|
27
|
-
totalColumns: 12,
|
|
28
|
-
gutterWidth: spacing.xLarge_32,
|
|
29
|
-
marginWidth: mediaDefaultSpecLargeMarginWidth,
|
|
30
|
-
maxWidth: 1120 + mediaDefaultSpecLargeMarginWidth * 2,
|
|
31
|
-
},
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
// Used for internal tools
|
|
35
|
-
export const MEDIA_INTERNAL_SPEC: MediaSpec = {
|
|
36
|
-
large: {
|
|
37
|
-
query: "(min-width: 1px)",
|
|
38
|
-
totalColumns: 12,
|
|
39
|
-
gutterWidth: spacing.xLarge_32,
|
|
40
|
-
marginWidth: spacing.medium_16,
|
|
41
|
-
},
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
// The default used for modals
|
|
45
|
-
export const MEDIA_MODAL_SPEC: MediaSpec = {
|
|
46
|
-
small: {
|
|
47
|
-
query: "(max-width: 767px)",
|
|
48
|
-
totalColumns: 4,
|
|
49
|
-
gutterWidth: spacing.medium_16,
|
|
50
|
-
marginWidth: spacing.medium_16,
|
|
51
|
-
},
|
|
52
|
-
large: {
|
|
53
|
-
query: "(min-width: 768px)",
|
|
54
|
-
totalColumns: 12,
|
|
55
|
-
gutterWidth: spacing.xLarge_32,
|
|
56
|
-
marginWidth: spacing.xxLarge_48,
|
|
57
|
-
},
|
|
58
|
-
};
|