diff --git a/.changeset/fifty-houses-occur.md b/.changeset/fifty-houses-occur.md
new file mode 100644
index 00000000..d62c6925
--- /dev/null
+++ b/.changeset/fifty-houses-occur.md
@@ -0,0 +1,5 @@
+---
+'focus-trap-react': minor
+---
+
+Bump focus-trap to v6.9.0 to get bug fixes and new features to help fix some bugs.
diff --git a/.changeset/good-islands-enjoy.md b/.changeset/good-islands-enjoy.md
new file mode 100644
index 00000000..f2e9205e
--- /dev/null
+++ b/.changeset/good-islands-enjoy.md
@@ -0,0 +1,5 @@
+---
+'focus-trap-react': patch
+---
+
+Fix onDeactivate, onPostDeactivate, and checkCanReturnFocus options not being called consistently on deactivation.
diff --git a/.changeset/honest-mice-call.md b/.changeset/honest-mice-call.md
new file mode 100644
index 00000000..82ec589f
--- /dev/null
+++ b/.changeset/honest-mice-call.md
@@ -0,0 +1,5 @@
+---
+'focus-trap-react': patch
+---
+
+Fix focus not being allowed to remain on outside node post-deactivation when `clickOutsideDeactivates` is true or returns true.
diff --git a/.gitignore b/.gitignore
index e75beb31..1c877cf2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -70,3 +70,4 @@ dist/
.Thumbs
cypress/videos
+cypress/screenshots
diff --git a/demo/js/demo-setReturnFocus.js b/demo/js/demo-setReturnFocus.js
index a1a5d82c..25f8f40e 100644
--- a/demo/js/demo-setReturnFocus.js
+++ b/demo/js/demo-setReturnFocus.js
@@ -1,17 +1,21 @@
-const { useState } = require('react');
+const { useState, useMemo } = require('react');
const React = require('react');
const { createRoot } = require('react-dom/client');
const FocusTrap = require('../../dist/focus-trap-react');
const container = document.getElementById('demo-setReturnFocus');
-const focusTrapOptions = {
- setReturnFocus: '#AlternateReturnFocusElement',
-};
-
const DemoSetReturnFocusDialog = () => {
const [isTrapActive, setIsTrapActive] = useState(false);
+ const focusTrapOptions = useMemo(
+ () => ({
+ setReturnFocus: '#AlternateReturnFocusElement',
+ onDeactivate: () => setIsTrapActive(false),
+ }),
+ []
+ );
+
return (
<>
diff --git a/demo/js/demo-special-element.js b/demo/js/demo-special-element.js
index 7aa756e7..ddf58bf4 100644
--- a/demo/js/demo-special-element.js
+++ b/demo/js/demo-special-element.js
@@ -51,7 +51,7 @@ class DemoSpecialElement extends React.Component {
{
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({
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 {
finishDeactivation();
}
@@ -157,7 +262,7 @@ class FocusTrap extends React.Component {
// eslint-disable-next-line react/prop-types -- _createFocusTrap is an internal prop
this.focusTrap = this.props._createFocusTrap(
focusTrapElementDOMNodes,
- this.tailoredFocusTrapOptions
+ this.internalOptions
);
if (this.props.active) {
diff --git a/test/focus-trap-react.test.js b/test/focus-trap-react.test.js
index aa5b248c..d5883474 100644
--- a/test/focus-trap-react.test.js
+++ b/test/focus-trap-react.test.js
@@ -43,8 +43,11 @@ const FocusTrapExample = ({ focusTrapOptions, ...otherProps }) => {
const unmountTrap = () => setTrapIsActive(false);
const options = getTestFocusTrapOptions({
- onDeactivate: unmountTrap,
...focusTrapOptions,
+ onDeactivate: () => {
+ focusTrapOptions?.onDeactivate?.();
+ unmountTrap();
+ },
});
const trap = (
@@ -482,8 +485,10 @@ describe('FocusTrap', () => {
// Deactivate the focus trap
fireEvent.click(screen.getByText('deactivate trap'));
- expect(onDeactivate).toHaveBeenCalled();
- expect(onPostDeactivate).toHaveBeenCalled();
+ await waitFor(() => {
+ expect(onDeactivate).toHaveBeenCalled();
+ expect(onPostDeactivate).toHaveBeenCalled();
+ });
});
it('Will call onPostDeactivate() even if returnFocusOnDeactivate is false', async () => {
@@ -516,8 +521,10 @@ describe('FocusTrap', () => {
// Deactivate the focus trap
fireEvent.click(screen.getByText('deactivate trap'));
- expect(onDeactivate).toHaveBeenCalled();
- expect(onPostDeactivate).toHaveBeenCalled();
+ await waitFor(() => {
+ expect(onDeactivate).toHaveBeenCalled();
+ expect(onPostDeactivate).toHaveBeenCalled();
+ });
});
['string', 'element', 'function'].forEach((elementSelectionMethod) => {
diff --git a/yarn.lock b/yarn.lock
index f556bb99..6cfffa97 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4329,10 +4329,10 @@ flatted@^3.1.0:
resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.1.0.tgz#a5d06b4a8b01e3a63771daa5cb7a1903e2e57067"
integrity sha512-tW+UkmtNg/jv9CSofAKvgVcO7c2URjhTdW1ZTkcAritblu8tajiYy7YisnIflEwtKssCtOxpnBRoCB7iap0/TA==
-focus-trap@^6.8.1:
- version "6.8.1"
- resolved "https://registry.yarnpkg.com/focus-trap/-/focus-trap-6.8.1.tgz#0c9e4e44db8f7242f3d4b1056a518747d9c97125"
- integrity sha512-sdz/jAPiP/9cyElo31+X3/estGPi6wgHutg+R/3MFmJtMM5AeeBlFGplejQyy89Ouyds/9xW+qPEH3jFlOAuKg==
+focus-trap@^6.9.0:
+ version "6.9.0"
+ resolved "https://registry.yarnpkg.com/focus-trap/-/focus-trap-6.9.0.tgz#d72a1ba17ac1b500bd857c6b01f072b8cfd97f6e"
+ integrity sha512-Yv3ieSeAPbfjzjU6xIuF1yAGw0kIKO5EkEJL9o/8MYfBcr99cV7dE6rJM4slk1itDHHeEhoNorQVzvEIT1rNsw==
dependencies:
tabbable "^5.3.1"