react-spectrum icon indicating copy to clipboard operation
react-spectrum copied to clipboard

Press seems to be double triggered for overlaying buttons on chrome simulated touch

Open djfarly opened this issue 5 years ago β€’ 14 comments

πŸ› Bug Report

Given a dialog with a close button exactly overlaying the open button (not unusual for burger-menu-style navigation), clicking the open button with chrome simulated touch, triggers the close callback of the close button directly after the open callback (in ~90% of the cases?! πŸ˜“) resulting in the dialog closing immediately. (See Code Sample)

(open and close refer to stately's useOverlayTriggerState setters)

πŸ€” Expected Behavior

Clicking the open button with simulated touch in chrome only triggers open.

😯 Current Behavior

Clicking the open button with simulated touch in chrome triggers open, then close immediately after.

πŸ’ Possible Solution

🧐

πŸ”¦ Context

We're building a "Drawer" component / mobile navigation where the open and close button are in the same screen location.

It's probably not the worst thing ever. I could not reproduce this with any form of real click or real touch input. Also Firefox simulated touch seems to work fine.

It still is kinda frustrating since chrome is our main development environment.

πŸ’» Code Sample

Here is a code sandbox showcasing the behavior.

https://codesandbox.io/s/ckmr5?file=/src/App.js

https://ckmr5.csb.app/

  • Open the sandbox in chrome
  • open devtools
  • go to any touch simulated device preview
  • press menu

🌍 Your Environment

Software Version(s)
react-aria 3.1.1
react-stately 3.0.1
Browser Chrome Version 87.0.4280.63
Operating System macOS 10.15.7 (19H15)

🧒 Your Company/Team

πŸ•· Tracking Issue (optional)

djfarly avatar Nov 17 '20 12:11 djfarly

Also note, that this only happens the first few times using the button. After a clicking for a while it magically ✨ starts working as intended - adding to my confusion.

djfarly avatar Nov 17 '20 12:11 djfarly

Could you disable one of the buttons based on which one should be interacted with? or just not render it? <Button onPress={open} isDisabled={isOpen}>MENU</Button> or {!isOpen && <Button onPress={open}>MENU</Button>}

I've also updated the sandbox to display all the events: https://codesandbox.io/s/nervous-volhard-rut48?file=/src/Button.js

which gives us this in chrome

MENU onPointerDown
MENU onTouchStart
MENU onPointerUp
MENU onTouchEnd
CLOSE onClick

which now that i'm looking at it, looks like my above suggestion wouldn't work, though i didn't try it

snowystinger avatar Nov 17 '20 18:11 snowystinger

So… simulated chrome touches add an additional click event a tick(? - since the overlay had time to render) later… that seems super strange. πŸ˜– To circumvent the click event hitting the close button we could try to render the close button after a short timeout. That doesn't quite sound like a reasonable solution for something strange in one browsers dev mode. πŸ˜„

To be honest I don't know how big of a deal this is, still it is very intriguing to me. Im not familiar how react-aria handles all these events. Maybe it's a chrome bug even?

thanks for looking at it! πŸ₯³

djfarly avatar Nov 17 '20 18:11 djfarly

Yes, I believe this to be a browser bug, I've recreated it here without react or react-spectrum https://codesandbox.io/s/inspiring-lehmann-i1hjf?file=/index.html In Chrome click is fired on the new button, in Safari and FF it is not, I'll log a bug with them and post a link here

snowystinger avatar Nov 17 '20 20:11 snowystinger

Here's the bug link https://bugs.chromium.org/p/chromium/issues/detail?id=1150073

snowystinger avatar Nov 17 '20 20:11 snowystinger

tyvm @snowystinger - let's see how that goes. :)

djfarly avatar Nov 18 '20 10:11 djfarly

Just FYI I've run into this issue on a real device, my Pixel 2. My use case has an overlay that just happens to be on top of some links. The overlay is removed when a button is clicked. The links underneath the overlay fire an unexpected click event. This is a problem in projects that use a client side router.

For now I've worked around this issue by using the deprecated onClick prop instead of onPress. I'm not really sure how that's fixed the problem but it seems to be working πŸ€·β€β™‚οΈ .

https://codepen.io/j-arens/full/ExgjrdP

josharens avatar Dec 01 '20 16:12 josharens

Hey, guys!

I really appreciate things you’re doing with react-aria!

Are there any plans on providing any workaround or fix inside react-aria? Seems like it add extra mess with setTimeout or any other hacks inside E2E tests :( Also spend around couple days to figure out why it does not work properly

Fixing with onClick also adds extra problems for developers to write code with react-aria.

I understand that it's a Chromium issue but it seems like react-aria tries to encapsulate different browser-specific behaviour inside

Thank you ☺️

LosYear avatar Mar 16 '23 12:03 LosYear

The only way we've seen to prevent the compat click event fired is to preventDefault on touch start. Unfortunately, I think this also comes with scroll prevention, so we don't have a good way to encapsulate it in usePress at the moment. We're open to suggestions or PRs to address it though.

However, what you can do, is attach your own onTouchStart (make sure it's non-passive) and preventDefault on the event See https://codesandbox.io/s/jovial-hooks-bg82y5?file=/src/Button.js for how to do that and noticed the compat 'click' is not fired.

snowystinger avatar Mar 16 '23 19:03 snowystinger

Extra history on us trying to do exactly this https://github.com/adobe/react-spectrum/pull/2942

snowystinger avatar Mar 17 '23 01:03 snowystinger

Is there any progress on this topic? We are running into the same problems using react-aria-components .

I set up a minimal example only using RACs: https://stackblitz.com/edit/vitejs-vite-guzxrh?file=src%2FApp.jsx

Opening the CombobBox and then selecting either Aardvark or Dog also triggers the link behind the options. The actual touch position seems to matter. If unable to reproduce, try again with a different touch position.

This problem affects both actual touch devices (Android tablets using Chrome) and emulated touch.

How would the workaround described in https://github.com/adobe/react-spectrum/issues/1279#issuecomment-1472634908 apply here as the RACs handle the events internally?

rodogir avatar May 15 '24 13:05 rodogir

as a workaround, I added a 200ms await on the first line of the onPress handler. So hacky, so bad. But nothing else I can do:

<Button
    onPress={async () => {
        await sleep(200)
        setIsVisible(false)
    }}
>
    <X size={16} weight="bold" />
</Button>

this is my hacky sleep function:

export function sleep(duration: number) {
    return new Promise(resolve => setTimeout(resolve, duration))
}

IonelLupu avatar Jun 05 '24 14:06 IonelLupu

Can using onClick be a valid work around for this issue? I know it says its deprecated, but I have found that it doesn't seem to have the same issues as onPress.

My worry is that this also gives up all the niceities that react-aria provides

cevr avatar Jun 12 '24 18:06 cevr

Facing the same issue with a modal that has a Button placed exactly over a Link, where the Link was also triggered by the button onPress, thus navigating when I just wanted to confirm my cookie choices.

Adding await new Promise((resolve) => setTimeout(resolve, 0)); to the very beginning of the onPress handler solved the issue. I don't get why it works, but it does. Maybe this helps somebody that knows the library better when debugging this.

pnicolli avatar Jun 20 '24 13:06 pnicolli

This is a really bad issue for me in my projects. When a Button is pressed via touch, the touch event is triggered a second time. Let's say the button triggers a view change with another button, the button is also triggered by the touch event. This seems to work though, probably because the handleStart() is triggered after view change in the next frame:

onPress={async(e) => e.pointerType === "touch" ? setTimeout(handleStart, 0) : handleStart()}

On a native HTML button this also works: onTouchStart={(e) => { e.preventDefault(); handleStart(); }}

Please pass the native event to the onPress handler to solve this issue

s-com-borchardtj avatar Sep 25 '24 08:09 s-com-borchardtj

I think this should already be fixed by #7026 which will be in the next release. Can you try the nightly version?

devongovett avatar Sep 25 '24 13:09 devongovett

@devongovett can you confirm if this has been fixed? I see this behaviour on Android Chrome when I press a button in a container that collapses and immediately after the next button in the dom is pressed (not always, just sometimes). The button that is triggered afterwards is type submit

george-roussos avatar Feb 21 '25 16:02 george-roussos

@george-roussos Can you reproduce it in a codesandbox either on the latest release or in the latest nightly version? Either I'm doing the original codesandbox incorrectly, or the bug in chrome:

Here's the bug link https://bugs.chromium.org/p/chromium/issues/detail?id=1150073

is fixed in my version.

snowystinger avatar Feb 24 '25 04:02 snowystinger

@george-roussos Can you reproduce it in a codesandbox either on the latest release or in the latest nightly version? Either I'm doing the original codesandbox incorrectly, or the bug in chrome:

Here's the bug link https://bugs.chromium.org/p/chromium/issues/detail?id=1150073

is fixed in my version.

I tried but (of course law of demo) I haven't been able to. I will try again. But locally in my flow it happens in a multistep form, where one section a time is shown as expanded and on button press

  1. the first flow collapses
  2. it is brought to the top with window.scroll
  3. the second one opens
  4. the button press for the second one triggers

What is weird is that the second press does not seem "complete", for example when pressing it I would expect form errors to show up (the section has checkboxes and if they are not checked, errors show). The errors do not show, but I get the message Chrome shows "please tick this box if you wish to continue).

I also see similar behaviour when I click a button that opens a modal; in that case, a button inside the modal is selected (but not clicked/pressed)

george-roussos avatar Feb 24 '25 12:02 george-roussos