Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[pickers] Avoid stealing focus on click away #13434

Open
wants to merge 7 commits into
base: master
Choose a base branch
from

Conversation

LukasTy
Copy link
Member

@LukasTy LukasTy commented Jun 10, 2024

Fixes #13332
Fixes #14801

  • Fix a related issue with Range Pickers: the stopPropagation calls were preventing click away listener from working properly
  • Fix a related issue with Pickers using Digital Clocks: The .focus() calls in effects were stealing the focus before closing the popper

Preview: https://deploy-preview-13434--material-ui-x.netlify.app/x/react-date-pickers/

@LukasTy LukasTy added bug 🐛 Something doesn't work component: pickers This is the name of the generic UI component, not the React module! needs cherry-pick The PR should be cherry-picked to master after merge labels Jun 10, 2024
@LukasTy LukasTy self-assigned this Jun 10, 2024
@mui-bot
Copy link

mui-bot commented Jun 10, 2024

Deploy preview: https://deploy-preview-13434--material-ui-x.netlify.app/

Generated by 🚫 dangerJS against c393b79

@@ -195,15 +195,15 @@ const useMultiInputFieldSlotProps = <
}, [rangePosition, open, currentView, startFieldRef, endFieldRef]);

const openRangeStartSelection: React.UIEventHandler = (event) => {
event.stopPropagation();
event.preventDefault();
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These fixes prevent the following issue:

Screen.Recording.2024-06-10.at.13.43.10.mov

The regular Picker is not closed when opening a range picker

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we add a comment on top of this preventDefault to explain why it's here? In general, I think we should aim to justify each, they (preventDefault and especially stopPropagation) tend to be quickly food guns.

@@ -219,7 +220,8 @@ export const DigitalClock = React.forwardRef(function DigitalClock<TDate extends
return;
}
const offsetTop = activeItem.offsetTop;
if (autoFocus || !!focusedView) {
if ((autoFocus || !!focusedView) && activeItem !== lastActiveRef.current) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fix the issue of stealing the focus when closing a picker with DigitalClock:

Screen.Recording.2024-06-10.at.15.37.51.mov

@@ -172,7 +173,7 @@ export const MultiSectionDigitalClockSection = React.forwardRef(
const activeItem = containerRef.current.querySelector<HTMLElement>(
'[role="option"][tabindex="0"], [role="option"][aria-selected="true"]',
);
if (active && autoFocus && activeItem) {
if (activeItem && active && autoFocus && activeItem !== previousActive.current) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fix the issue of stealing the focus when closing a picker with MultiSectionDigitalClock:

Screen.Recording.2024-06-10.at.15.38.47.mov

@@ -360,6 +360,16 @@ export function PickersPopper(inProps: PickerPopperProps) {
}, [onDismiss, open]);

const lastFocusedElementRef = React.useRef<Element | null>(null);

const handleClickAway: OnClickAway = useEventCallback(() => {
lastFocusedElementRef.current = null;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fix the root cause why pickers where returning the focus back to the open button after being clicked away:

Screen.Recording.2024-06-10.at.15.39.53.mov

@LukasTy LukasTy requested review from oliviertassinari and a team June 10, 2024 13:26
Copy link
Member

@michelengelen michelengelen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! 🚢

Copy link
Member

@flaviendelangle flaviendelangle left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks solid!

Copy link
Member

@oliviertassinari oliviertassinari left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In HEAD, we restore the focus back to the trigger. e.g. https://mui.com/material-ui/react-menu/

Screen.Recording.2024-06-10.at.18.37.04.mov

It doesn't do it anymore:

Screen.Recording.2024-06-10.at.18.34.53.mov

A bug?

@@ -195,15 +195,15 @@ const useMultiInputFieldSlotProps = <
}, [rangePosition, open, currentView, startFieldRef, endFieldRef]);

const openRangeStartSelection: React.UIEventHandler = (event) => {
event.stopPropagation();
event.preventDefault();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we add a comment on top of this preventDefault to explain why it's here? In general, I think we should aim to justify each, they (preventDefault and especially stopPropagation) tend to be quickly food guns.

@@ -777,6 +777,76 @@ async function initializeEnvironment(
// check that selecting a day doesn't change the day text
expect(await day11.textContent()).to.equal('11');
});

describe('Focus behavior', () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suspect a regular karma test would be enough here (so better since faster to run)? We are doing relatively simple events.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I'm not mistaken, I tried writing unit tests initially, but failed to make them work.
I didn't try skipping them in jest though. I'll look into that.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've tried karma tests and they have the same problem...
Maybe it's some sort of a race condition with timeouts, but on those tests, the document.activeElement is resolved/set on the button element. 🙈

@LukasTy
Copy link
Member Author

LukasTy commented Jun 11, 2024

In HEAD, we restore the focus back to the trigger. e.g. https://mui.com/material-ui/react-menu/
It doesn't do it anymore:
A bug?

Thank you for flagging this @oliviertassinari! 🙏
I've used the behavior in here as an example.
There is a clear issue that closing the picker with an Esc key no longer returns the focus to the button as in the referred example.
Given the W3C-ARIA Menu example you could think that our Menu implementation is a bit too intrusive with moving the focus back to the button even on click away.
I'll look into possible options a bit more in-depth. 😉

@LukasTy
Copy link
Member Author

LukasTy commented Jun 11, 2024

@oliviertassinari I've explored other solutions and here is the rundown:

  • Angular Material and React Spectrum behave like we currently do (click away returns focus back to the opening button), but they both are using the dialog approach, where all the rest of the page becomes non-interactive—under a fullscreen backdrop, in our case—we don't do this.
  • Native date input element behaves like in this PR implementation in both Chrome and Firefox—Esc returns the focus to the opening button, but clicking away doesn't. That seems to be inline with W3C-ARIA example guidelines.
  • Next UI seems to have the same behavior like we currently do, no backdrop and proper click-away handling. If that's a role model we want to follow, when I need to "get back to the drawing board".

Given these arguments, I think that the behavioral solution in this PR is valid and more correct than the current one.
Unless we want to have a full-screen backdrop visually "disabling" page interactiveness, in which case we need more changes.

WDYT @flaviendelangle @arthurbalduini @joserodolfofreitas?

  1. Do you think that adding a fullscreen backdrop makes sense in this case?
  2. Or do you feel our approach closer resembling a native date input behavior makes sense to you?
  3. Or do we want to strive for making no behavioral changes and just fixing the underlying problem—Next UI approach?

@oliviertassinari
Copy link
Member

oliviertassinari commented Jun 11, 2024

@LukasTy Thanks for the dive. In my view, the behavior before feels better (Angular Material and React Spectrum ). At least, the things that are important properties we want seem to be:

  • Tabs go to the next form input after closing a date picker, on all platforms
  • the screen readers announce the right focus element, it's not lost

On the solution, I don't think that 1. (using a backdrop) is that much correlated to the root of the problem, this feels like taking a different path (which works, true). Now, it's not the end of the world, maybe it can work ok like this.

At the root, there might be nothing to change in MUI X (I assume what we do here is to trigger ). The problem might really be with the FocusTrap logic. Maybe the logic that restores the focus should give up if that focus is already set outside of its boundary. I mean, this would make a lot of sense to me. It makes me think of mui/material-ui#35307. For this, we should involve the FocusTrap's ower? https://www.notion.so/mui-org/0c8156b0b1634ea5bfd903dd005ec1cf?v=89ea8d7b7e104bf8b3bb68a9edca5e16&p=fbe1aed295f748c19576d024e9a48c62&pm=s

@jbblanchet
Copy link

Hi! Is this fix still being considered? I just spent the better part of last week migrating from react-dates to this date picker, but that bug is a deal breaker for us. I haven't see any comments since June on this issue, I was wondering if maybe it had been forgotten?

Thanks

@github-actions github-actions bot added the PR: out-of-date The pull request has merge conflicts and can't be merged label Oct 10, 2024
Copy link

This pull request has conflicts, please resolve those before we can evaluate the pull request.

@LukasTy
Copy link
Member Author

LukasTy commented Oct 10, 2024

Hi! Is this fix still being considered? I just spent the better part of last week migrating from react-dates to this date picker, but that bug is a deal breaker for us. I haven't see any comments since June on this issue, I was wondering if maybe it had been forgotten?

Hi, thank you for reminding us about this problem. 🙏
It's been parked, because of higher-priority topics for now, but we plan to finalize this PR in the very near future. 👌

@BrunMartins
Copy link

Hello! Was there any progress on this issue in the meanwhile? If not, can the community help in anyway?
Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug 🐛 Something doesn't work component: pickers This is the name of the generic UI component, not the React module! needs cherry-pick The PR should be cherry-picked to master after merge PR: out-of-date The pull request has merge conflicts and can't be merged
Projects
None yet
7 participants