@khanacademy/wonder-blocks-tooltip 1.3.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.
Files changed (36) hide show
  1. package/LICENSE +21 -0
  2. package/dist/es/index.js +1133 -0
  3. package/dist/index.js +1389 -0
  4. package/dist/index.js.flow +2 -0
  5. package/docs.md +11 -0
  6. package/package.json +37 -0
  7. package/src/__tests__/__snapshots__/generated-snapshot.test.js.snap +2674 -0
  8. package/src/__tests__/generated-snapshot.test.js +475 -0
  9. package/src/components/__tests__/__snapshots__/tooltip-tail.test.js.snap +9 -0
  10. package/src/components/__tests__/__snapshots__/tooltip.test.js.snap +47 -0
  11. package/src/components/__tests__/tooltip-anchor.test.js +987 -0
  12. package/src/components/__tests__/tooltip-bubble.test.js +80 -0
  13. package/src/components/__tests__/tooltip-popper.test.js +71 -0
  14. package/src/components/__tests__/tooltip-tail.test.js +117 -0
  15. package/src/components/__tests__/tooltip.integration.test.js +79 -0
  16. package/src/components/__tests__/tooltip.test.js +401 -0
  17. package/src/components/tooltip-anchor.js +330 -0
  18. package/src/components/tooltip-bubble.js +150 -0
  19. package/src/components/tooltip-bubble.md +92 -0
  20. package/src/components/tooltip-content.js +76 -0
  21. package/src/components/tooltip-content.md +34 -0
  22. package/src/components/tooltip-popper.js +101 -0
  23. package/src/components/tooltip-tail.js +462 -0
  24. package/src/components/tooltip-tail.md +143 -0
  25. package/src/components/tooltip.js +235 -0
  26. package/src/components/tooltip.md +194 -0
  27. package/src/components/tooltip.stories.js +76 -0
  28. package/src/index.js +12 -0
  29. package/src/util/__tests__/__snapshots__/active-tracker.test.js.snap +3 -0
  30. package/src/util/__tests__/__snapshots__/ref-tracker.test.js.snap +3 -0
  31. package/src/util/__tests__/active-tracker.test.js +142 -0
  32. package/src/util/__tests__/ref-tracker.test.js +153 -0
  33. package/src/util/active-tracker.js +94 -0
  34. package/src/util/constants.js +7 -0
  35. package/src/util/ref-tracker.js +46 -0
  36. package/src/util/types.js +29 -0
@@ -0,0 +1,987 @@
1
+ /* eslint-disable max-lines */
2
+ /* eslint-disable no-unused-vars */
3
+ // @flow
4
+ import * as React from "react";
5
+ import {View} from "@khanacademy/wonder-blocks-core";
6
+ import {mount} from "enzyme";
7
+
8
+ import TooltipAnchor from "../tooltip-anchor.js";
9
+ import {
10
+ TooltipAppearanceDelay,
11
+ TooltipDisappearanceDelay,
12
+ } from "../../util/constants.js";
13
+
14
+ jest.mock("../../util/active-tracker.js");
15
+
16
+ describe("TooltipAnchor", () => {
17
+ beforeEach(async () => {
18
+ // $FlowIgnore[method-unbinding]
19
+ if (typeof document.addEventListener.mockReset === "function") {
20
+ // $FlowIgnore[method-unbinding]
21
+ document.addEventListener.mockRestore();
22
+ }
23
+ // $FlowIgnore[method-unbinding]
24
+ if (typeof document.removeEventListener.mockReset === "function") {
25
+ // $FlowIgnore[method-unbinding]
26
+ document.removeEventListener.mockRestore();
27
+ }
28
+ jest.clearAllTimers();
29
+ jest.useFakeTimers();
30
+
31
+ const {default: ActiveTracker} = await import(
32
+ "../../util/active-tracker.js"
33
+ );
34
+ // We know there's one global instance of this import, so let's
35
+ // reset it.
36
+ // Flow doesn't know this is a mock
37
+ // $FlowFixMe[prop-missing]
38
+ const mockTracker = ActiveTracker.mock.instances[0];
39
+ mockTracker.steal.mockClear();
40
+ mockTracker.giveup.mockClear();
41
+ });
42
+
43
+ test("on mount, subscribes to focus and hover events", () => {
44
+ // Arrange
45
+ const nodes = (
46
+ <TooltipAnchor anchorRef={() => {}} onActiveChanged={() => {}}>
47
+ Anchor text
48
+ </TooltipAnchor>
49
+ );
50
+ const addEventListenerSpy = jest.spyOn(
51
+ HTMLElement.prototype,
52
+ "addEventListener",
53
+ );
54
+ addEventListenerSpy.mockClear();
55
+
56
+ // Act
57
+ mount(nodes);
58
+
59
+ // Assert
60
+ expect(addEventListenerSpy).toHaveBeenCalledWith(
61
+ "focusin",
62
+ expect.any(Function),
63
+ );
64
+ expect(addEventListenerSpy).toHaveBeenCalledWith(
65
+ "focusout",
66
+ expect.any(Function),
67
+ );
68
+ expect(addEventListenerSpy).toHaveBeenCalledWith(
69
+ "mouseenter",
70
+ expect.any(Function),
71
+ );
72
+ expect(addEventListenerSpy).toHaveBeenCalledWith(
73
+ "mouseleave",
74
+ expect.any(Function),
75
+ );
76
+ });
77
+
78
+ test("on unmount, unsubscribes from focus and hover events", () => {
79
+ // Arrange
80
+ const nodes = (
81
+ <TooltipAnchor anchorRef={() => {}} onActiveChanged={() => {}}>
82
+ Anchor text
83
+ </TooltipAnchor>
84
+ );
85
+ const removeEventListenerSpy = jest.spyOn(
86
+ HTMLElement.prototype,
87
+ "removeEventListener",
88
+ );
89
+ removeEventListenerSpy.mockClear();
90
+ const wrapper = mount(nodes);
91
+
92
+ // Act
93
+ wrapper.unmount();
94
+
95
+ // Assert
96
+ expect(removeEventListenerSpy).toHaveBeenCalledWith(
97
+ "focusin",
98
+ expect.any(Function),
99
+ );
100
+ expect(removeEventListenerSpy).toHaveBeenCalledWith(
101
+ "focusout",
102
+ expect.any(Function),
103
+ );
104
+ expect(removeEventListenerSpy).toHaveBeenCalledWith(
105
+ "mouseenter",
106
+ expect.any(Function),
107
+ );
108
+ expect(removeEventListenerSpy).toHaveBeenCalledWith(
109
+ "mouseleave",
110
+ expect.any(Function),
111
+ );
112
+ });
113
+
114
+ describe("forceAnchorFocusivity is true", () => {
115
+ test("if not set, sets tabindex on anchor target", async () => {
116
+ // Arrange
117
+ const ref = await new Promise((resolve) => {
118
+ const nodes = (
119
+ <TooltipAnchor
120
+ forceAnchorFocusivity={true}
121
+ anchorRef={resolve}
122
+ onActiveChanged={() => {}}
123
+ >
124
+ <View id="portal">This is the anchor</View>
125
+ </TooltipAnchor>
126
+ );
127
+ mount(nodes);
128
+ });
129
+
130
+ // Act
131
+ const result = ref && ref.getAttribute("tabindex");
132
+
133
+ // Assert
134
+ expect(result).toBe("0");
135
+ });
136
+
137
+ test("if tabindex already set, leaves it as-is", async () => {
138
+ // Arrange
139
+ const ref = await new Promise((resolve) => {
140
+ const nodes = (
141
+ <TooltipAnchor
142
+ forceAnchorFocusivity={true}
143
+ anchorRef={resolve}
144
+ onActiveChanged={() => {}}
145
+ >
146
+ <View tabIndex={-1}>This is the anchor</View>
147
+ </TooltipAnchor>
148
+ );
149
+ mount(nodes);
150
+ });
151
+
152
+ // Act
153
+ const result = ref && ref.getAttribute("tabindex");
154
+
155
+ // Assert
156
+ expect(result).toBe("-1");
157
+ });
158
+ });
159
+
160
+ describe("forceAnchorFocusivity is false", () => {
161
+ test("does not set tabindex on anchor target", async () => {
162
+ // Arrange
163
+ const ref = await new Promise((resolve) => {
164
+ const nodes = (
165
+ <TooltipAnchor
166
+ forceAnchorFocusivity={false}
167
+ anchorRef={resolve}
168
+ onActiveChanged={() => {}}
169
+ >
170
+ <View>This is the anchor</View>
171
+ </TooltipAnchor>
172
+ );
173
+ mount(nodes);
174
+ });
175
+
176
+ // Act
177
+ const result = ref && ref.getAttribute("tabindex");
178
+
179
+ // Assert
180
+ expect(result).toBeNull();
181
+ });
182
+
183
+ test("if we had added tabindex, removes it", async () => {
184
+ // Arrange
185
+ let wrapper;
186
+ const ref = await new Promise((resolve) => {
187
+ const TestFixture = (props: any) => (
188
+ <TooltipAnchor
189
+ forceAnchorFocusivity={props.force}
190
+ anchorRef={resolve}
191
+ onActiveChanged={() => {}}
192
+ >
193
+ <View>This is the anchor</View>
194
+ </TooltipAnchor>
195
+ );
196
+ wrapper = mount(<TestFixture force={true} />);
197
+ });
198
+
199
+ // Act
200
+ const tabindex = ref && ref.getAttribute("tabindex");
201
+ expect(tabindex).toBe("0");
202
+
203
+ wrapper && wrapper.setProps({force: false});
204
+ const result = ref && ref.getAttribute("tabindex");
205
+
206
+ // Assert
207
+ expect(result).toBeNull();
208
+ });
209
+
210
+ test("if we had not added tabindex, leaves it", async () => {
211
+ // Arrange
212
+ const ref = await new Promise((resolve) => {
213
+ const TestFixture = (props: any) => (
214
+ <TooltipAnchor
215
+ forceAnchorFocusivity={props.force}
216
+ anchorRef={resolve}
217
+ onActiveChanged={() => {}}
218
+ >
219
+ <View tabIndex={-1}>This is the anchor</View>
220
+ </TooltipAnchor>
221
+ );
222
+ const wrapper = mount(<TestFixture force={true} />);
223
+ wrapper.setProps({force: false});
224
+ });
225
+
226
+ // Act
227
+ const result = ref && ref.getAttribute("tabindex");
228
+
229
+ // Assert
230
+ expect(result).not.toBeNull();
231
+ });
232
+ });
233
+
234
+ describe("receives keyboard focus", () => {
235
+ test("active state was not stolen, delays set active", async () => {
236
+ // Arrange
237
+ const {default: ActiveTracker} = await import(
238
+ "../../util/active-tracker.js"
239
+ );
240
+ // Let's tell the tooltip it isn't stealing and therefore it should
241
+ // be using a delay to show the tooltip.
242
+ // Flow doesn't know this is a mock
243
+ // $FlowFixMe[prop-missing]
244
+ const mockTracker = ActiveTracker.mock.instances[0];
245
+ mockTracker.steal.mockImplementationOnce(() => false);
246
+
247
+ let activeState = false;
248
+
249
+ const ref = await new Promise((resolve) => {
250
+ const nodes = (
251
+ <TooltipAnchor
252
+ anchorRef={resolve}
253
+ onActiveChanged={(active) => {
254
+ activeState = active;
255
+ }}
256
+ >
257
+ Anchor Text
258
+ </TooltipAnchor>
259
+ );
260
+ mount(nodes);
261
+ });
262
+
263
+ // Act
264
+ // Let's fake a focusin (this is the event that the anchor gets
265
+ // whether focused directly or a child is focused). We have to
266
+ // fake directly because there's no real browser here handling
267
+ // focus and real events.
268
+ ref && ref.dispatchEvent(new FocusEvent("focusin"));
269
+ // Check that we didn't go active before the delay
270
+ expect(activeState).toBe(false);
271
+ expect(setTimeout).toHaveBeenLastCalledWith(
272
+ expect.any(Function),
273
+ TooltipAppearanceDelay,
274
+ );
275
+ jest.runOnlyPendingTimers();
276
+
277
+ // Assert
278
+ expect(activeState).toBe(true);
279
+ });
280
+
281
+ test("active state was stolen, set active immediately", async () => {
282
+ // Arrange
283
+ const {default: ActiveTracker} = await import(
284
+ "../../util/active-tracker.js"
285
+ );
286
+ // Let's tell the tooltip it is stealing and therefore it should
287
+ // not be using a delay to show the tooltip.
288
+ // Flow doesn't know this is a mock
289
+ // $FlowFixMe[prop-missing]
290
+ const mockTracker = ActiveTracker.mock.instances[0];
291
+ mockTracker.steal.mockImplementationOnce(() => true);
292
+
293
+ let activeState = false;
294
+ const ref = await new Promise((resolve) => {
295
+ const nodes = (
296
+ <TooltipAnchor
297
+ anchorRef={resolve}
298
+ onActiveChanged={(active) => {
299
+ activeState = active;
300
+ }}
301
+ >
302
+ Anchor Text
303
+ </TooltipAnchor>
304
+ );
305
+ mount(nodes);
306
+ });
307
+
308
+ // Act
309
+ // Let's fake a focusin (this is the event that the anchor gets
310
+ // whether focused directly or a child is focused). We have to
311
+ // fake directly because there's no real browser here handling
312
+ // focus and real events.
313
+ ref && ref.dispatchEvent(new FocusEvent("focusin"));
314
+
315
+ // Assert
316
+ expect(activeState).toBe(true);
317
+ });
318
+ });
319
+
320
+ describe("loses keyboard focus", () => {
321
+ test("active state was not stolen, active is set to false with delay", async () => {
322
+ // Arrange
323
+ let activeState = false;
324
+ const ref = await new Promise((resolve) => {
325
+ const nodes = (
326
+ <TooltipAnchor
327
+ anchorRef={resolve}
328
+ onActiveChanged={(active) => {
329
+ activeState = active;
330
+ }}
331
+ >
332
+ Anchor Text
333
+ </TooltipAnchor>
334
+ );
335
+ mount(nodes);
336
+ });
337
+
338
+ ref && ref.dispatchEvent(new FocusEvent("focusin"));
339
+ expect(setTimeout).toHaveBeenLastCalledWith(
340
+ expect.any(Function),
341
+ TooltipAppearanceDelay,
342
+ );
343
+ jest.runOnlyPendingTimers();
344
+ expect(activeState).toBe(true);
345
+
346
+ // Act
347
+ ref && ref.dispatchEvent(new FocusEvent("focusout"));
348
+ expect(activeState).toBe(true);
349
+ expect(setTimeout).toHaveBeenLastCalledWith(
350
+ expect.any(Function),
351
+ TooltipDisappearanceDelay,
352
+ );
353
+ jest.runOnlyPendingTimers();
354
+
355
+ // Assert
356
+ expect(activeState).toBe(false);
357
+ });
358
+
359
+ test("active state was not stolen, gives up active state", async () => {
360
+ // Arrange
361
+ const {default: ActiveTracker} = await import(
362
+ "../../util/active-tracker.js"
363
+ );
364
+ // Flow doesn't know this is a mock
365
+ // $FlowFixMe[prop-missing]
366
+ const mockTracker = ActiveTracker.mock.instances[0];
367
+
368
+ let activeState = false;
369
+ const ref = await new Promise((resolve) => {
370
+ const nodes = (
371
+ <TooltipAnchor
372
+ anchorRef={resolve}
373
+ onActiveChanged={(active) => {
374
+ activeState = active;
375
+ }}
376
+ >
377
+ Anchor Text
378
+ </TooltipAnchor>
379
+ );
380
+ mount(nodes);
381
+ });
382
+
383
+ ref && ref.dispatchEvent(new FocusEvent("focusin"));
384
+ expect(setTimeout).toHaveBeenLastCalledWith(
385
+ expect.any(Function),
386
+ TooltipAppearanceDelay,
387
+ );
388
+ jest.runOnlyPendingTimers();
389
+ expect(activeState).toBe(true);
390
+
391
+ // Act
392
+ ref && ref.dispatchEvent(new FocusEvent("focusout"));
393
+ expect(activeState).toBe(true);
394
+ expect(setTimeout).toHaveBeenLastCalledWith(
395
+ expect.any(Function),
396
+ TooltipDisappearanceDelay,
397
+ );
398
+ jest.runOnlyPendingTimers();
399
+
400
+ // Assert
401
+ expect(mockTracker.giveup).toHaveBeenCalledTimes(1);
402
+ });
403
+
404
+ test("active state was stolen, active is set to false immediately", async () => {
405
+ // Arrange
406
+ let wrapper;
407
+ let activeState = false;
408
+ const ref = await new Promise((resolve) => {
409
+ const nodes = (
410
+ <TooltipAnchor
411
+ anchorRef={resolve}
412
+ onActiveChanged={(active) => {
413
+ activeState = active;
414
+ }}
415
+ >
416
+ Anchor Text
417
+ </TooltipAnchor>
418
+ );
419
+ wrapper = mount(nodes);
420
+ });
421
+
422
+ ref && ref.dispatchEvent(new FocusEvent("focusin"));
423
+ expect(setTimeout).toHaveBeenLastCalledWith(
424
+ expect.any(Function),
425
+ TooltipAppearanceDelay,
426
+ );
427
+ jest.runOnlyPendingTimers();
428
+ expect(activeState).toBe(true);
429
+
430
+ // Act
431
+ ref && ref.dispatchEvent(new FocusEvent("focusout"));
432
+ wrapper && wrapper.instance().activeStateStolen();
433
+
434
+ // Assert
435
+ expect(activeState).toBe(false);
436
+ });
437
+
438
+ test("active state was stolen, so it does not have it to give up", async () => {
439
+ // Arrange
440
+ const {default: ActiveTracker} = await import(
441
+ "../../util/active-tracker.js"
442
+ );
443
+ // Flow doesn't know this is a mock
444
+ // $FlowFixMe[prop-missing]
445
+ const mockTracker = ActiveTracker.mock.instances[0];
446
+ // Arrange
447
+ let wrapper;
448
+ let activeState = false;
449
+ const ref = await new Promise((resolve) => {
450
+ const nodes = (
451
+ <TooltipAnchor
452
+ anchorRef={resolve}
453
+ onActiveChanged={(active) => {
454
+ activeState = active;
455
+ }}
456
+ >
457
+ Anchor Text
458
+ </TooltipAnchor>
459
+ );
460
+ wrapper = mount(nodes);
461
+ });
462
+ ref && ref.dispatchEvent(new FocusEvent("focusin"));
463
+ expect(setTimeout).toHaveBeenLastCalledWith(
464
+ expect.any(Function),
465
+ TooltipAppearanceDelay,
466
+ );
467
+ jest.runOnlyPendingTimers();
468
+ expect(activeState).toBe(true);
469
+
470
+ // Act
471
+ ref && ref.dispatchEvent(new FocusEvent("focusout"));
472
+ wrapper && wrapper.instance().activeStateStolen();
473
+
474
+ // Assert
475
+ expect(mockTracker.giveup).not.toHaveBeenCalled();
476
+ });
477
+
478
+ test("if hovered, remains active", async () => {
479
+ // Arrange
480
+ let activeState = false;
481
+ const ref = await new Promise((resolve) => {
482
+ const nodes = (
483
+ <TooltipAnchor
484
+ anchorRef={resolve}
485
+ onActiveChanged={(active) => {
486
+ activeState = active;
487
+ }}
488
+ >
489
+ Anchor Text
490
+ </TooltipAnchor>
491
+ );
492
+ mount(nodes);
493
+ });
494
+ ref && ref.dispatchEvent(new FocusEvent("focusin"));
495
+ expect(setTimeout).toHaveBeenLastCalledWith(
496
+ expect.any(Function),
497
+ TooltipAppearanceDelay,
498
+ );
499
+ jest.runOnlyPendingTimers();
500
+ // Flow doesn't know we added jest mocks to this
501
+ // $FlowFixMe[prop-missing]
502
+ setTimeout.mockClear();
503
+ ref && ref.dispatchEvent(new MouseEvent("mouseenter"));
504
+
505
+ // Act
506
+ ref && ref.dispatchEvent(new FocusEvent("focusout"));
507
+
508
+ // Assert
509
+ // Make sure that we're not delay hiding as well.
510
+ expect(activeState).toBe(true);
511
+ expect(setTimeout).not.toHaveBeenCalled();
512
+ });
513
+ });
514
+
515
+ describe("is hovered", () => {
516
+ test("active state was not stolen, delays set active", async () => {
517
+ // Arrange
518
+ const {default: ActiveTracker} = await import(
519
+ "../../util/active-tracker.js"
520
+ );
521
+ // Let's tell the tooltip it isn't stealing and therefore it should
522
+ // be using a delay to show the tooltip.
523
+ // Flow doesn't know this is a mock
524
+ // $FlowFixMe[prop-missing]
525
+ const mockTracker = ActiveTracker.mock.instances[0];
526
+ mockTracker.steal.mockImplementationOnce(() => false);
527
+
528
+ let activeState = false;
529
+ const ref = await new Promise((resolve) => {
530
+ const nodes = (
531
+ <TooltipAnchor
532
+ anchorRef={resolve}
533
+ onActiveChanged={(active) => {
534
+ activeState = active;
535
+ }}
536
+ >
537
+ Anchor Text
538
+ </TooltipAnchor>
539
+ );
540
+ mount(nodes);
541
+ });
542
+
543
+ // Act
544
+ ref && ref.dispatchEvent(new MouseEvent("mouseenter"));
545
+ // Check that we didn't go active before the delay
546
+ expect(activeState).toBe(false);
547
+ expect(setTimeout).toHaveBeenLastCalledWith(
548
+ expect.any(Function),
549
+ TooltipAppearanceDelay,
550
+ );
551
+ jest.runOnlyPendingTimers();
552
+
553
+ // Assert
554
+ expect(activeState).toBe(true);
555
+ });
556
+
557
+ test("active state was stolen, set active immediately", async () => {
558
+ // Arrange
559
+ const {default: ActiveTracker} = await import(
560
+ "../../util/active-tracker.js"
561
+ );
562
+ // Let's tell the tooltip it is stealing and therefore it should
563
+ // not be using a delay to show the tooltip.
564
+ // Flow doesn't know this is a mock
565
+ // $FlowFixMe[prop-missing]
566
+ const mockTracker = ActiveTracker.mock.instances[0];
567
+ mockTracker.steal.mockImplementationOnce(() => true);
568
+
569
+ let activeState = false;
570
+ const ref = await new Promise((resolve) => {
571
+ const nodes = (
572
+ <TooltipAnchor
573
+ anchorRef={resolve}
574
+ onActiveChanged={(active) => {
575
+ activeState = active;
576
+ }}
577
+ >
578
+ Anchor Text
579
+ </TooltipAnchor>
580
+ );
581
+ mount(nodes);
582
+ });
583
+
584
+ // Act
585
+ ref && ref.dispatchEvent(new MouseEvent("mouseenter"));
586
+
587
+ // Assert
588
+ expect(activeState).toBe(true);
589
+ });
590
+ });
591
+
592
+ describe("is unhovered", () => {
593
+ test("active state was not stolen, active is set to false with delay", async () => {
594
+ // Arrange
595
+ let activeState = false;
596
+ const ref = await new Promise((resolve) => {
597
+ const nodes = (
598
+ <TooltipAnchor
599
+ anchorRef={resolve}
600
+ onActiveChanged={(active) => {
601
+ activeState = active;
602
+ }}
603
+ >
604
+ Anchor Text
605
+ </TooltipAnchor>
606
+ );
607
+ mount(nodes);
608
+ });
609
+ ref && ref.dispatchEvent(new MouseEvent("mouseenter"));
610
+ expect(setTimeout).toHaveBeenLastCalledWith(
611
+ expect.any(Function),
612
+ TooltipAppearanceDelay,
613
+ );
614
+ jest.runOnlyPendingTimers();
615
+ expect(activeState).toBe(true);
616
+
617
+ // Act
618
+ ref && ref.dispatchEvent(new MouseEvent("mouseleave"));
619
+ expect(activeState).toBe(true);
620
+ expect(setTimeout).toHaveBeenLastCalledWith(
621
+ expect.any(Function),
622
+ TooltipDisappearanceDelay,
623
+ );
624
+ jest.runOnlyPendingTimers();
625
+
626
+ // Assert
627
+ expect(activeState).toBe(false);
628
+ });
629
+
630
+ test("active state was not stolen, gives up active state", async () => {
631
+ // Arrange
632
+ const {default: ActiveTracker} = await import(
633
+ "../../util/active-tracker.js"
634
+ );
635
+ // Flow doesn't know this is a mock
636
+ // $FlowFixMe[prop-missing]
637
+ const mockTracker = ActiveTracker.mock.instances[0];
638
+ let activeState = false;
639
+ const ref = await new Promise((resolve) => {
640
+ const nodes = (
641
+ <TooltipAnchor
642
+ anchorRef={resolve}
643
+ onActiveChanged={(active) => {
644
+ activeState = active;
645
+ }}
646
+ >
647
+ Anchor Text
648
+ </TooltipAnchor>
649
+ );
650
+ mount(nodes);
651
+ });
652
+ ref && ref.dispatchEvent(new MouseEvent("mouseenter"));
653
+ expect(setTimeout).toHaveBeenLastCalledWith(
654
+ expect.any(Function),
655
+ TooltipAppearanceDelay,
656
+ );
657
+ jest.runOnlyPendingTimers();
658
+ expect(activeState).toBe(true);
659
+
660
+ // Act
661
+ ref && ref.dispatchEvent(new MouseEvent("mouseleave"));
662
+ expect(activeState).toBe(true);
663
+ expect(setTimeout).toHaveBeenLastCalledWith(
664
+ expect.any(Function),
665
+ TooltipDisappearanceDelay,
666
+ );
667
+ jest.runOnlyPendingTimers();
668
+
669
+ // Assert
670
+ expect(mockTracker.giveup).toHaveBeenCalledTimes(1);
671
+ });
672
+
673
+ test("active state was stolen, active is set to false immediately", async () => {
674
+ // Arrange
675
+ let wrapper;
676
+ let activeState = false;
677
+ const ref = await new Promise((resolve) => {
678
+ const nodes = (
679
+ <TooltipAnchor
680
+ anchorRef={resolve}
681
+ onActiveChanged={(active) => {
682
+ activeState = active;
683
+ }}
684
+ >
685
+ Anchor Text
686
+ </TooltipAnchor>
687
+ );
688
+ wrapper = mount(nodes);
689
+ });
690
+ ref && ref.dispatchEvent(new MouseEvent("mouseenter"));
691
+ expect(setTimeout).toHaveBeenLastCalledWith(
692
+ expect.any(Function),
693
+ TooltipAppearanceDelay,
694
+ );
695
+ jest.runOnlyPendingTimers();
696
+ expect(activeState).toBe(true);
697
+
698
+ // Act
699
+ ref && ref.dispatchEvent(new MouseEvent("mouseleave"));
700
+ wrapper && wrapper.instance().activeStateStolen();
701
+
702
+ // Assert
703
+ expect(activeState).toBe(false);
704
+ });
705
+
706
+ test("active state was stolen, so it does not have it to give up", async () => {
707
+ // Arrange
708
+ const {default: ActiveTracker} = await import(
709
+ "../../util/active-tracker.js"
710
+ );
711
+ // Flow doesn't know this is a mock
712
+ // $FlowFixMe[prop-missing]
713
+ const mockTracker = ActiveTracker.mock.instances[0];
714
+ // Arrange
715
+ let wrapper;
716
+ let activeState = false;
717
+ const ref = await new Promise((resolve) => {
718
+ const nodes = (
719
+ <TooltipAnchor
720
+ anchorRef={resolve}
721
+ onActiveChanged={(active) => {
722
+ activeState = active;
723
+ }}
724
+ >
725
+ Anchor Text
726
+ </TooltipAnchor>
727
+ );
728
+ wrapper = mount(nodes);
729
+ });
730
+ ref && ref.dispatchEvent(new MouseEvent("mouseenter"));
731
+ expect(setTimeout).toHaveBeenLastCalledWith(
732
+ expect.any(Function),
733
+ TooltipAppearanceDelay,
734
+ );
735
+ jest.runOnlyPendingTimers();
736
+ expect(activeState).toBe(true);
737
+
738
+ // Act
739
+ ref && ref.dispatchEvent(new MouseEvent("mouseleave"));
740
+ wrapper && wrapper.instance().activeStateStolen();
741
+
742
+ // Assert
743
+ expect(mockTracker.giveup).not.toHaveBeenCalled();
744
+ });
745
+
746
+ test("if focused, remains active", async () => {
747
+ // Arrange
748
+ let activeState = false;
749
+ const ref = await new Promise((resolve) => {
750
+ const nodes = (
751
+ <TooltipAnchor
752
+ anchorRef={resolve}
753
+ onActiveChanged={(active) => {
754
+ activeState = active;
755
+ }}
756
+ >
757
+ Anchor Text
758
+ </TooltipAnchor>
759
+ );
760
+ mount(nodes);
761
+ });
762
+ ref && ref.dispatchEvent(new MouseEvent("mouseenter"));
763
+ expect(setTimeout).toHaveBeenLastCalledWith(
764
+ expect.any(Function),
765
+ TooltipAppearanceDelay,
766
+ );
767
+ jest.runOnlyPendingTimers();
768
+ // Flow doesn't know we added jest mocks to this
769
+ // $FlowFixMe[prop-missing]
770
+ setTimeout.mockClear();
771
+ ref && ref.dispatchEvent(new FocusEvent("focusin"));
772
+
773
+ // Act
774
+ ref && ref.dispatchEvent(new MouseEvent("mouseleave"));
775
+
776
+ // Assert
777
+ // Make sure that we're not delay hiding as well.
778
+ expect(activeState).toBe(true);
779
+ expect(setTimeout).not.toHaveBeenCalled();
780
+ });
781
+ });
782
+
783
+ describe("dismiss behavior", () => {
784
+ test("subscribes to keydown event on active", async () => {
785
+ // Arrange
786
+ const spy = jest.spyOn(document, "addEventListener");
787
+ const ref = await new Promise((resolve) => {
788
+ const nodes = (
789
+ <TooltipAnchor
790
+ anchorRef={resolve}
791
+ onActiveChanged={() => {}}
792
+ >
793
+ Anchor Text
794
+ </TooltipAnchor>
795
+ );
796
+ mount(nodes);
797
+ });
798
+
799
+ // Act
800
+ ref && ref.dispatchEvent(new MouseEvent("mouseenter"));
801
+ expect(setTimeout).toHaveBeenLastCalledWith(
802
+ expect.any(Function),
803
+ TooltipAppearanceDelay,
804
+ );
805
+ jest.runOnlyPendingTimers();
806
+
807
+ // Assert
808
+ expect(spy).toHaveBeenCalledTimes(1);
809
+ expect(spy).toHaveBeenLastCalledWith("keyup", expect.any(Function));
810
+ });
811
+
812
+ test("does not subscribe to keydown event if already active", async () => {
813
+ // Arrange
814
+ const spy = jest.spyOn(document, "addEventListener");
815
+ const ref = await new Promise((resolve) => {
816
+ const nodes = (
817
+ <TooltipAnchor
818
+ anchorRef={resolve}
819
+ onActiveChanged={() => {}}
820
+ >
821
+ Anchor Text
822
+ </TooltipAnchor>
823
+ );
824
+ mount(nodes);
825
+ });
826
+
827
+ ref && ref.dispatchEvent(new KeyboardEvent("focusin"));
828
+ expect(setTimeout).toHaveBeenLastCalledWith(
829
+ expect.any(Function),
830
+ TooltipAppearanceDelay,
831
+ );
832
+ jest.runOnlyPendingTimers();
833
+ expect(spy).toHaveBeenCalledTimes(1);
834
+ expect(spy).toHaveBeenLastCalledWith("keyup", expect.any(Function));
835
+ spy.mockClear();
836
+
837
+ // Act
838
+ ref && ref.dispatchEvent(new MouseEvent("mouseenter"));
839
+
840
+ // Assert
841
+ expect(spy).not.toHaveBeenCalled();
842
+ });
843
+
844
+ test("unsubscribes from keydown event on inactive", async () => {
845
+ // Arrange
846
+ const spy = jest.spyOn(document, "removeEventListener");
847
+ const ref = await new Promise((resolve) => {
848
+ const nodes = (
849
+ <TooltipAnchor
850
+ anchorRef={resolve}
851
+ onActiveChanged={() => {}}
852
+ >
853
+ Anchor Text
854
+ </TooltipAnchor>
855
+ );
856
+ mount(nodes);
857
+ });
858
+
859
+ ref && ref.dispatchEvent(new KeyboardEvent("focusin"));
860
+ expect(setTimeout).toHaveBeenLastCalledWith(
861
+ expect.any(Function),
862
+ TooltipAppearanceDelay,
863
+ );
864
+ jest.runOnlyPendingTimers();
865
+
866
+ // Act
867
+ ref && ref.dispatchEvent(new KeyboardEvent("focusout"));
868
+ expect(setTimeout).toHaveBeenLastCalledWith(
869
+ expect.any(Function),
870
+ TooltipDisappearanceDelay,
871
+ );
872
+ jest.runOnlyPendingTimers();
873
+
874
+ // Assert
875
+ expect(spy).toHaveBeenCalledTimes(1);
876
+ expect(spy).toHaveBeenLastCalledWith("keyup", expect.any(Function));
877
+ });
878
+
879
+ test("unsubscribes from keydown event on unmount", async () => {
880
+ // Arrange
881
+ let wrapper;
882
+ const spy = jest.spyOn(document, "removeEventListener");
883
+ const ref = await new Promise((resolve) => {
884
+ const nodes = (
885
+ <TooltipAnchor
886
+ anchorRef={resolve}
887
+ onActiveChanged={() => {}}
888
+ >
889
+ Anchor Text
890
+ </TooltipAnchor>
891
+ );
892
+ wrapper = mount(nodes);
893
+ });
894
+
895
+ ref && ref.dispatchEvent(new KeyboardEvent("focusin"));
896
+ expect(setTimeout).toHaveBeenLastCalledWith(
897
+ expect.any(Function),
898
+ TooltipAppearanceDelay,
899
+ );
900
+ jest.runOnlyPendingTimers();
901
+
902
+ // Act
903
+ wrapper && wrapper.unmount();
904
+
905
+ // Assert
906
+ expect(spy).toHaveBeenCalledTimes(1);
907
+ expect(spy).toHaveBeenLastCalledWith("keyup", expect.any(Function));
908
+ });
909
+
910
+ test("when active, escape dismisses tooltip", async () => {
911
+ // Arrange
912
+ let activeState = false;
913
+ const ref = await new Promise((resolve) => {
914
+ const nodes = (
915
+ <TooltipAnchor
916
+ anchorRef={resolve}
917
+ onActiveChanged={(active) => {
918
+ activeState = active;
919
+ }}
920
+ >
921
+ Anchor Text
922
+ </TooltipAnchor>
923
+ );
924
+ mount(nodes);
925
+ });
926
+
927
+ ref && ref.dispatchEvent(new KeyboardEvent("focusin"));
928
+ expect(setTimeout).toHaveBeenLastCalledWith(
929
+ expect.any(Function),
930
+ TooltipAppearanceDelay,
931
+ );
932
+ jest.runOnlyPendingTimers();
933
+ const event: KeyboardEvent = (document.createEvent("Event"): any);
934
+ event.key = "Escape";
935
+ event.which = 27;
936
+ event.initEvent("keyup", true, true);
937
+
938
+ // Act
939
+ document.dispatchEvent(event);
940
+ expect(setTimeout).toHaveBeenLastCalledWith(
941
+ expect.any(Function),
942
+ TooltipDisappearanceDelay,
943
+ );
944
+ jest.runOnlyPendingTimers();
945
+
946
+ // Assert
947
+ expect(activeState).toBe(false);
948
+ });
949
+
950
+ test("when active, escape stops event propagation", async () => {
951
+ // Arrange
952
+ const ref = await new Promise((resolve) => {
953
+ const nodes = (
954
+ <TooltipAnchor
955
+ anchorRef={resolve}
956
+ onActiveChanged={() => {}}
957
+ >
958
+ Anchor Text
959
+ </TooltipAnchor>
960
+ );
961
+ mount(nodes);
962
+ });
963
+
964
+ ref && ref.dispatchEvent(new KeyboardEvent("focusin"));
965
+ expect(setTimeout).toHaveBeenLastCalledWith(
966
+ expect.any(Function),
967
+ TooltipAppearanceDelay,
968
+ );
969
+ jest.runOnlyPendingTimers();
970
+ const event: KeyboardEvent = (document.createEvent("Event"): any);
971
+ const spyOnStopPropagation = jest.spyOn(event, "stopPropagation");
972
+ event.key = "Escape";
973
+ event.initEvent("keyup", true, true);
974
+
975
+ // Act
976
+ document.dispatchEvent(event);
977
+ expect(setTimeout).toHaveBeenLastCalledWith(
978
+ expect.any(Function),
979
+ TooltipDisappearanceDelay,
980
+ );
981
+ jest.runOnlyPendingTimers();
982
+
983
+ // Assert
984
+ expect(spyOnStopPropagation).toHaveBeenCalled();
985
+ });
986
+ });
987
+ });