@khanacademy/wonder-blocks-popover 3.0.23 → 3.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.
@@ -153,39 +153,6 @@ describe("Popover", () => {
153
153
  expect(onCloseMock).toBeCalled();
154
154
  });
155
155
 
156
- it("should close the Popover if dismissEnabled is set", async () => {
157
- // Arrange
158
- render(
159
- <Popover
160
- dismissEnabled={true}
161
- placement="top"
162
- content={<PopoverContent title="Title" content="content" />}
163
- >
164
- {({open}: any) => (
165
- <button data-anchor onClick={open}>
166
- Open default popover
167
- </button>
168
- )}
169
- </Popover>,
170
- );
171
-
172
- // open the popover
173
- userEvent.click(
174
- screen.getByRole("button", {name: "Open default popover"}),
175
- );
176
-
177
- // Act
178
- // we try to close it using the same trigger element
179
- userEvent.click(
180
- screen.getByRole("button", {name: "Open default popover"}),
181
- );
182
-
183
- // Assert
184
- await waitFor(() => {
185
- expect(screen.queryByText("Title")).not.toBeInTheDocument();
186
- });
187
- });
188
-
189
156
  it("should shift-tab back to the anchor after popover is closed", async () => {
190
157
  // Arrange
191
158
  const PopoverComponent = () => {
@@ -229,8 +196,16 @@ describe("Popover", () => {
229
196
  name: "Click to close the popover",
230
197
  });
231
198
  closeButton.click();
232
- // Shift-tab over to the anchor button
199
+
200
+ // At this point, the focus returns to the anchor element
201
+
202
+ // Shift-tab over to the document body
233
203
  userEvent.tab({shift: true});
204
+
205
+ // Shift-tab over to the outside button
206
+ userEvent.tab({shift: true});
207
+
208
+ // Shift-tab over to the anchor element
234
209
  userEvent.tab({shift: true});
235
210
 
236
211
  // Assert
@@ -288,6 +263,226 @@ describe("Popover", () => {
288
263
  expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
289
264
  });
290
265
 
266
+ describe("return focus", () => {
267
+ it("should return focus to the trigger element by default", async () => {
268
+ // Arrange
269
+ render(
270
+ <Popover
271
+ dismissEnabled={true}
272
+ content={
273
+ <PopoverContent
274
+ closeButtonVisible={true}
275
+ title="Returning focus to a specific element"
276
+ content='After dismissing the popover, the focus will be set on the button labeled "Focus here after close."'
277
+ />
278
+ }
279
+ >
280
+ <Button>Open popover</Button>
281
+ </Popover>,
282
+ );
283
+
284
+ const anchorButton = screen.getByRole("button", {
285
+ name: "Open popover",
286
+ });
287
+
288
+ // open the popover
289
+ userEvent.click(anchorButton);
290
+ await screen.findByRole("dialog");
291
+
292
+ // Act
293
+ const closeButton = screen.getByRole("button", {
294
+ name: "Close Popover",
295
+ });
296
+ closeButton.click();
297
+
298
+ // Assert
299
+ expect(anchorButton).toHaveFocus();
300
+ });
301
+
302
+ it("should return focus to a specific element if closedFocusId is set", async () => {
303
+ // Arrange
304
+ render(
305
+ <View>
306
+ <Button id="button-to-focus-on">
307
+ Focus here after close
308
+ </Button>
309
+ <Popover
310
+ closedFocusId="button-to-focus-on"
311
+ dismissEnabled={true}
312
+ content={
313
+ <PopoverContent
314
+ closeButtonVisible={true}
315
+ title="Returning focus to a specific element"
316
+ content='After dismissing the popover, the focus will be set on the button labeled "Focus here after close."'
317
+ />
318
+ }
319
+ >
320
+ <Button>Open popover</Button>
321
+ </Popover>
322
+ </View>,
323
+ );
324
+
325
+ const anchorButton = screen.getByRole("button", {
326
+ name: "Open popover",
327
+ });
328
+
329
+ // open the popover
330
+ userEvent.click(anchorButton);
331
+ await screen.findByRole("dialog");
332
+
333
+ // Act
334
+ const closeButton = screen.getByRole("button", {
335
+ name: "Close Popover",
336
+ });
337
+ closeButton.click();
338
+
339
+ // Assert
340
+ const buttonToFocusOn = screen.getByRole("button", {
341
+ name: "Focus here after close",
342
+ });
343
+ expect(buttonToFocusOn).toHaveFocus();
344
+ });
345
+ });
346
+
347
+ describe("dismissEnabled", () => {
348
+ it("should close the Popover if dismissEnabled is set", async () => {
349
+ // Arrange
350
+ render(
351
+ <Popover
352
+ dismissEnabled={true}
353
+ placement="top"
354
+ content={<PopoverContent title="Title" content="content" />}
355
+ >
356
+ {({open}: any) => (
357
+ <button data-anchor onClick={open}>
358
+ Open default popover
359
+ </button>
360
+ )}
361
+ </Popover>,
362
+ );
363
+
364
+ // open the popover
365
+ userEvent.click(
366
+ screen.getByRole("button", {name: "Open default popover"}),
367
+ );
368
+
369
+ // Act
370
+ // we try to close it using the same trigger element
371
+ userEvent.click(
372
+ screen.getByRole("button", {name: "Open default popover"}),
373
+ );
374
+
375
+ // Assert
376
+ await waitFor(() => {
377
+ expect(screen.queryByText("Title")).not.toBeInTheDocument();
378
+ });
379
+ });
380
+
381
+ it("should return focus to the anchor element when pressing Esc", async () => {
382
+ // Arrange
383
+ render(
384
+ <Popover
385
+ dismissEnabled={true}
386
+ placement="top"
387
+ content={<PopoverContent title="Title" content="content" />}
388
+ >
389
+ {({open}: any) => (
390
+ <button data-anchor onClick={open}>
391
+ Open default popover
392
+ </button>
393
+ )}
394
+ </Popover>,
395
+ );
396
+
397
+ // open the popover
398
+ userEvent.click(
399
+ screen.getByRole("button", {name: "Open default popover"}),
400
+ );
401
+
402
+ // Act
403
+ // we try to close it pressing the Escape key
404
+ userEvent.keyboard("{esc}");
405
+
406
+ // Assert
407
+ expect(
408
+ screen.getByRole("button", {name: "Open default popover"}),
409
+ ).toHaveFocus();
410
+ });
411
+
412
+ it("should return focus to the anchor element when clicking outside", async () => {
413
+ // Arrange
414
+ const {container} = render(
415
+ <Popover
416
+ dismissEnabled={true}
417
+ placement="top"
418
+ content={<PopoverContent title="Title" content="content" />}
419
+ >
420
+ {({open}: any) => (
421
+ <button data-anchor onClick={open}>
422
+ Open default popover
423
+ </button>
424
+ )}
425
+ </Popover>,
426
+ );
427
+
428
+ // open the popover
429
+ userEvent.click(
430
+ screen.getByRole("button", {name: "Open default popover"}),
431
+ );
432
+
433
+ // Act
434
+ // we try to close it clicking outside the popover
435
+ userEvent.click(container);
436
+ // NOTE: We need to click twice because the first click is handled
437
+ // by the trigger element.
438
+ userEvent.click(container);
439
+
440
+ // Assert
441
+ expect(
442
+ screen.getByRole("button", {name: "Open default popover"}),
443
+ ).toHaveFocus();
444
+ });
445
+
446
+ it("should NOT return focus to the anchor element when clicking on an interactive element", async () => {
447
+ // Arrange
448
+ render(
449
+ <View>
450
+ <Popover
451
+ dismissEnabled={true}
452
+ placement="top"
453
+ content={
454
+ <PopoverContent title="Title" content="content" />
455
+ }
456
+ >
457
+ {({open}: any) => (
458
+ <button data-anchor onClick={open}>
459
+ Open default popover
460
+ </button>
461
+ )}
462
+ </Popover>
463
+ <Button>Next button outside</Button>
464
+ </View>,
465
+ );
466
+
467
+ // open the popover
468
+ userEvent.click(
469
+ screen.getByRole("button", {name: "Open default popover"}),
470
+ );
471
+
472
+ // Act
473
+ // we try to close it clicking outside the popover
474
+ userEvent.click(
475
+ screen.getByRole("button", {name: "Next button outside"}),
476
+ );
477
+
478
+ // Assert
479
+ // The focus should remain on the button outside the popover
480
+ expect(
481
+ screen.getByRole("button", {name: "Next button outside"}),
482
+ ).toHaveFocus();
483
+ });
484
+ });
485
+
291
486
  describe("a11y", () => {
292
487
  it("should announce a popover correctly by reading the title contents", async () => {
293
488
  // Arrange
@@ -356,4 +551,196 @@ describe("Popover", () => {
356
551
  ).toBeInTheDocument();
357
552
  });
358
553
  });
554
+
555
+ describe("keyboard navigation", () => {
556
+ it("should move focus to the first focusable element after popover is open", async () => {
557
+ // Arrange
558
+ render(
559
+ <>
560
+ <Button>Prev focusable element outside</Button>
561
+ <Popover
562
+ onClose={jest.fn()}
563
+ content={
564
+ <PopoverContent
565
+ title="Popover title"
566
+ content="content"
567
+ actions={
568
+ <>
569
+ <Button>Button 1 inside popover</Button>
570
+ <Button>Button 2 inside popover</Button>
571
+ </>
572
+ }
573
+ />
574
+ }
575
+ >
576
+ <Button>Open default popover</Button>
577
+ </Popover>
578
+ <Button>Next focusable element outside</Button>
579
+ </>,
580
+ );
581
+
582
+ // Focus on the first element outside the popover
583
+ userEvent.tab();
584
+ // open the popover by focusing on the trigger element
585
+ userEvent.tab();
586
+ userEvent.keyboard("{enter}");
587
+
588
+ // Act
589
+ // Wait for the popover to be open.
590
+ await screen.findByRole("dialog");
591
+
592
+ // Assert
593
+ // Focus should move to the first button inside the popover
594
+ expect(
595
+ screen.getByRole("button", {
596
+ name: "Button 1 inside popover",
597
+ }),
598
+ ).toHaveFocus();
599
+ });
600
+
601
+ it("should allow flowing focus correctly even if the popover remains open", async () => {
602
+ // Arrange
603
+ render(
604
+ <>
605
+ <Button>Prev focusable element outside</Button>
606
+ <Popover
607
+ onClose={jest.fn()}
608
+ content={
609
+ <PopoverContent
610
+ title="Popover title"
611
+ content="content"
612
+ actions={<Button>Button inside popover</Button>}
613
+ />
614
+ }
615
+ >
616
+ <Button>Open default popover</Button>
617
+ </Popover>
618
+ <Button>Next focusable element outside</Button>
619
+ </>,
620
+ );
621
+
622
+ // Focus on the first element outside the popover
623
+ userEvent.tab();
624
+ // open the popover by focusing on the trigger element
625
+ userEvent.tab();
626
+ userEvent.keyboard("{enter}");
627
+
628
+ // Wait for the popover to be open.
629
+ await screen.findByRole("dialog");
630
+
631
+ // Act
632
+ // Focus on the next element after the popover
633
+ userEvent.tab();
634
+
635
+ // Assert
636
+ expect(
637
+ screen.getByRole("button", {
638
+ name: "Next focusable element outside",
639
+ }),
640
+ ).toHaveFocus();
641
+ });
642
+
643
+ it("should allow circular navigation when the popover is open", async () => {
644
+ // Arrange
645
+ render(
646
+ <>
647
+ <Button>Prev focusable element outside</Button>
648
+ <Popover
649
+ onClose={jest.fn()}
650
+ content={
651
+ <PopoverContent
652
+ title="Popover title"
653
+ content="content"
654
+ actions={<Button>Button inside popover</Button>}
655
+ />
656
+ }
657
+ >
658
+ <Button>Open default popover</Button>
659
+ </Popover>
660
+ <Button>Next focusable element outside</Button>
661
+ </>,
662
+ );
663
+
664
+ // Focus on the first element outside the popover
665
+ userEvent.tab();
666
+ // open the popover by focusing on the trigger element
667
+ userEvent.tab();
668
+ userEvent.keyboard("{enter}");
669
+
670
+ // Wait for the popover to be open.
671
+ await screen.findByRole("dialog");
672
+
673
+ // Focus on the next element after the popover
674
+ userEvent.tab();
675
+
676
+ // Focus on the document body
677
+ userEvent.tab();
678
+
679
+ // Act
680
+ // Focus again on the first element in the document.
681
+ userEvent.tab();
682
+
683
+ // Assert
684
+ expect(
685
+ screen.getByRole("button", {
686
+ name: "Prev focusable element outside",
687
+ }),
688
+ ).toHaveFocus();
689
+ });
690
+
691
+ it("should allow navigating backwards when the popover is open", async () => {
692
+ // Arrange
693
+ render(
694
+ <>
695
+ <Button>Prev focusable element outside</Button>
696
+ <Popover
697
+ onClose={jest.fn()}
698
+ content={
699
+ <PopoverContent
700
+ title="Popover title"
701
+ content="content"
702
+ actions={<Button>Button inside popover</Button>}
703
+ />
704
+ }
705
+ >
706
+ <Button>Open default popover</Button>
707
+ </Popover>
708
+ <Button>Next focusable element outside</Button>
709
+ </>,
710
+ );
711
+
712
+ // Open the popover
713
+ userEvent.click(
714
+ screen.getByRole("button", {name: "Open default popover"}),
715
+ );
716
+
717
+ // Wait for the popover to be open.
718
+ await screen.findByRole("dialog");
719
+
720
+ // At this point, the focus moves to the focusable element inside
721
+ // the popover, so we need to move the focus back to the trigger
722
+ // element.
723
+ userEvent.tab({shift: true});
724
+
725
+ // Focus on the first element in the document
726
+ userEvent.tab({shift: true});
727
+
728
+ // Focus on the document body
729
+ userEvent.tab({shift: true});
730
+
731
+ // Focus on the last element in the document
732
+ userEvent.tab({shift: true});
733
+
734
+ // Act
735
+ // Focus again on element inside the popover.
736
+ userEvent.tab({shift: true});
737
+
738
+ // Assert
739
+ expect(
740
+ screen.getByRole("button", {
741
+ name: "Button inside popover",
742
+ }),
743
+ ).toHaveFocus();
744
+ });
745
+ });
359
746
  });