focus-trap-react icon indicating copy to clipboard operation
focus-trap-react copied to clipboard

Loosing focus after second popup opening with Redux Form

Open Delagardi opened this issue 11 months ago • 3 comments

Hello

I have login and sing up buttons in the header. Then clicking on login button Login popup opens. I want to add Focus trap on it. And I added. Works fine. When I close the popup focus returns to login button as intended.

But then I open the popup again focus do not moved to popup and staying on the login button. It's not clear to me why.

Could someone clarify for is this a bug or expected behaviour please?

My components:

// LoginPopup
import FocusTrap from 'focus-trap-react';
// ...

let interval: NodeJS.Timeout;
const focusTrapOptions = {
  escapeDeactivates: true,
  clickOutsideDeactivates: true,
  checkCanFocusTrap: (trapContainers) => {
    const results = trapContainers.map((trapContainer) => {
      return new Promise<void>((resolve) => {
        interval = setInterval(() => {
          if (getComputedStyle(trapContainer).visibility !== 'hidden') {
            resolve();
            clearInterval(interval);
          }
        }, 120);
      });
    });
    return Promise.all(results);
  },
};

const TrapChild = React.forwardRef<HTMLInputElement, IProps>(function TrapChildInner (
  { isOpen, onClose, ...params },
  ref,
) {
  return (
    <ControlDialog
      // ...
      focusTrapRef={ref}
    />
  );
});

const LoginPopup = ({ isOpen, onClose, focusOnLoginButton, ...params }: IProps) => {
  const [isTrapActive, setIsTrapActive] = useState(false);
  const focusTrapRef = React.useRef();

  useEffect(() => {
    if (isOpen) {
      setIsTrapActive(true);
    }

    return () => {
      clearInterval(interval);
    };
  }, [isOpen]);

  return (
    <FocusTrap
      active={isTrapActive}
      focusTrapOptions={focusTrapOptions}
    >
      <TrapChild isOpen={isOpen} onClose={onClose} {...params} ref={focusTrapRef} />
    </FocusTrap>
  );
};

export { LoginPopup };

And LoginForm

// LoginForm
// ...
import { Field, InjectedFormProps, reduxForm } from 'redux-form';
// ...
import classes from './login-form.scss';

const LoginForm = ({
  // ...props
}) => {
  return (
    <div className={classes.loginWrapper}>
      <form
        // ...
      >
        <Field
          name="login"
          component={UnderlinedInput}
          type="text"
          validate={[required, email]}
          label="Your email"
          dataInput="Email"
          autoComplete="off"
          floatingLabelFixed={isLabelFixed}
          errorClassName={classes.errorClassName}
          className={classes.classNameInput}
        />
        <Field
          name="password"
          /// ...props
        />
        <Button
          /// ...props
        >
          Log in
        </Button>
      </form>
    </div>
  );
};

const LoginReduxForm = reduxForm({
  form: 'login',
  validate,
})(LoginForm);

export { LoginReduxForm as LoginForm };



Delagardi avatar Mar 07 '24 16:03 Delagardi

@Delagardi It's not immediately clear to me just based on reading your code. A minimal repro in a codesandbox would be more useful to diagnose the problem, but before you do that, have you tried using focus-trap's onPostActivate() hook to just log something to the console to see if the trap is even getting activated the second time you click on the button?

Based on your description of the symptoms, it sounds like the trap isn't getting re-activated for some reason, since the focus remains outside of the Login dialog. If the trap were activated, it should have pulled focus into itself.

I would also try putting a console statement in the effect that clears the interval timer on unmount since I've been surprised by React unmounting and remounting my effects sometimes when I wouldn't have expected it:

  useEffect(() => {
    if (isOpen) {
      setIsTrapActive(true);
    }

    return () => {
      console.log('unmounting, clearing interval');  // <- here
      clearInterval(interval);
    };
  }, [isOpen]);

Maybe your interval is getting cleared by this on the second time clicking the button for some reason, causing the promise you return in the focusTrapOptions.checkCanFocusTrap option to never resolve.

Or, the second time through, since your focusTrapOptions are static (you aren't making a new set options), you end-up giving focus-trap an already-resolved promise, so it tries to activate the trap too early before the container is visible, and so focus gets messed-up.

stefcameron avatar Mar 07 '24 23:03 stefcameron

@stefcameron added to codesandbox

Here it works. The focus jumping to popup but not jumping back to Login button. But onPostActivate fires only once. Can be checked in dev tools: ****** POST ACTIVATE ******* log

For me it seems like FocusTrap isn't re-activated as you said. But not clear why. I cleared intervals in useEffect here and locally but the problem persists.

P. S. in codesandbox I get ranomly ModuleNotFoundError, but after reload the iFrame it works fine. I think this is some codesanbox internal issue

UPD: seems like ModuleNotFoundError fixed now

Delagardi avatar Mar 11 '24 22:03 Delagardi

@Delagardi Thanks for posting your sandbox. I poked around at it a bit, and what I see happening is that your code is leaving the focus trap's active/inactive state out of sync with the dialog's displayed state.

For example, by using the function option for the clickOutsideDeactivate and logging when that function is invoked, you can see that when you click outside the dialog, the trap deactivates, but the dialog stays rendered when it should be dismissed.

When you press ESC (since you set escapeDeactivates: true), it works better because the KeyDown event bubbles, and your code handles the key down event just like the trap does, so in this case, the trap deactivates and your code hides the dialog.

But there's another problem with pressing ESC, which triggers the onClose handler on line 64 of login-popup.tsx in that it doesn't call setIsTrapActive(false). Doing that yields better results when trying to re-open the dialog afterward.

And then when the trap is properly deactivated, if you click on the Login button again, you'll get an exception from focus-trap stating that it didn't find any focusable elements in your trap and you must have at least one, which means the second time around, your checkCanFocusTrap() handler isn't working properly. Perhaps it needs more delay.

All in all, I think you might be more successful if you move your focusTrapOptions object right into <LoginPopup> so you're giving focus-trap-react a fresh set of options every time the popup is rendered (or you could useMemo() to cache them until isOpen because false again). In fact, you don't really have a choice but to move them into the LoginPopup component because you need to call onClose() whenever the trap gets deactivated. And to that point, make sure you handle all the possible close events in your code to keep the trap's active state in sync with your dialog's open state.

stefcameron avatar Mar 14 '24 15:03 stefcameron

I'm going to assume this has been resolved now and close it. LMK if that's not the case.

stefcameron avatar Aug 25 '24 00:08 stefcameron