@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/dist/index.js CHANGED
@@ -8,9 +8,8 @@ var wonderBlocksCore = require('@khanacademy/wonder-blocks-core');
8
8
  var wonderBlocksTooltip = require('@khanacademy/wonder-blocks-tooltip');
9
9
  var wonderBlocksModal = require('@khanacademy/wonder-blocks-modal');
10
10
  var aphrodite = require('aphrodite');
11
- var Spacing = require('@khanacademy/wonder-blocks-spacing');
11
+ var wonderBlocksTokens = require('@khanacademy/wonder-blocks-tokens');
12
12
  var wonderBlocksTypography = require('@khanacademy/wonder-blocks-typography');
13
- var Colors = require('@khanacademy/wonder-blocks-color');
14
13
  var xIcon = require('@phosphor-icons/core/regular/x.svg');
15
14
  var IconButton = require('@khanacademy/wonder-blocks-icon-button');
16
15
 
@@ -36,8 +35,6 @@ function _interopNamespace(e) {
36
35
 
37
36
  var React__namespace = /*#__PURE__*/_interopNamespace(React);
38
37
  var ReactDOM__namespace = /*#__PURE__*/_interopNamespace(ReactDOM);
39
- var Spacing__default = /*#__PURE__*/_interopDefaultLegacy(Spacing);
40
- var Colors__default = /*#__PURE__*/_interopDefaultLegacy(Colors);
41
38
  var xIcon__default = /*#__PURE__*/_interopDefaultLegacy(xIcon);
42
39
  var IconButton__default = /*#__PURE__*/_interopDefaultLegacy(IconButton);
43
40
 
@@ -164,6 +161,9 @@ const FOCUSABLE_ELEMENTS = 'button, [href], input, select, textarea, [tabindex]:
164
161
  function findFocusableNodes(root) {
165
162
  return Array.from(root.querySelectorAll(FOCUSABLE_ELEMENTS));
166
163
  }
164
+ function isFocusable(element) {
165
+ return element.matches(FOCUSABLE_ELEMENTS);
166
+ }
167
167
 
168
168
  class InitialFocus extends React__namespace.Component {
169
169
  constructor(...args) {
@@ -211,17 +211,49 @@ class FocusManager extends React__namespace.Component {
211
211
  super(...args);
212
212
  this.nextElementAfterPopover = void 0;
213
213
  this.rootNode = void 0;
214
- this.focusableElementsInPopover = [];
214
+ this.elementsThatCanBeFocusableInsidePopover = [];
215
+ this.firstFocusableElementInPopover = null;
216
+ this.lastFocusableElementInPopover = null;
215
217
  this.addEventListeners = () => {
216
218
  const {
217
219
  anchorElement
218
220
  } = this.props;
219
221
  if (anchorElement) {
220
- anchorElement.addEventListener("keydown", this.handleKeydownPreviousFocusableElement, true);
222
+ anchorElement.addEventListener("keydown", this.handleKeydownPreviousFocusableElement);
223
+ }
224
+ if (this.rootNode) {
225
+ this.elementsThatCanBeFocusableInsidePopover = findFocusableNodes(this.rootNode);
226
+ this.firstFocusableElementInPopover = this.elementsThatCanBeFocusableInsidePopover[0];
227
+ this.lastFocusableElementInPopover = this.elementsThatCanBeFocusableInsidePopover[this.elementsThatCanBeFocusableInsidePopover.length - 1];
221
228
  }
222
229
  this.nextElementAfterPopover = this.getNextFocusableElement();
230
+ if (!this.nextElementAfterPopover) {
231
+ window.addEventListener("blur", () => {
232
+ this.changeFocusabilityInsidePopover(true);
233
+ });
234
+ }
235
+ if (this.firstFocusableElementInPopover) {
236
+ this.firstFocusableElementInPopover.addEventListener("keydown", this.handleKeydownFirstFocusableElement);
237
+ }
238
+ if (this.lastFocusableElementInPopover) {
239
+ this.lastFocusableElementInPopover.addEventListener("keydown", this.handleKeydownLastFocusableElement);
240
+ }
223
241
  if (this.nextElementAfterPopover) {
224
- this.nextElementAfterPopover.addEventListener("keydown", this.handleKeydownNextFocusableElement, true);
242
+ this.nextElementAfterPopover.addEventListener("keydown", this.handleKeydownNextFocusableElement);
243
+ }
244
+ };
245
+ this.handleKeydownFirstFocusableElement = e => {
246
+ if (e.key === "Tab" && e.shiftKey) {
247
+ var _this$props$anchorEle;
248
+ e.preventDefault();
249
+ (_this$props$anchorEle = this.props.anchorElement) == null ? void 0 : _this$props$anchorEle.focus();
250
+ }
251
+ };
252
+ this.handleKeydownLastFocusableElement = e => {
253
+ if (this.nextElementAfterPopover && e.key === "Tab" && !e.shiftKey) {
254
+ var _this$nextElementAfte;
255
+ e.preventDefault();
256
+ (_this$nextElementAfte = this.nextElementAfterPopover) == null ? void 0 : _this$nextElementAfte.focus();
225
257
  }
226
258
  };
227
259
  this.getNextFocusableElement = () => {
@@ -232,10 +264,14 @@ class FocusManager extends React__namespace.Component {
232
264
  return;
233
265
  }
234
266
  const focusableElements = findFocusableNodes(document);
235
- const anchorIndex = focusableElements.indexOf(anchorElement);
236
- if (anchorIndex >= 0) {
237
- const nextElementIndex = anchorIndex < focusableElements.length - 1 ? anchorIndex + 1 : 0;
238
- return focusableElements[nextElementIndex];
267
+ const focusableElementsOutside = focusableElements.filter(element => {
268
+ const index = this.elementsThatCanBeFocusableInsidePopover.indexOf(element);
269
+ return index < 0;
270
+ });
271
+ const anchorIndex = focusableElementsOutside.indexOf(anchorElement);
272
+ if (anchorIndex >= 0 && anchorIndex !== focusableElementsOutside.length - 1) {
273
+ const nextElementIndex = anchorIndex < focusableElementsOutside.length - 1 ? anchorIndex + 1 : 0;
274
+ return focusableElementsOutside[nextElementIndex];
239
275
  }
240
276
  return;
241
277
  };
@@ -248,13 +284,18 @@ class FocusManager extends React__namespace.Component {
248
284
  throw new Error("Assertion error: root node should exist after mount");
249
285
  }
250
286
  this.rootNode = rootNode;
251
- this.focusableElementsInPopover = findFocusableNodes(this.rootNode);
252
287
  };
253
288
  this.handleFocusPreviousFocusableElement = () => {
254
289
  if (this.props.anchorElement) {
255
290
  this.props.anchorElement.focus();
256
291
  }
257
292
  };
293
+ this.changeFocusabilityInsidePopover = (enabled = true) => {
294
+ const tabIndex = enabled ? "0" : "-1";
295
+ this.elementsThatCanBeFocusableInsidePopover.forEach(element => {
296
+ element.setAttribute("tabIndex", tabIndex);
297
+ });
298
+ };
258
299
  this.handleFocusNextFocusableElement = () => {
259
300
  if (this.nextElementAfterPopover) {
260
301
  this.nextElementAfterPopover.focus();
@@ -262,15 +303,16 @@ class FocusManager extends React__namespace.Component {
262
303
  };
263
304
  this.handleKeydownPreviousFocusableElement = e => {
264
305
  if (e.key === "Tab" && !e.shiftKey) {
306
+ var _this$firstFocusableE;
265
307
  e.preventDefault();
266
- this.focusableElementsInPopover[0].focus();
308
+ (_this$firstFocusableE = this.firstFocusableElementInPopover) == null ? void 0 : _this$firstFocusableE.focus();
267
309
  }
268
310
  };
269
311
  this.handleKeydownNextFocusableElement = e => {
270
312
  if (e.key === "Tab" && e.shiftKey) {
313
+ var _this$lastFocusableEl;
271
314
  e.preventDefault();
272
- const lastElementIndex = this.focusableElementsInPopover.length - 1;
273
- this.focusableElementsInPopover[lastElementIndex].focus();
315
+ (_this$lastFocusableEl = this.lastFocusableElementInPopover) == null ? void 0 : _this$lastFocusableEl.focus();
274
316
  }
275
317
  };
276
318
  }
@@ -278,41 +320,53 @@ class FocusManager extends React__namespace.Component {
278
320
  this.addEventListeners();
279
321
  }
280
322
  componentDidUpdate() {
323
+ this.removeEventListeners();
281
324
  this.addEventListeners();
282
325
  }
283
326
  componentWillUnmount() {
327
+ this.changeFocusabilityInsidePopover(true);
328
+ this.removeEventListeners();
329
+ }
330
+ removeEventListeners() {
284
331
  const {
285
332
  anchorElement
286
333
  } = this.props;
287
334
  if (anchorElement) {
288
- setTimeout(() => anchorElement.focus(), 0);
289
- anchorElement.removeEventListener("keydown", this.handleKeydownPreviousFocusableElement, true);
335
+ anchorElement.removeEventListener("keydown", this.handleKeydownPreviousFocusableElement);
336
+ }
337
+ if (!this.nextElementAfterPopover) {
338
+ window.removeEventListener("blur", () => {
339
+ this.changeFocusabilityInsidePopover(true);
340
+ });
341
+ }
342
+ if (this.firstFocusableElementInPopover) {
343
+ this.firstFocusableElementInPopover.removeEventListener("keydown", this.handleKeydownFirstFocusableElement);
344
+ }
345
+ if (this.lastFocusableElementInPopover) {
346
+ this.lastFocusableElementInPopover.removeEventListener("keydown", this.handleKeydownLastFocusableElement);
290
347
  }
291
348
  if (this.nextElementAfterPopover) {
292
- this.nextElementAfterPopover.removeEventListener("keydown", this.handleKeydownNextFocusableElement, true);
349
+ this.nextElementAfterPopover.removeEventListener("keydown", this.handleKeydownNextFocusableElement);
293
350
  }
294
351
  }
295
352
  render() {
296
353
  const {
297
354
  children
298
355
  } = this.props;
299
- return React__namespace.createElement(React__namespace.Fragment, null, React__namespace.createElement("div", {
300
- tabIndex: 0,
301
- onFocus: this.handleFocusPreviousFocusableElement,
302
- style: {
303
- position: "fixed"
356
+ return React__namespace.createElement("div", {
357
+ ref: this.getComponentRootNode,
358
+ onClick: () => {
359
+ this.changeFocusabilityInsidePopover(true);
360
+ },
361
+ onFocus: () => {
362
+ this.changeFocusabilityInsidePopover(true);
363
+ },
364
+ onBlur: () => {
365
+ this.changeFocusabilityInsidePopover(false);
304
366
  }
305
- }), React__namespace.createElement("div", {
306
- ref: this.getComponentRootNode
307
367
  }, React__namespace.createElement(InitialFocus, {
308
368
  initialFocusId: this.props.initialFocusId
309
- }, children)), React__namespace.createElement("div", {
310
- tabIndex: 0,
311
- onFocus: this.handleFocusNextFocusableElement,
312
- style: {
313
- position: "fixed"
314
- }
315
- }));
369
+ }, children));
316
370
  }
317
371
  }
318
372
 
@@ -326,7 +380,7 @@ class PopoverEventListener extends React__namespace.Component {
326
380
  if (e.key === "Escape") {
327
381
  e.preventDefault();
328
382
  e.stopPropagation();
329
- this.props.onClose();
383
+ this.props.onClose(true);
330
384
  }
331
385
  };
332
386
  this._handleClick = e => {
@@ -341,7 +395,8 @@ class PopoverEventListener extends React__namespace.Component {
341
395
  if (node && !node.contains(e.target)) {
342
396
  e.preventDefault();
343
397
  e.stopPropagation();
344
- this.props.onClose();
398
+ const shouldReturnFocus = !isFocusable(e.target);
399
+ this.props.onClose(shouldReturnFocus);
345
400
  }
346
401
  };
347
402
  }
@@ -366,19 +421,36 @@ class Popover extends React__namespace.Component {
366
421
  placement: this.props.placement
367
422
  };
368
423
  this.contentRef = React__namespace.createRef();
369
- this.handleClose = () => {
424
+ this.maybeReturnFocus = () => {
425
+ const {
426
+ anchorElement
427
+ } = this.state;
428
+ const {
429
+ closedFocusId
430
+ } = this.props;
431
+ if (closedFocusId) {
432
+ const focusElement = ReactDOM__namespace.findDOMNode(document.getElementById(closedFocusId));
433
+ focusElement == null ? void 0 : focusElement.focus();
434
+ return;
435
+ }
436
+ if (anchorElement) {
437
+ anchorElement.focus();
438
+ }
439
+ };
440
+ this.handleClose = (shouldReturnFocus = true) => {
370
441
  this.setState({
371
442
  opened: false
372
443
  }, () => {
373
444
  var _this$props$onClose, _this$props;
374
445
  (_this$props$onClose = (_this$props = this.props).onClose) == null ? void 0 : _this$props$onClose.call(_this$props);
446
+ if (shouldReturnFocus) {
447
+ this.maybeReturnFocus();
448
+ }
375
449
  });
376
450
  };
377
451
  this.handleOpen = () => {
378
452
  if (this.props.dismissEnabled && this.state.opened) {
379
- this.setState({
380
- opened: false
381
- });
453
+ this.handleClose(true);
382
454
  } else {
383
455
  this.setState({
384
456
  opened: true
@@ -531,29 +603,29 @@ PopoverContentCore.defaultProps = {
531
603
  };
532
604
  const styles$1 = aphrodite.StyleSheet.create({
533
605
  content: {
534
- borderRadius: Spacing__default["default"].xxxSmall_4,
535
- border: `solid 1px ${Colors__default["default"].offBlack16}`,
536
- backgroundColor: Colors__default["default"].white,
537
- boxShadow: `0 ${Spacing__default["default"].xSmall_8}px ${Spacing__default["default"].xSmall_8}px 0 ${Colors__default["default"].offBlack8}`,
606
+ borderRadius: wonderBlocksTokens.spacing.xxxSmall_4,
607
+ border: `solid 1px ${wonderBlocksTokens.color.offBlack16}`,
608
+ backgroundColor: wonderBlocksTokens.color.white,
609
+ boxShadow: `0 ${wonderBlocksTokens.spacing.xSmall_8}px ${wonderBlocksTokens.spacing.xSmall_8}px 0 ${wonderBlocksTokens.color.offBlack8}`,
538
610
  margin: 0,
539
- maxWidth: Spacing__default["default"].medium_16 * 18,
540
- padding: Spacing__default["default"].large_24,
611
+ maxWidth: wonderBlocksTokens.spacing.medium_16 * 18,
612
+ padding: wonderBlocksTokens.spacing.large_24,
541
613
  overflow: "hidden",
542
614
  justifyContent: "center"
543
615
  },
544
616
  blue: {
545
- backgroundColor: Colors__default["default"].blue,
546
- color: Colors__default["default"].white
617
+ backgroundColor: wonderBlocksTokens.color.blue,
618
+ color: wonderBlocksTokens.color.white
547
619
  },
548
620
  darkBlue: {
549
- backgroundColor: Colors__default["default"].darkBlue,
550
- color: Colors__default["default"].white
621
+ backgroundColor: wonderBlocksTokens.color.darkBlue,
622
+ color: wonderBlocksTokens.color.white
551
623
  },
552
624
  closeButton: {
553
625
  margin: 0,
554
626
  position: "absolute",
555
- right: Spacing__default["default"].xxxSmall_4,
556
- top: Spacing__default["default"].xxxSmall_4,
627
+ right: wonderBlocksTokens.spacing.xxxSmall_4,
628
+ top: wonderBlocksTokens.spacing.xxxSmall_4,
557
629
  zIndex: 1
558
630
  }
559
631
  });
@@ -667,7 +739,7 @@ PopoverContent.defaultProps = {
667
739
  };
668
740
  const styles = aphrodite.StyleSheet.create({
669
741
  actions: {
670
- marginTop: Spacing__default["default"].large_24,
742
+ marginTop: wonderBlocksTokens.spacing.large_24,
671
743
  flexDirection: "row",
672
744
  alignItems: "center",
673
745
  justifyContent: "flex-end"
@@ -676,15 +748,15 @@ const styles = aphrodite.StyleSheet.create({
676
748
  justifyContent: "center"
677
749
  },
678
750
  title: {
679
- marginBottom: Spacing__default["default"].xSmall_8
751
+ marginBottom: wonderBlocksTokens.spacing.xSmall_8
680
752
  },
681
753
  iconContainer: {
682
754
  alignItems: "center",
683
755
  justifyContent: "center",
684
- height: Spacing__default["default"].xxxLarge_64,
685
- width: Spacing__default["default"].xxxLarge_64,
686
- minWidth: Spacing__default["default"].xxxLarge_64,
687
- marginRight: Spacing__default["default"].medium_16,
756
+ height: wonderBlocksTokens.spacing.xxxLarge_64,
757
+ width: wonderBlocksTokens.spacing.xxxLarge_64,
758
+ minWidth: wonderBlocksTokens.spacing.xxxLarge_64,
759
+ marginRight: wonderBlocksTokens.spacing.medium_16,
688
760
  overflow: "hidden"
689
761
  },
690
762
  icon: {
@@ -694,15 +766,15 @@ const styles = aphrodite.StyleSheet.create({
694
766
  flexDirection: "row"
695
767
  },
696
768
  image: {
697
- marginBottom: Spacing__default["default"].large_24,
698
- marginLeft: -Spacing__default["default"].large_24,
699
- marginRight: -Spacing__default["default"].large_24,
700
- marginTop: -Spacing__default["default"].large_24,
701
- width: `calc(100% + ${Spacing__default["default"].large_24 * 2}px)`
769
+ marginBottom: wonderBlocksTokens.spacing.large_24,
770
+ marginLeft: -wonderBlocksTokens.spacing.large_24,
771
+ marginRight: -wonderBlocksTokens.spacing.large_24,
772
+ marginTop: -wonderBlocksTokens.spacing.large_24,
773
+ width: `calc(100% + ${wonderBlocksTokens.spacing.large_24 * 2}px)`
702
774
  },
703
775
  imageToBottom: {
704
- marginBottom: -Spacing__default["default"].large_24,
705
- marginTop: Spacing__default["default"].large_24,
776
+ marginBottom: -wonderBlocksTokens.spacing.large_24,
777
+ marginTop: wonderBlocksTokens.spacing.large_24,
706
778
  order: 1
707
779
  }
708
780
  });
@@ -1 +1,6 @@
1
1
  export declare function findFocusableNodes(root: HTMLElement | Document): Array<HTMLElement>;
2
+ /**
3
+ * Checks if an element is focusable
4
+ * @see https://html.spec.whatwg.org/multipage/interaction.html#focusable-area
5
+ */
6
+ export declare function isFocusable(element: HTMLElement): boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@khanacademy/wonder-blocks-popover",
3
- "version": "3.0.23",
3
+ "version": "3.1.1",
4
4
  "design": "v1",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -16,12 +16,11 @@
16
16
  "license": "MIT",
17
17
  "dependencies": {
18
18
  "@babel/runtime": "^7.18.6",
19
- "@khanacademy/wonder-blocks-color": "^3.0.0",
20
19
  "@khanacademy/wonder-blocks-core": "^6.3.1",
21
- "@khanacademy/wonder-blocks-icon-button": "^5.1.8",
22
- "@khanacademy/wonder-blocks-modal": "^4.2.1",
23
- "@khanacademy/wonder-blocks-spacing": "^4.0.1",
24
- "@khanacademy/wonder-blocks-tooltip": "^2.1.25",
20
+ "@khanacademy/wonder-blocks-icon-button": "^5.1.9",
21
+ "@khanacademy/wonder-blocks-modal": "^4.2.2",
22
+ "@khanacademy/wonder-blocks-tokens": "^0.2.0",
23
+ "@khanacademy/wonder-blocks-tooltip": "^2.1.26",
25
24
  "@khanacademy/wonder-blocks-typography": "^2.1.10"
26
25
  },
27
26
  "peerDependencies": {
@@ -1,31 +1,19 @@
1
1
  import * as React from "react";
2
- import * as ReactDOM from "react-dom";
3
2
  import {render, screen} from "@testing-library/react";
4
3
  import userEvent from "@testing-library/user-event";
5
4
 
6
5
  import FocusManager from "../focus-manager";
7
- import {findFocusableNodes} from "../../util/util";
8
6
 
9
7
  describe("FocusManager", () => {
10
8
  it("should focus on the first focusable element inside the popover", async () => {
11
9
  // Arrange
12
- const ref = await new Promise((resolve: any) => {
13
- const nodes = (
14
- <div ref={resolve}>
15
- <button>Open popover</button>
16
- <button>Next focusable element outside</button>
17
- </div>
18
- );
19
- render(nodes);
20
- });
21
- // @ts-expect-error [FEI-5019] - TS2345 - Argument of type 'unknown' is not assignable to parameter of type 'ReactInstance | null | undefined'.
22
- const domNode = ReactDOM.findDOMNode(ref) as HTMLElement;
23
-
24
- // mock focusable elements in document
25
- // eslint-disable-next-line testing-library/no-node-access
26
- global.document.querySelectorAll = jest
27
- .fn()
28
- .mockImplementation(() => findFocusableNodes(domNode));
10
+ const externalNodes = (
11
+ <div>
12
+ <button>Open popover</button>
13
+ <button>Next focusable element outside</button>
14
+ </div>
15
+ );
16
+ render(externalNodes);
29
17
 
30
18
  // get the anchor reference to be able pass it to the FocusManager
31
19
  const anchorElementNode = screen.getByRole("button", {
@@ -58,23 +46,13 @@ describe("FocusManager", () => {
58
46
 
59
47
  it("should focus on the last focusable element inside the popover", async () => {
60
48
  // Arrange
61
- const ref = await new Promise((resolve: any) => {
62
- const nodes = (
63
- <div ref={resolve}>
64
- <button>Open popover</button>
65
- <button>Next focusable element outside</button>
66
- </div>
67
- );
68
- render(nodes);
69
- });
70
- // @ts-expect-error [FEI-5019] - TS2345 - Argument of type 'unknown' is not assignable to parameter of type 'ReactInstance | null | undefined'.
71
- const domNode = ReactDOM.findDOMNode(ref) as HTMLElement;
72
-
73
- // mock focusable elements in document
74
- // eslint-disable-next-line testing-library/no-node-access
75
- global.document.querySelectorAll = jest
76
- .fn()
77
- .mockImplementation(() => findFocusableNodes(domNode));
49
+ const externalNodes = (
50
+ <div>
51
+ <button>Open popover</button>
52
+ <button>Next focusable element outside</button>
53
+ </div>
54
+ );
55
+ render(externalNodes);
78
56
 
79
57
  // get the anchor reference to be able pass it to the FocusManager
80
58
  const anchorElementNode = screen.getByRole("button", {
@@ -109,4 +87,105 @@ describe("FocusManager", () => {
109
87
  // Assert
110
88
  expect(lastFocusableElementInside).toHaveFocus();
111
89
  });
90
+
91
+ it("should allow flowing the focus correctly", async () => {
92
+ // Arrange
93
+ const externalNodes = (
94
+ <div>
95
+ <button>Prev focusable element outside</button>
96
+ <button>Open popover</button>
97
+ <button>Next focusable element outside</button>
98
+ </div>
99
+ );
100
+ render(externalNodes);
101
+
102
+ // get the anchor reference to be able pass it to the FocusManager
103
+ const anchorElementNode = screen.getByRole("button", {
104
+ name: "Open popover",
105
+ });
106
+
107
+ render(
108
+ <FocusManager anchorElement={anchorElementNode}>
109
+ <div>
110
+ <button>first focusable element inside</button>
111
+ </div>
112
+ </FocusManager>,
113
+ );
114
+
115
+ // Act
116
+ // 1. focus on the previous element before the popover
117
+ userEvent.tab();
118
+
119
+ // 2. focus on the anchor element
120
+ userEvent.tab();
121
+
122
+ // 3. focus on focusable element inside the popover
123
+ userEvent.tab();
124
+
125
+ // 4. focus on the next focusable element outside the popover (this will
126
+ // be the first focusable element outside the popover)
127
+ userEvent.tab();
128
+
129
+ // NOTE: At this point, the focus moves to the document body, so we need
130
+ // to press tab again to move the focus to the next focusable element.
131
+ userEvent.tab();
132
+
133
+ // 5. Finally focus on the first element in the document
134
+ userEvent.tab();
135
+
136
+ // find previous focusable element outside the popover
137
+ const prevFocusableElementOutside = screen.getByRole("button", {
138
+ name: "Prev focusable element outside",
139
+ });
140
+
141
+ // Assert
142
+ expect(prevFocusableElementOutside).toHaveFocus();
143
+ });
144
+
145
+ it("should disallow focusability on internal elements if the user focus out of the focus manager", async () => {
146
+ // Arrange
147
+ const externalNodes = (
148
+ <div>
149
+ <button>Prev focusable element outside</button>
150
+ <button>Open popover</button>
151
+ <button>Next focusable element outside</button>
152
+ </div>
153
+ );
154
+ render(externalNodes);
155
+
156
+ // get the anchor reference to be able pass it to the FocusManager
157
+ const anchorElementNode = screen.getByRole("button", {
158
+ name: "Open popover",
159
+ });
160
+
161
+ render(
162
+ <FocusManager anchorElement={anchorElementNode}>
163
+ <div>
164
+ <button>first focusable element inside</button>
165
+ </div>
166
+ </FocusManager>,
167
+ );
168
+
169
+ // Act
170
+ // 1. focus on the previous element before the popover
171
+ userEvent.tab();
172
+
173
+ // 2. focus on the anchor element
174
+ userEvent.tab();
175
+
176
+ // 3. focus on focusable element inside the popover
177
+ userEvent.tab();
178
+
179
+ // 4. focus on the next focusable element outside the popover (this will
180
+ // be the first focusable element outside the popover)
181
+ userEvent.tab();
182
+
183
+ // The elements inside the focus manager should not be focusable anymore.
184
+ const focusableElementInside = screen.getByRole("button", {
185
+ name: "first focusable element inside",
186
+ });
187
+
188
+ // Assert
189
+ expect(focusableElementInside).toHaveAttribute("tabIndex", "-1");
190
+ });
112
191
  });