@khanacademy/wonder-blocks-timing 2.0.3 → 2.1.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.
Files changed (41) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/es/index.js +107 -230
  3. package/dist/index.js +260 -547
  4. package/dist/index.js.flow +1 -1
  5. package/package.json +3 -4
  6. package/src/__docs__/_overview_.stories.mdx +17 -0
  7. package/src/components/__docs__/migration.stories.mdx +112 -0
  8. package/{docs.md → src/components/__docs__/types.ischedule-actions.stories.mdx} +11 -260
  9. package/src/components/__docs__/with-action-scheduler-examples.js +80 -0
  10. package/src/components/__docs__/with-action-scheduler.stories.mdx +218 -0
  11. package/src/components/__tests__/action-scheduler-provider.test.js +7 -7
  12. package/src/components/__tests__/with-action-scheduler.test.js +6 -6
  13. package/src/components/action-scheduler-provider.js +2 -2
  14. package/src/components/with-action-scheduler.js +2 -2
  15. package/src/hooks/__docs__/use-interval.stories.mdx +80 -0
  16. package/src/hooks/__docs__/use-scheduled-interval.stories.mdx +147 -0
  17. package/src/hooks/{use-timeout.stories.mdx → __docs__/use-scheduled-timeout.stories.mdx} +13 -17
  18. package/src/hooks/__docs__/use-timeout.stories.mdx +80 -0
  19. package/src/hooks/__tests__/use-interval.test.js +124 -0
  20. package/src/hooks/__tests__/use-scheduled-interval.test.js +461 -0
  21. package/src/hooks/__tests__/use-scheduled-timeout.test.js +479 -0
  22. package/src/hooks/__tests__/use-timeout.test.js +94 -294
  23. package/src/hooks/internal/use-updating-ref.js +20 -0
  24. package/src/hooks/use-interval.js +37 -0
  25. package/src/hooks/use-scheduled-interval.js +73 -0
  26. package/src/hooks/use-scheduled-timeout.js +80 -0
  27. package/src/hooks/use-timeout.js +22 -55
  28. package/src/index.js +7 -4
  29. package/src/util/__tests__/action-scheduler.test.js +9 -9
  30. package/src/util/__tests__/animation-frame.test.js +2 -2
  31. package/src/util/__tests__/interval.test.js +2 -2
  32. package/src/util/__tests__/timeout.test.js +2 -2
  33. package/src/util/action-scheduler.js +4 -4
  34. package/src/util/animation-frame.js +2 -2
  35. package/src/util/interval.js +2 -2
  36. package/src/util/timeout.js +2 -2
  37. package/src/util/types.flowtest.js +2 -2
  38. package/LICENSE +0 -21
  39. package/src/__tests__/__snapshots__/generated-snapshot.test.js.snap +0 -353
  40. package/src/__tests__/generated-snapshot.test.js +0 -156
  41. package/src/components/with-action-scheduler.md +0 -18
package/CHANGELOG.md ADDED
@@ -0,0 +1,18 @@
1
+ # @khanacademy/wonder-blocks-timing
2
+
3
+ ## 2.1.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 91cb727c: Remove file extensions from imports
8
+
9
+ ## 2.1.0
10
+
11
+ ### Minor Changes
12
+
13
+ - 029b4810: Adds `useInterval()` hook that mimics the behavior of `ActionScheduler`'s
14
+ `interval()` method.
15
+ - c57cd770: Rename `useInterval` and `useTimeout` to `useScheduledInterval`
16
+ and `useScheduledTimeout` respectively.
17
+ - 29766c8e: Add `useInterval` and `useTimeout` hooks to provide an API for
18
+ using intervals and timeouts.
package/dist/es/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import _extends from '@babel/runtime/helpers/extends';
2
- import { Component, forwardRef, createElement, useState, useRef, useEffect, useCallback } from 'react';
2
+ import * as React from 'react';
3
+ import { useRef, useEffect, useState, useCallback } from 'react';
3
4
 
4
5
  const SchedulePolicy = {
5
6
  Immediately: "schedule-immediately",
@@ -10,29 +11,7 @@ const ClearPolicy = {
10
11
  Cancel: "cancel-on-clear"
11
12
  };
12
13
 
13
- /**
14
- * Encapsulates everything associated with calling setTimeout/clearTimeout, and
15
- * managing the lifecycle of that timer, including the ability to resolve or
16
- * cancel a pending timeout action.
17
- *
18
- * @export
19
- * @class Timeout
20
- * @implements {ITimeout}
21
- */
22
14
  class Timeout {
23
- /**
24
- * Creates a timeout that will invoke the given action after
25
- * the given period. The timeout does not start until set is called.
26
- *
27
- * @param {() => mixed} action The action to be invoked when the timeout
28
- * period has passed.
29
- * @param {number} timeoutMs The timeout period.
30
- * @param {SchedulePolicy} [schedulePolicy] When SchedulePolicy.Immediately,
31
- * the timer is set immediately on instantiation; otherwise, `set` must be
32
- * called to set the timeout.
33
- * Defaults to `SchedulePolicy.Immediately`.
34
- * @memberof Timeout
35
- */
36
15
  constructor(action, timeoutMs, schedulePolicy = SchedulePolicy.Immediately) {
37
16
  if (typeof action !== "function") {
38
17
  throw new Error("Action must be a function");
@@ -49,28 +28,10 @@ class Timeout {
49
28
  this.set();
50
29
  }
51
30
  }
52
- /**
53
- * Determine if the timeout is set or not.
54
- *
55
- * @returns {boolean} true if the timeout is set (aka pending), otherwise
56
- * false.
57
- * @memberof Timeout
58
- */
59
-
60
31
 
61
32
  get isSet() {
62
33
  return this._timeoutId != null;
63
34
  }
64
- /**
65
- * Set the timeout.
66
- *
67
- * If the timeout is pending, this cancels that pending timeout and
68
- * sets the timeout afresh. If the timeout is not pending, this
69
- * sets a new timeout.
70
- *
71
- * @memberof Timeout
72
- */
73
-
74
35
 
75
36
  set() {
76
37
  if (this.isSet) {
@@ -79,21 +40,6 @@ class Timeout {
79
40
 
80
41
  this._timeoutId = setTimeout(() => this.clear(ClearPolicy.Resolve), this._timeoutMs);
81
42
  }
82
- /**
83
- * Clear the set timeout.
84
- *
85
- * If the timeout is pending, this cancels that pending timeout without
86
- * invoking the action. If no timeout is pending, this does nothing.
87
- *
88
- * @param {ClearPolicy} [policy] When ClearPolicy.Resolve, if the request
89
- * was set when called, the request action is invoked after cancelling
90
- * the request; otherwise, the pending action is cancelled.
91
- * Defaults to `ClearPolicy.Cancel`.
92
- *
93
- * @returns {void}
94
- * @memberof Timeout
95
- */
96
-
97
43
 
98
44
  clear(policy = ClearPolicy.Cancel) {
99
45
  const timeoutId = this._timeoutId;
@@ -112,29 +58,7 @@ class Timeout {
112
58
 
113
59
  }
114
60
 
115
- /**
116
- * Encapsulates everything associated with calling setInterval/clearInterval,
117
- * and managing the lifecycle of that interval. This includes the ability to
118
- * cancel the interval, and knowing if the interval is active.
119
- *
120
- * @export
121
- * @class Interval
122
- * @implements {IInterval}
123
- */
124
61
  class Interval {
125
- /**
126
- * Creates an interval that will invoke the given action after
127
- * the given period. The interval does not start until set is called.
128
- *
129
- * @param {() => mixed} action The action to be invoked each time the
130
- * interval period has passed.
131
- * @param {number} intervalMs The interval period.
132
- * @param {SchedulePolicy} [schedulePolicy] When SchedulePolicy.Immediately,
133
- * the interval is set immediately on instantiation; otherwise, `set` must be
134
- * called to set the interval.
135
- * Defaults to `SchedulePolicy.Immediately`.
136
- * @memberof Interval
137
- */
138
62
  constructor(action, intervalMs, schedulePolicy = SchedulePolicy.Immediately) {
139
63
  if (typeof action !== "function") {
140
64
  throw new Error("Action must be a function");
@@ -151,26 +75,10 @@ class Interval {
151
75
  this.set();
152
76
  }
153
77
  }
154
- /**
155
- * Determine if the interval is active or not.
156
- *
157
- * @returns {boolean} true if the interval is active, otherwise false.
158
- * @memberof Interval
159
- */
160
-
161
78
 
162
79
  get isSet() {
163
80
  return this._intervalId != null;
164
81
  }
165
- /**
166
- * Activate the interval.
167
- *
168
- * If the interval is active, this cancels that interval and starts the
169
- * interval afresh. If the interval is not active, this starts it.
170
- *
171
- * @memberof Interval
172
- */
173
-
174
82
 
175
83
  set() {
176
84
  if (this.isSet) {
@@ -179,21 +87,6 @@ class Interval {
179
87
 
180
88
  this._intervalId = setInterval(() => this._action(), this._intervalMs);
181
89
  }
182
- /**
183
- * Clear the active interval.
184
- *
185
- * If the interval is active, this cancels that interval. If no interval is
186
- * pending, this does nothing.
187
- *
188
- * @param {ClearPolicy} [policy] When ClearPolicy.Resolve, if the request
189
- * was set when called, the request action is invoked after cancelling
190
- * the request; otherwise, the pending action is cancelled.
191
- * Defaults to `ClearPolicy.Cancel`.
192
- *
193
- * @returns {void}
194
- * @memberof Interval
195
- */
196
-
197
90
 
198
91
  clear(policy = ClearPolicy.Cancel) {
199
92
  const intervalId = this._intervalId;
@@ -212,27 +105,7 @@ class Interval {
212
105
 
213
106
  }
214
107
 
215
- /**
216
- * Encapsulates everything associated with calling requestAnimationFrame/
217
- * cancelAnimationFrame, and managing the lifecycle of that request, including
218
- * the ability to resolve or cancel a pending request action.
219
- *
220
- * @export
221
- * @class AnimationFrame
222
- * @implements {IAnimationFrame}
223
- */
224
108
  class AnimationFrame {
225
- /**
226
- * Creates an animation frame request that will invoke the given action.
227
- * The request is not made until set is called.
228
- *
229
- * @param {DOMHighResTimeStamp => mixed} action The action to be invoked.
230
- * @param {SchedulePolicy} [schedulePolicy] When SchedulePolicy.Immediately,
231
- * the interval is set immediately on instantiation; otherwise, `set` must be
232
- * called to set the interval.
233
- * Defaults to `SchedulePolicy.Immediately`.
234
- * @memberof AnimationFrame
235
- */
236
109
  constructor(action, schedulePolicy = SchedulePolicy.Immediately) {
237
110
  if (typeof action !== "function") {
238
111
  throw new Error("Action must be a function");
@@ -244,28 +117,10 @@ class AnimationFrame {
244
117
  this.set();
245
118
  }
246
119
  }
247
- /**
248
- * Determine if the request is pending or not.
249
- *
250
- * @returns {boolean} true if the request is pending, otherwise
251
- * false.
252
- * @memberof AnimationFrame
253
- */
254
-
255
120
 
256
121
  get isSet() {
257
122
  return this._animationFrameId != null;
258
123
  }
259
- /**
260
- * Make the animation frame request.
261
- *
262
- * If the request is pending, this cancels that pending request and
263
- * makes the request afresh. If the request is not pending, this
264
- * makes a new request.
265
- *
266
- * @memberof AnimationFrame
267
- */
268
-
269
124
 
270
125
  set() {
271
126
  if (this.isSet) {
@@ -274,24 +129,6 @@ class AnimationFrame {
274
129
 
275
130
  this._animationFrameId = requestAnimationFrame(time => this.clear(ClearPolicy.Resolve, time));
276
131
  }
277
- /**
278
- * Clear the pending request.
279
- *
280
- * If the request is pending, this cancels that pending request without
281
- * invoking the action. If no request is pending, this does nothing.
282
- *
283
- * @param {ClearPolicy} [policy] When ClearPolicy.Resolve, if the request
284
- * was set when called, the request action is invoked after cancelling
285
- * the request; otherwise, the pending action is cancelled.
286
- * Defaults to `ClearPolicy.Cancel`.
287
- * @param {DOMHighResTimeStamp} [time] Timestamp to pass to the action when
288
- * ClearPolicy.Resolve is specified. Ignored when ClearPolicy.Cancel is
289
- * specified.
290
- *
291
- * @returns {void}
292
- * @memberof AnimationFrame
293
- */
294
-
295
132
 
296
133
  clear(policy = ClearPolicy.Cancel, time) {
297
134
  const animationFrameId = this._animationFrameId;
@@ -310,12 +147,6 @@ class AnimationFrame {
310
147
 
311
148
  }
312
149
 
313
- /**
314
- * Implements the `IScheduleActions` API to provide timeout, interval, and
315
- * animation frame support. This is not intended for direct use, but instead
316
- * is to be used solely by the `ActionSchedulerProvider` to provide an
317
- * `IScheduleActions` instance.
318
- */
319
150
  class ActionScheduler {
320
151
  constructor() {
321
152
  this._disabled = false;
@@ -363,11 +194,6 @@ class ActionScheduler {
363
194
  this._registeredActions = [];
364
195
  registered.forEach(clearFn => clearFn());
365
196
  }
366
- /**
367
- * Prevents this scheduler from creating any additional actions.
368
- * This also clears any pending actions.
369
- */
370
-
371
197
 
372
198
  disable() {
373
199
  this._disabled = true;
@@ -385,17 +211,7 @@ ActionScheduler.NoopAction = {
385
211
  clear: () => {}
386
212
  };
387
213
 
388
- /**
389
- * A provider component that passes our action scheduling API to its children
390
- * and ensures that all scheduled actions are cleared on unmount.
391
- *
392
- * ```jsx
393
- * <ActionSchedulerProvider>
394
- * {schedule => this.renderThingThatNeedsTimers(schedule)}
395
- * </ActionSchedulerProvider>
396
- * ```
397
- */
398
- class ActionSchedulerProvider extends Component {
214
+ class ActionSchedulerProvider extends React.Component {
399
215
  constructor(...args) {
400
216
  super(...args);
401
217
  this._actionScheduler = new ActionScheduler();
@@ -414,69 +230,130 @@ class ActionSchedulerProvider extends Component {
414
230
 
415
231
  }
416
232
 
417
- /**
418
- * A higher order component that attaches the given component to an
419
- * `IScheduleActions` instance. Any actions scheduled will automatically be
420
- * cleared on unmount.
421
- *
422
- * @template TOwnProps The own props of the component being rendered, without
423
- * the additional action scheduler prop. To attach the additional prop to
424
- * these props use the `WithActionScheduler` type.
425
- */
426
233
  function withActionScheduler(WrappedComponent) {
427
- return /*#__PURE__*/forwardRef((props, ref) => /*#__PURE__*/createElement(ActionSchedulerProvider, null, schedule => /*#__PURE__*/createElement(WrappedComponent, _extends({}, props, {
234
+ return React.forwardRef((props, ref) => React.createElement(ActionSchedulerProvider, null, schedule => React.createElement(WrappedComponent, _extends({}, props, {
428
235
  ref: ref,
429
236
  schedule: schedule
430
237
  }))));
431
238
  }
432
239
 
433
- function useTimeout(action, timeoutMs, options) {
240
+ const useUpdatingRef = value => {
241
+ const ref = useRef(value);
242
+ useEffect(() => {
243
+ ref.current = value;
244
+ }, [value]);
245
+ return ref;
246
+ };
247
+
248
+ function useInterval(action, intervalMs, active) {
249
+ const actionRef = useUpdatingRef(action);
250
+ useEffect(() => {
251
+ if (active) {
252
+ const intervalId = setInterval(() => {
253
+ actionRef.current();
254
+ }, intervalMs);
255
+ return () => {
256
+ clearInterval(intervalId);
257
+ };
258
+ }
259
+ }, [intervalMs, active, actionRef]);
260
+ }
261
+
262
+ function useTimeout(action, timeoutMs, active) {
263
+ const actionRef = useUpdatingRef(action);
264
+ useEffect(() => {
265
+ if (active) {
266
+ const timeoutId = setTimeout(() => {
267
+ actionRef.current();
268
+ }, timeoutMs);
269
+ return () => {
270
+ clearTimeout(timeoutId);
271
+ };
272
+ }
273
+ }, [timeoutMs, active, actionRef]);
274
+ }
275
+
276
+ function useScheduledInterval(action, intervalMs, options) {
434
277
  var _options$schedulePoli;
435
278
 
279
+ if (typeof action !== "function") {
280
+ throw new Error("Action must be a function");
281
+ }
282
+
283
+ if (intervalMs < 1) {
284
+ throw new Error("Interval period must be >= 1");
285
+ }
286
+
436
287
  const schedulePolicy = (_options$schedulePoli = options == null ? void 0 : options.schedulePolicy) != null ? _options$schedulePoli : SchedulePolicy.Immediately;
437
288
  const [isSet, setIsSet] = useState(schedulePolicy === SchedulePolicy.Immediately);
438
- const actionRef = useRef(action);
439
- const mountedRef = useRef(false);
289
+ const set = useCallback(() => setIsSet(true), []);
290
+ const actionRef = useUpdatingRef(action);
291
+ const clear = useCallback(policy => {
292
+ var _policy;
293
+
294
+ policy = (_policy = policy) != null ? _policy : options == null ? void 0 : options.clearPolicy;
295
+
296
+ if (isSet && policy === ClearPolicy.Resolve) {
297
+ actionRef.current();
298
+ }
299
+
300
+ setIsSet(false);
301
+ }, [actionRef, isSet, options == null ? void 0 : options.clearPolicy]);
302
+ const runOnUnmountRef = useUpdatingRef(isSet && (options == null ? void 0 : options.clearPolicy) === ClearPolicy.Resolve);
440
303
  useEffect(() => {
441
- mountedRef.current = true;
442
304
  return () => {
443
- mountedRef.current = false;
305
+ if (runOnUnmountRef.current) {
306
+ actionRef.current();
307
+ }
444
308
  };
445
309
  }, []);
446
- useEffect(() => {
447
- actionRef.current = action;
448
- }, [action]);
449
- const clear = useCallback(policy => {
450
- if ((policy != null ? policy : options == null ? void 0 : options.clearPolicy) === ClearPolicy.Resolve) {
451
- actionRef.current();
452
- } // This will cause the useEffect below to re-run
310
+ useInterval(action, intervalMs, isSet);
311
+ return {
312
+ isSet,
313
+ set,
314
+ clear
315
+ };
316
+ }
453
317
 
318
+ function useScheduledTimeout(action, timeoutMs, options) {
319
+ var _options$schedulePoli;
320
+
321
+ if (typeof action !== "function") {
322
+ throw new Error("Action must be a function");
323
+ }
454
324
 
325
+ if (timeoutMs < 0) {
326
+ throw new Error("Timeout period must be >= 0");
327
+ }
328
+
329
+ const schedulePolicy = (_options$schedulePoli = options == null ? void 0 : options.schedulePolicy) != null ? _options$schedulePoli : SchedulePolicy.Immediately;
330
+ const [isSet, setIsSet] = useState(schedulePolicy === SchedulePolicy.Immediately);
331
+ const set = useCallback(() => setIsSet(true), []);
332
+ const wrappedAction = useCallback(() => {
455
333
  setIsSet(false);
456
- }, [options == null ? void 0 : options.clearPolicy]);
457
- const set = useCallback(() => {
458
- if (isSet) {
459
- clear();
460
- } // This will cause the useEffect below to re-run
334
+ action();
335
+ }, [action]);
336
+ const actionRef = useUpdatingRef(wrappedAction);
337
+ const clear = useCallback(policy => {
338
+ var _policy;
461
339
 
340
+ policy = (_policy = policy) != null ? _policy : options == null ? void 0 : options.clearPolicy;
462
341
 
463
- setIsSet(true);
464
- }, [clear, isSet]);
342
+ if (isSet && policy === ClearPolicy.Resolve) {
343
+ actionRef.current();
344
+ }
345
+
346
+ setIsSet(false);
347
+ }, [actionRef, isSet, options == null ? void 0 : options.clearPolicy]);
348
+ const runOnUnmountRef = useUpdatingRef(isSet && (options == null ? void 0 : options.clearPolicy) === ClearPolicy.Resolve);
465
349
  useEffect(() => {
466
- if (isSet && mountedRef.current) {
467
- const timeout = window.setTimeout(() => {
350
+ return () => {
351
+ if (runOnUnmountRef.current) {
468
352
  actionRef.current();
469
- setIsSet(false);
470
- }, timeoutMs);
471
- return () => {
472
- window.clearTimeout(timeout);
473
-
474
- if (!mountedRef.current) {
475
- clear();
476
- }
477
- };
478
- }
479
- }, [clear, isSet, timeoutMs]);
353
+ }
354
+ };
355
+ }, []);
356
+ useTimeout(wrappedAction, timeoutMs, isSet);
480
357
  return {
481
358
  isSet,
482
359
  set,
@@ -484,4 +361,4 @@ function useTimeout(action, timeoutMs, options) {
484
361
  };
485
362
  }
486
363
 
487
- export { ClearPolicy, SchedulePolicy, useTimeout, withActionScheduler };
364
+ export { ClearPolicy, SchedulePolicy, useInterval, useScheduledInterval, useScheduledTimeout, useTimeout, withActionScheduler };