focus-trap-react 8.4.2 → 8.7.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,35 @@
1
1
  # Changelog
2
2
 
3
+ ## 8.7.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 7fbe8ca: Update to support new features in `focus-trap@6.6.0` including `initialFocus` which can now be false to prevent initial focus, and `escapeDeactivates` which can now alternately be a function that returns a boolean instead of a straight boolean.
8
+
9
+ ## 8.6.0
10
+
11
+ ### Minor Changes
12
+
13
+ - 5292ae8:
14
+ - Adding support for new focus-trap options from focus-trap v6.5.0: `checkCanFocusTrap()`, `onPostActivate()`, `checkCanReturnFocus()`, and `onPostDeactivate()`.
15
+ - Adding support (bug fix) for existing focus-trap `setReturnFocus` option that had thus far been ignored, with focus-trap-react always returning focus to the previously-focused element prior to activation regardless of the use of the `setReturnFocus` option. The option is now respected the same as it is when using focus-trap directly.
16
+
17
+ ### Patch Changes
18
+
19
+ - 24704c7: Bump focus-trap dependency to 6.5.1 for bug fix to onPostDeactivate.
20
+
21
+ ## 8.5.1
22
+
23
+ ### Patch Changes
24
+
25
+ - b8d7071: Bump focus-trap dependency to 6.5.0 to get new features
26
+
27
+ ## 8.5.0
28
+
29
+ ### Minor Changes
30
+
31
+ - 6ee37fb: Bump focus-trap from 6.3.0 to 6.4.0. There should be no changes in behavior as a result of this upgrade.
32
+
3
33
  ## 8.4.2
4
34
 
5
35
  ### Patch Changes
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # focus-trap-react [![CI](https://github.com/focus-trap/focus-trap-react/workflows/CI/badge.svg?branch=master&event=push)](https://github.com/focus-trap/focus-trap-react/actions?query=workflow:CI+branch:master) [![Codecov](https://img.shields.io/codecov/c/github/focus-trap/focus-trap-react)](https://codecov.io/gh/focus-trap/focus-trap-react) [![license](https://badgen.now.sh/badge/license/MIT)](./LICENSE)
2
2
 
3
3
  <!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
4
- [![All Contributors](https://img.shields.io/badge/all_contributors-17-orange.svg?style=flat-square)](#contributors)
4
+ [![All Contributors](https://img.shields.io/badge/all_contributors-18-orange.svg?style=flat-square)](#contributors)
5
5
  <!-- ALL-CONTRIBUTORS-BADGE:END -->
6
6
 
7
7
  A React component that traps focus.
@@ -186,20 +186,21 @@ In alphabetical order:
186
186
  <td align="center"><a href="https://ofcr.se/"><img src="https://avatars1.githubusercontent.com/u/813865?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Benjamin Tan</b></sub></a><br /><a href="https://github.com/focus-trap/focus-trap-react/commits?author=bnjmnt4n" title="Documentation">📖</a></td>
187
187
  <td align="center"><a href="https://clintgoodman.com"><img src="https://avatars3.githubusercontent.com/u/5473697?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Clint Goodman</b></sub></a><br /><a href="https://github.com/focus-trap/focus-trap-react/commits?author=cgood92" title="Code">💻</a> <a href="https://github.com/focus-trap/focus-trap-react/commits?author=cgood92" title="Documentation">📖</a> <a href="#example-cgood92" title="Examples">💡</a> <a href="https://github.com/focus-trap/focus-trap-react/commits?author=cgood92" title="Tests">⚠️</a></td>
188
188
  <td align="center"><a href="https://github.com/DSil"><img src="https://avatars1.githubusercontent.com/u/6265045?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Daniel</b></sub></a><br /><a href="#maintenance-DSil" title="Maintenance">🚧</a> <a href="https://github.com/focus-trap/focus-trap-react/commits?author=DSil" title="Tests">⚠️</a></td>
189
+ <td align="center"><a href="https://github.com/Dan503"><img src="https://avatars.githubusercontent.com/u/10610368?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Daniel Tonon</b></sub></a><br /><a href="https://github.com/focus-trap/focus-trap-react/commits?author=Dan503" title="Documentation">📖</a> <a href="https://github.com/focus-trap/focus-trap-react/commits?author=Dan503" title="Code">💻</a> <a href="https://github.com/focus-trap/focus-trap-react/commits?author=Dan503" title="Tests">⚠️</a></td>
189
190
  <td align="center"><a href="http://davidtheclark.com/"><img src="https://avatars2.githubusercontent.com/u/628431?v=4?s=100" width="100px;" alt=""/><br /><sub><b>David Clark</b></sub></a><br /><a href="https://github.com/focus-trap/focus-trap-react/commits?author=davidtheclark" title="Code">💻</a> <a href="https://github.com/focus-trap/focus-trap-react/issues?q=author%3Adavidtheclark" title="Bug reports">🐛</a> <a href="#infra-davidtheclark" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/focus-trap/focus-trap-react/commits?author=davidtheclark" title="Tests">⚠️</a> <a href="https://github.com/focus-trap/focus-trap-react/commits?author=davidtheclark" title="Documentation">📖</a> <a href="#maintenance-davidtheclark" title="Maintenance">🚧</a></td>
190
191
  <td align="center"><a href="https://github.com/features/security"><img src="https://avatars1.githubusercontent.com/u/27347476?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Dependabot</b></sub></a><br /><a href="#maintenance-dependabot" title="Maintenance">🚧</a></td>
191
- <td align="center"><a href="http://josuzuki.me"><img src="https://avatars1.githubusercontent.com/u/9583920?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Jonathan Suzuki</b></sub></a><br /><a href="https://github.com/focus-trap/focus-trap-react/issues?q=author%3AJoSuzuki" title="Bug reports">🐛</a></td>
192
192
  </tr>
193
193
  <tr>
194
+ <td align="center"><a href="http://josuzuki.me"><img src="https://avatars1.githubusercontent.com/u/9583920?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Jonathan Suzuki</b></sub></a><br /><a href="https://github.com/focus-trap/focus-trap-react/issues?q=author%3AJoSuzuki" title="Bug reports">🐛</a></td>
194
195
  <td align="center"><a href="http://kathleenmcmahon.dev/"><img src="https://avatars1.githubusercontent.com/u/11621935?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Kathleen McMahon</b></sub></a><br /><a href="#maintenance-resource11" title="Maintenance">🚧</a></td>
195
196
  <td align="center"><a href="https://marais.io/"><img src="https://avatars2.githubusercontent.com/u/599459?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Marais Rossouw</b></sub></a><br /><a href="#infra-maraisr" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
196
197
  <td align="center"><a href="https://github.com/liunate"><img src="https://avatars2.githubusercontent.com/u/38996291?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Nate Liu</b></sub></a><br /><a href="https://github.com/focus-trap/focus-trap-react/commits?author=liunate" title="Tests">⚠️</a></td>
197
198
  <td align="center"><a href="https://www.linkedin.com/in/rivajunior/"><img src="https://avatars1.githubusercontent.com/u/11370172?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Rivaldo Junior</b></sub></a><br /><a href="#maintenance-rivajunior" title="Maintenance">🚧</a></td>
198
199
  <td align="center"><a href="https://scottrippey.github.io/"><img src="https://avatars3.githubusercontent.com/u/430608?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Scott Rippey</b></sub></a><br /><a href="https://github.com/focus-trap/focus-trap-react/commits?author=scottrippey" title="Code">💻</a> <a href="https://github.com/focus-trap/focus-trap-react/issues?q=author%3Ascottrippey" title="Bug reports">🐛</a></td>
199
200
  <td align="center"><a href="https://seanmcp.com/"><img src="https://avatars1.githubusercontent.com/u/6360367?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Sean McPherson</b></sub></a><br /><a href="https://github.com/focus-trap/focus-trap-react/commits?author=SeanMcP" title="Code">💻</a></td>
200
- <td align="center"><a href="https://recollectr.io"><img src="https://avatars2.githubusercontent.com/u/6835891?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Slapbox</b></sub></a><br /><a href="https://github.com/focus-trap/focus-trap-react/commits?author=Slapbox" title="Documentation">📖</a></td>
201
201
  </tr>
202
202
  <tr>
203
+ <td align="center"><a href="https://recollectr.io"><img src="https://avatars2.githubusercontent.com/u/6835891?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Slapbox</b></sub></a><br /><a href="https://github.com/focus-trap/focus-trap-react/commits?author=Slapbox" title="Documentation">📖</a></td>
203
204
  <td align="center"><a href="https://stefancameron.com/"><img src="https://avatars3.githubusercontent.com/u/2855350?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Stefan Cameron</b></sub></a><br /><a href="https://github.com/focus-trap/focus-trap-react/commits?author=stefcameron" title="Code">💻</a> <a href="https://github.com/focus-trap/focus-trap-react/issues?q=author%3Astefcameron" title="Bug reports">🐛</a> <a href="#infra-stefcameron" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/focus-trap/focus-trap-react/commits?author=stefcameron" title="Tests">⚠️</a> <a href="https://github.com/focus-trap/focus-trap-react/commits?author=stefcameron" title="Documentation">📖</a> <a href="#maintenance-stefcameron" title="Maintenance">🚧</a></td>
204
205
  <td align="center"><a href="http://tylerhawkins.info/201R/"><img src="https://avatars0.githubusercontent.com/u/13806458?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Tyler Hawkins</b></sub></a><br /><a href="https://github.com/focus-trap/focus-trap-react/commits?author=thawkin3" title="Documentation">📖</a> <a href="#example-thawkin3" title="Examples">💡</a> <a href="https://github.com/focus-trap/focus-trap-react/commits?author=thawkin3" title="Tests">⚠️</a> <a href="#tool-thawkin3" title="Tools">🔧</a></td>
205
206
  <td align="center"><a href="https://github.com/krikienoid"><img src="https://avatars3.githubusercontent.com/u/8528227?v=4?s=100" width="100px;" alt=""/><br /><sub><b>krikienoid</b></sub></a><br /><a href="https://github.com/focus-trap/focus-trap-react/issues?q=author%3Akrikienoid" title="Bug reports">🐛</a></td>
@@ -18,7 +18,7 @@ function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) ===
18
18
 
19
19
  function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; }
20
20
 
21
- function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Date.prototype.toString.call(Reflect.construct(Date, [], function () {})); return true; } catch (e) { return false; } }
21
+ function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); return true; } catch (e) { return false; } }
22
22
 
23
23
  function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }
24
24
 
@@ -70,6 +70,11 @@ var FocusTrap = /*#__PURE__*/function (_React$Component) {
70
70
  continue;
71
71
  }
72
72
 
73
+ if (optionName === 'onPostDeactivate') {
74
+ _this.onPostDeactivate = focusTrapOptions[optionName];
75
+ continue;
76
+ }
77
+
73
78
  _this.tailoredFocusTrapOptions[optionName] = focusTrapOptions[optionName];
74
79
  } // elements from which to create the focus trap on mount; if a child is used
75
80
  // instead of the `containerElements` prop, we'll get the child's related
@@ -81,24 +86,87 @@ var FocusTrap = /*#__PURE__*/function (_React$Component) {
81
86
  _this.updatePreviousElement();
82
87
 
83
88
  return _this;
84
- }
85
- /** Update the previously focused element with the currently focused element. */
89
+ } // TODO: Need more test coverage for this function
86
90
 
87
91
 
88
92
  _createClass(FocusTrap, [{
93
+ key: "getNodeForOption",
94
+ value: function getNodeForOption(optionName) {
95
+ var optionValue = this.tailoredFocusTrapOptions[optionName];
96
+
97
+ if (!optionValue) {
98
+ return null;
99
+ }
100
+
101
+ var node = optionValue;
102
+
103
+ if (typeof optionValue === 'string') {
104
+ node = document.querySelector(optionValue);
105
+
106
+ if (!node) {
107
+ throw new Error("`".concat(optionName, "` refers to no known node"));
108
+ }
109
+ }
110
+
111
+ if (typeof optionValue === 'function') {
112
+ node = optionValue();
113
+
114
+ if (!node) {
115
+ throw new Error("`".concat(optionName, "` did not return a node"));
116
+ }
117
+ }
118
+
119
+ return node;
120
+ }
121
+ }, {
122
+ key: "getReturnFocusNode",
123
+ value: function getReturnFocusNode() {
124
+ var node = this.getNodeForOption('setReturnFocus');
125
+ return node ? node : this.previouslyFocusedElement;
126
+ }
127
+ /** Update the previously focused element with the currently focused element. */
128
+
129
+ }, {
89
130
  key: "updatePreviousElement",
90
131
  value: function updatePreviousElement() {
91
132
  if (typeof document !== 'undefined') {
92
133
  this.previouslyFocusedElement = document.activeElement;
93
134
  }
94
135
  }
95
- /** Returns focus to the element that had focus when the trap was activated. */
96
-
97
136
  }, {
98
- key: "returnFocus",
99
- value: function returnFocus() {
100
- if (this.previouslyFocusedElement && this.previouslyFocusedElement.focus) {
101
- this.previouslyFocusedElement.focus();
137
+ key: "deactivateTrap",
138
+ value: function deactivateTrap() {
139
+ var _this2 = this;
140
+
141
+ var checkCanReturnFocus = this.tailoredFocusTrapOptions.checkCanReturnFocus;
142
+
143
+ if (this.focusTrap) {
144
+ // NOTE: we never let the trap return the focus since we do that ourselves
145
+ this.focusTrap.deactivate({
146
+ returnFocus: false
147
+ });
148
+ }
149
+
150
+ var finishDeactivation = function finishDeactivation() {
151
+ var returnFocusNode = _this2.getReturnFocusNode();
152
+
153
+ var canReturnFocus = (returnFocusNode === null || returnFocusNode === void 0 ? void 0 : returnFocusNode.focus) && _this2.returnFocusOnDeactivate;
154
+
155
+ if (canReturnFocus) {
156
+ /** Returns focus to the element that had focus when the trap was activated. */
157
+ returnFocusNode.focus();
158
+ }
159
+
160
+ if (_this2.onPostDeactivate) {
161
+ _this2.onPostDeactivate.call(null); // don't call it in context of "this"
162
+
163
+ }
164
+ };
165
+
166
+ if (checkCanReturnFocus) {
167
+ checkCanReturnFocus(this.getReturnFocusNode()).then(finishDeactivation, finishDeactivation);
168
+ } else {
169
+ finishDeactivation();
102
170
  }
103
171
  }
104
172
  }, {
@@ -138,28 +206,27 @@ var FocusTrap = /*#__PURE__*/function (_React$Component) {
138
206
  this.focusTrap.updateContainerElements(this.props.containerElements);
139
207
  }
140
208
 
141
- if (prevProps.active && !this.props.active) {
142
- // NOTE: we never let the trap return the focus since we do that ourselves
143
- this.focusTrap.deactivate({
144
- returnFocus: false
145
- });
209
+ var hasActivated = !prevProps.active && this.props.active;
210
+ var hasDeactivated = prevProps.active && !this.props.active;
211
+ var hasPaused = !prevProps.paused && this.props.paused;
212
+ var hasUnpaused = prevProps.paused && !this.props.paused;
146
213
 
147
- if (this.returnFocusOnDeactivate) {
148
- this.returnFocus();
149
- }
214
+ if (hasActivated) {
215
+ this.updatePreviousElement();
216
+ this.focusTrap.activate();
217
+ }
150
218
 
219
+ if (hasDeactivated) {
220
+ this.deactivateTrap();
151
221
  return; // un/pause does nothing on an inactive trap
152
222
  }
153
223
 
154
- if (!prevProps.active && this.props.active) {
155
- this.updatePreviousElement();
156
- this.focusTrap.activate();
224
+ if (hasPaused) {
225
+ this.focusTrap.pause();
157
226
  }
158
227
 
159
- if (prevProps.paused && !this.props.paused) {
228
+ if (hasUnpaused) {
160
229
  this.focusTrap.unpause();
161
- } else if (!prevProps.paused && this.props.paused) {
162
- this.focusTrap.pause();
163
230
  }
164
231
  } else if (prevProps.containerElements !== this.props.containerElements) {
165
232
  this.focusTrapElements = this.props.containerElements;
@@ -169,21 +236,12 @@ var FocusTrap = /*#__PURE__*/function (_React$Component) {
169
236
  }, {
170
237
  key: "componentWillUnmount",
171
238
  value: function componentWillUnmount() {
172
- if (this.focusTrap) {
173
- // NOTE: we never let the trap return the focus since we do that ourselves
174
- this.focusTrap.deactivate({
175
- returnFocus: false
176
- });
177
- }
178
-
179
- if (this.returnFocusOnDeactivate) {
180
- this.returnFocus();
181
- }
239
+ this.deactivateTrap();
182
240
  }
183
241
  }, {
184
242
  key: "render",
185
243
  value: function render() {
186
- var _this2 = this;
244
+ var _this3 = this;
187
245
 
188
246
  var child = this.props.children ? React.Children.only(this.props.children) : undefined;
189
247
 
@@ -193,7 +251,7 @@ var FocusTrap = /*#__PURE__*/function (_React$Component) {
193
251
  }
194
252
 
195
253
  var composedRefCallback = function composedRefCallback(element) {
196
- var containerElements = _this2.props.containerElements;
254
+ var containerElements = _this3.props.containerElements;
197
255
 
198
256
  if (child) {
199
257
  if (typeof child.ref === 'function') {
@@ -203,7 +261,7 @@ var FocusTrap = /*#__PURE__*/function (_React$Component) {
203
261
  }
204
262
  }
205
263
 
206
- _this2.focusTrapElements = containerElements ? containerElements : [element];
264
+ _this3.focusTrapElements = containerElements ? containerElements : [element];
207
265
  };
208
266
 
209
267
  var childWithRef = React.cloneElement(child, {
@@ -226,10 +284,14 @@ FocusTrap.propTypes = {
226
284
  paused: PropTypes.bool,
227
285
  focusTrapOptions: PropTypes.shape({
228
286
  onActivate: PropTypes.func,
287
+ onPostActivate: PropTypes.func,
288
+ checkCanFocusTrap: PropTypes.func,
229
289
  onDeactivate: PropTypes.func,
230
- initialFocus: PropTypes.oneOfType([PropTypes.instanceOf(ElementType), PropTypes.string, PropTypes.func]),
290
+ onPostDeactivate: PropTypes.func,
291
+ checkCanReturnFocus: PropTypes.func,
292
+ initialFocus: PropTypes.oneOfType([PropTypes.instanceOf(ElementType), PropTypes.string, PropTypes.func, PropTypes.bool]),
231
293
  fallbackFocus: PropTypes.oneOfType([PropTypes.instanceOf(ElementType), PropTypes.string, PropTypes.func]),
232
- escapeDeactivates: PropTypes.bool,
294
+ escapeDeactivates: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]),
233
295
  clickOutsideDeactivates: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]),
234
296
  returnFocusOnDeactivate: PropTypes.bool,
235
297
  setReturnFocus: PropTypes.oneOfType([PropTypes.instanceOf(ElementType), PropTypes.string, PropTypes.func]),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "focus-trap-react",
3
- "version": "8.4.2",
3
+ "version": "8.7.0",
4
4
  "description": "A React component that traps focus.",
5
5
  "main": "dist/focus-trap-react.js",
6
6
  "types": "index.d.ts",
@@ -24,7 +24,6 @@
24
24
  "test:types": "tsc index.d.ts",
25
25
  "test:unit": "jest",
26
26
  "test:coverage": "jest --coverage",
27
- "test:coverage:ci": "yarn test:coverage && codecov",
28
27
  "test:cypress": "start-server-and-test start 9966 'cypress open'",
29
28
  "test:cypress:ci": "start-server-and-test start 9966 'cypress run --browser $CYPRESS_BROWSER --headless'",
30
29
  "test": "yarn format:check && yarn lint && yarn test:unit && yarn test:types && CYPRESS_BROWSER=chrome yarn test:cypress:ci",
@@ -56,44 +55,43 @@
56
55
  },
57
56
  "homepage": "https://github.com/focus-trap/focus-trap-react#readme",
58
57
  "devDependencies": {
59
- "@babel/cli": "^7.12.13",
60
- "@babel/core": "^7.12.13",
58
+ "@babel/cli": "^7.14.5",
59
+ "@babel/core": "^7.14.6",
61
60
  "@babel/plugin-proposal-class-properties": "^7.12.13",
62
- "@babel/preset-env": "^7.12.13",
63
- "@babel/preset-react": "^7.12.13",
64
- "@changesets/cli": "^2.14.0",
65
- "@testing-library/cypress": "^7.0.3",
66
- "@testing-library/dom": "^7.29.4",
67
- "@testing-library/jest-dom": "^5.11.9",
68
- "@testing-library/react": "^11.2.5",
69
- "@testing-library/user-event": "^12.6.3",
61
+ "@babel/preset-env": "^7.14.7",
62
+ "@babel/preset-react": "^7.14.5",
63
+ "@changesets/cli": "^2.16.0",
64
+ "@testing-library/cypress": "^7.0.6",
65
+ "@testing-library/dom": "^8.0.0",
66
+ "@testing-library/jest-dom": "^5.14.1",
67
+ "@testing-library/react": "^12.0.0",
68
+ "@testing-library/user-event": "^13.1.9",
70
69
  "@types/jquery": "^3.5.5",
71
- "all-contributors-cli": "^6.19.0",
70
+ "all-contributors-cli": "^6.20.0",
72
71
  "babel-eslint": "^10.1.0",
73
- "babel-jest": "^26.6.3",
72
+ "babel-jest": "^27.0.6",
74
73
  "babelify": "^10.0.0",
75
74
  "browserify": "^17.0.0",
76
75
  "budo": "^11.6.4",
77
- "codecov": "^3.8.1",
78
- "cypress": "^6.4.0",
76
+ "cypress": "^7.6.0",
79
77
  "cypress-plugin-tab": "^1.0.5",
80
- "eslint": "^7.19.0",
81
- "eslint-config-prettier": "^7.2.0",
82
- "eslint-plugin-cypress": "^2.11.2",
83
- "eslint-plugin-react": "^7.22.0",
84
- "jest": "^26.6.3",
85
- "jest-watch-typeahead": "^0.6.1",
78
+ "eslint": "^7.29.0",
79
+ "eslint-config-prettier": "^8.3.0",
80
+ "eslint-plugin-cypress": "^2.11.3",
81
+ "eslint-plugin-react": "^7.24.0",
82
+ "jest": "^27.0.6",
83
+ "jest-watch-typeahead": "^0.6.4",
86
84
  "onchange": "^7.1.0",
87
- "prettier": "^2.2.1",
85
+ "prettier": "^2.3.2",
88
86
  "prop-types": "^15.7.2",
89
- "react": "^17.0.1",
90
- "react-dom": "^17.0.1",
87
+ "react": "^17.0.2",
88
+ "react-dom": "^17.0.2",
91
89
  "regenerator-runtime": "^0.13.7",
92
- "start-server-and-test": "^1.12.0",
93
- "typescript": "^4.1.3"
90
+ "start-server-and-test": "^1.12.5",
91
+ "typescript": "^4.3.4"
94
92
  },
95
93
  "dependencies": {
96
- "focus-trap": "^6.3.0"
94
+ "focus-trap": "^6.6.0"
97
95
  },
98
96
  "peerDependencies": {
99
97
  "prop-types": "^15.7.2",
@@ -36,6 +36,11 @@ class FocusTrap extends React.Component {
36
36
  continue;
37
37
  }
38
38
 
39
+ if (optionName === 'onPostDeactivate') {
40
+ this.onPostDeactivate = focusTrapOptions[optionName];
41
+ continue;
42
+ }
43
+
39
44
  this.tailoredFocusTrapOptions[optionName] = focusTrapOptions[optionName];
40
45
  }
41
46
 
@@ -48,6 +53,38 @@ class FocusTrap extends React.Component {
48
53
  this.updatePreviousElement();
49
54
  }
50
55
 
56
+ // TODO: Need more test coverage for this function
57
+ getNodeForOption(optionName) {
58
+ const optionValue = this.tailoredFocusTrapOptions[optionName];
59
+ if (!optionValue) {
60
+ return null;
61
+ }
62
+
63
+ let node = optionValue;
64
+
65
+ if (typeof optionValue === 'string') {
66
+ node = document.querySelector(optionValue);
67
+ if (!node) {
68
+ throw new Error(`\`${optionName}\` refers to no known node`);
69
+ }
70
+ }
71
+
72
+ if (typeof optionValue === 'function') {
73
+ node = optionValue();
74
+ if (!node) {
75
+ throw new Error(`\`${optionName}\` did not return a node`);
76
+ }
77
+ }
78
+
79
+ return node;
80
+ }
81
+
82
+ getReturnFocusNode() {
83
+ const node = this.getNodeForOption('setReturnFocus');
84
+
85
+ return node ? node : this.previouslyFocusedElement;
86
+ }
87
+
51
88
  /** Update the previously focused element with the currently focused element. */
52
89
  updatePreviousElement() {
53
90
  if (typeof document !== 'undefined') {
@@ -55,10 +92,36 @@ class FocusTrap extends React.Component {
55
92
  }
56
93
  }
57
94
 
58
- /** Returns focus to the element that had focus when the trap was activated. */
59
- returnFocus() {
60
- if (this.previouslyFocusedElement && this.previouslyFocusedElement.focus) {
61
- this.previouslyFocusedElement.focus();
95
+ deactivateTrap() {
96
+ const { checkCanReturnFocus } = this.tailoredFocusTrapOptions;
97
+
98
+ if (this.focusTrap) {
99
+ // NOTE: we never let the trap return the focus since we do that ourselves
100
+ this.focusTrap.deactivate({ returnFocus: false });
101
+ }
102
+
103
+ const finishDeactivation = () => {
104
+ const returnFocusNode = this.getReturnFocusNode();
105
+ const canReturnFocus =
106
+ returnFocusNode?.focus && this.returnFocusOnDeactivate;
107
+
108
+ if (canReturnFocus) {
109
+ /** Returns focus to the element that had focus when the trap was activated. */
110
+ returnFocusNode.focus();
111
+ }
112
+
113
+ if (this.onPostDeactivate) {
114
+ this.onPostDeactivate.call(null); // don't call it in context of "this"
115
+ }
116
+ };
117
+
118
+ if (checkCanReturnFocus) {
119
+ checkCanReturnFocus(this.getReturnFocusNode()).then(
120
+ finishDeactivation,
121
+ finishDeactivation
122
+ );
123
+ } else {
124
+ finishDeactivation();
62
125
  }
63
126
  }
64
127
 
@@ -100,25 +163,28 @@ class FocusTrap extends React.Component {
100
163
  this.focusTrap.updateContainerElements(this.props.containerElements);
101
164
  }
102
165
 
103
- if (prevProps.active && !this.props.active) {
104
- // NOTE: we never let the trap return the focus since we do that ourselves
105
- this.focusTrap.deactivate({ returnFocus: false });
106
- if (this.returnFocusOnDeactivate) {
107
- this.returnFocus();
108
- }
109
- return; // un/pause does nothing on an inactive trap
110
- }
166
+ const hasActivated = !prevProps.active && this.props.active;
167
+ const hasDeactivated = prevProps.active && !this.props.active;
168
+ const hasPaused = !prevProps.paused && this.props.paused;
169
+ const hasUnpaused = prevProps.paused && !this.props.paused;
111
170
 
112
- if (!prevProps.active && this.props.active) {
171
+ if (hasActivated) {
113
172
  this.updatePreviousElement();
114
173
  this.focusTrap.activate();
115
174
  }
116
175
 
117
- if (prevProps.paused && !this.props.paused) {
118
- this.focusTrap.unpause();
119
- } else if (!prevProps.paused && this.props.paused) {
176
+ if (hasDeactivated) {
177
+ this.deactivateTrap();
178
+ return; // un/pause does nothing on an inactive trap
179
+ }
180
+
181
+ if (hasPaused) {
120
182
  this.focusTrap.pause();
121
183
  }
184
+
185
+ if (hasUnpaused) {
186
+ this.focusTrap.unpause();
187
+ }
122
188
  } else if (prevProps.containerElements !== this.props.containerElements) {
123
189
  this.focusTrapElements = this.props.containerElements;
124
190
  this.setupFocusTrap();
@@ -126,14 +192,7 @@ class FocusTrap extends React.Component {
126
192
  }
127
193
 
128
194
  componentWillUnmount() {
129
- if (this.focusTrap) {
130
- // NOTE: we never let the trap return the focus since we do that ourselves
131
- this.focusTrap.deactivate({ returnFocus: false });
132
- }
133
-
134
- if (this.returnFocusOnDeactivate) {
135
- this.returnFocus();
136
- }
195
+ this.deactivateTrap();
137
196
  }
138
197
 
139
198
  render() {
@@ -183,18 +242,23 @@ FocusTrap.propTypes = {
183
242
  paused: PropTypes.bool,
184
243
  focusTrapOptions: PropTypes.shape({
185
244
  onActivate: PropTypes.func,
245
+ onPostActivate: PropTypes.func,
246
+ checkCanFocusTrap: PropTypes.func,
186
247
  onDeactivate: PropTypes.func,
248
+ onPostDeactivate: PropTypes.func,
249
+ checkCanReturnFocus: PropTypes.func,
187
250
  initialFocus: PropTypes.oneOfType([
188
251
  PropTypes.instanceOf(ElementType),
189
252
  PropTypes.string,
190
253
  PropTypes.func,
254
+ PropTypes.bool,
191
255
  ]),
192
256
  fallbackFocus: PropTypes.oneOfType([
193
257
  PropTypes.instanceOf(ElementType),
194
258
  PropTypes.string,
195
259
  PropTypes.func,
196
260
  ]),
197
- escapeDeactivates: PropTypes.bool,
261
+ escapeDeactivates: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]),
198
262
  clickOutsideDeactivates: PropTypes.oneOfType([
199
263
  PropTypes.bool,
200
264
  PropTypes.func,