@khanacademy/wonder-blocks-timing 1.2.3 → 2.0.3

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.
@@ -0,0 +1,152 @@
1
+ import {Meta, Story, Source, Canvas} from "@storybook/addon-docs";
2
+
3
+ import {Body, HeadingSmall} from "@khanacademy/wonder-blocks-typography";
4
+ import {View} from "@khanacademy/wonder-blocks-core";
5
+ import Button from "@khanacademy/wonder-blocks-button";
6
+
7
+ import {ClearPolicy, SchedulePolicy} from "../util/policies.js";
8
+ import {useTimeout} from "./use-timeout.js";
9
+
10
+ <Meta
11
+ title="Timing/useTimeout"
12
+ parameters={{
13
+ chromatic: {
14
+ disableSnapshot: true,
15
+ },
16
+ }}
17
+ />
18
+
19
+ # `useTimeout`
20
+
21
+ `useTimeout` is a hook that provides a convenient API for setting and clearing
22
+ a timeout. It is defined as follows:
23
+
24
+ ```ts
25
+ function useTimeout(
26
+ action: () => mixed,
27
+ timeoutMs: number,
28
+ options?: {|
29
+ schedulePolicy?: "schedule-immediately" | "schedule-on-demand",
30
+ clearPolicy?: "resolve-on-clear" | "cancel-on-clear",
31
+ |},
32
+ ): ITimeout;
33
+
34
+ interface ITimeout {
35
+ get isSet(): boolean;
36
+ set(): void;
37
+ clear(policy?: ClearPolicy): void;
38
+ }
39
+ ```
40
+
41
+ By default the timeout will be set immediately up creation. The `options` parameter can
42
+ be used to control when when the timeout is schedule and whether or not `action` should be
43
+ called when the timeout is cleared.
44
+
45
+ Notes:
46
+
47
+ - Because `clear` takes a param, it's import that you don't pass it directly to an event handler,
48
+ e.g. `<Button onClick={clear} />` will not work as expected.
49
+ - Calling `set` after the timeout has expired will restart the timeout.
50
+ - Updating the second paramter, `timeoutMs`, will also restart the timeout.
51
+ - When the component using this hooks is unmounted, the timeout will automatically be cleared.
52
+ - Calling `set` after the timeout is set but before it expires means that the timeout will be
53
+ reset and will call `action`, `timeoutMs` after the most recent call to `set` was made.
54
+
55
+ export const Immediately = () => {
56
+ const [callCount, setCallCount] = React.useState(0);
57
+ const callback = React.useCallback(() => {
58
+ setCallCount((callCount) => callCount + 1);
59
+ }, []);
60
+ const {isSet, set, clear} = useTimeout(callback, 1000);
61
+ return (
62
+ <View>
63
+ <View>isSet = {isSet.toString()}</View>
64
+ <View>callCount = {callCount}</View>
65
+ <View style={{flexDirection: "row"}}>
66
+ <Button onClick={set}>Set timeout</Button>
67
+ <Button onClick={clear}>Clear timeout</Button>
68
+ </View>
69
+ </View>
70
+ );
71
+ };
72
+
73
+ <Canvas>
74
+ <Story name="Immediately">
75
+ <Immediately />
76
+ </Story>
77
+ </Canvas>
78
+
79
+ ```jsx
80
+ const Immediately = () => {
81
+ const [callCount, setCallCount] = React.useState(0);
82
+ const callback = React.useCallback(() => {
83
+ setCallCount((callCount) => callCount + 1);
84
+ }, []);
85
+ const {isSet, set, clear} = useTimeout(callback, 1000);
86
+ return (
87
+ <View>
88
+ <View>isSet = {isSet.toString()}</View>
89
+ <View>callCount = {callCount}</View>
90
+ <View style={{flexDirection: "row"}}>
91
+ <Button onClick={() => set()}>Set timeout</Button>
92
+ <Button onClick={() => clear()}>Clear timeout</Button>
93
+ </View>
94
+ </View>
95
+ );
96
+ };
97
+ ```
98
+
99
+ export const OnDemandAndResolveOnClear = () => {
100
+ const [callCount, setCallCount] = React.useState(0);
101
+ const callback = React.useCallback(() => {
102
+ console.log("action called");
103
+ setCallCount((callCount) => callCount + 1);
104
+ }, []);
105
+ const {isSet, set, clear} = useTimeout(callback, 1000, {
106
+ clearPolicy: ClearPolicy.Resolve,
107
+ schedulePolicy: SchedulePolicy.OnDemand,
108
+ });
109
+ return (
110
+ <View>
111
+ <View>isSet = {isSet.toString()}</View>
112
+ <View>callCount = {callCount}</View>
113
+ <View style={{flexDirection: "row"}}>
114
+ <Button onClick={() => set()}>Set timeout</Button>
115
+ <Button onClick={() => clear()}>Clear timeout</Button>
116
+ </View>
117
+ </View>
118
+ );
119
+ };
120
+
121
+ <Canvas>
122
+ <Story name="OnDemandAndResolveOnClear">
123
+ <OnDemandAndResolveOnClear />
124
+ </Story>
125
+ </Canvas>
126
+
127
+ ```jsx
128
+ const OnDemandAndResolveOnClear = () => {
129
+ const [callCount, setCallCount] = React.useState(0);
130
+ const callback = React.useCallback(() => {
131
+ setCallCount((callCount) => callCount + 1);
132
+ }, []);
133
+ const {isSet, set, clear} = useTimeout(
134
+ callback,
135
+ 1000,
136
+ {
137
+ clearPolicy: ClearPolicy.Resolve,
138
+ schedulePolicy: SchedulePolicy.OnDemand,
139
+ },
140
+ );
141
+ return (
142
+ <View>
143
+ <View>isSet = {isSet.toString()}</View>
144
+ <View>callCount = {callCount}</View>
145
+ <View style={{flexDirection: "row"}}>
146
+ <Button onClick={() => set()}>Set timeout</Button>
147
+ <Button onClick={() => clear()}>Clear timeout</Button>
148
+ </View>
149
+ </View>
150
+ );
151
+ };
152
+ ```
package/src/index.js CHANGED
@@ -19,4 +19,6 @@ export type {
19
19
  WithoutActionScheduler,
20
20
  };
21
21
 
22
+ export {SchedulePolicy, ClearPolicy} from "./util/policies.js";
22
23
  export {default as withActionScheduler} from "./components/with-action-scheduler.js";
24
+ export {useTimeout} from "./hooks/use-timeout.js";
@@ -3,6 +3,7 @@ import ActionScheduler from "../action-scheduler.js";
3
3
  import Timeout from "../timeout.js";
4
4
  import Interval from "../interval.js";
5
5
  import AnimationFrame from "../animation-frame.js";
6
+ import {SchedulePolicy, ClearPolicy} from "../policies.js";
6
7
 
7
8
  jest.mock("../timeout.js");
8
9
  jest.mock("../interval.js");
@@ -38,55 +39,54 @@ describe("ActionScheduler", () => {
38
39
  expect(result).toBeDefined();
39
40
  });
40
41
 
41
- it("should pass arguments to Timeout", () => {
42
+ it("should pass schedule policy to Timeout", () => {
42
43
  // Arrange
43
44
  const actionScheduler = new ActionScheduler();
44
45
  const action = jest.fn();
46
+ const options = {
47
+ schedulePolicy: SchedulePolicy.Immediately,
48
+ };
45
49
 
46
50
  // Act
47
- actionScheduler.timeout(action, 42, true);
51
+ actionScheduler.timeout(action, 42, options);
48
52
 
49
53
  // Assert
50
- expect(Timeout).toHaveBeenCalledWith(action, 42, true);
54
+ expect(Timeout).toHaveBeenCalledWith(
55
+ action,
56
+ 42,
57
+ SchedulePolicy.Immediately,
58
+ );
51
59
  });
52
60
 
53
- describe("clearOnResolve", () => {
54
- it("when true, should call timeout.clear(true) on clearAll", () => {
55
- // Arrange
56
- const actionScheduler = new ActionScheduler();
57
- const action = jest.fn();
58
- const testTimeout = actionScheduler.timeout(
59
- action,
60
- 42,
61
- false,
62
- true,
63
- );
61
+ it("should pass clear policy to timeout.clear on clearAll", () => {
62
+ // Arrange
63
+ const actionScheduler = new ActionScheduler();
64
+ const action = jest.fn();
65
+ const options = {
66
+ clearPolicy: ClearPolicy.Resolve,
67
+ };
68
+ const testTimeout = actionScheduler.timeout(action, 42, options);
64
69
 
65
- // Act
66
- actionScheduler.clearAll();
70
+ // Act
71
+ actionScheduler.clearAll();
67
72
 
68
- // Assert
69
- // $FlowIgnore[method-unbinding]
70
- expect(testTimeout.clear).toHaveBeenCalledWith(true);
71
- });
73
+ // Assert
74
+ // $FlowIgnore[method-unbinding]
75
+ expect(testTimeout.clear).toHaveBeenCalledWith(ClearPolicy.Resolve);
76
+ });
72
77
 
73
- it("when falsy, should call timeout.clear() on clearAll", () => {
78
+ describe("when scheduler is disabled", () => {
79
+ it("should return a noop timeout", () => {
74
80
  // Arrange
75
81
  const actionScheduler = new ActionScheduler();
76
82
  const action = jest.fn();
77
- const testTimeout = actionScheduler.timeout(
78
- action,
79
- 42,
80
- false,
81
- false,
82
- );
83
83
 
84
84
  // Act
85
- actionScheduler.clearAll();
85
+ actionScheduler.disable();
86
+ const result = actionScheduler.timeout(action, 42);
86
87
 
87
88
  // Assert
88
- // $FlowIgnore[method-unbinding]
89
- expect(testTimeout.clear).toHaveBeenCalledWith(false);
89
+ expect(result).toBe(ActionScheduler.NoopAction);
90
90
  });
91
91
  });
92
92
  });
@@ -104,55 +104,56 @@ describe("ActionScheduler", () => {
104
104
  expect(result).toBeDefined();
105
105
  });
106
106
 
107
- it("should pass arguments to Interval", () => {
107
+ it("should pass schedule policy to Interval", () => {
108
108
  // Arrange
109
109
  const actionScheduler = new ActionScheduler();
110
110
  const action = jest.fn();
111
+ const options = {
112
+ schedulePolicy: SchedulePolicy.Immediately,
113
+ };
111
114
 
112
115
  // Act
113
- actionScheduler.interval(action, 42, true);
116
+ actionScheduler.interval(action, 42, options);
114
117
 
115
118
  // Assert
116
- expect(Interval).toHaveBeenCalledWith(action, 42, true);
119
+ expect(Interval).toHaveBeenCalledWith(
120
+ action,
121
+ 42,
122
+ SchedulePolicy.Immediately,
123
+ );
117
124
  });
118
125
 
119
- describe("clearOnResolve", () => {
120
- it("when true, should call interval.clear(true) on clearAll", () => {
121
- // Arrange
122
- const actionScheduler = new ActionScheduler();
123
- const action = jest.fn();
124
- const testInterval = actionScheduler.interval(
125
- action,
126
- 42,
127
- false,
128
- true,
129
- );
126
+ it("should pass clear policy to interval.clear on clearAll", () => {
127
+ // Arrange
128
+ const actionScheduler = new ActionScheduler();
129
+ const action = jest.fn();
130
+ const options = {
131
+ clearPolicy: ClearPolicy.Resolve,
132
+ };
133
+ const testInterval = actionScheduler.interval(action, 42, options);
130
134
 
131
- // Act
132
- actionScheduler.clearAll();
135
+ // Act
136
+ actionScheduler.clearAll();
133
137
 
134
- // Assert
135
- // $FlowIgnore[method-unbinding]
136
- expect(testInterval.clear).toHaveBeenCalledWith(true);
137
- });
138
+ // Assert
139
+ // $FlowIgnore[method-unbinding]
140
+ expect(testInterval.clear).toHaveBeenCalledWith(
141
+ ClearPolicy.Resolve,
142
+ );
143
+ });
138
144
 
139
- it("when falsy, should call interval.clear() on clearAll", () => {
145
+ describe("when scheduler is disabled", () => {
146
+ it("should return a noop interval", () => {
140
147
  // Arrange
141
148
  const actionScheduler = new ActionScheduler();
142
149
  const action = jest.fn();
143
- const testInterval = actionScheduler.interval(
144
- action,
145
- 42,
146
- false,
147
- false,
148
- );
149
150
 
150
151
  // Act
151
- actionScheduler.clearAll();
152
+ actionScheduler.disable();
153
+ const result = actionScheduler.interval(action, 42);
152
154
 
153
155
  // Assert
154
- // $FlowIgnore[method-unbinding]
155
- expect(testInterval.clear).toHaveBeenCalledWith(false);
156
+ expect(result).toBe(ActionScheduler.NoopAction);
156
157
  });
157
158
  });
158
159
  });
@@ -170,53 +171,53 @@ describe("ActionScheduler", () => {
170
171
  expect(result).toBeDefined();
171
172
  });
172
173
 
173
- it("should pass arguments to AnimationFrame", () => {
174
+ it("should pass schedule policy to AnimationFrame", () => {
174
175
  // Arrange
175
176
  const actionScheduler = new ActionScheduler();
176
177
  const action = jest.fn();
178
+ const options = {
179
+ schedulePolicy: SchedulePolicy.Immediately,
180
+ };
177
181
 
178
182
  // Act
179
- actionScheduler.animationFrame(action, true);
183
+ actionScheduler.animationFrame(action, options);
180
184
 
181
185
  // Assert
182
- expect(AnimationFrame).toHaveBeenCalledWith(action, true);
186
+ expect(AnimationFrame).toHaveBeenCalledWith(
187
+ action,
188
+ SchedulePolicy.Immediately,
189
+ );
183
190
  });
184
191
 
185
- describe("clearOnResolve", () => {
186
- it("when true, should call animationFrame.clear(true) on clearAll", () => {
187
- // Arrange
188
- const actionScheduler = new ActionScheduler();
189
- const action = jest.fn();
190
- const testFrame = actionScheduler.animationFrame(
191
- action,
192
- false,
193
- true,
194
- );
192
+ it("should pass clear policy to animationFrame.clear on clearAll", () => {
193
+ // Arrange
194
+ const actionScheduler = new ActionScheduler();
195
+ const action = jest.fn();
196
+ const options = {
197
+ clearPolicy: ClearPolicy.Resolve,
198
+ };
199
+ const testFrame = actionScheduler.animationFrame(action, options);
195
200
 
196
- // Act
197
- actionScheduler.clearAll();
201
+ // Act
202
+ actionScheduler.clearAll();
198
203
 
199
- // Assert
200
- // $FlowIgnore[method-unbinding]
201
- expect(testFrame.clear).toHaveBeenCalledWith(true);
202
- });
204
+ // Assert
205
+ // $FlowIgnore[method-unbinding]
206
+ expect(testFrame.clear).toHaveBeenCalledWith(ClearPolicy.Resolve);
207
+ });
203
208
 
204
- it("when falsy, should call animationFrame.clear() on clearAll", () => {
209
+ describe("when scheduler is disabled", () => {
210
+ it("should return a noop interval", () => {
205
211
  // Arrange
206
212
  const actionScheduler = new ActionScheduler();
207
213
  const action = jest.fn();
208
- const testFrame = actionScheduler.animationFrame(
209
- action,
210
- false,
211
- false,
212
- );
213
214
 
214
215
  // Act
215
- actionScheduler.clearAll();
216
+ actionScheduler.disable();
217
+ const result = actionScheduler.animationFrame(action);
216
218
 
217
219
  // Assert
218
- // $FlowIgnore[method-unbinding]
219
- expect(testFrame.clear).toHaveBeenCalledWith(false);
220
+ expect(result).toBe(ActionScheduler.NoopAction);
220
221
  });
221
222
  });
222
223
  });
@@ -263,4 +264,18 @@ describe("ActionScheduler", () => {
263
264
  expect(animationFrame.clear).toHaveBeenCalledTimes(1);
264
265
  });
265
266
  });
267
+
268
+ describe("#disable", () => {
269
+ it("should clear all scheduled actions", () => {
270
+ // Arrange
271
+ const actionScheduler = new ActionScheduler();
272
+ const clearAllSpy = jest.spyOn(actionScheduler, "clearAll");
273
+
274
+ // Act
275
+ actionScheduler.disable();
276
+
277
+ // Assert
278
+ expect(clearAllSpy).toHaveBeenCalledTimes(1);
279
+ });
280
+ });
266
281
  });
@@ -1,5 +1,6 @@
1
1
  // @flow
2
2
  import AnimationFrame from "../animation-frame.js";
3
+ import {SchedulePolicy, ClearPolicy} from "../policies.js";
3
4
 
4
5
  describe("AnimationFrame", () => {
5
6
  beforeEach(() => {
@@ -8,14 +9,12 @@ describe("AnimationFrame", () => {
8
9
  // Jest doesn't fake out the animation frame API, so we're going to do
9
10
  // it here and map it to timeouts, that way we can use the fake timer
10
11
  // API to test our animation frame things.
11
- jest.spyOn(
12
- global,
13
- "requestAnimationFrame",
14
- ).mockImplementation((fn, ...args) => setTimeout(fn, 0));
15
- jest.spyOn(
16
- global,
17
- "cancelAnimationFrame",
18
- ).mockImplementation((id, ...args) => clearTimeout(id));
12
+ jest.spyOn(global, "requestAnimationFrame").mockImplementation(
13
+ (fn, ...args) => setTimeout(fn, 0),
14
+ );
15
+ jest.spyOn(global, "cancelAnimationFrame").mockImplementation(
16
+ (id, ...args) => clearTimeout(id),
17
+ );
19
18
  });
20
19
 
21
20
  afterEach(() => {
@@ -45,12 +44,12 @@ describe("AnimationFrame", () => {
45
44
  );
46
45
  });
47
46
 
48
- it("requests an animation frame when autoSchedule is true", () => {
47
+ it("requests an animation frame when schedule policy is SchedulePolicy.Immediately", () => {
49
48
  // Arrange
50
49
 
51
50
  // Act
52
51
  // eslint-disable-next-line no-new
53
- new AnimationFrame(() => {}, true);
52
+ new AnimationFrame(() => {}, SchedulePolicy.Immediately);
54
53
 
55
54
  // Assert
56
55
  expect(requestAnimationFrame).toHaveBeenCalledTimes(1);
@@ -60,7 +59,8 @@ describe("AnimationFrame", () => {
60
59
  describe("isSet", () => {
61
60
  it("is false when the request has not been set", () => {
62
61
  // Arrange
63
- const animationFrame = new AnimationFrame(() => {}, false);
62
+ const animationFrame = new AnimationFrame(() => {},
63
+ SchedulePolicy.OnDemand);
64
64
 
65
65
  // Act
66
66
  const result = animationFrame.isSet;
@@ -144,7 +144,10 @@ describe("AnimationFrame", () => {
144
144
  it("should set the timeout again if it has already executed", () => {
145
145
  // Arrange
146
146
  const action = jest.fn();
147
- const animationFrame = new AnimationFrame(action, false);
147
+ const animationFrame = new AnimationFrame(
148
+ action,
149
+ SchedulePolicy.OnDemand,
150
+ );
148
151
  animationFrame.set();
149
152
  jest.runOnlyPendingTimers();
150
153
 
@@ -171,7 +174,7 @@ describe("AnimationFrame", () => {
171
174
  expect(action).not.toHaveBeenCalled();
172
175
  });
173
176
 
174
- it("should invoke the action if resolve is true", () => {
177
+ it("should invoke the action if clear policy is ClearPolicy.Resolve", () => {
175
178
  // Arrange
176
179
  jest.spyOn(performance, "now").mockReturnValue(42);
177
180
  const action = jest.fn();
@@ -179,7 +182,7 @@ describe("AnimationFrame", () => {
179
182
  animationFrame.set();
180
183
 
181
184
  // Act
182
- animationFrame.clear(true);
185
+ animationFrame.clear(ClearPolicy.Resolve);
183
186
  jest.runOnlyPendingTimers();
184
187
 
185
188
  // Assert
@@ -187,26 +190,32 @@ describe("AnimationFrame", () => {
187
190
  expect(action).toHaveBeenCalledWith(42);
188
191
  });
189
192
 
190
- it("should not invoke the action if resolve is false", () => {
193
+ it("should not invoke the action if clear policy is ClearPolicy.Cancel", () => {
191
194
  // Arrange
192
195
  const action = jest.fn();
193
- const animationFrame = new AnimationFrame(action, true);
196
+ const animationFrame = new AnimationFrame(
197
+ action,
198
+ SchedulePolicy.Immediately,
199
+ );
194
200
 
195
201
  // Act
196
- animationFrame.clear(false);
202
+ animationFrame.clear(ClearPolicy.Cancel);
197
203
  jest.runOnlyPendingTimers();
198
204
 
199
205
  // Assert
200
206
  expect(action).not.toHaveBeenCalled();
201
207
  });
202
208
 
203
- it("should not invoke the action if timeout is not pending", () => {
209
+ it("should not invoke the action if timeout is not pending and clear policy is ClearPolicy.Resolve", () => {
204
210
  // Arrange
205
211
  const action = jest.fn();
206
- const animationFrame = new AnimationFrame(action, false);
212
+ const animationFrame = new AnimationFrame(
213
+ action,
214
+ SchedulePolicy.OnDemand,
215
+ );
207
216
 
208
217
  // Act
209
- animationFrame.clear(true);
218
+ animationFrame.clear(ClearPolicy.Resolve);
210
219
  jest.runOnlyPendingTimers();
211
220
 
212
221
  // Assert