focus-trap-react
focus-trap-react copied to clipboard
Loosing focus after second popup opening with Redux Form
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 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 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 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.
I'm going to assume this has been resolved now and close it. LMK if that's not the case.