@khanacademy/wonder-blocks-popover 3.0.22 → 3.1.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # @khanacademy/wonder-blocks-popover
2
2
 
3
+ ## 3.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 7944c7d3: Add `closedFocusId` prop to manually return focus to a specific element
8
+
9
+ ### Patch Changes
10
+
11
+ - e5dd6215: Don't return focus to trigger element if the focus has to go to a different interactive element
12
+ - 163cfca3: Fix tab navigation order
13
+
14
+ ## 3.0.23
15
+
16
+ ### Patch Changes
17
+
18
+ - @khanacademy/wonder-blocks-icon-button@5.1.8
19
+ - @khanacademy/wonder-blocks-modal@4.2.1
20
+ - @khanacademy/wonder-blocks-tooltip@2.1.25
21
+
3
22
  ## 3.0.22
4
23
 
5
24
  ### Patch Changes
@@ -49,11 +49,22 @@ export default class FocusManager extends React.Component<Props> {
49
49
  /**
50
50
  * List of focusable elements within the popover content
51
51
  */
52
- focusableElementsInPopover: Array<HTMLElement>;
52
+ elementsThatCanBeFocusableInsidePopover: Array<HTMLElement>;
53
+ /**
54
+ * The first focusable element inside the popover (if it exists)
55
+ */
56
+ firstFocusableElementInPopover: HTMLElement | null | undefined;
57
+ /**
58
+ * The last focusable element inside the popover (if it exists)
59
+ */
60
+ lastFocusableElementInPopover: HTMLElement | null | undefined;
53
61
  /**
54
62
  * Add keydown listeners
55
63
  */
56
64
  addEventListeners: () => void;
65
+ removeEventListeners(): void;
66
+ handleKeydownFirstFocusableElement: (e: KeyboardEvent) => void;
67
+ handleKeydownLastFocusableElement: (e: KeyboardEvent) => void;
57
68
  /**
58
69
  * Gets the next focusable element after the anchor element
59
70
  */
@@ -67,6 +78,12 @@ export default class FocusManager extends React.Component<Props> {
67
78
  * focus will be redirected to the anchor element.
68
79
  */
69
80
  handleFocusPreviousFocusableElement: () => void;
81
+ /**
82
+ * Toggle focusability for all the focusable elements inside the popover.
83
+ * This is useful to prevent the user from tabbing into the popover when it
84
+ * reaches to the last focusable element within the document.
85
+ */
86
+ changeFocusabilityInsidePopover: (enabled?: boolean) => void;
70
87
  /**
71
88
  * Triggered when the focus is set to the last sentinel. This way, the focus
72
89
  * will be redirected to next element after the anchor element.
@@ -5,7 +5,7 @@ type Props = {
5
5
  /**
6
6
  * Called when `esc` is pressed
7
7
  */
8
- onClose: () => unknown;
8
+ onClose: (shouldReturnFocus: boolean) => unknown;
9
9
  /**
10
10
  * Popover Content ref.
11
11
  * Will close the popover when clicking outside this element.
@@ -49,6 +49,12 @@ type Props = AriaProps & Readonly<{
49
49
  *
50
50
  */
51
51
  id?: string;
52
+ /**
53
+ * The selector for the element that will be focused after the popover
54
+ * dialog closes. When not set, the element that triggered the popover
55
+ * will be used.
56
+ */
57
+ closedFocusId?: string;
52
58
  /**
53
59
  * The selector for the element that will be focused when the popover
54
60
  * content shows. When not set, the first focusable element within the
@@ -129,10 +135,14 @@ export default class Popover extends React.Component<Props, State> {
129
135
  * Popover content ref
130
136
  */
131
137
  contentRef: React.RefObject<PopoverContent | PopoverContentCore>;
138
+ /**
139
+ * Returns focus to a given element.
140
+ */
141
+ maybeReturnFocus: () => void;
132
142
  /**
133
143
  * Popover dialog closed
134
144
  */
135
- handleClose: () => void;
145
+ handleClose: (shouldReturnFocus?: boolean) => void;
136
146
  /**
137
147
  * Popover dialog opened
138
148
  */
package/dist/es/index.js CHANGED
@@ -133,6 +133,9 @@ const FOCUSABLE_ELEMENTS = 'button, [href], input, select, textarea, [tabindex]:
133
133
  function findFocusableNodes(root) {
134
134
  return Array.from(root.querySelectorAll(FOCUSABLE_ELEMENTS));
135
135
  }
136
+ function isFocusable(element) {
137
+ return element.matches(FOCUSABLE_ELEMENTS);
138
+ }
136
139
 
137
140
  class InitialFocus extends React.Component {
138
141
  constructor(...args) {
@@ -180,17 +183,49 @@ class FocusManager extends React.Component {
180
183
  super(...args);
181
184
  this.nextElementAfterPopover = void 0;
182
185
  this.rootNode = void 0;
183
- this.focusableElementsInPopover = [];
186
+ this.elementsThatCanBeFocusableInsidePopover = [];
187
+ this.firstFocusableElementInPopover = null;
188
+ this.lastFocusableElementInPopover = null;
184
189
  this.addEventListeners = () => {
185
190
  const {
186
191
  anchorElement
187
192
  } = this.props;
188
193
  if (anchorElement) {
189
- anchorElement.addEventListener("keydown", this.handleKeydownPreviousFocusableElement, true);
194
+ anchorElement.addEventListener("keydown", this.handleKeydownPreviousFocusableElement);
195
+ }
196
+ if (this.rootNode) {
197
+ this.elementsThatCanBeFocusableInsidePopover = findFocusableNodes(this.rootNode);
198
+ this.firstFocusableElementInPopover = this.elementsThatCanBeFocusableInsidePopover[0];
199
+ this.lastFocusableElementInPopover = this.elementsThatCanBeFocusableInsidePopover[this.elementsThatCanBeFocusableInsidePopover.length - 1];
190
200
  }
191
201
  this.nextElementAfterPopover = this.getNextFocusableElement();
202
+ if (!this.nextElementAfterPopover) {
203
+ window.addEventListener("blur", () => {
204
+ this.changeFocusabilityInsidePopover(true);
205
+ });
206
+ }
207
+ if (this.firstFocusableElementInPopover) {
208
+ this.firstFocusableElementInPopover.addEventListener("keydown", this.handleKeydownFirstFocusableElement);
209
+ }
210
+ if (this.lastFocusableElementInPopover) {
211
+ this.lastFocusableElementInPopover.addEventListener("keydown", this.handleKeydownLastFocusableElement);
212
+ }
192
213
  if (this.nextElementAfterPopover) {
193
- this.nextElementAfterPopover.addEventListener("keydown", this.handleKeydownNextFocusableElement, true);
214
+ this.nextElementAfterPopover.addEventListener("keydown", this.handleKeydownNextFocusableElement);
215
+ }
216
+ };
217
+ this.handleKeydownFirstFocusableElement = e => {
218
+ if (e.key === "Tab" && e.shiftKey) {
219
+ var _this$props$anchorEle;
220
+ e.preventDefault();
221
+ (_this$props$anchorEle = this.props.anchorElement) == null ? void 0 : _this$props$anchorEle.focus();
222
+ }
223
+ };
224
+ this.handleKeydownLastFocusableElement = e => {
225
+ if (this.nextElementAfterPopover && e.key === "Tab" && !e.shiftKey) {
226
+ var _this$nextElementAfte;
227
+ e.preventDefault();
228
+ (_this$nextElementAfte = this.nextElementAfterPopover) == null ? void 0 : _this$nextElementAfte.focus();
194
229
  }
195
230
  };
196
231
  this.getNextFocusableElement = () => {
@@ -201,10 +236,14 @@ class FocusManager extends React.Component {
201
236
  return;
202
237
  }
203
238
  const focusableElements = findFocusableNodes(document);
204
- const anchorIndex = focusableElements.indexOf(anchorElement);
205
- if (anchorIndex >= 0) {
206
- const nextElementIndex = anchorIndex < focusableElements.length - 1 ? anchorIndex + 1 : 0;
207
- return focusableElements[nextElementIndex];
239
+ const focusableElementsOutside = focusableElements.filter(element => {
240
+ const index = this.elementsThatCanBeFocusableInsidePopover.indexOf(element);
241
+ return index < 0;
242
+ });
243
+ const anchorIndex = focusableElementsOutside.indexOf(anchorElement);
244
+ if (anchorIndex >= 0 && anchorIndex !== focusableElementsOutside.length - 1) {
245
+ const nextElementIndex = anchorIndex < focusableElementsOutside.length - 1 ? anchorIndex + 1 : 0;
246
+ return focusableElementsOutside[nextElementIndex];
208
247
  }
209
248
  return;
210
249
  };
@@ -217,13 +256,18 @@ class FocusManager extends React.Component {
217
256
  throw new Error("Assertion error: root node should exist after mount");
218
257
  }
219
258
  this.rootNode = rootNode;
220
- this.focusableElementsInPopover = findFocusableNodes(this.rootNode);
221
259
  };
222
260
  this.handleFocusPreviousFocusableElement = () => {
223
261
  if (this.props.anchorElement) {
224
262
  this.props.anchorElement.focus();
225
263
  }
226
264
  };
265
+ this.changeFocusabilityInsidePopover = (enabled = true) => {
266
+ const tabIndex = enabled ? "0" : "-1";
267
+ this.elementsThatCanBeFocusableInsidePopover.forEach(element => {
268
+ element.setAttribute("tabIndex", tabIndex);
269
+ });
270
+ };
227
271
  this.handleFocusNextFocusableElement = () => {
228
272
  if (this.nextElementAfterPopover) {
229
273
  this.nextElementAfterPopover.focus();
@@ -231,15 +275,16 @@ class FocusManager extends React.Component {
231
275
  };
232
276
  this.handleKeydownPreviousFocusableElement = e => {
233
277
  if (e.key === "Tab" && !e.shiftKey) {
278
+ var _this$firstFocusableE;
234
279
  e.preventDefault();
235
- this.focusableElementsInPopover[0].focus();
280
+ (_this$firstFocusableE = this.firstFocusableElementInPopover) == null ? void 0 : _this$firstFocusableE.focus();
236
281
  }
237
282
  };
238
283
  this.handleKeydownNextFocusableElement = e => {
239
284
  if (e.key === "Tab" && e.shiftKey) {
285
+ var _this$lastFocusableEl;
240
286
  e.preventDefault();
241
- const lastElementIndex = this.focusableElementsInPopover.length - 1;
242
- this.focusableElementsInPopover[lastElementIndex].focus();
287
+ (_this$lastFocusableEl = this.lastFocusableElementInPopover) == null ? void 0 : _this$lastFocusableEl.focus();
243
288
  }
244
289
  };
245
290
  }
@@ -247,41 +292,53 @@ class FocusManager extends React.Component {
247
292
  this.addEventListeners();
248
293
  }
249
294
  componentDidUpdate() {
295
+ this.removeEventListeners();
250
296
  this.addEventListeners();
251
297
  }
252
298
  componentWillUnmount() {
299
+ this.changeFocusabilityInsidePopover(true);
300
+ this.removeEventListeners();
301
+ }
302
+ removeEventListeners() {
253
303
  const {
254
304
  anchorElement
255
305
  } = this.props;
256
306
  if (anchorElement) {
257
- setTimeout(() => anchorElement.focus(), 0);
258
- anchorElement.removeEventListener("keydown", this.handleKeydownPreviousFocusableElement, true);
307
+ anchorElement.removeEventListener("keydown", this.handleKeydownPreviousFocusableElement);
308
+ }
309
+ if (!this.nextElementAfterPopover) {
310
+ window.removeEventListener("blur", () => {
311
+ this.changeFocusabilityInsidePopover(true);
312
+ });
313
+ }
314
+ if (this.firstFocusableElementInPopover) {
315
+ this.firstFocusableElementInPopover.removeEventListener("keydown", this.handleKeydownFirstFocusableElement);
316
+ }
317
+ if (this.lastFocusableElementInPopover) {
318
+ this.lastFocusableElementInPopover.removeEventListener("keydown", this.handleKeydownLastFocusableElement);
259
319
  }
260
320
  if (this.nextElementAfterPopover) {
261
- this.nextElementAfterPopover.removeEventListener("keydown", this.handleKeydownNextFocusableElement, true);
321
+ this.nextElementAfterPopover.removeEventListener("keydown", this.handleKeydownNextFocusableElement);
262
322
  }
263
323
  }
264
324
  render() {
265
325
  const {
266
326
  children
267
327
  } = this.props;
268
- return React.createElement(React.Fragment, null, React.createElement("div", {
269
- tabIndex: 0,
270
- onFocus: this.handleFocusPreviousFocusableElement,
271
- style: {
272
- position: "fixed"
328
+ return React.createElement("div", {
329
+ ref: this.getComponentRootNode,
330
+ onClick: () => {
331
+ this.changeFocusabilityInsidePopover(true);
332
+ },
333
+ onFocus: () => {
334
+ this.changeFocusabilityInsidePopover(true);
335
+ },
336
+ onBlur: () => {
337
+ this.changeFocusabilityInsidePopover(false);
273
338
  }
274
- }), React.createElement("div", {
275
- ref: this.getComponentRootNode
276
339
  }, React.createElement(InitialFocus, {
277
340
  initialFocusId: this.props.initialFocusId
278
- }, children)), React.createElement("div", {
279
- tabIndex: 0,
280
- onFocus: this.handleFocusNextFocusableElement,
281
- style: {
282
- position: "fixed"
283
- }
284
- }));
341
+ }, children));
285
342
  }
286
343
  }
287
344
 
@@ -295,7 +352,7 @@ class PopoverEventListener extends React.Component {
295
352
  if (e.key === "Escape") {
296
353
  e.preventDefault();
297
354
  e.stopPropagation();
298
- this.props.onClose();
355
+ this.props.onClose(true);
299
356
  }
300
357
  };
301
358
  this._handleClick = e => {
@@ -310,7 +367,8 @@ class PopoverEventListener extends React.Component {
310
367
  if (node && !node.contains(e.target)) {
311
368
  e.preventDefault();
312
369
  e.stopPropagation();
313
- this.props.onClose();
370
+ const shouldReturnFocus = !isFocusable(e.target);
371
+ this.props.onClose(shouldReturnFocus);
314
372
  }
315
373
  };
316
374
  }
@@ -335,19 +393,36 @@ class Popover extends React.Component {
335
393
  placement: this.props.placement
336
394
  };
337
395
  this.contentRef = React.createRef();
338
- this.handleClose = () => {
396
+ this.maybeReturnFocus = () => {
397
+ const {
398
+ anchorElement
399
+ } = this.state;
400
+ const {
401
+ closedFocusId
402
+ } = this.props;
403
+ if (closedFocusId) {
404
+ const focusElement = ReactDOM.findDOMNode(document.getElementById(closedFocusId));
405
+ focusElement == null ? void 0 : focusElement.focus();
406
+ return;
407
+ }
408
+ if (anchorElement) {
409
+ anchorElement.focus();
410
+ }
411
+ };
412
+ this.handleClose = (shouldReturnFocus = true) => {
339
413
  this.setState({
340
414
  opened: false
341
415
  }, () => {
342
416
  var _this$props$onClose, _this$props;
343
417
  (_this$props$onClose = (_this$props = this.props).onClose) == null ? void 0 : _this$props$onClose.call(_this$props);
418
+ if (shouldReturnFocus) {
419
+ this.maybeReturnFocus();
420
+ }
344
421
  });
345
422
  };
346
423
  this.handleOpen = () => {
347
424
  if (this.props.dismissEnabled && this.state.opened) {
348
- this.setState({
349
- opened: false
350
- });
425
+ this.handleClose(true);
351
426
  } else {
352
427
  this.setState({
353
428
  opened: true
package/dist/index.js CHANGED
@@ -164,6 +164,9 @@ const FOCUSABLE_ELEMENTS = 'button, [href], input, select, textarea, [tabindex]:
164
164
  function findFocusableNodes(root) {
165
165
  return Array.from(root.querySelectorAll(FOCUSABLE_ELEMENTS));
166
166
  }
167
+ function isFocusable(element) {
168
+ return element.matches(FOCUSABLE_ELEMENTS);
169
+ }
167
170
 
168
171
  class InitialFocus extends React__namespace.Component {
169
172
  constructor(...args) {
@@ -211,17 +214,49 @@ class FocusManager extends React__namespace.Component {
211
214
  super(...args);
212
215
  this.nextElementAfterPopover = void 0;
213
216
  this.rootNode = void 0;
214
- this.focusableElementsInPopover = [];
217
+ this.elementsThatCanBeFocusableInsidePopover = [];
218
+ this.firstFocusableElementInPopover = null;
219
+ this.lastFocusableElementInPopover = null;
215
220
  this.addEventListeners = () => {
216
221
  const {
217
222
  anchorElement
218
223
  } = this.props;
219
224
  if (anchorElement) {
220
- anchorElement.addEventListener("keydown", this.handleKeydownPreviousFocusableElement, true);
225
+ anchorElement.addEventListener("keydown", this.handleKeydownPreviousFocusableElement);
226
+ }
227
+ if (this.rootNode) {
228
+ this.elementsThatCanBeFocusableInsidePopover = findFocusableNodes(this.rootNode);
229
+ this.firstFocusableElementInPopover = this.elementsThatCanBeFocusableInsidePopover[0];
230
+ this.lastFocusableElementInPopover = this.elementsThatCanBeFocusableInsidePopover[this.elementsThatCanBeFocusableInsidePopover.length - 1];
221
231
  }
222
232
  this.nextElementAfterPopover = this.getNextFocusableElement();
233
+ if (!this.nextElementAfterPopover) {
234
+ window.addEventListener("blur", () => {
235
+ this.changeFocusabilityInsidePopover(true);
236
+ });
237
+ }
238
+ if (this.firstFocusableElementInPopover) {
239
+ this.firstFocusableElementInPopover.addEventListener("keydown", this.handleKeydownFirstFocusableElement);
240
+ }
241
+ if (this.lastFocusableElementInPopover) {
242
+ this.lastFocusableElementInPopover.addEventListener("keydown", this.handleKeydownLastFocusableElement);
243
+ }
223
244
  if (this.nextElementAfterPopover) {
224
- this.nextElementAfterPopover.addEventListener("keydown", this.handleKeydownNextFocusableElement, true);
245
+ this.nextElementAfterPopover.addEventListener("keydown", this.handleKeydownNextFocusableElement);
246
+ }
247
+ };
248
+ this.handleKeydownFirstFocusableElement = e => {
249
+ if (e.key === "Tab" && e.shiftKey) {
250
+ var _this$props$anchorEle;
251
+ e.preventDefault();
252
+ (_this$props$anchorEle = this.props.anchorElement) == null ? void 0 : _this$props$anchorEle.focus();
253
+ }
254
+ };
255
+ this.handleKeydownLastFocusableElement = e => {
256
+ if (this.nextElementAfterPopover && e.key === "Tab" && !e.shiftKey) {
257
+ var _this$nextElementAfte;
258
+ e.preventDefault();
259
+ (_this$nextElementAfte = this.nextElementAfterPopover) == null ? void 0 : _this$nextElementAfte.focus();
225
260
  }
226
261
  };
227
262
  this.getNextFocusableElement = () => {
@@ -232,10 +267,14 @@ class FocusManager extends React__namespace.Component {
232
267
  return;
233
268
  }
234
269
  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];
270
+ const focusableElementsOutside = focusableElements.filter(element => {
271
+ const index = this.elementsThatCanBeFocusableInsidePopover.indexOf(element);
272
+ return index < 0;
273
+ });
274
+ const anchorIndex = focusableElementsOutside.indexOf(anchorElement);
275
+ if (anchorIndex >= 0 && anchorIndex !== focusableElementsOutside.length - 1) {
276
+ const nextElementIndex = anchorIndex < focusableElementsOutside.length - 1 ? anchorIndex + 1 : 0;
277
+ return focusableElementsOutside[nextElementIndex];
239
278
  }
240
279
  return;
241
280
  };
@@ -248,13 +287,18 @@ class FocusManager extends React__namespace.Component {
248
287
  throw new Error("Assertion error: root node should exist after mount");
249
288
  }
250
289
  this.rootNode = rootNode;
251
- this.focusableElementsInPopover = findFocusableNodes(this.rootNode);
252
290
  };
253
291
  this.handleFocusPreviousFocusableElement = () => {
254
292
  if (this.props.anchorElement) {
255
293
  this.props.anchorElement.focus();
256
294
  }
257
295
  };
296
+ this.changeFocusabilityInsidePopover = (enabled = true) => {
297
+ const tabIndex = enabled ? "0" : "-1";
298
+ this.elementsThatCanBeFocusableInsidePopover.forEach(element => {
299
+ element.setAttribute("tabIndex", tabIndex);
300
+ });
301
+ };
258
302
  this.handleFocusNextFocusableElement = () => {
259
303
  if (this.nextElementAfterPopover) {
260
304
  this.nextElementAfterPopover.focus();
@@ -262,15 +306,16 @@ class FocusManager extends React__namespace.Component {
262
306
  };
263
307
  this.handleKeydownPreviousFocusableElement = e => {
264
308
  if (e.key === "Tab" && !e.shiftKey) {
309
+ var _this$firstFocusableE;
265
310
  e.preventDefault();
266
- this.focusableElementsInPopover[0].focus();
311
+ (_this$firstFocusableE = this.firstFocusableElementInPopover) == null ? void 0 : _this$firstFocusableE.focus();
267
312
  }
268
313
  };
269
314
  this.handleKeydownNextFocusableElement = e => {
270
315
  if (e.key === "Tab" && e.shiftKey) {
316
+ var _this$lastFocusableEl;
271
317
  e.preventDefault();
272
- const lastElementIndex = this.focusableElementsInPopover.length - 1;
273
- this.focusableElementsInPopover[lastElementIndex].focus();
318
+ (_this$lastFocusableEl = this.lastFocusableElementInPopover) == null ? void 0 : _this$lastFocusableEl.focus();
274
319
  }
275
320
  };
276
321
  }
@@ -278,41 +323,53 @@ class FocusManager extends React__namespace.Component {
278
323
  this.addEventListeners();
279
324
  }
280
325
  componentDidUpdate() {
326
+ this.removeEventListeners();
281
327
  this.addEventListeners();
282
328
  }
283
329
  componentWillUnmount() {
330
+ this.changeFocusabilityInsidePopover(true);
331
+ this.removeEventListeners();
332
+ }
333
+ removeEventListeners() {
284
334
  const {
285
335
  anchorElement
286
336
  } = this.props;
287
337
  if (anchorElement) {
288
- setTimeout(() => anchorElement.focus(), 0);
289
- anchorElement.removeEventListener("keydown", this.handleKeydownPreviousFocusableElement, true);
338
+ anchorElement.removeEventListener("keydown", this.handleKeydownPreviousFocusableElement);
339
+ }
340
+ if (!this.nextElementAfterPopover) {
341
+ window.removeEventListener("blur", () => {
342
+ this.changeFocusabilityInsidePopover(true);
343
+ });
344
+ }
345
+ if (this.firstFocusableElementInPopover) {
346
+ this.firstFocusableElementInPopover.removeEventListener("keydown", this.handleKeydownFirstFocusableElement);
347
+ }
348
+ if (this.lastFocusableElementInPopover) {
349
+ this.lastFocusableElementInPopover.removeEventListener("keydown", this.handleKeydownLastFocusableElement);
290
350
  }
291
351
  if (this.nextElementAfterPopover) {
292
- this.nextElementAfterPopover.removeEventListener("keydown", this.handleKeydownNextFocusableElement, true);
352
+ this.nextElementAfterPopover.removeEventListener("keydown", this.handleKeydownNextFocusableElement);
293
353
  }
294
354
  }
295
355
  render() {
296
356
  const {
297
357
  children
298
358
  } = 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"
359
+ return React__namespace.createElement("div", {
360
+ ref: this.getComponentRootNode,
361
+ onClick: () => {
362
+ this.changeFocusabilityInsidePopover(true);
363
+ },
364
+ onFocus: () => {
365
+ this.changeFocusabilityInsidePopover(true);
366
+ },
367
+ onBlur: () => {
368
+ this.changeFocusabilityInsidePopover(false);
304
369
  }
305
- }), React__namespace.createElement("div", {
306
- ref: this.getComponentRootNode
307
370
  }, React__namespace.createElement(InitialFocus, {
308
371
  initialFocusId: this.props.initialFocusId
309
- }, children)), React__namespace.createElement("div", {
310
- tabIndex: 0,
311
- onFocus: this.handleFocusNextFocusableElement,
312
- style: {
313
- position: "fixed"
314
- }
315
- }));
372
+ }, children));
316
373
  }
317
374
  }
318
375
 
@@ -326,7 +383,7 @@ class PopoverEventListener extends React__namespace.Component {
326
383
  if (e.key === "Escape") {
327
384
  e.preventDefault();
328
385
  e.stopPropagation();
329
- this.props.onClose();
386
+ this.props.onClose(true);
330
387
  }
331
388
  };
332
389
  this._handleClick = e => {
@@ -341,7 +398,8 @@ class PopoverEventListener extends React__namespace.Component {
341
398
  if (node && !node.contains(e.target)) {
342
399
  e.preventDefault();
343
400
  e.stopPropagation();
344
- this.props.onClose();
401
+ const shouldReturnFocus = !isFocusable(e.target);
402
+ this.props.onClose(shouldReturnFocus);
345
403
  }
346
404
  };
347
405
  }
@@ -366,19 +424,36 @@ class Popover extends React__namespace.Component {
366
424
  placement: this.props.placement
367
425
  };
368
426
  this.contentRef = React__namespace.createRef();
369
- this.handleClose = () => {
427
+ this.maybeReturnFocus = () => {
428
+ const {
429
+ anchorElement
430
+ } = this.state;
431
+ const {
432
+ closedFocusId
433
+ } = this.props;
434
+ if (closedFocusId) {
435
+ const focusElement = ReactDOM__namespace.findDOMNode(document.getElementById(closedFocusId));
436
+ focusElement == null ? void 0 : focusElement.focus();
437
+ return;
438
+ }
439
+ if (anchorElement) {
440
+ anchorElement.focus();
441
+ }
442
+ };
443
+ this.handleClose = (shouldReturnFocus = true) => {
370
444
  this.setState({
371
445
  opened: false
372
446
  }, () => {
373
447
  var _this$props$onClose, _this$props;
374
448
  (_this$props$onClose = (_this$props = this.props).onClose) == null ? void 0 : _this$props$onClose.call(_this$props);
449
+ if (shouldReturnFocus) {
450
+ this.maybeReturnFocus();
451
+ }
375
452
  });
376
453
  };
377
454
  this.handleOpen = () => {
378
455
  if (this.props.dismissEnabled && this.state.opened) {
379
- this.setState({
380
- opened: false
381
- });
456
+ this.handleClose(true);
382
457
  } else {
383
458
  this.setState({
384
459
  opened: true
@@ -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.22",
3
+ "version": "3.1.0",
4
4
  "design": "v1",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -18,10 +18,10 @@
18
18
  "@babel/runtime": "^7.18.6",
19
19
  "@khanacademy/wonder-blocks-color": "^3.0.0",
20
20
  "@khanacademy/wonder-blocks-core": "^6.3.1",
21
- "@khanacademy/wonder-blocks-icon-button": "^5.1.7",
22
- "@khanacademy/wonder-blocks-modal": "^4.2.0",
21
+ "@khanacademy/wonder-blocks-icon-button": "^5.1.8",
22
+ "@khanacademy/wonder-blocks-modal": "^4.2.1",
23
23
  "@khanacademy/wonder-blocks-spacing": "^4.0.1",
24
- "@khanacademy/wonder-blocks-tooltip": "^2.1.24",
24
+ "@khanacademy/wonder-blocks-tooltip": "^2.1.25",
25
25
  "@khanacademy/wonder-blocks-typography": "^2.1.10"
26
26
  },
27
27
  "peerDependencies": {