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

fix(autocomplete-js): leave the modal open on reset on pointer devices #987

Merged
merged 12 commits into from
Jun 21, 2022

Conversation

sarahdayan
Copy link
Member

@sarahdayan sarahdayan commented Jun 16, 2022

This PR fixes a bug in detached mode where clicking the reset button would also close the modal, even with openOnFocus=true. This bug appeared only on pointer devices, not on touch devices.

Preview

Here are a gew scenarios that this PR fixes. You can test it out in this sandbox. To compare, here's the same sandbox without the fix.

BeforeAfter
detached-desktop.mov

The modal closes on reset.

detached-desktop.mov

The modal stays open on reset.

desktop-flicker.mov

The panel "flickers": it closes then reopens on focus.

desktop-no-flicker.mov

The panel stays open, there's no flicker.

detached-touch.mov

The modal stays open in detached mode on a touch device

detached-touch.mov

The modal still stays open in detached mode on a touch device

Root cause

Clicking the reset button triggers the DOM blur event on the <input>. Before this fix, we had an onBlur handler that triggered logic that closed the search modal. This didn't happen on touch devices because of some guard logic that only applied to touch devices.

This is a problem, because detached mode isn't touch-specific: it's the default mode on screens below 500px, and users can enable it with manually.

More context in the issue →

Solution

This PR fixes the problem by no longer relying on the blur input event, and instead using the mousedown event at the environment level. This is a similar approach to what we're doing with the touchstart event, but for pointer devices. Both handlers reuse the same logic, and are colocated.

The blur event isn't only triggered by "clicks" or "touches" outside of the focused element, it also happens when we hit Enter or Escape—the difference being that these are intentional blurs that we don't need to guard. The relevant part of the onBlur handler for these events (canceling all pending requests) was added to these event's handlers, in onKeyDown.

The rationale behind choosing this path is the following:

  • We should avoid conditionals as much as we can on large pieces of logic, especially for core behaviors.
  • The current logic needs access to the formElement and panelElement. Keeping the onBlur handler would mean making these elements available in getInputProps, which would be complicated, and a breaking change.
  • It's easier to respond to direct events (e.g., reset, keydown, etc.) than consequential events, particularly because we know exactly what caused them. Tracing the origin on a blur isn't always possible.

Output

This PR fixes the issue, and solves other problems in the process:

  • When resetting the form, but you're not in detached mode, you no longer have a flash of the panel closing then reopening (see preview at the top).
  • Triggering Escape in detached mode on touch devices was still vulnerable to stale promises due to some outdated logic that was removed in this PR. This was an unlikely scenario anyway, since you don't use Escape on most mobile and tablet devices. Still, it could technically happen on some computers with touch screens.

Next steps

  • We might not have enough high-level tests, and it's difficult to cover all scenarios on all devices. In the future, it would be highly beneficial to be able to write high-level test suites and run them on multiple devices. We could probably use our BrowserStack account for this.
  • We should ensure our behavioral tests interact with the library like a user would do—users don't call blur() on an input, they click on things or press keys. They don't call reset() on a form, they click a reset button, etc. We do this irregularly in our tests, which causes false negatives and positives. We should do a pass to straighten this out.

fixes #971

@codesandbox-ci
Copy link

codesandbox-ci bot commented Jun 16, 2022

This pull request is automatically built and testable in CodeSandbox.

To see build info of the built libraries, click here or the icon next to each commit SHA.

Latest deployment of this branch, based on commit 22ff478:

Sandbox Source
clever-violet-hxw44i Issue #971

Comment on lines 43 to 47
if (onDetachedEscape && event.key === 'Escape') {
event.preventDefault();
onDetachedEscape();
return;
}
Copy link
Member Author

@sarahdayan sarahdayan Jun 17, 2022

Choose a reason for hiding this comment

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

Comment on lines -104 to -109
onDetachedEscape: isDetached
? () => {
autocomplete.setIsOpen(false);
setIsModalOpen(false);
}
: undefined,
Copy link
Member Author

Choose a reason for hiding this comment

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

This was no longer needed because the logic in onDetachedEscape has been covered by this since #556.

This is likely stale code from the first Autocomplete Touch (now Detached Mode) PR.

Comment on lines +203 to +205
await waitForElementToBeRemoved(
document.querySelector('.aa-DetachedOverlay')
).catch(() => {});
Copy link
Member Author

Choose a reason for hiding this comment

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

We want to check that the modal stays open, but the following assertions in waitFor will always pass since Testing Library just waits for this scenario to hold true. If the modal was later closing, the test would still pass, so we need to also make sure the element is never removed.

I couldn't find a cleaner way to do it, I'm open to suggestions.

@@ -1893,81 +1918,6 @@ describe('getInputProps', () => {
});
});

describe('onBlur', () => {
Copy link
Member Author

Choose a reason for hiding this comment

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

The test cases from this sub-suite are now split across multiple tests in the onKeyDown sub-suite and the getEnvironmentProps suite.

@sarahdayan sarahdayan marked this pull request as ready for review June 17, 2022 08:20
Copy link
Member

@dhayab dhayab left a comment

Choose a reason for hiding this comment

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

👏

Comment on lines +117 to +120
// If requests are still pending when the panel closes, they could reopen
// the panel once they resolve.
// We want to prevent any subsequent query from reopening the panel
// because it would result in an unsolicited UI behavior.
Copy link
Contributor

Choose a reason for hiding this comment

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

maybe here it should be qualified that the requests are still happening, but the handlers ignored, what do you think?

Copy link
Member Author

Choose a reason for hiding this comment

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

Since we call cancelAll in multiple places, do you think it would make more sense to comment the method directly?

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'll do this in a separate PR to keep this one focused.

Copy link
Member

@francoischalifour francoischalifour left a comment

Choose a reason for hiding this comment

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

Great refactoring! I suspect some more dead code because this behavior was the original nature of Autocomplete.

There's one behavioral change that I think we shouldn't do: hitting Tab on the input doesn't close the panel anymore. Can you think of a way to support this?

Also, a future possible improvement is to make the results keyboard navigable in Detached Mode. This is related to focus trap.

@sarahdayan
Copy link
Member Author

@francoischalifour Do you think hitting Tab should close the panel? This seems odd to me, I expect it to circle through focusable elements like it does right now, this seems more accessible.

@francoischalifour
Copy link
Member

@sarahdayan This is the behavior Google, YouTube, Amazon, etc. are doing. So I think this is desirable.

@sarahdayan
Copy link
Member Author

@francoischalifour It's the case on YouTube and Google indeed, but not on Amazon. Still, the combobox spec examples seem to abide by this: https://w3c.github.io/aria-practices/examples/combobox/combobox-select-only.html

Should we do this only in dropdown mode so we give ourselves the possibility to implement focus trap later on in detached mode?

@francoischalifour
Copy link
Member

Should we do this only in dropdown mode so we give ourselves the possibility to implement focus trap later on in detached mode?

Yes, only in dropdown mode.

@sarahdayan sarahdayan merged commit 3e387e6 into next Jun 21, 2022
@sarahdayan sarahdayan deleted the fix/detached-reset branch June 21, 2022 13:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Search modal closes when clicking the reset button
4 participants