focus-trap-react 9.0.2 → 10.0.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 +22 -0
- package/README.md +136 -11
- package/dist/focus-trap-react.js +48 -91
- package/package.json +28 -28
- package/src/focus-trap-react.js +6 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 10.0.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- c772db0: Add help for Strict Mode in README [#796](https://github.com/focus-trap/focus-trap-react/issues/796)
|
|
8
|
+
- d0de500: Bump focus-trap to 7.1.0 and tabbable to 6.0.1 for new trap features and bug fixes
|
|
9
|
+
|
|
10
|
+
## 10.0.0
|
|
11
|
+
|
|
12
|
+
### Major Changes
|
|
13
|
+
|
|
14
|
+
- af69c14: 🚨 **Breaking:** Underlying `tabbable` dependency has been updated to v6.0.0 and contains a breaking change related to detached nodes with its default `displayCheck` setting. See tabbable's [changelog](https://github.com/focus-trap/tabbable/blob/master/CHANGELOG.md#600) for more information.
|
|
15
|
+
- The `focus-trap` dependency has also be updated to v7.0.0 but only contains the underlying `tabbable` changes.
|
|
16
|
+
- The `tabbableOptions.displayCheck` prop type has been updated to include the new "legacy-full" option.
|
|
17
|
+
- 018732c: 🚨 **Breaking:** Dropped support of IE browsers, all versions.
|
|
18
|
+
- IE11 was [officially retired](https://blogs.windows.com/windowsexperience/2022/06/15/internet-explorer-11-has-retired-and-is-officially-out-of-support-what-you-need-to-know/) on June 15, 2022 (6 weeks ago). There are no longer any versions of IE that are still maintained or even supported by Microsoft.
|
|
19
|
+
- 018732c: Revised and clarified official browser support (still as broad and deep as _reasonably_ possible).
|
|
20
|
+
|
|
21
|
+
### Patch Changes
|
|
22
|
+
|
|
23
|
+
- b0bbbd4: Update README with a note about the `children` prop stating that the trap requires a single child, and that if a component is used, it must be a **functional** component that forwards refs.
|
|
24
|
+
|
|
3
25
|
## 9.0.2
|
|
4
26
|
|
|
5
27
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -33,13 +33,21 @@ npm install focus-trap-react
|
|
|
33
33
|
|
|
34
34
|
### React dependency
|
|
35
35
|
|
|
36
|
-
React `>= 16.
|
|
36
|
+
React `>= 16.3.0`
|
|
37
37
|
|
|
38
38
|
## Browser Support
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
As old and as broad as _reasonably_ possible, excluding browsers that are out of support or have nearly no user base.
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
Focused on desktop browsers, particularly Chrome, Edge, FireFox, Safari, and Opera.
|
|
43
|
+
|
|
44
|
+
Focus-trap-react is not officially tested on any mobile browsers or devices.
|
|
45
|
+
|
|
46
|
+
> ⚠️ Microsoft [no longer supports](https://blogs.windows.com/windowsexperience/2022/06/15/internet-explorer-11-has-retired-and-is-officially-out-of-support-what-you-need-to-know/) any version of IE, so IE is no longer supported by this library.
|
|
47
|
+
|
|
48
|
+
> 💬 Focus-trap-react relies on focus-trap so its browser support is at least [what focus-trap supports](https://github.com/focus-trap/focus-trap#browser-support).
|
|
49
|
+
|
|
50
|
+
> 💬 Keep in mind that performance optimization and old browser support are often at odds, so tabbable may not always be able to use the most optimal (typically modern) APIs in all cases.
|
|
43
51
|
|
|
44
52
|
## Usage
|
|
45
53
|
|
|
@@ -66,7 +74,7 @@ You can read further code examples in `demo/` (it's very simple), and [see how i
|
|
|
66
74
|
|
|
67
75
|
Here's one more simple example:
|
|
68
76
|
|
|
69
|
-
```
|
|
77
|
+
```jsx
|
|
70
78
|
const React = require('react');
|
|
71
79
|
const ReactDOM = require('react-dom'); // React 16-17
|
|
72
80
|
const { createRoot } = require('react-dom/client'); // React 18
|
|
@@ -137,9 +145,124 @@ ReactDOM.render(<Demo />, document.getElementById('root')); // React 16-17
|
|
|
137
145
|
createRoot(document.getElementById('root')).render(<Demo />); // React 18
|
|
138
146
|
```
|
|
139
147
|
|
|
140
|
-
|
|
148
|
+
## ❗️❗️ React 18 Strict Mode ❗️❗️
|
|
149
|
+
|
|
150
|
+
React 18 introduced [new behavior](https://reactjs.org/docs/strict-mode.html#ensuring-reusable-state) in Strict Mode whereby it mimics a possible future behavior where React might optimize an app's performance by unmounting certain components that aren't in use and later remounting them with previous, reused state when the user needs them again. What constitutes "not in use" and "needs them again" is as yet undefined.
|
|
151
|
+
|
|
152
|
+
_Remounted with reused state_ is the key difference between what is otherwise expected about [unmounted components](https://reactjs.org/docs/react-component.html#componentwillunmount).
|
|
141
153
|
|
|
142
|
-
|
|
154
|
+
__[v9.0.2](https://github.com/focus-trap/focus-trap-react/pull/721) adds support__ for this new Strict Mode behavior: The trap attempts to detect that it has been remounted with previous state: If the `active` prop's value is `true`, and an internal focus trap instance already exists, the focus trap is re-activated on remount in order to reconcile stated expectations.
|
|
155
|
+
|
|
156
|
+
> 🚨 In Strict Mode (and so in dev builds only, since this behavior of Strict Mode only affects dev builds), the trap __will be deactivated as soon as it is mounted__, and then reactivated again, almost immediately, because React will immediately unmount and remount the trap as soon as it's rendered.
|
|
157
|
+
|
|
158
|
+
Therefore, __avoid using options like onActivate, onPostActivate, onDeactivate, or onPostDeactivate to affect component state__.
|
|
159
|
+
|
|
160
|
+
<details>
|
|
161
|
+
<summary>Explanation and sample anti-pattern to <strong>avoid</strong></summary>
|
|
162
|
+
<p>
|
|
163
|
+
See <a href="https://github.com/focus-trap/focus-trap-react/issues/796">this discussion</a> for an example sandbox (issue description) where <code>onDeactivate</code> was used to trigger the close of a dialog when the trap was deactivated (e.g. to react to the user clicking outside the trap with <code>focusTrapOptions.clickOutsideDeactivates=true</code>).
|
|
164
|
+
</p>
|
|
165
|
+
<p>
|
|
166
|
+
The result can be that (depending on how you render the trap) in Strict Mode, the dialog never appears because it gets closed as soon as the trap renders, since the trap is deactivated as soon as it's unmounted, and so the <code>onDeactivate</code> handler is called, thus hiding the dialog...
|
|
167
|
+
</p>
|
|
168
|
+
<p>
|
|
169
|
+
<strong>This is intentional</strong>: If the trap gets unmounted, it has no idea if it's being unmounted <em>for good</em> or if it's going to be remounted <em>at some future point in time</em>. It also has no idea of knowing <em>how long</em> it will be until it's remounted again. So it must be deactivated as though it's going away for good in order to prevent unintentional behavior and memory leaks (from orphaned document event listeners).
|
|
170
|
+
</p>
|
|
171
|
+
</details>
|
|
172
|
+
|
|
173
|
+
## Props
|
|
174
|
+
|
|
175
|
+
### children
|
|
176
|
+
|
|
177
|
+
> ⚠️ The `<FocusTrap>` component requires a __single__ child, and this child must __forward refs__ onto the element which will ultimately be considered the trap's container. Since React does not provide for a way to forward refs to class-based components, this means the child must be a __functional__ component that uses the `React.forwardRef()` API.
|
|
178
|
+
>
|
|
179
|
+
> If you must use a __class__-based component as the trap's container, then you will need to get your own ref to it upon render, and use the `containerElements` prop (initially set to an empty array `[]`) in order to provide the ref's element to it once updated by React (hint: use a [callback ref](https://reactjs.org/docs/refs-and-the-dom.html#callback-refs)).
|
|
180
|
+
|
|
181
|
+
> 💬 The child is ignored (but still rendered) if the `containerElements` prop is used to imperatively provide trap container elements.
|
|
182
|
+
|
|
183
|
+
Example:
|
|
184
|
+
|
|
185
|
+
```jsx
|
|
186
|
+
const React = require('react');
|
|
187
|
+
const { createRoot } = require('react-dom/client');
|
|
188
|
+
const propTypes = require('prop-types');
|
|
189
|
+
const FocusTrap = require('../../dist/focus-trap-react');
|
|
190
|
+
|
|
191
|
+
const container = document.getElementById('demo-function-child');
|
|
192
|
+
|
|
193
|
+
const TrapChild = React.forwardRef(function ({ onDeactivate }, ref) {
|
|
194
|
+
return (
|
|
195
|
+
<div ref={ref}>
|
|
196
|
+
<p>
|
|
197
|
+
Here is a focus trap <a href="#">with</a> <a href="#">some</a>{' '}
|
|
198
|
+
<a href="#">focusable</a> parts.
|
|
199
|
+
</p>
|
|
200
|
+
<p>
|
|
201
|
+
<button
|
|
202
|
+
onClick={onDeactivate}
|
|
203
|
+
aria-describedby="class-child-heading"
|
|
204
|
+
>
|
|
205
|
+
deactivate trap
|
|
206
|
+
</button>
|
|
207
|
+
</p>
|
|
208
|
+
</div>
|
|
209
|
+
);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
TrapChild.displayName = 'TrapChild';
|
|
213
|
+
TrapChild.propTypes = {
|
|
214
|
+
onDeactivate: propTypes.func,
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
class DemoFunctionChild extends React.Component {
|
|
218
|
+
constructor(props) {
|
|
219
|
+
super(props);
|
|
220
|
+
|
|
221
|
+
this.state = {
|
|
222
|
+
activeTrap: false,
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
this.mountTrap = this.mountTrap.bind(this);
|
|
226
|
+
this.unmountTrap = this.unmountTrap.bind(this);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
mountTrap() {
|
|
230
|
+
this.setState({ activeTrap: true });
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
unmountTrap() {
|
|
234
|
+
this.setState({ activeTrap: false });
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
render() {
|
|
238
|
+
const trap = this.state.activeTrap && (
|
|
239
|
+
<FocusTrap
|
|
240
|
+
focusTrapOptions={{
|
|
241
|
+
onDeactivate: this.unmountTrap,
|
|
242
|
+
}}
|
|
243
|
+
>
|
|
244
|
+
<TrapChild />
|
|
245
|
+
</FocusTrap>
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
return (
|
|
249
|
+
<div>
|
|
250
|
+
<p>
|
|
251
|
+
<button onClick={this.mountTrap} aria-describedby="function-child-heading">
|
|
252
|
+
activate trap
|
|
253
|
+
</button>
|
|
254
|
+
</p>
|
|
255
|
+
{trap}
|
|
256
|
+
</div>
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const root = createRoot(container);
|
|
262
|
+
root.render(<DemoFunctionChild />);
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
### focusTrapOptions
|
|
143
266
|
|
|
144
267
|
Type: `Object`, optional
|
|
145
268
|
|
|
@@ -147,7 +270,7 @@ Pass any of the options available in focus-trap's [createOptions](https://github
|
|
|
147
270
|
|
|
148
271
|
> ⚠️ See notes about __[testing in JSDom](#testing-in-jsdom)__ (e.g. using Jest) if that's what you currently use.
|
|
149
272
|
|
|
150
|
-
|
|
273
|
+
### active
|
|
151
274
|
|
|
152
275
|
Type: `Boolean`, optional
|
|
153
276
|
|
|
@@ -155,19 +278,21 @@ By default, the `FocusTrap` activates when it mounts. So you activate and deacti
|
|
|
155
278
|
|
|
156
279
|
See `demo/demo-special-element.js`.
|
|
157
280
|
|
|
158
|
-
|
|
281
|
+
### paused
|
|
159
282
|
|
|
160
283
|
Type: `Boolean`, optional
|
|
161
284
|
|
|
162
285
|
If you would like to pause or unpause the focus trap (see [`focus-trap`'s documentation](https://github.com/focus-trap/focus-trap#focustrappause)), toggle this prop.
|
|
163
286
|
|
|
164
|
-
|
|
287
|
+
### containerElements
|
|
165
288
|
|
|
166
289
|
Type: `Array of HTMLElement`, optional
|
|
167
290
|
|
|
168
|
-
If
|
|
291
|
+
If specified, these elements will be used as the boundaries for the focus-trap, __instead of the child__. These get passed as arguments to `focus-trap`'s `updateContainerElements()` method.
|
|
169
292
|
|
|
170
|
-
> Note that when you use `containerElements`, the need for a child is eliminated as the child is __always__ ignored when the prop is specified, even if
|
|
293
|
+
> 💬 Note that when you use `containerElements`, the need for a child is eliminated as the child is __always__ ignored (though still rendered) when the prop is specified, even if this prop is `[]` (an empty array).
|
|
294
|
+
>
|
|
295
|
+
> Also note that if the refs you're putting into the array, like `containerElements={[ref1.current, ref2.current]}`, aren't resolved yet, resulting in `[null, null]` for example, the trap will not get created. The array must contain at least one valid `HTMLElement` in order for the trap to get created/updated.
|
|
171
296
|
|
|
172
297
|
If `containerElements` is subsequently updated (i.e. after the trap has been created) to an empty array (or an array of falsy values like `[null, null]`), the trap will still be active, but the TAB key will do nothing because the trap will not contain any tabbable groups of nodes. At this point, the trap can either be deactivated manually or by unmounting, or an updated set of elements can be given to `containerElements` to resume use of the TAB key.
|
|
173
298
|
|
package/dist/focus-trap-react.js
CHANGED
|
@@ -1,65 +1,40 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
function _typeof(obj) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) { return typeof obj; } : function (obj) { return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }, _typeof(obj); }
|
|
4
|
-
|
|
5
4
|
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
|
|
6
|
-
|
|
7
5
|
function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
|
|
8
|
-
|
|
9
6
|
function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; }
|
|
10
|
-
|
|
11
7
|
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); Object.defineProperty(subClass, "prototype", { writable: false }); if (superClass) _setPrototypeOf(subClass, superClass); }
|
|
12
|
-
|
|
13
8
|
function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf ? Object.setPrototypeOf.bind() : function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }
|
|
14
|
-
|
|
15
9
|
function _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; }
|
|
16
|
-
|
|
17
10
|
function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } else if (call !== void 0) { throw new TypeError("Derived constructors may only return object or undefined"); } return _assertThisInitialized(self); }
|
|
18
|
-
|
|
19
11
|
function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; }
|
|
20
|
-
|
|
21
12
|
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
|
-
|
|
23
13
|
function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf.bind() : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }
|
|
24
|
-
|
|
25
14
|
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
|
|
26
|
-
|
|
27
15
|
var React = require('react');
|
|
28
|
-
|
|
29
16
|
var PropTypes = require('prop-types');
|
|
30
|
-
|
|
31
17
|
var _require = require('focus-trap'),
|
|
32
|
-
|
|
33
|
-
|
|
18
|
+
createFocusTrap = _require.createFocusTrap;
|
|
34
19
|
var _require2 = require('tabbable'),
|
|
35
|
-
|
|
36
|
-
|
|
20
|
+
isFocusable = _require2.isFocusable;
|
|
37
21
|
var FocusTrap = /*#__PURE__*/function (_React$Component) {
|
|
38
22
|
_inherits(FocusTrap, _React$Component);
|
|
39
|
-
|
|
40
23
|
var _super = _createSuper(FocusTrap);
|
|
41
|
-
|
|
42
24
|
function FocusTrap(props) {
|
|
43
25
|
var _this;
|
|
44
|
-
|
|
45
26
|
_classCallCheck(this, FocusTrap);
|
|
46
|
-
|
|
47
27
|
_this = _super.call(this, props);
|
|
48
|
-
|
|
49
28
|
_defineProperty(_assertThisInitialized(_this), "getNodeForOption", function (optionName) {
|
|
50
29
|
var _this$internalOptions;
|
|
51
|
-
|
|
52
30
|
// use internal options first, falling back to original options
|
|
53
31
|
var optionValue = (_this$internalOptions = this.internalOptions[optionName]) !== null && _this$internalOptions !== void 0 ? _this$internalOptions : this.originalOptions[optionName];
|
|
54
|
-
|
|
55
32
|
if (typeof optionValue === 'function') {
|
|
56
33
|
for (var _len = arguments.length, params = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
|
|
57
34
|
params[_key - 1] = arguments[_key];
|
|
58
35
|
}
|
|
59
|
-
|
|
60
36
|
optionValue = optionValue.apply(void 0, params);
|
|
61
37
|
}
|
|
62
|
-
|
|
63
38
|
if (optionValue === true) {
|
|
64
39
|
optionValue = undefined; // use default value
|
|
65
40
|
}
|
|
@@ -67,31 +42,27 @@ var FocusTrap = /*#__PURE__*/function (_React$Component) {
|
|
|
67
42
|
if (!optionValue) {
|
|
68
43
|
if (optionValue === undefined || optionValue === false) {
|
|
69
44
|
return optionValue;
|
|
70
|
-
}
|
|
71
|
-
|
|
45
|
+
}
|
|
46
|
+
// else, empty string (invalid), null (invalid), 0 (invalid)
|
|
72
47
|
|
|
73
48
|
throw new Error("`".concat(optionName, "` was specified but was not a node, or did not return a node"));
|
|
74
49
|
}
|
|
75
|
-
|
|
76
50
|
var node = optionValue; // could be HTMLElement, SVGElement, or non-empty string at this point
|
|
77
51
|
|
|
78
52
|
if (typeof optionValue === 'string') {
|
|
79
53
|
var _this$getDocument;
|
|
80
|
-
|
|
81
54
|
node = (_this$getDocument = this.getDocument()) === null || _this$getDocument === void 0 ? void 0 : _this$getDocument.querySelector(optionValue); // resolve to node, or null if fails
|
|
82
|
-
|
|
83
55
|
if (!node) {
|
|
84
56
|
throw new Error("`".concat(optionName, "` as selector refers to no known node"));
|
|
85
57
|
}
|
|
86
58
|
}
|
|
87
|
-
|
|
88
59
|
return node;
|
|
89
60
|
});
|
|
90
|
-
|
|
91
61
|
_this.handleDeactivate = _this.handleDeactivate.bind(_assertThisInitialized(_this));
|
|
92
62
|
_this.handlePostDeactivate = _this.handlePostDeactivate.bind(_assertThisInitialized(_this));
|
|
93
|
-
_this.handleClickOutsideDeactivates = _this.handleClickOutsideDeactivates.bind(_assertThisInitialized(_this));
|
|
63
|
+
_this.handleClickOutsideDeactivates = _this.handleClickOutsideDeactivates.bind(_assertThisInitialized(_this));
|
|
94
64
|
|
|
65
|
+
// focus-trap options used internally when creating the trap
|
|
95
66
|
_this.internalOptions = {
|
|
96
67
|
// We need to hijack the returnFocusOnDeactivate option,
|
|
97
68
|
// because React can move focus into the element before we arrived at
|
|
@@ -109,8 +80,9 @@ var FocusTrap = /*#__PURE__*/function (_React$Component) {
|
|
|
109
80
|
// outside click (otherwise, we'll always think we should return focus because
|
|
110
81
|
// of how we manage that flag internally here)
|
|
111
82
|
clickOutsideDeactivates: _this.handleClickOutsideDeactivates
|
|
112
|
-
};
|
|
83
|
+
};
|
|
113
84
|
|
|
85
|
+
// original options provided by the consumer
|
|
114
86
|
_this.originalOptions = {
|
|
115
87
|
// because of the above `internalOptions`, we maintain our own flag for
|
|
116
88
|
// this option, and default it to `true` because that's focus-trap's default
|
|
@@ -125,48 +97,47 @@ var FocusTrap = /*#__PURE__*/function (_React$Component) {
|
|
|
125
97
|
clickOutsideDeactivates: false
|
|
126
98
|
};
|
|
127
99
|
var focusTrapOptions = props.focusTrapOptions;
|
|
128
|
-
|
|
129
100
|
for (var optionName in focusTrapOptions) {
|
|
130
101
|
if (!Object.prototype.hasOwnProperty.call(focusTrapOptions, optionName)) {
|
|
131
102
|
continue;
|
|
132
103
|
}
|
|
133
|
-
|
|
134
104
|
if (optionName === 'returnFocusOnDeactivate' || optionName === 'onDeactivate' || optionName === 'onPostDeactivate' || optionName === 'checkCanReturnFocus' || optionName === 'clickOutsideDeactivates') {
|
|
135
105
|
_this.originalOptions[optionName] = focusTrapOptions[optionName];
|
|
136
106
|
continue; // exclude from internalOptions
|
|
137
107
|
}
|
|
138
108
|
|
|
139
109
|
_this.internalOptions[optionName] = focusTrapOptions[optionName];
|
|
140
|
-
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// if set, `{ target: Node, allowDeactivation: boolean }` where `target` is the outside
|
|
141
113
|
// node that was clicked, and `allowDeactivation` is the result of the consumer's
|
|
142
114
|
// option (stored in `this.originalOptions.clickOutsideDeactivates`, which may be a
|
|
143
115
|
// function) whether to allow or deny auto-deactivation on click on this outside node
|
|
116
|
+
_this.outsideClick = null;
|
|
144
117
|
|
|
145
|
-
|
|
146
|
-
_this.outsideClick = null; // elements from which to create the focus trap on mount; if a child is used
|
|
118
|
+
// elements from which to create the focus trap on mount; if a child is used
|
|
147
119
|
// instead of the `containerElements` prop, we'll get the child's related
|
|
148
120
|
// element when the trap renders and then is declared 'mounted'
|
|
121
|
+
_this.focusTrapElements = props.containerElements || [];
|
|
149
122
|
|
|
150
|
-
|
|
151
|
-
|
|
123
|
+
// now we remember what the currently focused element is, not relying on focus-trap
|
|
152
124
|
_this.updatePreviousElement();
|
|
153
|
-
|
|
154
125
|
return _this;
|
|
155
126
|
}
|
|
127
|
+
|
|
156
128
|
/**
|
|
157
129
|
* Gets the configured document.
|
|
158
130
|
* @returns {Document|undefined} Configured document, falling back to the main
|
|
159
131
|
* document, if it exists. During SSR, `undefined` is returned since the
|
|
160
132
|
* document doesn't exist.
|
|
161
133
|
*/
|
|
162
|
-
|
|
163
|
-
|
|
164
134
|
_createClass(FocusTrap, [{
|
|
165
135
|
key: "getDocument",
|
|
166
136
|
value: function getDocument() {
|
|
167
137
|
// SSR: careful to check if `document` exists before accessing it as a variable
|
|
168
138
|
return this.props.focusTrapOptions.document || (typeof document !== 'undefined' ? document : undefined);
|
|
169
139
|
}
|
|
140
|
+
|
|
170
141
|
/**
|
|
171
142
|
* Gets the node for the given option, which is expected to be an option that
|
|
172
143
|
* can be either a DOM node, a string that is a selector to get a node, `false`
|
|
@@ -180,20 +151,18 @@ var FocusTrap = /*#__PURE__*/function (_React$Component) {
|
|
|
180
151
|
* @throws {Error} If the option is set, not `false`, and is not, or does not
|
|
181
152
|
* resolve to a node.
|
|
182
153
|
*/
|
|
183
|
-
|
|
184
154
|
}, {
|
|
185
155
|
key: "getReturnFocusNode",
|
|
186
156
|
value: function getReturnFocusNode() {
|
|
187
157
|
var node = this.getNodeForOption('setReturnFocus', this.previouslyFocusedElement);
|
|
188
158
|
return node ? node : node === false ? false : this.previouslyFocusedElement;
|
|
189
159
|
}
|
|
190
|
-
/** Update the previously focused element with the currently focused element. */
|
|
191
160
|
|
|
161
|
+
/** Update the previously focused element with the currently focused element. */
|
|
192
162
|
}, {
|
|
193
163
|
key: "updatePreviousElement",
|
|
194
164
|
value: function updatePreviousElement() {
|
|
195
165
|
var currentDocument = this.getDocument();
|
|
196
|
-
|
|
197
166
|
if (currentDocument) {
|
|
198
167
|
this.previouslyFocusedElement = currentDocument.activeElement;
|
|
199
168
|
}
|
|
@@ -209,7 +178,6 @@ var FocusTrap = /*#__PURE__*/function (_React$Component) {
|
|
|
209
178
|
if (!this.focusTrap || !this.focusTrap.active) {
|
|
210
179
|
return;
|
|
211
180
|
}
|
|
212
|
-
|
|
213
181
|
this.focusTrap.deactivate({
|
|
214
182
|
// NOTE: we never let the trap return the focus since we do that ourselves
|
|
215
183
|
returnFocus: false,
|
|
@@ -218,13 +186,13 @@ var FocusTrap = /*#__PURE__*/function (_React$Component) {
|
|
|
218
186
|
checkCanReturnFocus: null,
|
|
219
187
|
// let it call the user's original deactivate handler, if any, instead of
|
|
220
188
|
// our own which calls back into this function
|
|
221
|
-
onDeactivate: this.originalOptions.onDeactivate
|
|
189
|
+
onDeactivate: this.originalOptions.onDeactivate
|
|
190
|
+
// NOTE: for post deactivate, don't specify anything so that it calls the
|
|
222
191
|
// onPostDeactivate handler specified on `this.internalOptions`
|
|
223
192
|
// which will always be our own `handlePostDeactivate()` handler, which
|
|
224
193
|
// will finish things off by calling the user's provided onPostDeactivate
|
|
225
194
|
// handler, if any, at the right time
|
|
226
195
|
// onPostDeactivate: NOTHING
|
|
227
|
-
|
|
228
196
|
});
|
|
229
197
|
}
|
|
230
198
|
}, {
|
|
@@ -242,7 +210,6 @@ var FocusTrap = /*#__PURE__*/function (_React$Component) {
|
|
|
242
210
|
allowDeactivation: allowDeactivation
|
|
243
211
|
};
|
|
244
212
|
}
|
|
245
|
-
|
|
246
213
|
return allowDeactivation;
|
|
247
214
|
}
|
|
248
215
|
}, {
|
|
@@ -258,32 +225,35 @@ var FocusTrap = /*#__PURE__*/function (_React$Component) {
|
|
|
258
225
|
key: "handlePostDeactivate",
|
|
259
226
|
value: function handlePostDeactivate() {
|
|
260
227
|
var _this2 = this;
|
|
261
|
-
|
|
262
228
|
var finishDeactivation = function finishDeactivation() {
|
|
263
229
|
var returnFocusNode = _this2.getReturnFocusNode();
|
|
230
|
+
var canReturnFocus = !!(
|
|
231
|
+
// did the consumer allow it?
|
|
264
232
|
|
|
265
|
-
var canReturnFocus = !!( // did the consumer allow it?
|
|
266
233
|
_this2.originalOptions.returnFocusOnDeactivate && // can we actually focus the node?
|
|
267
|
-
returnFocusNode !== null && returnFocusNode !== void 0 && returnFocusNode.focus && (
|
|
268
|
-
|
|
269
|
-
_this2.outsideClick
|
|
234
|
+
returnFocusNode !== null && returnFocusNode !== void 0 && returnFocusNode.focus && (
|
|
235
|
+
// was there an outside click that allowed deactivation?
|
|
236
|
+
!_this2.outsideClick ||
|
|
237
|
+
// did the consumer allow deactivation when the outside node was clicked?
|
|
238
|
+
_this2.outsideClick.allowDeactivation &&
|
|
239
|
+
// is the outside node NOT focusable (implying that it did NOT receive focus
|
|
270
240
|
// as a result of the click-through) -- in which case do NOT restore focus
|
|
271
241
|
// to `returnFocusNode` because focus should remain on the outside node
|
|
272
|
-
!isFocusable(_this2.outsideClick.target, _this2.internalOptions.tabbableOptions))
|
|
242
|
+
!isFocusable(_this2.outsideClick.target, _this2.internalOptions.tabbableOptions))
|
|
243
|
+
|
|
244
|
+
// if no, the restore focus to `returnFocusNode` at this point
|
|
273
245
|
);
|
|
274
|
-
var _this2$internalOption = _this2.internalOptions.preventScroll,
|
|
275
|
-
preventScroll = _this2$internalOption === void 0 ? false : _this2$internalOption;
|
|
276
246
|
|
|
247
|
+
var _this2$internalOption = _this2.internalOptions.preventScroll,
|
|
248
|
+
preventScroll = _this2$internalOption === void 0 ? false : _this2$internalOption;
|
|
277
249
|
if (canReturnFocus) {
|
|
278
250
|
// return focus to the element that had focus when the trap was activated
|
|
279
251
|
returnFocusNode.focus({
|
|
280
252
|
preventScroll: preventScroll
|
|
281
253
|
});
|
|
282
254
|
}
|
|
283
|
-
|
|
284
255
|
if (_this2.originalOptions.onPostDeactivate) {
|
|
285
256
|
_this2.originalOptions.onPostDeactivate.call(null); // don't call it in context of "this"
|
|
286
|
-
|
|
287
257
|
}
|
|
288
258
|
|
|
289
259
|
_this2.outsideClick = null; // reset: no longer needed
|
|
@@ -315,22 +285,18 @@ var FocusTrap = /*#__PURE__*/function (_React$Component) {
|
|
|
315
285
|
// which means we need to reactivate and then pause. Otherwise, do nothing.
|
|
316
286
|
if (this.props.active && !this.focusTrap.active) {
|
|
317
287
|
this.focusTrap.activate();
|
|
318
|
-
|
|
319
288
|
if (this.props.paused) {
|
|
320
289
|
this.focusTrap.pause();
|
|
321
290
|
}
|
|
322
291
|
}
|
|
323
292
|
} else {
|
|
324
293
|
var nodesExist = this.focusTrapElements.some(Boolean);
|
|
325
|
-
|
|
326
294
|
if (nodesExist) {
|
|
327
295
|
// eslint-disable-next-line react/prop-types -- _createFocusTrap is an internal prop
|
|
328
296
|
this.focusTrap = this.props._createFocusTrap(this.focusTrapElements, this.internalOptions);
|
|
329
|
-
|
|
330
297
|
if (this.props.active) {
|
|
331
298
|
this.focusTrap.activate();
|
|
332
299
|
}
|
|
333
|
-
|
|
334
300
|
if (this.props.paused) {
|
|
335
301
|
this.focusTrap.pause();
|
|
336
302
|
}
|
|
@@ -342,12 +308,12 @@ var FocusTrap = /*#__PURE__*/function (_React$Component) {
|
|
|
342
308
|
value: function componentDidMount() {
|
|
343
309
|
if (this.props.active) {
|
|
344
310
|
this.setupFocusTrap();
|
|
345
|
-
}
|
|
311
|
+
}
|
|
312
|
+
// else, wait for later activation in case the `focusTrapOptions` will be updated
|
|
346
313
|
// again before the trap is activated (e.g. if waiting to know what the document
|
|
347
314
|
// object will be, so the Trap must be rendered, but the consumer is waiting to
|
|
348
315
|
// activate until they have obtained the document from a ref)
|
|
349
316
|
// @see https://github.com/focus-trap/focus-trap-react/issues/539
|
|
350
|
-
|
|
351
317
|
}
|
|
352
318
|
}, {
|
|
353
319
|
key: "componentDidUpdate",
|
|
@@ -356,17 +322,14 @@ var FocusTrap = /*#__PURE__*/function (_React$Component) {
|
|
|
356
322
|
if (prevProps.containerElements !== this.props.containerElements) {
|
|
357
323
|
this.focusTrap.updateContainerElements(this.props.containerElements);
|
|
358
324
|
}
|
|
359
|
-
|
|
360
325
|
var hasActivated = !prevProps.active && this.props.active;
|
|
361
326
|
var hasDeactivated = prevProps.active && !this.props.active;
|
|
362
327
|
var hasPaused = !prevProps.paused && this.props.paused;
|
|
363
328
|
var hasUnpaused = prevProps.paused && !this.props.paused;
|
|
364
|
-
|
|
365
329
|
if (hasActivated) {
|
|
366
330
|
this.updatePreviousElement();
|
|
367
331
|
this.focusTrap.activate();
|
|
368
332
|
}
|
|
369
|
-
|
|
370
333
|
if (hasDeactivated) {
|
|
371
334
|
this.deactivateTrap();
|
|
372
335
|
return; // un/pause does nothing on an inactive trap
|
|
@@ -375,7 +338,6 @@ var FocusTrap = /*#__PURE__*/function (_React$Component) {
|
|
|
375
338
|
if (hasPaused) {
|
|
376
339
|
this.focusTrap.pause();
|
|
377
340
|
}
|
|
378
|
-
|
|
379
341
|
if (hasUnpaused) {
|
|
380
342
|
this.focusTrap.unpause();
|
|
381
343
|
}
|
|
@@ -384,13 +346,14 @@ var FocusTrap = /*#__PURE__*/function (_React$Component) {
|
|
|
384
346
|
// it either means it shouldn't be active, or it should be but none of
|
|
385
347
|
// of given `containerElements` were present in the DOM the last time
|
|
386
348
|
// we tried to create the trap
|
|
349
|
+
|
|
387
350
|
if (prevProps.containerElements !== this.props.containerElements) {
|
|
388
351
|
this.focusTrapElements = this.props.containerElements;
|
|
389
|
-
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// don't create the trap unless it should be active in case the consumer
|
|
390
355
|
// is still updating `focusTrapOptions`
|
|
391
356
|
// @see https://github.com/focus-trap/focus-trap-react/issues/539
|
|
392
|
-
|
|
393
|
-
|
|
394
357
|
if (this.props.active) {
|
|
395
358
|
this.updatePreviousElement();
|
|
396
359
|
this.setupFocusTrap();
|
|
@@ -406,17 +369,13 @@ var FocusTrap = /*#__PURE__*/function (_React$Component) {
|
|
|
406
369
|
key: "render",
|
|
407
370
|
value: function render() {
|
|
408
371
|
var _this3 = this;
|
|
409
|
-
|
|
410
372
|
var child = this.props.children ? React.Children.only(this.props.children) : undefined;
|
|
411
|
-
|
|
412
373
|
if (child) {
|
|
413
374
|
if (child.type && child.type === React.Fragment) {
|
|
414
375
|
throw new Error('A focus-trap cannot use a Fragment as its child container. Try replacing it with a <div> element.');
|
|
415
376
|
}
|
|
416
|
-
|
|
417
377
|
var callbackRef = function callbackRef(element) {
|
|
418
378
|
var containerElements = _this3.props.containerElements;
|
|
419
|
-
|
|
420
379
|
if (child) {
|
|
421
380
|
if (typeof child.ref === 'function') {
|
|
422
381
|
child.ref(element);
|
|
@@ -424,24 +383,18 @@ var FocusTrap = /*#__PURE__*/function (_React$Component) {
|
|
|
424
383
|
child.ref.current = element;
|
|
425
384
|
}
|
|
426
385
|
}
|
|
427
|
-
|
|
428
386
|
_this3.focusTrapElements = containerElements ? containerElements : [element];
|
|
429
387
|
};
|
|
430
|
-
|
|
431
388
|
var childWithRef = React.cloneElement(child, {
|
|
432
389
|
ref: callbackRef
|
|
433
390
|
});
|
|
434
391
|
return childWithRef;
|
|
435
392
|
}
|
|
436
|
-
|
|
437
393
|
return null;
|
|
438
394
|
}
|
|
439
395
|
}]);
|
|
440
|
-
|
|
441
396
|
return FocusTrap;
|
|
442
397
|
}(React.Component); // support server-side rendering where `Element` will not be defined
|
|
443
|
-
|
|
444
|
-
|
|
445
398
|
var ElementType = typeof Element === 'undefined' ? Function : Element;
|
|
446
399
|
FocusTrap.propTypes = {
|
|
447
400
|
active: PropTypes.bool,
|
|
@@ -455,7 +408,8 @@ FocusTrap.propTypes = {
|
|
|
455
408
|
onPostDeactivate: PropTypes.func,
|
|
456
409
|
checkCanReturnFocus: PropTypes.func,
|
|
457
410
|
initialFocus: PropTypes.oneOfType([PropTypes.instanceOf(ElementType), PropTypes.string, PropTypes.bool, PropTypes.func]),
|
|
458
|
-
fallbackFocus: PropTypes.oneOfType([PropTypes.instanceOf(ElementType), PropTypes.string,
|
|
411
|
+
fallbackFocus: PropTypes.oneOfType([PropTypes.instanceOf(ElementType), PropTypes.string,
|
|
412
|
+
// NOTE: does not support `false` as value (or return value from function)
|
|
459
413
|
PropTypes.func]),
|
|
460
414
|
escapeDeactivates: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]),
|
|
461
415
|
clickOutsideDeactivates: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]),
|
|
@@ -464,19 +418,22 @@ FocusTrap.propTypes = {
|
|
|
464
418
|
allowOutsideClick: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]),
|
|
465
419
|
preventScroll: PropTypes.bool,
|
|
466
420
|
tabbableOptions: PropTypes.shape({
|
|
467
|
-
displayCheck: PropTypes.oneOf(['full', 'non-zero-area', 'none']),
|
|
421
|
+
displayCheck: PropTypes.oneOf(['full', 'legacy-full', 'non-zero-area', 'none']),
|
|
468
422
|
getShadowRoot: PropTypes.oneOfType([PropTypes.bool, PropTypes.func])
|
|
469
423
|
})
|
|
470
424
|
}),
|
|
471
425
|
containerElements: PropTypes.arrayOf(PropTypes.instanceOf(ElementType)),
|
|
472
426
|
// DOM element ONLY
|
|
473
|
-
children: PropTypes.oneOfType([PropTypes.element,
|
|
427
|
+
children: PropTypes.oneOfType([PropTypes.element,
|
|
428
|
+
// React element
|
|
474
429
|
PropTypes.instanceOf(ElementType) // DOM element
|
|
475
|
-
])
|
|
430
|
+
])
|
|
431
|
+
|
|
432
|
+
// NOTE: _createFocusTrap is internal, for testing purposes only, so we don't
|
|
476
433
|
// specify it here. It's expected to be set to the function returned from
|
|
477
434
|
// require('focus-trap'), or one with a compatible interface.
|
|
478
|
-
|
|
479
435
|
};
|
|
436
|
+
|
|
480
437
|
FocusTrap.defaultProps = {
|
|
481
438
|
active: true,
|
|
482
439
|
paused: false,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "focus-trap-react",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "10.0.1",
|
|
4
4
|
"description": "A React component that traps focus.",
|
|
5
5
|
"main": "dist/focus-trap-react.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -57,46 +57,46 @@
|
|
|
57
57
|
},
|
|
58
58
|
"homepage": "https://github.com/focus-trap/focus-trap-react#readme",
|
|
59
59
|
"devDependencies": {
|
|
60
|
-
"@babel/cli": "^7.
|
|
61
|
-
"@babel/core": "^7.
|
|
62
|
-
"@babel/eslint-parser": "^7.
|
|
63
|
-
"@babel/plugin-proposal-class-properties": "^7.
|
|
64
|
-
"@babel/preset-env": "^7.
|
|
65
|
-
"@babel/preset-react": "^7.
|
|
66
|
-
"@changesets/cli": "^2.
|
|
67
|
-
"@testing-library/cypress": "^8.0.
|
|
68
|
-
"@testing-library/dom": "^8.
|
|
69
|
-
"@testing-library/jest-dom": "^5.16.
|
|
70
|
-
"@testing-library/react": "^13.
|
|
71
|
-
"@testing-library/user-event": "^14.
|
|
60
|
+
"@babel/cli": "^7.19.3",
|
|
61
|
+
"@babel/core": "^7.20.2",
|
|
62
|
+
"@babel/eslint-parser": "^7.19.1",
|
|
63
|
+
"@babel/plugin-proposal-class-properties": "^7.18.6",
|
|
64
|
+
"@babel/preset-env": "^7.20.2",
|
|
65
|
+
"@babel/preset-react": "^7.18.6",
|
|
66
|
+
"@changesets/cli": "^2.25.2",
|
|
67
|
+
"@testing-library/cypress": "^8.0.7",
|
|
68
|
+
"@testing-library/dom": "^8.19.0",
|
|
69
|
+
"@testing-library/jest-dom": "^5.16.5",
|
|
70
|
+
"@testing-library/react": "^13.4.0",
|
|
71
|
+
"@testing-library/user-event": "^14.4.3",
|
|
72
72
|
"@types/jquery": "^3.5.14",
|
|
73
|
-
"all-contributors-cli": "^6.
|
|
74
|
-
"babel-jest": "^
|
|
73
|
+
"all-contributors-cli": "^6.24.0",
|
|
74
|
+
"babel-jest": "^29.3.1",
|
|
75
75
|
"babelify": "^10.0.0",
|
|
76
76
|
"browserify": "^17.0.0",
|
|
77
|
-
"budo": "^11.
|
|
78
|
-
"cypress": "^10.
|
|
77
|
+
"budo": "^11.8.4",
|
|
78
|
+
"cypress": "^10.11.0",
|
|
79
79
|
"cypress-plugin-tab": "^1.0.5",
|
|
80
|
-
"eslint": "^8.
|
|
80
|
+
"eslint": "^8.27.0",
|
|
81
81
|
"eslint-config-prettier": "^8.5.0",
|
|
82
82
|
"eslint-plugin-cypress": "^2.12.1",
|
|
83
|
-
"eslint-plugin-jest": "^
|
|
84
|
-
"eslint-plugin-react": "^7.
|
|
85
|
-
"jest": "^
|
|
86
|
-
"jest-environment-jsdom": "^
|
|
87
|
-
"jest-watch-typeahead": "^
|
|
83
|
+
"eslint-plugin-jest": "^27.1.5",
|
|
84
|
+
"eslint-plugin-react": "^7.31.10",
|
|
85
|
+
"jest": "^29.3.1",
|
|
86
|
+
"jest-environment-jsdom": "^29.3.1",
|
|
87
|
+
"jest-watch-typeahead": "^2.2.0",
|
|
88
88
|
"onchange": "^7.1.0",
|
|
89
|
-
"prettier": "^2.7.
|
|
89
|
+
"prettier": "^2.7.1",
|
|
90
90
|
"prop-types": "^15.8.1",
|
|
91
91
|
"react": "^18.2.0",
|
|
92
92
|
"react-dom": "^18.2.0",
|
|
93
|
-
"regenerator-runtime": "^0.13.
|
|
93
|
+
"regenerator-runtime": "^0.13.10",
|
|
94
94
|
"start-server-and-test": "^1.14.0",
|
|
95
|
-
"typescript": "^4.
|
|
95
|
+
"typescript": "^4.9.3"
|
|
96
96
|
},
|
|
97
97
|
"dependencies": {
|
|
98
|
-
"focus-trap": "^
|
|
99
|
-
"tabbable": "^
|
|
98
|
+
"focus-trap": "^7.1.0",
|
|
99
|
+
"tabbable": "^6.0.1"
|
|
100
100
|
},
|
|
101
101
|
"peerDependencies": {
|
|
102
102
|
"prop-types": "^15.8.1",
|
package/src/focus-trap-react.js
CHANGED
|
@@ -453,7 +453,12 @@ FocusTrap.propTypes = {
|
|
|
453
453
|
allowOutsideClick: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]),
|
|
454
454
|
preventScroll: PropTypes.bool,
|
|
455
455
|
tabbableOptions: PropTypes.shape({
|
|
456
|
-
displayCheck: PropTypes.oneOf([
|
|
456
|
+
displayCheck: PropTypes.oneOf([
|
|
457
|
+
'full',
|
|
458
|
+
'legacy-full',
|
|
459
|
+
'non-zero-area',
|
|
460
|
+
'none',
|
|
461
|
+
]),
|
|
457
462
|
getShadowRoot: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]),
|
|
458
463
|
}),
|
|
459
464
|
}),
|