@khanacademy/wonder-blocks-timing 2.0.0 → 2.1.0
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 +11 -0
- package/dist/es/index.js +188 -4
- package/dist/index.js +462 -179
- package/docs.md +28 -18
- package/package.json +4 -5
- package/src/__tests__/generated-snapshot.test.js +2 -3
- package/src/components/__tests__/action-scheduler-provider.test.js +1 -0
- package/src/components/__tests__/with-action-scheduler.test.js +1 -0
- package/src/hooks/__tests__/use-interval.test.js +124 -0
- package/src/hooks/__tests__/use-scheduled-interval.test.js +453 -0
- package/src/hooks/__tests__/use-scheduled-timeout.test.js +469 -0
- package/src/hooks/__tests__/use-timeout.test.js +136 -0
- package/src/hooks/internal/use-updating-ref.js +20 -0
- package/src/hooks/use-interval.js +37 -0
- package/src/hooks/use-interval.stories.mdx +80 -0
- package/src/hooks/use-scheduled-interval.js +73 -0
- package/src/hooks/use-scheduled-interval.stories.mdx +147 -0
- package/src/hooks/use-scheduled-timeout.js +80 -0
- package/src/hooks/use-scheduled-timeout.stories.mdx +148 -0
- package/src/hooks/use-timeout.js +37 -0
- package/src/hooks/use-timeout.stories.mdx +80 -0
- package/src/index.js +4 -0
- package/src/util/__tests__/animation-frame.test.js +6 -8
- package/src/util/__tests__/interval.test.js +31 -14
- package/src/util/__tests__/timeout.test.js +18 -8
- package/LICENSE +0 -21
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# @khanacademy/wonder-blocks-timing
|
|
2
|
+
|
|
3
|
+
## 2.1.0
|
|
4
|
+
### Minor Changes
|
|
5
|
+
|
|
6
|
+
- 029b4810: Adds `useInterval()` hook that mimics the behavior of `ActionScheduler`'s
|
|
7
|
+
`interval()` method.
|
|
8
|
+
- c57cd770: Rename `useInterval` and `useTimeout` to `useScheduledInterval`
|
|
9
|
+
and `useScheduledTimeout` respectively.
|
|
10
|
+
- 29766c8e: Add `useInterval` and `useTimeout` hooks to provide a simple API for
|
|
11
|
+
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
|
|
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",
|
|
@@ -395,7 +396,7 @@ ActionScheduler.NoopAction = {
|
|
|
395
396
|
* </ActionSchedulerProvider>
|
|
396
397
|
* ```
|
|
397
398
|
*/
|
|
398
|
-
class ActionSchedulerProvider extends Component {
|
|
399
|
+
class ActionSchedulerProvider extends React.Component {
|
|
399
400
|
constructor(...args) {
|
|
400
401
|
super(...args);
|
|
401
402
|
this._actionScheduler = new ActionScheduler();
|
|
@@ -424,10 +425,193 @@ class ActionSchedulerProvider extends Component {
|
|
|
424
425
|
* these props use the `WithActionScheduler` type.
|
|
425
426
|
*/
|
|
426
427
|
function withActionScheduler(WrappedComponent) {
|
|
427
|
-
return /*#__PURE__*/forwardRef((props, ref) => /*#__PURE__*/createElement(ActionSchedulerProvider, null, schedule => /*#__PURE__*/createElement(WrappedComponent, _extends({}, props, {
|
|
428
|
+
return /*#__PURE__*/React.forwardRef((props, ref) => /*#__PURE__*/React.createElement(ActionSchedulerProvider, null, schedule => /*#__PURE__*/React.createElement(WrappedComponent, _extends({}, props, {
|
|
428
429
|
ref: ref,
|
|
429
430
|
schedule: schedule
|
|
430
431
|
}))));
|
|
431
432
|
}
|
|
432
433
|
|
|
433
|
-
|
|
434
|
+
/**
|
|
435
|
+
* Returns a ref whose .current value is updated whenever
|
|
436
|
+
* the `value` passed to this hook changes.
|
|
437
|
+
*
|
|
438
|
+
* this is great for values that you want to reference from
|
|
439
|
+
* within a useCallback or useEffect event listener, without
|
|
440
|
+
* re-triggering the effect when the value changes
|
|
441
|
+
*
|
|
442
|
+
* @returns {{current: T}}
|
|
443
|
+
*/
|
|
444
|
+
|
|
445
|
+
const useUpdatingRef = value => {
|
|
446
|
+
const ref = useRef(value);
|
|
447
|
+
useEffect(() => {
|
|
448
|
+
ref.current = value;
|
|
449
|
+
}, [value]);
|
|
450
|
+
return ref;
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* A simple hook for using `setInterval`.
|
|
455
|
+
*
|
|
456
|
+
* @param action called every `intervalMs` when `active` is true
|
|
457
|
+
* @param intervalMs the duration between calls to `action`
|
|
458
|
+
* @param active whether or not the interval is active
|
|
459
|
+
*/
|
|
460
|
+
|
|
461
|
+
function useInterval(action, intervalMs, active) {
|
|
462
|
+
// We using a ref instead of a callback for `action` to avoid resetting
|
|
463
|
+
// the interval whenever the `action` changes.
|
|
464
|
+
const actionRef = useUpdatingRef(action);
|
|
465
|
+
useEffect(() => {
|
|
466
|
+
if (active) {
|
|
467
|
+
const intervalId = setInterval(() => {
|
|
468
|
+
actionRef.current();
|
|
469
|
+
}, intervalMs);
|
|
470
|
+
return () => {
|
|
471
|
+
clearInterval(intervalId);
|
|
472
|
+
};
|
|
473
|
+
} // actionRef isn't actually required, but react-hooks/exhaustive-deps
|
|
474
|
+
// doesn't recognize it as a ref and thus complains if it isn't in the
|
|
475
|
+
// deps list. It isn't a big deal though since the value ofactionRef
|
|
476
|
+
// never changes (only its contents do).
|
|
477
|
+
|
|
478
|
+
}, [intervalMs, active, actionRef]);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* A simple hook for using `setTimeout`.
|
|
483
|
+
*
|
|
484
|
+
* @param action called after `timeoutMs` when `active` is true
|
|
485
|
+
* @param timeoutMs the duration after which `action` is called
|
|
486
|
+
* @param active whether or not the interval is active
|
|
487
|
+
*/
|
|
488
|
+
|
|
489
|
+
function useTimeout(action, timeoutMs, active) {
|
|
490
|
+
// We using a ref instead of a callback for `action` to avoid resetting
|
|
491
|
+
// the interval whenever the `action` changes.
|
|
492
|
+
const actionRef = useUpdatingRef(action);
|
|
493
|
+
useEffect(() => {
|
|
494
|
+
if (active) {
|
|
495
|
+
const timeoutId = setTimeout(() => {
|
|
496
|
+
actionRef.current();
|
|
497
|
+
}, timeoutMs);
|
|
498
|
+
return () => {
|
|
499
|
+
clearTimeout(timeoutId);
|
|
500
|
+
};
|
|
501
|
+
} // actionRef isn't actually required, but react-hooks/exhaustive-deps
|
|
502
|
+
// doesn't recognize it as a ref and thus complains if it isn't in the
|
|
503
|
+
// deps list. It isn't a big deal though since the value ofactionRef
|
|
504
|
+
// never changes (only its contents do).
|
|
505
|
+
|
|
506
|
+
}, [timeoutMs, active, actionRef]);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function useScheduledInterval(action, intervalMs, options) {
|
|
510
|
+
var _options$schedulePoli;
|
|
511
|
+
|
|
512
|
+
if (typeof action !== "function") {
|
|
513
|
+
throw new Error("Action must be a function");
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
if (intervalMs < 1) {
|
|
517
|
+
throw new Error("Interval period must be >= 1");
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const schedulePolicy = (_options$schedulePoli = options == null ? void 0 : options.schedulePolicy) != null ? _options$schedulePoli : SchedulePolicy.Immediately;
|
|
521
|
+
const [isSet, setIsSet] = useState(schedulePolicy === SchedulePolicy.Immediately);
|
|
522
|
+
const set = useCallback(() => setIsSet(true), []);
|
|
523
|
+
const actionRef = useUpdatingRef(action);
|
|
524
|
+
const clear = useCallback(policy => {
|
|
525
|
+
var _policy;
|
|
526
|
+
|
|
527
|
+
policy = (_policy = policy) != null ? _policy : options == null ? void 0 : options.clearPolicy;
|
|
528
|
+
|
|
529
|
+
if (isSet && policy === ClearPolicy.Resolve) {
|
|
530
|
+
actionRef.current();
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
setIsSet(false);
|
|
534
|
+
}, // react-hooks/exhaustive-deps doesn't require refs to be
|
|
535
|
+
// listed in the deps array. Unfortunately, in this situation
|
|
536
|
+
// it doesn't recognized actionRef as a ref.
|
|
537
|
+
[actionRef, isSet, options == null ? void 0 : options.clearPolicy]);
|
|
538
|
+
const runOnUnmountRef = useUpdatingRef(isSet && (options == null ? void 0 : options.clearPolicy) === ClearPolicy.Resolve);
|
|
539
|
+
useEffect(() => {
|
|
540
|
+
return () => {
|
|
541
|
+
// This code will only run with the component using this
|
|
542
|
+
// hook is unmounted.
|
|
543
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
544
|
+
if (runOnUnmountRef.current) {
|
|
545
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
546
|
+
actionRef.current();
|
|
547
|
+
}
|
|
548
|
+
}; // This eslint rule doesn't realize actionRef and runOnUnmountRef
|
|
549
|
+
// a both refs and thus do not have to be listed as deps.
|
|
550
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
551
|
+
}, []);
|
|
552
|
+
useInterval(action, intervalMs, isSet);
|
|
553
|
+
return {
|
|
554
|
+
isSet,
|
|
555
|
+
set,
|
|
556
|
+
clear
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function useScheduledTimeout(action, timeoutMs, options) {
|
|
561
|
+
var _options$schedulePoli;
|
|
562
|
+
|
|
563
|
+
if (typeof action !== "function") {
|
|
564
|
+
throw new Error("Action must be a function");
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
if (timeoutMs < 0) {
|
|
568
|
+
throw new Error("Timeout period must be >= 0");
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
const schedulePolicy = (_options$schedulePoli = options == null ? void 0 : options.schedulePolicy) != null ? _options$schedulePoli : SchedulePolicy.Immediately;
|
|
572
|
+
const [isSet, setIsSet] = useState(schedulePolicy === SchedulePolicy.Immediately);
|
|
573
|
+
const set = useCallback(() => setIsSet(true), []); // This wrapper isn't present in useScheduledInterval because we
|
|
574
|
+
// don't need to update `isSet` in that situations.
|
|
575
|
+
|
|
576
|
+
const wrappedAction = useCallback(() => {
|
|
577
|
+
setIsSet(false);
|
|
578
|
+
action();
|
|
579
|
+
}, [action]);
|
|
580
|
+
const actionRef = useUpdatingRef(wrappedAction);
|
|
581
|
+
const clear = useCallback(policy => {
|
|
582
|
+
var _policy;
|
|
583
|
+
|
|
584
|
+
policy = (_policy = policy) != null ? _policy : options == null ? void 0 : options.clearPolicy;
|
|
585
|
+
|
|
586
|
+
if (isSet && policy === ClearPolicy.Resolve) {
|
|
587
|
+
actionRef.current();
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
setIsSet(false);
|
|
591
|
+
}, // react-hooks/exhaustive-deps doesn't require refs to be
|
|
592
|
+
// listed in the deps array. Unfortunately, in this situation
|
|
593
|
+
// it doesn't recognized actionRef as a ref.
|
|
594
|
+
[actionRef, isSet, options == null ? void 0 : options.clearPolicy]);
|
|
595
|
+
const runOnUnmountRef = useUpdatingRef(isSet && (options == null ? void 0 : options.clearPolicy) === ClearPolicy.Resolve);
|
|
596
|
+
useEffect(() => {
|
|
597
|
+
return () => {
|
|
598
|
+
// This code will only run with the component using this
|
|
599
|
+
// hook is unmounted.
|
|
600
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
601
|
+
if (runOnUnmountRef.current) {
|
|
602
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
603
|
+
actionRef.current();
|
|
604
|
+
}
|
|
605
|
+
}; // This eslint rule doesn't realize actionRef and runOnUnmountRef
|
|
606
|
+
// a both refs and thus do not have to be listed as deps.
|
|
607
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
608
|
+
}, []);
|
|
609
|
+
useTimeout(wrappedAction, timeoutMs, isSet);
|
|
610
|
+
return {
|
|
611
|
+
isSet,
|
|
612
|
+
set,
|
|
613
|
+
clear
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
export { ClearPolicy, SchedulePolicy, useInterval, useScheduledInterval, useScheduledTimeout, useTimeout, withActionScheduler };
|