From 7495680de94aa3b9d7ff96825894bcebd3d2a861 Mon Sep 17 00:00:00 2001 From: Stefan Cameron Date: Thu, 28 Apr 2022 15:49:39 -0500 Subject: [PATCH] Fix multiple issues surrounding trap deactivation (#659) - Consistently call onDeactivate, onPostDeactivate, checkCanReturnFocus These options were not being called consistently, particularly because of the way focus-trap-react tries to participate in the trap's deactivation process in order to control how focus is returned (if the trap is configured to return focus after deactivation). - Ensure an outside click permitted to deactivate the trap with the `clickOutsideDeactivates=true` option is actually permitted to so _and_ focus remains on the outside node clicked rather than returning to the node that had focus prior to the trap being activated (which is the default behavior). - Required bump to focus-trap v6.9.0 for bug fixes and new features to help with above fixes. --- .changeset/fifty-houses-occur.md | 5 + .changeset/good-islands-enjoy.md | 5 + .changeset/honest-mice-call.md | 5 + .gitignore | 1 + demo/js/demo-setReturnFocus.js | 14 ++- demo/js/demo-special-element.js | 2 +- package.json | 3 +- src/focus-trap-react.js | 173 +++++++++++++++++++++++++------ test/focus-trap-react.test.js | 17 ++- yarn.lock | 8 +- 10 files changed, 183 insertions(+), 50 deletions(-) create mode 100644 .changeset/fifty-houses-occur.md create mode 100644 .changeset/good-islands-enjoy.md create mode 100644 .changeset/honest-mice-call.md 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"