@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.
- package/CHANGELOG.md +27 -0
- package/dist/components/focus-manager.d.ts +18 -1
- package/dist/components/popover-event-listener.d.ts +1 -1
- package/dist/components/popover.d.ts +11 -1
- package/dist/es/index.js +135 -61
- package/dist/index.js +135 -63
- package/dist/util/util.d.ts +5 -0
- package/package.json +5 -6
- package/src/components/__tests__/focus-manager.test.tsx +115 -36
- package/src/components/__tests__/popover.test.tsx +421 -34
- package/src/components/focus-manager.tsx +155 -54
- package/src/components/popover-content-core.tsx +13 -14
- package/src/components/popover-content.tsx +14 -14
- package/src/components/popover-dialog.tsx +2 -2
- package/src/components/popover-event-listener.ts +12 -3
- package/src/components/popover.tsx +38 -2
- package/src/util/__tests__/util.test.tsx +38 -0
- package/src/util/util.ts +8 -0
- package/tsconfig-build.json +1 -2
- package/tsconfig-build.tsbuildinfo +1 -1
|
@@ -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
|
-
|
|
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
|
});
|