Socket
Socket
Sign inDemoInstall

focus-trap-react

Package Overview
Dependencies
1
Maintainers
5
Versions
69
Alerts
File Explorer

Advanced tools

Install Socket

Detect and block malicious and high-risk dependencies

Install

Comparing version 8.10.0 to 8.11.0

12

CHANGELOG.md
# Changelog
## 8.11.0
### Minor Changes
- 7495680: Bump focus-trap to v6.9.0 to get bug fixes and new features to help fix some bugs.
### Patch Changes
- 7495680: Fix onDeactivate, onPostDeactivate, and checkCanReturnFocus options not being called consistently on deactivation.
- 7495680: Fix focus not being allowed to remain on outside node post-deactivation when `clickOutsideDeactivates` is true or returns true.
## 8.10.0

@@ -8,2 +19,3 @@

- 659d44e: Bumps focus-trap to v6.8.1. The big new feature is opt-in Shadow DOM support in focus-trap (in tabbable), and new tabbable options exposed in a new `focusTrapOptions.tabbableOptions` configuration option.
- ⚠️ This will likely break your tests **if you're using JSDom** (e.g. with Jest). See [testing in JSDom](./README.md#testing-in-jsdom) for more info.

@@ -10,0 +22,0 @@ ## 8.9.2

163

dist/focus-trap-react.js

@@ -32,3 +32,6 @@ "use strict";

var _require = require('focus-trap'),
createFocusTrap = _require.createFocusTrap; // TODO: These issues are related to older React features which we'll likely need
createFocusTrap = _require.createFocusTrap;
var _require2 = require('tabbable'),
isFocusable = _require2.isFocusable; // TODO: These issues are related to older React features which we'll likely need
// to fix in order to move the code forward to the next major version of React.

@@ -50,14 +53,39 @@ // @see https://github.com/davidtheclark/focus-trap-react/issues/77

_this = _super.call(this, props); // We need to hijack the returnFocusOnDeactivate option,
// because React can move focus into the element before we arrived at
// this lifecycle hook (e.g. with autoFocus inputs). So the component
// captures the previouslyFocusedElement in componentWillMount,
// then (optionally) returns focus to it in componentWillUnmount.
_this = _super.call(this, props);
_this.handleDeactivate = _this.handleDeactivate.bind(_assertThisInitialized(_this));
_this.handlePostDeactivate = _this.handlePostDeactivate.bind(_assertThisInitialized(_this));
_this.handleClickOutsideDeactivates = _this.handleClickOutsideDeactivates.bind(_assertThisInitialized(_this)); // focus-trap options used internally when creating the trap
_this.tailoredFocusTrapOptions = {
returnFocusOnDeactivate: false
}; // because of the above, we maintain our own flag for this option, and
// default it to `true` because that's focus-trap's default
_this.internalOptions = {
// We need to hijack the returnFocusOnDeactivate option,
// because React can move focus into the element before we arrived at
// this lifecycle hook (e.g. with autoFocus inputs). So the component
// captures the previouslyFocusedElement in componentWillMount,
// then (optionally) returns focus to it in componentWillUnmount.
returnFocusOnDeactivate: false,
// the rest of these are also related to deactivation of the trap, and we
// need to use them and control them as well
checkCanReturnFocus: null,
onDeactivate: _this.handleDeactivate,
onPostDeactivate: _this.handlePostDeactivate,
// we need to special-case this setting as well so that we can know if we should
// NOT return focus if the trap gets auto-deactivated as the result of an
// outside click (otherwise, we'll always think we should return focus because
// of how we manage that flag internally here)
clickOutsideDeactivates: _this.handleClickOutsideDeactivates
}; // original options provided by the consumer
_this.returnFocusOnDeactivate = true;
_this.originalOptions = {
// because of the above `tailoredFocusTrapOptions`, we maintain our own flag for
// this option, and default it to `true` because that's focus-trap's default
returnFocusOnDeactivate: true,
// because of the above `tailoredFocusTrapOptions`, we keep these separate since
// they're part of the deactivation process which we configure (internally) to
// be shared between focus-trap and focus-trap-react
onDeactivate: null,
onPostDeactivate: null,
checkCanReturnFocus: null,
// the user's setting, defaulted to false since focus-trap defaults this to false
clickOutsideDeactivates: false
};
var focusTrapOptions = props.focusTrapOptions;

@@ -70,18 +98,18 @@

if (optionName === 'returnFocusOnDeactivate') {
_this.returnFocusOnDeactivate = !!focusTrapOptions[optionName];
continue;
if (optionName === 'returnFocusOnDeactivate' || optionName === 'onDeactivate' || optionName === 'onPostDeactivate' || optionName === 'checkCanReturnFocus' || optionName === 'clickOutsideDeactivates') {
_this.originalOptions[optionName] = focusTrapOptions[optionName];
continue; // exclude from tailoredFocusTrapOptions
}
if (optionName === 'onPostDeactivate') {
_this.onPostDeactivate = focusTrapOptions[optionName];
continue;
}
_this.internalOptions[optionName] = focusTrapOptions[optionName];
} // if set, `{ target: Node, allowDeactivation: boolean }` where `target` is the outside
// node that was clicked, and `allowDeactivation` is the result of the consumer's
// option (stored in `this.originalOptions.clickOutsideDeactivates`, which may be a
// function) whether to allow or deny auto-deactivation on click on this outside node
_this.tailoredFocusTrapOptions[optionName] = focusTrapOptions[optionName];
} // elements from which to create the focus trap on mount; if a child is used
_this.outsideClick = null; // elements from which to create the focus trap on mount; if a child is used
// instead of the `containerElements` prop, we'll get the child's related
// element when the trap renders and then is declared 'mounted'
_this.focusTrapElements = props.containerElements || []; // now we remember what the currently focused element is, not relying on focus-trap

@@ -111,3 +139,3 @@

value: function getNodeForOption(optionName) {
var optionValue = this.tailoredFocusTrapOptions[optionName];
var optionValue = this.internalOptions[optionName];

@@ -160,23 +188,77 @@ if (!optionValue) {

value: function deactivateTrap() {
var _this2 = this;
// NOTE: it's possible the focus trap has already been deactivated without our knowing it,
// especially if the user set the `clickOutsideDeactivates: true` option on the trap,
// and the mouse was clicked on some element outside the trap; at that point, focus-trap
// will initiate its auto-deactivation process, which will call our own
// handleDeactivate(), which will call into this method
if (!this.focusTrap || !this.focusTrap.active) {
return;
}
var _this$tailoredFocusTr = this.tailoredFocusTrapOptions,
checkCanReturnFocus = _this$tailoredFocusTr.checkCanReturnFocus,
_this$tailoredFocusTr2 = _this$tailoredFocusTr.preventScroll,
preventScroll = _this$tailoredFocusTr2 === void 0 ? false : _this$tailoredFocusTr2;
this.focusTrap.deactivate({
// NOTE: we never let the trap return the focus since we do that ourselves
returnFocus: false,
// we'll call this in our own post deactivate handler so make sure the trap doesn't
// do it prematurely
checkCanReturnFocus: null,
// let it call the user's original deactivate handler, if any, instead of
// our own which calls back into this function
onDeactivate: this.originalOptions.onDeactivate // NOTE: for post deactivate, don't specify anything so that it calls the
// onPostDeactivate handler specified on `this.internalOptions`
// which will always be our own `handlePostDeactivate()` handler, which
// will finish things off by calling the user's provided onPostDeactivate
// handler, if any, at the right time
// onPostDeactivate: NOTHING
if (this.focusTrap) {
// NOTE: we never let the trap return the focus since we do that ourselves
this.focusTrap.deactivate({
returnFocus: false
});
});
}
}, {
key: "handleClickOutsideDeactivates",
value: function handleClickOutsideDeactivates(event) {
// use consumer's option (or call their handler) as the permission or denial
var allowDeactivation = typeof this.originalOptions.clickOutsideDeactivates === 'function' ? this.originalOptions.clickOutsideDeactivates.call(null, event) // call out of context
: this.originalOptions.clickOutsideDeactivates; // boolean
if (allowDeactivation) {
// capture the outside target that was clicked so we can use it in the deactivation
// process since the consumer allowed it to cause auto-deactivation
this.outsideClick = {
target: event.target,
allowDeactivation: allowDeactivation
};
}
return allowDeactivation;
}
}, {
key: "handleDeactivate",
value: function handleDeactivate() {
if (this.originalOptions.onDeactivate) {
this.originalOptions.onDeactivate.call(null); // call user's handler out of context
}
this.deactivateTrap();
}
}, {
key: "handlePostDeactivate",
value: function handlePostDeactivate() {
var _this2 = this;
var finishDeactivation = function finishDeactivation() {
var returnFocusNode = _this2.getReturnFocusNode();
var canReturnFocus = (returnFocusNode === null || returnFocusNode === void 0 ? void 0 : returnFocusNode.focus) && _this2.returnFocusOnDeactivate;
var canReturnFocus = !!( // did the consumer allow it?
_this2.originalOptions.returnFocusOnDeactivate && // can we actually focus the node?
returnFocusNode !== null && returnFocusNode !== void 0 && returnFocusNode.focus && ( // was there an outside click that allowed deactivation?
!_this2.outsideClick || // did the consumer allow deactivation when the outside node was clicked?
_this2.outsideClick.allowDeactivation && // is the outside node NOT focusable (implying that it did NOT receive focus
// as a result of the click-through) -- in which case do NOT restore focus
// to `returnFocusNode` because focus should remain on the outside node
!isFocusable(_this2.outsideClick.target, _this2.internalOptions.tabbableOptions)) // if no, the restore focus to `returnFocusNode` at this point
);
var _this2$internalOption = _this2.internalOptions.preventScroll,
preventScroll = _this2$internalOption === void 0 ? false : _this2$internalOption;
if (canReturnFocus) {
/** Returns focus to the element that had focus when the trap was activated. */
// return focus to the element that had focus when the trap was activated
returnFocusNode.focus({

@@ -187,10 +269,13 @@ preventScroll: preventScroll

if (_this2.onPostDeactivate) {
_this2.onPostDeactivate.call(null); // don't call it in context of "this"
if (_this2.originalOptions.onPostDeactivate) {
_this2.originalOptions.onPostDeactivate.call(null); // don't call it in context of "this"
}
_this2.outsideClick = null; // reset: no longer needed
};
if (checkCanReturnFocus) {
checkCanReturnFocus(this.getReturnFocusNode()).then(finishDeactivation, finishDeactivation);
if (this.originalOptions.checkCanReturnFocus) {
this.originalOptions.checkCanReturnFocus.call(null, this.getReturnFocusNode()) // call out of context
.then(finishDeactivation, finishDeactivation);
} else {

@@ -212,3 +297,3 @@ finishDeactivation();

// eslint-disable-next-line react/prop-types -- _createFocusTrap is an internal prop
this.focusTrap = this.props._createFocusTrap(focusTrapElementDOMNodes, this.tailoredFocusTrapOptions);
this.focusTrap = this.props._createFocusTrap(focusTrapElementDOMNodes, this.internalOptions);

@@ -215,0 +300,0 @@ if (this.props.active) {

{
"name": "focus-trap-react",
"version": "8.10.0",
"version": "8.11.0",
"description": "A React component that traps focus.",

@@ -29,2 +29,3 @@ "main": "dist/focus-trap-react.js",

"test:cypress:ci": "start-server-and-test start 9966 'cypress run --browser $CYPRESS_BROWSER --headless'",
"test:chrome": "CYPRESS_BROWSER=chrome yarn test:cypress:ci",
"test": "yarn format:check && yarn lint && yarn test:unit && yarn test:types && CYPRESS_BROWSER=chrome yarn test:cypress:ci",

@@ -74,9 +75,9 @@ "prepare": "yarn build",

"all-contributors-cli": "^6.20.0",
"babel-jest": "^27.5.1",
"babel-jest": "^28.0.2",
"babelify": "^10.0.0",
"browserify": "^17.0.0",
"budo": "^11.7.0",
"cypress": "^9.5.4",
"cypress": "^9.6.0",
"cypress-plugin-tab": "^1.0.5",
"eslint": "^8.13.0",
"eslint": "^8.14.0",
"eslint-config-prettier": "^8.5.0",

@@ -86,7 +87,7 @@ "eslint-plugin-cypress": "^2.12.1",

"jest": "^27.5.1",
"jest-watch-typeahead": "^1.0.0",
"jest-watch-typeahead": "^1.1.0",
"onchange": "^7.1.0",
"prettier": "^2.6.2",
"prop-types": "^15.8.1",
"react": "^18.0.0",
"react": "^18.1.0",
"react-dom": "^18.0.0",

@@ -98,3 +99,3 @@ "regenerator-runtime": "^0.13.9",

"dependencies": {
"focus-trap": "^6.8.1"
"focus-trap": "^6.9.0"
},

@@ -101,0 +102,0 @@ "peerDependencies": {

@@ -71,3 +71,4 @@ # 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)

const React = require('react');
const ReactDOM = require('react-dom');
const ReactDOM = require('react-dom'); // React 16-17
const { createRoot } = require('react-dom/client'); // React 18
const FocusTrap = require('focus-trap-react');

@@ -136,3 +137,4 @@

ReactDOM.render(<Demo />, document.getElementById('root'));
ReactDOM.render(<Demo />, document.getElementById('root')); // React 16-17
createRoot(document.getElementById('root')).render(<Demo />); // React 18
```

@@ -139,0 +141,0 @@

@@ -5,2 +5,3 @@ const React = require('react');

const { createFocusTrap } = require('focus-trap');
const { isFocusable } = require('tabbable');

@@ -16,15 +17,46 @@ // TODO: These issues are related to older React features which we'll likely need

// We need to hijack the returnFocusOnDeactivate option,
// because React can move focus into the element before we arrived at
// this lifecycle hook (e.g. with autoFocus inputs). So the component
// captures the previouslyFocusedElement in componentWillMount,
// then (optionally) returns focus to it in componentWillUnmount.
this.tailoredFocusTrapOptions = {
this.handleDeactivate = this.handleDeactivate.bind(this);
this.handlePostDeactivate = this.handlePostDeactivate.bind(this);
this.handleClickOutsideDeactivates =
this.handleClickOutsideDeactivates.bind(this);
// focus-trap options used internally when creating the trap
this.internalOptions = {
// We need to hijack the returnFocusOnDeactivate option,
// because React can move focus into the element before we arrived at
// this lifecycle hook (e.g. with autoFocus inputs). So the component
// captures the previouslyFocusedElement in componentWillMount,
// then (optionally) returns focus to it in componentWillUnmount.
returnFocusOnDeactivate: false,
// the rest of these are also related to deactivation of the trap, and we
// need to use them and control them as well
checkCanReturnFocus: null,
onDeactivate: this.handleDeactivate,
onPostDeactivate: this.handlePostDeactivate,
// we need to special-case this setting as well so that we can know if we should
// NOT return focus if the trap gets auto-deactivated as the result of an
// outside click (otherwise, we'll always think we should return focus because
// of how we manage that flag internally here)
clickOutsideDeactivates: this.handleClickOutsideDeactivates,
};
// because of the above, we maintain our own flag for this option, and
// default it to `true` because that's focus-trap's default
this.returnFocusOnDeactivate = true;
// original options provided by the consumer
this.originalOptions = {
// because of the above `tailoredFocusTrapOptions`, we maintain our own flag for
// this option, and default it to `true` because that's focus-trap's default
returnFocusOnDeactivate: true,
// because of the above `tailoredFocusTrapOptions`, we keep these separate since
// they're part of the deactivation process which we configure (internally) to
// be shared between focus-trap and focus-trap-react
onDeactivate: null,
onPostDeactivate: null,
checkCanReturnFocus: null,
// the user's setting, defaulted to false since focus-trap defaults this to false
clickOutsideDeactivates: false,
};
const { focusTrapOptions } = props;

@@ -36,15 +68,22 @@ for (const optionName in focusTrapOptions) {

if (optionName === 'returnFocusOnDeactivate') {
this.returnFocusOnDeactivate = !!focusTrapOptions[optionName];
continue;
if (
optionName === 'returnFocusOnDeactivate' ||
optionName === 'onDeactivate' ||
optionName === 'onPostDeactivate' ||
optionName === 'checkCanReturnFocus' ||
optionName === 'clickOutsideDeactivates'
) {
this.originalOptions[optionName] = focusTrapOptions[optionName];
continue; // exclude from tailoredFocusTrapOptions
}
if (optionName === 'onPostDeactivate') {
this.onPostDeactivate = focusTrapOptions[optionName];
continue;
}
this.tailoredFocusTrapOptions[optionName] = focusTrapOptions[optionName];
this.internalOptions[optionName] = focusTrapOptions[optionName];
}
// if set, `{ target: Node, allowDeactivation: boolean }` where `target` is the outside
// node that was clicked, and `allowDeactivation` is the result of the consumer's
// option (stored in `this.originalOptions.clickOutsideDeactivates`, which may be a
// function) whether to allow or deny auto-deactivation on click on this outside node
this.outsideClick = null;
// elements from which to create the focus trap on mount; if a child is used

@@ -75,3 +114,3 @@ // instead of the `containerElements` prop, we'll get the child's related

getNodeForOption(optionName) {
const optionValue = this.tailoredFocusTrapOptions[optionName];
const optionValue = this.internalOptions[optionName];
if (!optionValue) {

@@ -115,17 +154,82 @@ return null;

deactivateTrap() {
const { checkCanReturnFocus, preventScroll = false } =
this.tailoredFocusTrapOptions;
// NOTE: it's possible the focus trap has already been deactivated without our knowing it,
// especially if the user set the `clickOutsideDeactivates: true` option on the trap,
// and the mouse was clicked on some element outside the trap; at that point, focus-trap
// will initiate its auto-deactivation process, which will call our own
// handleDeactivate(), which will call into this method
if (!this.focusTrap || !this.focusTrap.active) {
return;
}
if (this.focusTrap) {
this.focusTrap.deactivate({
// NOTE: we never let the trap return the focus since we do that ourselves
this.focusTrap.deactivate({ returnFocus: false });
returnFocus: false,
// we'll call this in our own post deactivate handler so make sure the trap doesn't
// do it prematurely
checkCanReturnFocus: null,
// let it call the user's original deactivate handler, if any, instead of
// our own which calls back into this function
onDeactivate: this.originalOptions.onDeactivate,
// NOTE: for post deactivate, don't specify anything so that it calls the
// onPostDeactivate handler specified on `this.internalOptions`
// which will always be our own `handlePostDeactivate()` handler, which
// will finish things off by calling the user's provided onPostDeactivate
// handler, if any, at the right time
// onPostDeactivate: NOTHING
});
}
handleClickOutsideDeactivates(event) {
// use consumer's option (or call their handler) as the permission or denial
const allowDeactivation =
typeof this.originalOptions.clickOutsideDeactivates === 'function'
? this.originalOptions.clickOutsideDeactivates.call(null, event) // call out of context
: this.originalOptions.clickOutsideDeactivates; // boolean
if (allowDeactivation) {
// capture the outside target that was clicked so we can use it in the deactivation
// process since the consumer allowed it to cause auto-deactivation
this.outsideClick = {
target: event.target,
allowDeactivation,
};
}
return allowDeactivation;
}
handleDeactivate() {
if (this.originalOptions.onDeactivate) {
this.originalOptions.onDeactivate.call(null); // call user's handler out of context
}
this.deactivateTrap();
}
handlePostDeactivate() {
const finishDeactivation = () => {
const returnFocusNode = this.getReturnFocusNode();
const canReturnFocus =
returnFocusNode?.focus && this.returnFocusOnDeactivate;
const canReturnFocus = !!(
// did the consumer allow it?
(
this.originalOptions.returnFocusOnDeactivate &&
// can we actually focus the node?
returnFocusNode?.focus &&
// was there an outside click that allowed deactivation?
(!this.outsideClick ||
// did the consumer allow deactivation when the outside node was clicked?
(this.outsideClick.allowDeactivation &&
// is the outside node NOT focusable (implying that it did NOT receive focus
// as a result of the click-through) -- in which case do NOT restore focus
// to `returnFocusNode` because focus should remain on the outside node
!isFocusable(
this.outsideClick.target,
this.internalOptions.tabbableOptions
)))
)
// if no, the restore focus to `returnFocusNode` at this point
);
const { preventScroll = false } = this.internalOptions;
if (canReturnFocus) {
/** Returns focus to the element that had focus when the trap was activated. */
// return focus to the element that had focus when the trap was activated
returnFocusNode.focus({

@@ -136,12 +240,13 @@ preventScroll,

if (this.onPostDeactivate) {
this.onPostDeactivate.call(null); // don't call it in context of "this"
if (this.originalOptions.onPostDeactivate) {
this.originalOptions.onPostDeactivate.call(null); // don't call it in context of "this"
}
this.outsideClick = null; // reset: no longer needed
};
if (checkCanReturnFocus) {
checkCanReturnFocus(this.getReturnFocusNode()).then(
finishDeactivation,
finishDeactivation
);
if (this.originalOptions.checkCanReturnFocus) {
this.originalOptions.checkCanReturnFocus
.call(null, this.getReturnFocusNode()) // call out of context
.then(finishDeactivation, finishDeactivation);
} else {

@@ -166,3 +271,3 @@ finishDeactivation();

focusTrapElementDOMNodes,
this.tailoredFocusTrapOptions
this.internalOptions
);

@@ -169,0 +274,0 @@

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap

Packages

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc