@khanacademy/wonder-blocks-grid 1.0.24

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2018 Khan Academy
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,276 @@
1
+ import { Component, createElement, Children } from 'react';
2
+ import { MediaLayout, queryMatchesSize, Strut } from '@khanacademy/wonder-blocks-layout';
3
+ import { View } from '@khanacademy/wonder-blocks-core';
4
+ import { StyleSheet } from 'aphrodite';
5
+
6
+ const WIDE_SCREEN = "@media (min-width: 1168px)";
7
+ const styles = StyleSheet.create({
8
+ row: {
9
+ flexDirection: "row",
10
+ alignItems: "center",
11
+ width: "100%"
12
+ },
13
+ rowMaxWidth: {
14
+ [WIDE_SCREEN]: {
15
+ margin: "0 auto"
16
+ }
17
+ },
18
+ cellGrow: {
19
+ flexGrow: 1
20
+ },
21
+ cellFixed: {
22
+ flexShrink: 0
23
+ }
24
+ });
25
+
26
+ const flexBasis = size => {
27
+ return {
28
+ MsFlexBasis: size,
29
+ MsFlexPreferredSize: size,
30
+ WebkitFlexBasis: size,
31
+ flexBasis: size
32
+ };
33
+ };
34
+
35
+ /**
36
+ * A Cell is a container whose width is set based on the width of the
37
+ * specified columns at the current grid size. You will specify the number
38
+ * of columns that you want this component to span at each grid size.
39
+ * This component should only be used as a child of a [Row](#row).
40
+ *
41
+ * This component renders a [View](#view) that
42
+ * uses Flex Box and has a [flex-basis](https://developer.mozilla.org/en-US/docs/Web/CSS/flex-basis)
43
+ * of the specified "width" and [flex-shrink](https://developer.mozilla.org/en-US/docs/Web/CSS/flex-shrink)
44
+ * of 0.
45
+ *
46
+ * By default (with no properties specified) it will display at all
47
+ * grid sizes. If you specify the `smallCols`, `mediumCols`, `largeCols`, or
48
+ * `cols` props then the component will only be shown at those grid sizes and
49
+ * using the specified column width.
50
+ */
51
+ class Cell extends Component {
52
+ static isClassOf(instance) {
53
+ return instance && instance.type && instance.type.__IS_CELL__;
54
+ }
55
+
56
+ static getCols(props, mediaSize) {
57
+ // If no option was specified then we just return undefined,
58
+ // components may handle this case differently.
59
+ // We go through all the ways in which a fixed width can be
60
+ // specified and find the one that matches our current grid size.
61
+ if (!props.smallCols && !props.mediumCols && !props.largeCols && !props.cols) {
62
+ return undefined;
63
+ } else if (props.smallCols && mediaSize === "small") {
64
+ return props.smallCols;
65
+ } else if (props.mediumCols && mediaSize === "medium") {
66
+ return props.mediumCols;
67
+ } else if (props.largeCols && mediaSize === "large") {
68
+ return props.largeCols;
69
+ } else if (typeof props.cols === "function") {
70
+ return props.cols(mediaSize);
71
+ } else if (props.cols) {
72
+ return props.cols;
73
+ } // If nothing applies then we return null (usually resulting
74
+ // in the component not being rendered)
75
+
76
+
77
+ return null;
78
+ } // HACK(kevinb): we use a stack method here because we can't get a ref to
79
+ // each cell in a row without cloning them all.
80
+
81
+
82
+ static shouldDisplay(props, mediaSize) {
83
+ const cols = Cell.getCols(props, mediaSize);
84
+ return cols !== null && cols !== 0;
85
+ }
86
+
87
+ render() {
88
+ const {
89
+ children,
90
+ style
91
+ } = this.props;
92
+ return /*#__PURE__*/createElement(MediaLayout, null, ({
93
+ mediaSize,
94
+ mediaSpec
95
+ }) => {
96
+ const spec = mediaSpec[mediaSize];
97
+
98
+ if (!spec) {
99
+ throw new Error(`mediaSpec.${mediaSize} is undefined`);
100
+ } // Get the settings for this particular size of grid
101
+
102
+
103
+ const {
104
+ totalColumns,
105
+ gutterWidth,
106
+ marginWidth
107
+ } = spec;
108
+ const cols = Cell.getCols(this.props, mediaSize); // If no columns are specified then we just don't render this cell
109
+
110
+ if (cols === undefined || cols === null || cols === 0) {
111
+ return null;
112
+ }
113
+
114
+ if (cols > totalColumns) {
115
+ throw new Error(`Specified columns ${cols} is greater than the maximum ` + `${totalColumns} at the ${mediaSize} grid size.`);
116
+ } // We need to start by calculating the total width of all the "content"
117
+ // We do this by starting with the full width (100%) and then
118
+ // subtracting all of the gutter spaces inbetween the cells
119
+ // (gutterWidth * (totalColumns - 1)) and the width of the two margins
120
+ // (marginWidth * 2).
121
+
122
+
123
+ const contentWidth = `(100% - ${gutterWidth * (totalColumns - 1)}px - ${marginWidth * 2}px)`; // Now that we have the full width we can calculate the width of this
124
+ // particular cell by multiplying the full width (allCellWidth) by
125
+ // the ratio of this cell (cols / totalColumns). But we then need to
126
+ // add back in the missing gutter widths:
127
+ // (gutterWidth * (cols - 1)). This gives us to full width of
128
+ // this particular cell.
129
+
130
+ const calcWidth = `calc(${contentWidth} * ${cols / totalColumns} + ${gutterWidth * (cols - 1)}px)`; // If the contents are a function then we call it with the mediaSize,
131
+ // totalColumns, and cols properties and render the return value.
132
+
133
+ const contents = typeof children === "function" ? children({
134
+ mediaSize,
135
+ totalColumns,
136
+ cols
137
+ }) : children; // Render a fixed-width cell (flex-basis: size, flex-shrink: 0)
138
+ // that matches the intended width of the cell
139
+
140
+ return /*#__PURE__*/createElement(View, {
141
+ style: [styles.cellFixed, flexBasis(calcWidth), style]
142
+ }, contents);
143
+ });
144
+ }
145
+
146
+ }
147
+ Cell.defaultProps = {
148
+ smallCols: 0,
149
+ mediumCols: 0,
150
+ largeCols: 0,
151
+ cols: 0
152
+ };
153
+ Cell.__IS_CELL__ = true;
154
+
155
+ /**
156
+ * Gutter is a component whose width is set based on the size of grid currently
157
+ * being displayed. Used for spacing out cells from each other. The gutter
158
+ * itself doesn't hold any content, it just spaces it out.
159
+ *
160
+ * Gutters are inserted automatically inside of a [Row](#row) in-between Cells.
161
+ * You may only need to use Gutters if you're manually building your own
162
+ * sub-grid, or some-such (this should be relatively rare).
163
+ *
164
+ * By default (with no properties specified) it will display at all
165
+ * grid sizes. If you specify the `small`, `medium`, or `large`
166
+ * props then the component will only be shown at those grid sizes.
167
+ */
168
+ class Gutter extends Component {
169
+ render() {
170
+ return /*#__PURE__*/createElement(MediaLayout, null, ({
171
+ mediaSize,
172
+ mediaSpec
173
+ }) => {
174
+ const spec = mediaSpec[mediaSize];
175
+
176
+ if (!spec) {
177
+ throw new Error(`mediaSpec.${mediaSize} is undefined`);
178
+ }
179
+
180
+ const {
181
+ gutterWidth
182
+ } = spec;
183
+
184
+ if (!queryMatchesSize(this.props.mediaQuery, mediaSize)) {
185
+ return null;
186
+ }
187
+
188
+ return /*#__PURE__*/createElement(Strut, {
189
+ size: gutterWidth
190
+ });
191
+ });
192
+ }
193
+
194
+ }
195
+ Gutter.defaultProps = {
196
+ mediaQuery: "all"
197
+ };
198
+
199
+ /**
200
+ * A Row holds all of the Cells that make up the contents of the grid. A row
201
+ * also provides the margins on the sides and inserts the gutter spacing
202
+ * in-between the cells. Typically this component will hold a [Cell](#cell),
203
+ * but it can also include any elements that could fit in a
204
+ * [flexbox](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Flexible_Box_Layout/Basic_Concepts_of_Flexbox).
205
+ *
206
+ * This component will automatically attempt to insert [Gutters](#gutter)
207
+ * in-between all child elements. Additionally, it'll perform some basic checks
208
+ * to ensure that no impossible layouts are accidentally generated.
209
+ *
210
+ * Typically this component will be used as a child of a [Grid](#grid-1),
211
+ * but it's not a requirement, you can use it as a descendant, as well.
212
+ *
213
+ * By default (with no properties specified) it will display at all
214
+ * grid sizes. If you specify the `small`, `medium`, or `large`
215
+ * props then the component will only be shown at those grid sizes.
216
+ */
217
+ class Row extends Component {
218
+ render() {
219
+ const {
220
+ style,
221
+ children
222
+ } = this.props;
223
+ return /*#__PURE__*/createElement(MediaLayout, null, ({
224
+ mediaSize,
225
+ mediaSpec
226
+ }) => {
227
+ const spec = mediaSpec[mediaSize];
228
+
229
+ if (!spec) {
230
+ throw new Error(`mediaSpec.${mediaSize} is undefined`);
231
+ }
232
+
233
+ const {
234
+ marginWidth,
235
+ maxWidth,
236
+ totalColumns
237
+ } = spec;
238
+ const shouldDisplay = queryMatchesSize(this.props.mediaQuery, mediaSize); // Don't render the row if it's been disabled at this size
239
+
240
+ if (!shouldDisplay) {
241
+ return null;
242
+ } // If the contents are a function then we call it with the mediaSize and
243
+ // totalColumns properties and render the return value.
244
+
245
+
246
+ const contents = typeof children === "function" ? children({
247
+ mediaSize,
248
+ totalColumns
249
+ }) : children;
250
+ const filteredContents = Children.toArray(contents).filter( // Flow doesn't let us check .type on a non-null React.Node so
251
+ // we have to cast it to any.
252
+ item => Cell.isClassOf(item) ? Cell.shouldDisplay(item.props, mediaSize) : true) // Intersperse Gutter elements between the cells.
253
+ .reduce((acc, elem, index) => [].concat(acc, [elem, /*#__PURE__*/createElement(Gutter, {
254
+ key: `gutter-${index}`
255
+ })]), []) // We only want gutters between each cell in the row. The reduce
256
+ // adds a gutter after every cell so we need to remove the last
257
+ // element which is an unnecessary gutteer.
258
+ .slice(0, -1);
259
+ return /*#__PURE__*/createElement(View, {
260
+ style: [styles.row, !!maxWidth && styles.rowMaxWidth, !!maxWidth && {
261
+ maxWidth
262
+ }, style]
263
+ }, /*#__PURE__*/createElement(Strut, {
264
+ size: marginWidth
265
+ }), filteredContents, /*#__PURE__*/createElement(Strut, {
266
+ size: marginWidth
267
+ }));
268
+ });
269
+ }
270
+
271
+ }
272
+ Row.defaultProps = {
273
+ mediaQuery: "all"
274
+ };
275
+
276
+ export { Cell, Row };