@khanacademy/wonder-blocks-core 4.2.0 → 4.3.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.
- package/CHANGELOG.md +10 -0
- package/dist/es/index.js +38 -284
- package/dist/index.js +40 -8
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/generated-snapshot.test.js.snap +1 -1
- package/src/hooks/__tests__/use-force-update.test.js +54 -0
- package/src/hooks/use-force-update.js +23 -0
- package/src/index.js +1 -0
- package/src/util/add-style.md +1 -1
package/CHANGELOG.md
CHANGED
package/dist/es/index.js
CHANGED
|
@@ -29,24 +29,17 @@ function processStyleList(style) {
|
|
|
29
29
|
style: {},
|
|
30
30
|
className: ""
|
|
31
31
|
};
|
|
32
|
-
}
|
|
33
|
-
|
|
32
|
+
}
|
|
34
33
|
|
|
35
34
|
const shouldInlineStyles = typeof global !== "undefined" && global.SNAPSHOT_INLINE_APHRODITE;
|
|
36
35
|
flatten(style).forEach(child => {
|
|
37
|
-
// Check for aphrodite internal property
|
|
38
36
|
const _definition = child._definition;
|
|
39
37
|
|
|
40
38
|
if (_definition != null) {
|
|
41
39
|
if (shouldInlineStyles) {
|
|
42
|
-
const def = {};
|
|
43
|
-
// It doesn't accept kebab-case in media queries and instead
|
|
44
|
-
// prefers camelCase.
|
|
40
|
+
const def = {};
|
|
45
41
|
|
|
46
42
|
for (const [key, value] of Object.entries(_definition)) {
|
|
47
|
-
// This regex converts all instances of -{lowercaseLetter}
|
|
48
|
-
// to the uppercase version of that letter, without the
|
|
49
|
-
// leading dash.
|
|
50
43
|
def[key.replace(/-[a-z]/g, match => match[1].toUpperCase())] = value;
|
|
51
44
|
}
|
|
52
45
|
|
|
@@ -58,11 +51,7 @@ function processStyleList(style) {
|
|
|
58
51
|
inlineStyles.push(child);
|
|
59
52
|
}
|
|
60
53
|
});
|
|
61
|
-
const inlineStylesObject = Object.assign.apply(Object, [{}].concat(inlineStyles));
|
|
62
|
-
// all the styles, remove this <ADD JIRA ISSUE HERE IF THIS PASSES REVIEW>
|
|
63
|
-
// If we're not snapshotting styles, let's create a class for the inline
|
|
64
|
-
// styles so that they can apply to the element even with aphrodite's
|
|
65
|
-
// use of !important.
|
|
54
|
+
const inlineStylesObject = Object.assign.apply(Object, [{}].concat(inlineStyles));
|
|
66
55
|
|
|
67
56
|
if (inlineStyles.length > 0 && !shouldInlineStyles) {
|
|
68
57
|
const inlineStylesStyleSheet = StyleSheet.create({
|
|
@@ -81,32 +70,14 @@ const _excluded$2 = ["children", "style", "tag", "testId"];
|
|
|
81
70
|
const isHeaderRegex = /^h[1-6]$/;
|
|
82
71
|
const styles$1 = StyleSheet.create({
|
|
83
72
|
text: {
|
|
84
|
-
// Disable subpixel antialiasing on Mac desktop for consistency of
|
|
85
|
-
// rendering with mobile and Sketch (neither of which support it).
|
|
86
|
-
// See https://bjango.com/articles/subpixeltext/ for more details.
|
|
87
73
|
WebkitFontSmoothing: "antialiased",
|
|
88
74
|
MozOsxFontSmoothing: "grayscale"
|
|
89
75
|
},
|
|
90
76
|
header: {
|
|
91
|
-
// User agent stylesheets add vertical margins to header tags by
|
|
92
|
-
// default. We prefer to be more deliberate in our spacing instead.
|
|
93
77
|
marginTop: 0,
|
|
94
78
|
marginBottom: 0
|
|
95
79
|
}
|
|
96
80
|
});
|
|
97
|
-
/**
|
|
98
|
-
* Text is a building block for constructing other components. `Text` roughly
|
|
99
|
-
* maps to `span`. You can override which tag is used to render the component
|
|
100
|
-
* (for semantic purposes) by specifying the `tag` prop.
|
|
101
|
-
*
|
|
102
|
-
* These components can take styles (via the `style` prop) in a variety of
|
|
103
|
-
* manners:
|
|
104
|
-
*
|
|
105
|
-
* - An inline style object
|
|
106
|
-
* - An `aphrodite` StyleSheet style
|
|
107
|
-
* - An array combining the above
|
|
108
|
-
*/
|
|
109
|
-
|
|
110
81
|
class Text extends React.Component {
|
|
111
82
|
render() {
|
|
112
83
|
const _this$props = this.props,
|
|
@@ -120,7 +91,7 @@ class Text extends React.Component {
|
|
|
120
91
|
|
|
121
92
|
const isHeader = isHeaderRegex.test(Tag);
|
|
122
93
|
const styleAttributes = processStyleList([styles$1.text, isHeader && styles$1.header, style]);
|
|
123
|
-
return
|
|
94
|
+
return React.createElement(Tag, _extends({}, otherProps, {
|
|
124
95
|
style: styleAttributes.style,
|
|
125
96
|
className: styleAttributes.className,
|
|
126
97
|
"data-test-id": testId
|
|
@@ -133,16 +104,13 @@ Text.defaultProps = {
|
|
|
133
104
|
};
|
|
134
105
|
|
|
135
106
|
const _excluded$1 = ["className", "style"];
|
|
136
|
-
// TODO(kevinb): have an a version which uses exact object types
|
|
137
107
|
function addStyle(Component, defaultStyle) {
|
|
138
108
|
function StyleComponent(props) {
|
|
139
109
|
const {
|
|
140
110
|
className,
|
|
141
111
|
style
|
|
142
112
|
} = props,
|
|
143
|
-
tmpOtherProps = _objectWithoutPropertiesLoose(props, _excluded$1);
|
|
144
|
-
// value to ensure that they're typed properly.
|
|
145
|
-
|
|
113
|
+
tmpOtherProps = _objectWithoutPropertiesLoose(props, _excluded$1);
|
|
146
114
|
|
|
147
115
|
const otherProps = tmpOtherProps;
|
|
148
116
|
const reset = typeof Component === "string" ? overrides[Component] : null;
|
|
@@ -150,7 +118,7 @@ function addStyle(Component, defaultStyle) {
|
|
|
150
118
|
className: aphroditeClassName,
|
|
151
119
|
style: inlineStyles
|
|
152
120
|
} = processStyleList([reset, defaultStyle, style]);
|
|
153
|
-
return
|
|
121
|
+
return React.createElement(Component, _extends({}, otherProps, {
|
|
154
122
|
className: [aphroditeClassName, className].filter(Boolean).join(" "),
|
|
155
123
|
style: inlineStyles
|
|
156
124
|
}));
|
|
@@ -158,26 +126,17 @@ function addStyle(Component, defaultStyle) {
|
|
|
158
126
|
|
|
159
127
|
return StyleComponent;
|
|
160
128
|
}
|
|
161
|
-
/**
|
|
162
|
-
* These are necessary to override various custom styles that browsers add so that
|
|
163
|
-
* elements have consistent styles across all browsers. Only add styles here if
|
|
164
|
-
* they appear in https://github.com/necolas/normalize.css/blob/master/normalize.css.
|
|
165
|
-
*/
|
|
166
|
-
|
|
167
129
|
const overrides = StyleSheet.create({
|
|
168
130
|
button: {
|
|
169
131
|
margin: 0,
|
|
170
|
-
// Safari adds 2px left/right margins
|
|
171
132
|
"::-moz-focus-inner": {
|
|
172
|
-
border: 0
|
|
173
|
-
|
|
133
|
+
border: 0
|
|
174
134
|
}
|
|
175
135
|
}
|
|
176
136
|
});
|
|
177
137
|
|
|
178
138
|
const _excluded = ["testId", "tag"];
|
|
179
139
|
const styles = StyleSheet.create({
|
|
180
|
-
// https://github.com/facebook/css-layout#default-values
|
|
181
140
|
default: {
|
|
182
141
|
alignItems: "stretch",
|
|
183
142
|
borderWidth: 0,
|
|
@@ -189,7 +148,6 @@ const styles = StyleSheet.create({
|
|
|
189
148
|
padding: 0,
|
|
190
149
|
position: "relative",
|
|
191
150
|
zIndex: 0,
|
|
192
|
-
// fix flexbox bugs
|
|
193
151
|
minHeight: 0,
|
|
194
152
|
minWidth: 0
|
|
195
153
|
}
|
|
@@ -199,20 +157,6 @@ const StyledArticle = addStyle("article", styles.default);
|
|
|
199
157
|
const StyledAside = addStyle("aside", styles.default);
|
|
200
158
|
const StyledNav = addStyle("nav", styles.default);
|
|
201
159
|
const StyledSection = addStyle("section", styles.default);
|
|
202
|
-
/**
|
|
203
|
-
* View is a building block for constructing other components. `View` roughly
|
|
204
|
-
* maps to `div` and `Text` roughly maps to `span`. You can override which tag
|
|
205
|
-
* is used to render the component (for semantic purposes) by specifying the
|
|
206
|
-
* `tag` prop.
|
|
207
|
-
*
|
|
208
|
-
* These components can take styles (via the `style` prop) in a variety of
|
|
209
|
-
* manners:
|
|
210
|
-
*
|
|
211
|
-
* - An inline style object
|
|
212
|
-
* - An `aphrodite` StyleSheet style
|
|
213
|
-
* - An array combining the above
|
|
214
|
-
*/
|
|
215
|
-
|
|
216
160
|
class View extends React.Component {
|
|
217
161
|
render() {
|
|
218
162
|
const _this$props = this.props,
|
|
@@ -228,19 +172,19 @@ class View extends React.Component {
|
|
|
228
172
|
|
|
229
173
|
switch (tag) {
|
|
230
174
|
case "article":
|
|
231
|
-
return
|
|
175
|
+
return React.createElement(StyledArticle, props);
|
|
232
176
|
|
|
233
177
|
case "aside":
|
|
234
|
-
return
|
|
178
|
+
return React.createElement(StyledAside, props);
|
|
235
179
|
|
|
236
180
|
case "nav":
|
|
237
|
-
return
|
|
181
|
+
return React.createElement(StyledNav, props);
|
|
238
182
|
|
|
239
183
|
case "section":
|
|
240
|
-
return
|
|
184
|
+
return React.createElement(StyledSection, props);
|
|
241
185
|
|
|
242
186
|
case "div":
|
|
243
|
-
return
|
|
187
|
+
return React.createElement(StyledDiv, props);
|
|
244
188
|
|
|
245
189
|
default:
|
|
246
190
|
throw Error(`${tag} is not an allowed value for the 'tag' prop`);
|
|
@@ -257,53 +201,8 @@ const RenderState = require("flow-enums-runtime")({
|
|
|
257
201
|
Initial: "initial",
|
|
258
202
|
Standard: "standard"
|
|
259
203
|
});
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
*
|
|
263
|
-
* root:
|
|
264
|
-
* no one has instigated an initial SSR render so the component that sees
|
|
265
|
-
* this "root" state is responsible for controlling initial versus standard
|
|
266
|
-
* rendering semantics
|
|
267
|
-
*
|
|
268
|
-
* initial:
|
|
269
|
-
* this means the SSR render has started, and all SSR components should act
|
|
270
|
-
* as though they are on the server
|
|
271
|
-
*
|
|
272
|
-
* standard:
|
|
273
|
-
* means that we're all now doing non-SSR rendering
|
|
274
|
-
*/
|
|
275
|
-
|
|
276
|
-
const RenderStateContext = /*#__PURE__*/React.createContext(RenderState.Root);
|
|
277
|
-
|
|
278
|
-
/**
|
|
279
|
-
* We use render functions so that we don't do any work unless we need to.
|
|
280
|
-
* This avoids rendering but not mounting potentially complex component trees.
|
|
281
|
-
*/
|
|
282
|
-
|
|
283
|
-
/**
|
|
284
|
-
* Defer or change rendering until the component did mount.
|
|
285
|
-
*
|
|
286
|
-
* The purpose of this component is to disable or modify serverside rendering
|
|
287
|
-
* of certain components. Disabling rendering on the server, by itself, would
|
|
288
|
-
* not be sufficient, since the initial render of the component must match
|
|
289
|
-
* what is rendered on the server. Therefore, this component also disables
|
|
290
|
-
* rendering the first time around on the client.
|
|
291
|
-
*
|
|
292
|
-
* If `WithSSRPlaceholder` components are nested within one another,
|
|
293
|
-
* the root `WithSSRPlaceholder` component will handle the initial
|
|
294
|
-
* render, but nested `WithSSRPlaceholder` components will delegate to
|
|
295
|
-
* the root one, meaning that we don't cascade delayed rendering down
|
|
296
|
-
* the component tree. This will also be the case across portal
|
|
297
|
-
* boundaries.
|
|
298
|
-
*
|
|
299
|
-
* Example:
|
|
300
|
-
*
|
|
301
|
-
* ```js
|
|
302
|
-
* <WithSSRPlaceholder placeholder={() => <div>Renders on the server!</div>}>
|
|
303
|
-
* {() => <div>Only renders on the client (after rehydration).</div>}
|
|
304
|
-
* </WithSSRPlaceholder>
|
|
305
|
-
* ```
|
|
306
|
-
*/
|
|
204
|
+
const RenderStateContext = React.createContext(RenderState.Root);
|
|
205
|
+
|
|
307
206
|
class WithSSRPlaceholder extends React.Component {
|
|
308
207
|
constructor(...args) {
|
|
309
208
|
super(...args);
|
|
@@ -315,9 +214,6 @@ class WithSSRPlaceholder extends React.Component {
|
|
|
315
214
|
|
|
316
215
|
componentDidMount() {
|
|
317
216
|
if (this._isTheRootComponent) {
|
|
318
|
-
// We only want to force a new render if we were responsible for
|
|
319
|
-
// the first render, so we guard that state change here.
|
|
320
|
-
// eslint-disable-next-line react/no-did-mount-set-state
|
|
321
217
|
this.setState({
|
|
322
218
|
mounted: true
|
|
323
219
|
});
|
|
@@ -331,30 +227,20 @@ class WithSSRPlaceholder extends React.Component {
|
|
|
331
227
|
const {
|
|
332
228
|
children,
|
|
333
229
|
placeholder
|
|
334
|
-
} = this.props;
|
|
335
|
-
// We are in control of instigating a second render for our
|
|
336
|
-
// component tree.
|
|
337
|
-
|
|
230
|
+
} = this.props;
|
|
338
231
|
this._isTheRootComponent = true;
|
|
339
232
|
|
|
340
233
|
if (mounted) {
|
|
341
|
-
|
|
342
|
-
// do their thing.
|
|
343
|
-
return /*#__PURE__*/React.createElement(RenderStateContext.Provider, {
|
|
234
|
+
return React.createElement(RenderStateContext.Provider, {
|
|
344
235
|
value: RenderState.Standard
|
|
345
236
|
}, children());
|
|
346
|
-
}
|
|
347
|
-
// If we have a placeholder, we render it, and ensure that any
|
|
348
|
-
// nested SSR components know we're still on that first render
|
|
349
|
-
// but they're not in charge of instigating the second render.
|
|
350
|
-
|
|
237
|
+
}
|
|
351
238
|
|
|
352
239
|
if (placeholder) {
|
|
353
|
-
return
|
|
240
|
+
return React.createElement(RenderStateContext.Provider, {
|
|
354
241
|
value: RenderState.Initial
|
|
355
242
|
}, placeholder());
|
|
356
|
-
}
|
|
357
|
-
|
|
243
|
+
}
|
|
358
244
|
|
|
359
245
|
return null;
|
|
360
246
|
}
|
|
@@ -370,71 +256,29 @@ class WithSSRPlaceholder extends React.Component {
|
|
|
370
256
|
return this._renderAsRootComponent();
|
|
371
257
|
|
|
372
258
|
case RenderState.Initial:
|
|
373
|
-
// We're not the root component, so we just have to either
|
|
374
|
-
// render our placeholder or nothing.
|
|
375
|
-
// The second render is going to be triggered for us.
|
|
376
259
|
if (placeholder) {
|
|
377
260
|
return placeholder();
|
|
378
|
-
}
|
|
379
|
-
|
|
261
|
+
}
|
|
380
262
|
|
|
381
263
|
return null;
|
|
382
264
|
|
|
383
265
|
case RenderState.Standard:
|
|
384
|
-
// We have covered the SSR render, we're now rendering with
|
|
385
|
-
// standard rendering semantics.
|
|
386
266
|
return children();
|
|
387
|
-
}
|
|
388
|
-
// context value here. So far it seems to be when we're nested in a
|
|
389
|
-
// v1 WithSSRPlaceholder equivalent component, or in some older
|
|
390
|
-
// React v16 situations where we're nested in the provider of a
|
|
391
|
-
// different context.
|
|
392
|
-
//
|
|
393
|
-
// We ignore this from coverage. It's a maintenance case to help
|
|
394
|
-
// us catch code changes that affect the control flow unexpectedly,
|
|
395
|
-
// but it's not something we need to write a test case for.
|
|
396
|
-
//
|
|
397
|
-
// Flow will assert exhaustiveness of the switch because Flow enums
|
|
398
|
-
// rock.
|
|
399
|
-
//
|
|
400
|
-
|
|
401
|
-
/* istanbul ignore next */
|
|
402
|
-
|
|
267
|
+
}
|
|
403
268
|
|
|
404
269
|
{
|
|
405
|
-
|
|
406
|
-
// Then fall through to the root case.
|
|
407
|
-
|
|
408
|
-
/* eslint-disable-next-line no-console */
|
|
409
|
-
console.log(`We got a render state we don't understand: "${JSON.stringify(renderState)}"`); // We "fallthrough" to the root case. This is more obvious
|
|
410
|
-
// and maintainable code than just ignoring the no-fallthrough
|
|
411
|
-
// lint rule.
|
|
412
|
-
|
|
270
|
+
console.log(`We got a render state we don't understand: "${JSON.stringify(renderState)}"`);
|
|
413
271
|
return this._maybeRender(RenderState.Root);
|
|
414
272
|
}
|
|
415
273
|
}
|
|
416
274
|
|
|
417
275
|
render() {
|
|
418
|
-
return
|
|
276
|
+
return React.createElement(RenderStateContext.Consumer, null, value => this._maybeRender(value));
|
|
419
277
|
}
|
|
420
278
|
|
|
421
279
|
}
|
|
422
280
|
|
|
423
|
-
/**
|
|
424
|
-
* This is NOT for direct use. Instead, see the UniqueIDProvider component.
|
|
425
|
-
*
|
|
426
|
-
* Implements IIdentifierFactory to provide unique identifiers.
|
|
427
|
-
*/
|
|
428
281
|
class UniqueIDFactory {
|
|
429
|
-
/**
|
|
430
|
-
* Creates a UniqueIDFactory instance.
|
|
431
|
-
*
|
|
432
|
-
* @param {string} scope An optional case-insensitive scope for the
|
|
433
|
-
* factory. This will be used as part of the identifier. Useful for
|
|
434
|
-
* providing context to the identifiers, which can be useful in
|
|
435
|
-
* differentiating elements when debugging the DOM. This must contain only
|
|
436
|
-
* hyphen and alphanumeric characters.
|
|
437
|
-
*/
|
|
438
282
|
constructor(scope) {
|
|
439
283
|
this.get = key => {
|
|
440
284
|
const normalizedKey = key.toLowerCase();
|
|
@@ -455,13 +299,6 @@ class UniqueIDFactory {
|
|
|
455
299
|
|
|
456
300
|
this._uniqueFactoryName = `uid-${normalizedScope}-${UniqueIDFactory._factoryUniquenessCounter++}`;
|
|
457
301
|
}
|
|
458
|
-
/**
|
|
459
|
-
* This method verifies that a string contains valid characters for an
|
|
460
|
-
* identifier. It does not assert that a string IS a valid identifier (for
|
|
461
|
-
* example, that it doesn't start with numbers). We don't need to do that
|
|
462
|
-
* here because all identifiers are prefixed to avoid needing that check.
|
|
463
|
-
*/
|
|
464
|
-
|
|
465
302
|
|
|
466
303
|
_hasValidIdChars(value) {
|
|
467
304
|
if (typeof value !== "string") {
|
|
@@ -471,30 +308,10 @@ class UniqueIDFactory {
|
|
|
471
308
|
const invalidCharsReplaced = value.replace(/[^\d\w-]/g, "-");
|
|
472
309
|
return value === invalidCharsReplaced;
|
|
473
310
|
}
|
|
474
|
-
/**
|
|
475
|
-
* Provides a unique identifier with the given key.
|
|
476
|
-
*
|
|
477
|
-
* @param {string} key The case-insensitive key of the identifier.
|
|
478
|
-
*
|
|
479
|
-
* @returns {string} A unique identifier that will remain the same for this
|
|
480
|
-
* key in this factory. This must contain only hyphen and alphanumeric
|
|
481
|
-
* characters.
|
|
482
|
-
*/
|
|
483
|
-
|
|
484
311
|
|
|
485
312
|
}
|
|
486
313
|
UniqueIDFactory._factoryUniquenessCounter = 0;
|
|
487
314
|
|
|
488
|
-
/**
|
|
489
|
-
* This is NOT for direct use. Instead, see the UniqueIDProvider component.
|
|
490
|
-
*
|
|
491
|
-
* Implements a version of IIdentifierFactory that can be used for providing
|
|
492
|
-
* identifiers on initial render of components that are eligible for server-side
|
|
493
|
-
* rendering.
|
|
494
|
-
*
|
|
495
|
-
* The identifiers are not guaranteed to be unique, but they will match between
|
|
496
|
-
* server and the first client render.
|
|
497
|
-
*/
|
|
498
315
|
class SsrIDFactory {
|
|
499
316
|
get(id) {
|
|
500
317
|
return id;
|
|
@@ -505,76 +322,37 @@ class SsrIDFactory {
|
|
|
505
322
|
SsrIDFactory.Default = new SsrIDFactory();
|
|
506
323
|
var SsrIDFactory$1 = SsrIDFactory.Default;
|
|
507
324
|
|
|
508
|
-
/**
|
|
509
|
-
* The `UniqueIDProvider` component is how Wonder Blocks components obtain
|
|
510
|
-
* unique identifiers. This component ensures that server-side rendering and
|
|
511
|
-
* initial client rendering match while allowing the provision of unique
|
|
512
|
-
* identifiers for the client.
|
|
513
|
-
*
|
|
514
|
-
* In all but the first render, the children are rendered with the same
|
|
515
|
-
* `IIdentifierFactory` instance, ensuring that the same calls will return the
|
|
516
|
-
* same identifiers.
|
|
517
|
-
*
|
|
518
|
-
* The `get` method of the identifier factory ensures that the same identifier
|
|
519
|
-
* is returned for like requests, but also that all identifiers provided are
|
|
520
|
-
* unique. Therefore, `get("test")` will always equal `get("test")`, and
|
|
521
|
-
* `get("test2")` will always equal `get("test2")`, but `get("test")` will
|
|
522
|
-
* never equal `get("test2")`.
|
|
523
|
-
*/
|
|
524
325
|
class UniqueIDProvider extends React.Component {
|
|
525
326
|
_performRender(firstRender) {
|
|
526
327
|
const {
|
|
527
328
|
children,
|
|
528
329
|
mockOnFirstRender,
|
|
529
330
|
scope
|
|
530
|
-
} = this.props;
|
|
531
|
-
// Note: `firstRender` will be `false` on the first render if this
|
|
532
|
-
// component is a descendant of a `WithSSRPlaceholder`.
|
|
331
|
+
} = this.props;
|
|
533
332
|
|
|
534
333
|
if (firstRender) {
|
|
535
334
|
if (mockOnFirstRender) {
|
|
536
|
-
// We're allowing an initial render, so let's pass our mock
|
|
537
|
-
// identifier factory to support SSR.
|
|
538
335
|
return children(SsrIDFactory$1);
|
|
539
336
|
}
|
|
540
337
|
|
|
541
338
|
return null;
|
|
542
|
-
}
|
|
543
|
-
|
|
339
|
+
}
|
|
544
340
|
|
|
545
341
|
if (!this._idFactory) {
|
|
546
342
|
this._idFactory = new UniqueIDFactory(scope);
|
|
547
|
-
}
|
|
548
|
-
|
|
343
|
+
}
|
|
549
344
|
|
|
550
345
|
return children(this._idFactory);
|
|
551
346
|
}
|
|
552
347
|
|
|
553
348
|
render() {
|
|
554
|
-
|
|
555
|
-
// when we render and whether we provide a mock or real
|
|
556
|
-
// identifier factory.
|
|
557
|
-
return /*#__PURE__*/React.createElement(WithSSRPlaceholder, {
|
|
349
|
+
return React.createElement(WithSSRPlaceholder, {
|
|
558
350
|
placeholder: () => this._performRender(true)
|
|
559
351
|
}, () => this._performRender(false));
|
|
560
352
|
}
|
|
561
353
|
|
|
562
354
|
}
|
|
563
355
|
|
|
564
|
-
/**
|
|
565
|
-
* This is a wrapper that returns an identifier. If the `id` prop is set, the component will
|
|
566
|
-
* return the same id to be consumed by its children. Otherwise, a unique id will be provided.
|
|
567
|
-
* This is beneficial for accessibility purposes, among other things.
|
|
568
|
-
*
|
|
569
|
-
* The main difference with UniqueIDProvider is that IDProvider has a single responsibility,
|
|
570
|
-
* to return an identifier that can by used by the children that are rendered internally.
|
|
571
|
-
*
|
|
572
|
-
* This way, the wrapped component will receive this custom ID and will use it to connect
|
|
573
|
-
* different elements.
|
|
574
|
-
*
|
|
575
|
-
* e.g. It uses the same generated id to connect a Dialog with its main title, or form label
|
|
576
|
-
* with the associated input element, etc.
|
|
577
|
-
*/
|
|
578
356
|
class IDProvider extends React.Component {
|
|
579
357
|
renderChildren(ids) {
|
|
580
358
|
const {
|
|
@@ -597,11 +375,9 @@ class IDProvider extends React.Component {
|
|
|
597
375
|
} = this.props;
|
|
598
376
|
|
|
599
377
|
if (id) {
|
|
600
|
-
// Let's bypass the extra weight of an id provider since we don't
|
|
601
|
-
// need it.
|
|
602
378
|
return this.renderChildren();
|
|
603
379
|
} else {
|
|
604
|
-
return
|
|
380
|
+
return React.createElement(UniqueIDProvider, {
|
|
605
381
|
scope: scope,
|
|
606
382
|
mockOnFirstRender: true
|
|
607
383
|
}, ids => this.renderChildren(ids));
|
|
@@ -613,30 +389,12 @@ IDProvider.defaultId = "wb-id";
|
|
|
613
389
|
|
|
614
390
|
let serverSide = false;
|
|
615
391
|
var server = {
|
|
616
|
-
/**
|
|
617
|
-
* Check if we are running in server-side mode.
|
|
618
|
-
*
|
|
619
|
-
* @returns {boolean} `true` if we are in server-side mode; otherwise,
|
|
620
|
-
* `false`
|
|
621
|
-
*/
|
|
622
392
|
isServerSide: () => serverSide,
|
|
623
|
-
|
|
624
|
-
/**
|
|
625
|
-
* Set server-side mode to true.
|
|
626
|
-
*/
|
|
627
393
|
setServerSide: () => {
|
|
628
394
|
serverSide = true;
|
|
629
395
|
}
|
|
630
396
|
};
|
|
631
397
|
|
|
632
|
-
/**
|
|
633
|
-
* Returns a unique identifier factory. If the parent component hasn't
|
|
634
|
-
* been mounted yet, the global SsrIDFactory will be returned until the
|
|
635
|
-
* component becomes mounted.
|
|
636
|
-
*
|
|
637
|
-
* @param {string} [scope] optional string to prefix generated ids with.
|
|
638
|
-
* @returns {IIdentifierFactory}
|
|
639
|
-
*/
|
|
640
398
|
const useUniqueIdWithMock = scope => {
|
|
641
399
|
const renderState = useContext$1(RenderStateContext);
|
|
642
400
|
const idFactory = useRef(null);
|
|
@@ -655,14 +413,6 @@ const useUniqueIdWithMock = scope => {
|
|
|
655
413
|
|
|
656
414
|
return idFactory.current;
|
|
657
415
|
};
|
|
658
|
-
/**
|
|
659
|
-
* Returns a unique identifier factory. If the parent component hasn't
|
|
660
|
-
* been mounted yet, null will be returned.
|
|
661
|
-
*
|
|
662
|
-
* @param {string} [scope] optional string to prefix generated ids with.
|
|
663
|
-
* @returns {?IIdentifierFactory}
|
|
664
|
-
*/
|
|
665
|
-
|
|
666
416
|
const useUniqueIdWithoutMock = scope => {
|
|
667
417
|
const renderState = useContext$1(RenderStateContext);
|
|
668
418
|
const idFactory = useRef(null);
|
|
@@ -682,6 +432,12 @@ const useUniqueIdWithoutMock = scope => {
|
|
|
682
432
|
return idFactory.current;
|
|
683
433
|
};
|
|
684
434
|
|
|
435
|
+
const useForceUpdate = () => {
|
|
436
|
+
const [, setState] = React.useState(false);
|
|
437
|
+
const forceUpdate = React.useCallback(() => setState(state => !state), []);
|
|
438
|
+
return forceUpdate;
|
|
439
|
+
};
|
|
440
|
+
|
|
685
441
|
const {
|
|
686
442
|
useContext,
|
|
687
443
|
useEffect,
|
|
@@ -695,20 +451,18 @@ const RenderStateRoot = ({
|
|
|
695
451
|
const contextValue = useContext(RenderStateContext);
|
|
696
452
|
useEffect(() => {
|
|
697
453
|
setFirstRender(false);
|
|
698
|
-
}, []);
|
|
454
|
+
}, []);
|
|
699
455
|
|
|
700
456
|
if (contextValue !== RenderState.Root) {
|
|
701
457
|
if (throwIfNested) {
|
|
702
458
|
throw new Error("There's already a <RenderStateRoot> above this instance in " + "the render tree. This instance should be removed.");
|
|
703
|
-
}
|
|
704
|
-
// is nested inside another one.
|
|
705
|
-
|
|
459
|
+
}
|
|
706
460
|
|
|
707
461
|
return children;
|
|
708
462
|
}
|
|
709
463
|
|
|
710
464
|
const value = firstRender ? RenderState.Initial : RenderState.Standard;
|
|
711
|
-
return
|
|
465
|
+
return React.createElement(RenderStateContext.Provider, {
|
|
712
466
|
value: value
|
|
713
467
|
}, children);
|
|
714
468
|
};
|
|
@@ -716,4 +470,4 @@ RenderStateRoot.defaultProps = {
|
|
|
716
470
|
throwIfNested: true
|
|
717
471
|
};
|
|
718
472
|
|
|
719
|
-
export { IDProvider, RenderStateRoot, server as Server, Text, UniqueIDProvider, View, WithSSRPlaceholder, addStyle, useUniqueIdWithMock, useUniqueIdWithoutMock };
|
|
473
|
+
export { IDProvider, RenderStateRoot, server as Server, Text, UniqueIDProvider, View, WithSSRPlaceholder, addStyle, useForceUpdate, useUniqueIdWithMock, useUniqueIdWithoutMock };
|
package/dist/index.js
CHANGED
|
@@ -82,7 +82,7 @@ module.exports =
|
|
|
82
82
|
/******/
|
|
83
83
|
/******/
|
|
84
84
|
/******/ // Load entry module and return exports
|
|
85
|
-
/******/ return __webpack_require__(__webpack_require__.s =
|
|
85
|
+
/******/ return __webpack_require__(__webpack_require__.s = 16);
|
|
86
86
|
/******/ })
|
|
87
87
|
/************************************************************************/
|
|
88
88
|
/******/ ([
|
|
@@ -101,7 +101,7 @@ module.exports = require("react");
|
|
|
101
101
|
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(0);
|
|
102
102
|
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__);
|
|
103
103
|
|
|
104
|
-
const RenderState = __webpack_require__(
|
|
104
|
+
const RenderState = __webpack_require__(18)({
|
|
105
105
|
Root: "root",
|
|
106
106
|
Initial: "initial",
|
|
107
107
|
Standard: "standard"
|
|
@@ -573,7 +573,7 @@ function processStyleList(style) {
|
|
|
573
573
|
className: aphrodite__WEBPACK_IMPORTED_MODULE_0__["css"].apply(void 0, stylesheetStyles)
|
|
574
574
|
};
|
|
575
575
|
}
|
|
576
|
-
/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(
|
|
576
|
+
/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(17)))
|
|
577
577
|
|
|
578
578
|
/***/ }),
|
|
579
579
|
/* 8 */
|
|
@@ -917,6 +917,34 @@ let serverSide = false;
|
|
|
917
917
|
/* 14 */
|
|
918
918
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
919
919
|
|
|
920
|
+
"use strict";
|
|
921
|
+
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a", function() { return useForceUpdate; });
|
|
922
|
+
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(0);
|
|
923
|
+
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__);
|
|
924
|
+
|
|
925
|
+
/**
|
|
926
|
+
* Hook for forcing a component to update on demand.
|
|
927
|
+
*
|
|
928
|
+
* This is for use inside other hooks that do some advanced
|
|
929
|
+
* trickery with storing state outside of React's own state
|
|
930
|
+
* mechanisms. As such this should never be called directly
|
|
931
|
+
* outside of a hook, and more often than not, is the wrong
|
|
932
|
+
* choice for whatever you are trying to do. If in doubt,
|
|
933
|
+
* don't use it.
|
|
934
|
+
*
|
|
935
|
+
* @returns {() => void} A function that forces the component to update.
|
|
936
|
+
*/
|
|
937
|
+
|
|
938
|
+
const useForceUpdate = () => {
|
|
939
|
+
const [, setState] = react__WEBPACK_IMPORTED_MODULE_0__["useState"](false);
|
|
940
|
+
const forceUpdate = react__WEBPACK_IMPORTED_MODULE_0__["useCallback"](() => setState(state => !state), []);
|
|
941
|
+
return forceUpdate;
|
|
942
|
+
};
|
|
943
|
+
|
|
944
|
+
/***/ }),
|
|
945
|
+
/* 15 */
|
|
946
|
+
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
947
|
+
|
|
920
948
|
"use strict";
|
|
921
949
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a", function() { return RenderStateRoot; });
|
|
922
950
|
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(0);
|
|
@@ -963,7 +991,7 @@ RenderStateRoot.defaultProps = {
|
|
|
963
991
|
};
|
|
964
992
|
|
|
965
993
|
/***/ }),
|
|
966
|
-
/*
|
|
994
|
+
/* 16 */
|
|
967
995
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
|
968
996
|
|
|
969
997
|
"use strict";
|
|
@@ -994,8 +1022,12 @@ __webpack_require__.r(__webpack_exports__);
|
|
|
994
1022
|
|
|
995
1023
|
/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "useUniqueIdWithoutMock", function() { return _hooks_use_unique_id_js__WEBPACK_IMPORTED_MODULE_7__["b"]; });
|
|
996
1024
|
|
|
997
|
-
/* harmony import */ var
|
|
998
|
-
/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "
|
|
1025
|
+
/* harmony import */ var _hooks_use_force_update_js__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(14);
|
|
1026
|
+
/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "useForceUpdate", function() { return _hooks_use_force_update_js__WEBPACK_IMPORTED_MODULE_8__["a"]; });
|
|
1027
|
+
|
|
1028
|
+
/* harmony import */ var _components_render_state_root_js__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(15);
|
|
1029
|
+
/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "RenderStateRoot", function() { return _components_render_state_root_js__WEBPACK_IMPORTED_MODULE_9__["a"]; });
|
|
1030
|
+
|
|
999
1031
|
|
|
1000
1032
|
|
|
1001
1033
|
|
|
@@ -1008,7 +1040,7 @@ __webpack_require__.r(__webpack_exports__);
|
|
|
1008
1040
|
|
|
1009
1041
|
|
|
1010
1042
|
/***/ }),
|
|
1011
|
-
/*
|
|
1043
|
+
/* 17 */
|
|
1012
1044
|
/***/ (function(module, exports) {
|
|
1013
1045
|
|
|
1014
1046
|
var g;
|
|
@@ -1034,7 +1066,7 @@ module.exports = g;
|
|
|
1034
1066
|
|
|
1035
1067
|
|
|
1036
1068
|
/***/ }),
|
|
1037
|
-
/*
|
|
1069
|
+
/* 18 */
|
|
1038
1070
|
/***/ (function(module, exports, __webpack_require__) {
|
|
1039
1071
|
|
|
1040
1072
|
"use strict";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@khanacademy/wonder-blocks-core",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.3.1",
|
|
4
4
|
"design": "v1",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"react-router-dom": "5.3.0"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
|
-
"wb-dev-build-settings": "^0.
|
|
27
|
+
"wb-dev-build-settings": "^0.4.0"
|
|
28
28
|
},
|
|
29
29
|
"author": "",
|
|
30
30
|
"license": "MIT"
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import {render, act} from "@testing-library/react";
|
|
4
|
+
import {renderHook} from "@testing-library/react-hooks";
|
|
5
|
+
|
|
6
|
+
import {useForceUpdate} from "../use-force-update.js";
|
|
7
|
+
|
|
8
|
+
describe("#useForceUpdate", () => {
|
|
9
|
+
it("should return a function", () => {
|
|
10
|
+
// Arrange
|
|
11
|
+
|
|
12
|
+
// Act
|
|
13
|
+
const {
|
|
14
|
+
result: {current: result},
|
|
15
|
+
} = renderHook(() => useForceUpdate());
|
|
16
|
+
|
|
17
|
+
// Assert
|
|
18
|
+
expect(result).toBeInstanceOf(Function);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe("returned function", () => {
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
jest.useFakeTimers();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("should cause component to render", () => {
|
|
27
|
+
// Arrange
|
|
28
|
+
const Component = (props): React.Node => {
|
|
29
|
+
const countRef = React.useRef(0);
|
|
30
|
+
const forceUpdate = useForceUpdate();
|
|
31
|
+
React.useEffect(() => {
|
|
32
|
+
countRef.current++;
|
|
33
|
+
|
|
34
|
+
setTimeout(forceUpdate, 50);
|
|
35
|
+
});
|
|
36
|
+
return countRef.current;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// Act
|
|
40
|
+
const wrapper = render(<Component />);
|
|
41
|
+
act(() => {
|
|
42
|
+
// Advance enough for the timeout to run 4 times.
|
|
43
|
+
// Which means the component should have rendered 4 times,
|
|
44
|
+
// with one more pending for the timeout that was setup in
|
|
45
|
+
// the last render.
|
|
46
|
+
jest.advanceTimersByTime(204);
|
|
47
|
+
});
|
|
48
|
+
const result = wrapper.container.textContent;
|
|
49
|
+
|
|
50
|
+
// Assert
|
|
51
|
+
expect(result).toBe("4");
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Hook for forcing a component to update on demand.
|
|
6
|
+
*
|
|
7
|
+
* This is for use inside other hooks that do some advanced
|
|
8
|
+
* trickery with storing state outside of React's own state
|
|
9
|
+
* mechanisms. As such this should never be called directly
|
|
10
|
+
* outside of a hook, and more often than not, is the wrong
|
|
11
|
+
* choice for whatever you are trying to do. If in doubt,
|
|
12
|
+
* don't use it.
|
|
13
|
+
*
|
|
14
|
+
* @returns {() => void} A function that forces the component to update.
|
|
15
|
+
*/
|
|
16
|
+
export const useForceUpdate = (): (() => void) => {
|
|
17
|
+
const [, setState] = React.useState(false);
|
|
18
|
+
const forceUpdate = React.useCallback(
|
|
19
|
+
() => setState((state) => !state),
|
|
20
|
+
[],
|
|
21
|
+
);
|
|
22
|
+
return forceUpdate;
|
|
23
|
+
};
|
package/src/index.js
CHANGED
|
@@ -12,6 +12,7 @@ export {
|
|
|
12
12
|
useUniqueIdWithMock,
|
|
13
13
|
useUniqueIdWithoutMock,
|
|
14
14
|
} from "./hooks/use-unique-id.js";
|
|
15
|
+
export {useForceUpdate} from "./hooks/use-force-update.js";
|
|
15
16
|
export {RenderStateRoot} from "./components/render-state-root.js";
|
|
16
17
|
|
|
17
18
|
export type {AriaProps, IIdentifierFactory, StyleType};
|
package/src/util/add-style.md
CHANGED
|
@@ -45,7 +45,7 @@ StyleSheet as well inline style objects (see example 4).
|
|
|
45
45
|
|
|
46
46
|
#### CSSProperties
|
|
47
47
|
|
|
48
|
-
[See source file](https://github.com/Khan/wonder-blocks/blob/
|
|
48
|
+
[See source file](https://github.com/Khan/wonder-blocks/blob/main/flow-typed/aphrodite.flow.js#L13)
|
|
49
49
|
|
|
50
50
|
|
|
51
51
|
### Examples
|