@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 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 { Component, forwardRef, createElement } 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",
@@ -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
- export { ClearPolicy, SchedulePolicy, withActionScheduler };
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 };