So, I have a problem. I want to respond to a user pressing the mouse button (on desktop) or touching a div (on mobile). I'm trying to be compatile with evergreen browsers. This is what I tried so far:
listen only to mouseDown event. This works on desktop but doesn't work in mobile if the user is dragging. I want the handler to be called as soon as the user touches the screen, no matter if they're moving their finger in the process.
listen only to touchStart event. This works on mobile and desktop, except for Edge and Safari desktop, which don't support touch events.
listen to both, then preventDefault. This causes a double handler call on Chrome mobile. It seems that touch events are passive to allow uninterrupted scrolling on mobile Chrome, so preventDefualt has no effect on them . What I get is a warning message saying "[Intervention] Unable to preventDefault inside passive event listener due to target being treated as passive. See https://www.chromestatus.com/features/5093566007214080" in the console, preventDefault is ignored and my event is called twice.
Obviously this can be solved by sniffing touch events, but the net is full of self-righteous rants on how one has to be device-agnostic and that it's dangerous to detect touch events before the user interacted.
So I guess that the question is: is there a way to do what I want to do without sniffing for touch events?
Below my sample React code:
function handler(e) {
console.log('handler called')
e.preventDefault()
}
export default function MyElement() {
return (
<div
onMouseDown={handler}
onTouchStart={handler}
>
Hello
</div>
)
}
It turn out it's not yet possible in React. One workaround is set a flag the first time touchStart it's received.
touchHandler = () => {
this.useTouch = true
this.realHandler()
}
mouseHandler = () => {
if (this.useTouch) return
this.realHandler()
}
With the caveat that the first touchStart can be lost in case of dragging.
Quite disappointing.
Related
I've used onclick events in my website. But when I open it in google chromes' developer mode's mobile view, nothing happens on touch on the elements which work on click with mouse. So my question is:
Do I have to also add ontouch events along with onclick events, or onClick event work on touch on all touch-screen devices?
P.S: You can see all of my codes here: https://github.com/SycoScientistRecords/sycoscientistrecords.github.io/
Or at the live website: http://sycoscientistrecords.github.io
And no I haven't tested the website on real phone.
onclick works fine on touchscreens; I've used it several times and have never had any problem.
You could consider using onmousedown instead of onclick. Or use jQuery to detect taps.
I found this detailed writeup at MDN very helpful. In particular:
the browser may fire both touch events and mouse events in response to the same user input [emphasis mine]
and
the element's touch event handlers should call preventDefault() and no additional mouse events will be dispatched
So, your touchstart or touchend listener can call evt.preventDefault() and your mousedown / mouseup listeners won't fire because they come later in the chain.
In Angular, I was able to detect whether I'd clicked a button using my mouse or my laptop's touchscreen, by changing (click)="doSomething()" to (mouseup)="doSomething(false)" (touchend)="doSomething(true); $event.preventDefault()". The method is called with true for touch events and false for mouse events.
onclick may not work on touch devices, I had this issue and the event ontouchstart sorts it.
if you use ontouchstart and onclick watch that you don't trigger the event twice.
this is another post related
onClick not working on mobile (touch)
New browsers have a pointerType which determines if the onClick is made by a mouse or via a touch. If you just want make adjustments in user behavior based on the input, using pointerType is the safest way.
if you are using jQuery:
$(selector).click(e => {
if (e.pointerType === "mouse") {} // mouse event
else {} // touch event
});
if you are using vanilla JS:
element.addEventListener('click', e => {
if (e.pointerType === "mouse") {} // mouse event
else {} // touch event
});
If you are using React, the event is wrapped around a synthetic event. To access the pointerType, you have to use the nativeEvent of the react event. Here is what you need to consider (especially if you are using Typescript). If the event is triggered by a mouse, the native event is an instance of MouseEvent which does not have pointerType, so, first you need to check the type of native event which will also take care of the typing problems in TS
<div
onClick={e => {
if (e.nativeEvent instanceof PointerEvent && e.nativeEvent.pointerType === 'touch') {} // Touch Event
else {} // Mouse Event
}}
></div>
Pro tip: If you want to test the touch event in development, use Chrome following this. Note that Safari has a responsive mode which simulates the framework of iPhones and iPads. However, Safari always registers a mouse event even when you are in responsive design mode and have selected an iPhone or iPad.
In iOS 7 Safari there are now two ways to navigate back/forward -- using the traditional back/forward button arrows at the bottom or by swiping from the edge of the screen. I'm using an animation to transition between pages in my ajax app, but I don't want to fire that transition if users are navigating via the edge swipe, because that is an animation itself.
However, the popstate event objects appear to be identical for both types of navigation -- is there any way to differentiate between these two types of user navigations so we can respond accordingly?
UPDATE: I was able to use (what appears to be) a bug in iOS7 Safari to detect correctly the edge swipe vs. back button tap. The bug is that the touchend event is not triggered (until the next touch event) when using the edge swipe (but touchstart and touchmove are). So I set a shouldAnimate flag and disable it on touchmove -- then if the flag is disabled and the popstate occurs, I know it's an edge swipe.
It's correct 99% of the time -- the only time where it could potentially fail is when a user edge-swipes partially and but then lets go and lets the current page snap back into place (at which point my flag would still be disabled) and then taps the back button (which fires no touch events). To handle that last [edge] case I set a timer on touchmove to re-enable the flag after 50ms.
Yes it's "dirty" but for now it gets me what I want in almost every case so I'm ok with it -- until Apple fixes the bug, but hopefully they'll also provide an indicator in the popstate event object that tells us what kind of navigation it is.
You can track edge drag navigation by monitoring the touch events. If the user starts dragging within a certain threshold of the edge of their screen, it will trigger an edge drag navigation transition.
I wrote an extended explanation of how to monitor and act on this using React code here: https://gist.github.com/MartijnHols/709965559cbdb6b241c12e5866941e69. The essential detection part can be achieved in regular JavaScript, like so:
window.isEdgeDragNavigating = false
const IOS_EDGE_DRAG_NAVIGATION_THRESHOLD = 25
let timer
const handleTouchStart = (e) => {
if (
e.touches[0].pageX > IOS_EDGE_DRAG_NAVIGATION_THRESHOLD &&
e.touches[0].pageX <
window.innerWidth - IOS_EDGE_DRAG_NAVIGATION_THRESHOLD
) {
return
}
window.isEdgeDragNavigating = true
if (timer) {
clearTimeout(timer)
}
}
const handleTouchEnd = () => {
timer = setTimeout(() => window.isEdgeDragNavigating = false, 200)
}
document.addEventListener('touchstart', handleTouchStart)
document.addEventListener('touchend', handleTouchEnd)
Short and sad answer: No. This back/forward-swipes are not propagated to the actual page but happen on an OS-level.
I currently have this for my touch events:
if( 'ontouchstart' in document.body) {
usevkeys = true;
canvas.addEventListener("touchstart",function(e) {evt.call(this,e);},false);
canvas.addEventListener("touchend",function(e) {evt.call(this,e);},false);
canvas.addEventListener("touchmove",function(e) {evt.call(this,e);},false);
}
else {
canvas.addEventListener("mousemove",function(e) {evt.call(this,e);},false);
canvas.addEventListener("click",function(e) {evt.call(this,e);},false);
}
This works fine on my laptop, and on my phone. However, I have to wonder, how would this react in an environment that has both a touchscreen and a normal mouse? Does the mouse trigger touch events, like the phone triggers mousemove events?
What can I do to make sure it works?
a touchstart triggers the click event as soon as touchend has fired (-> if it hasn't been canceled). So you should just remove the else clause and should be fine!
Microsoft went a different way with their touch events in IE10 to harmonize all pointer-devices into Pointer and gesture events where you can check if it has been fired by a mouse, pen or a finger - or perhaps a kinect-style device in the future.
I'm making an Javascript web app and I can't for the life of me get the touchstart event to fire. I get the touchmove and touchend events no problem. This is a problem because as I see it the best way to distinguish between a tap and a scrolling motion is to zero a counter on the touchstart event, update it at touchmove and then compare it at touchend. I'm doing this so I can do some action at the end of tap but not a scroll. For instance, it would be very confusing if a page opened for an item in a listed after you finished scrolling down that list, but it would be nice to be able to tap on an item to open its page.
This is what I have:
// FIXME: this doesn't seem to ever fire
el.addEventListener('touchstart', function(e) {
// make sure that at the start of every touch we're not considered to be moving
alert("Touch starting");
app.__touchMoving = 0;
}, false);
el.addEventListener('touchmove', function(e) {
app.__touchMoving++;
}, false);
el.addEventListener('touchend', function(e) {
alert("Touch ended. We moved beforehand this many times: " + app.__touchMoving);
// if we are moving
if (app.__touchMoving > 0) {
// stop, since we're dragging, not tapping
return false;
}
// else we're no longer moving, so it was a tap
}
I never see the touchstart alert. If I scroll the touchend will fire and app__touchMoving will have some sort of decent value. On a side note, I've noticed that sometimes the touchend will seem to fire multiple times.
Am I missing something basic here? Plenty of people say that this should work just fine on Android (and iPhone) yet the first listener never seems to fire.
Update: I should mention that I've been testing on a Samsung Galaxy S running Android 2.1.
I don't know if u can use it: iScroll
I am trying to implement a scrollable element for a mobile app and it looks like you must use preventDefault on the initial touchStart event, otherwise the browser will not fire all the touchMove events (presumably for performance reasons).
So it would seem that if I want to allow touch scrolling on an overflown element, the user will not be able to scroll the page as per usual when touching that element. This is problematic if the overflown element takes up a large portion of the viewport.
Is their a workaround for this?
Take a look at this library
http://api.mutado.com/mobile/mtdtouch/js/
The "core" javascript includes a base UIComponent optimized for touch events (webkit).
The UI.Scroll component in example manage the "prevent default issue" for you.
Try to subclass the UIComponent and implement your own events handler like this
$MTD.YourOwnComponent = $.klass( $MTD.UIComponent, {
touchesBegan: function( e ) {
// your stuff
},
touchesMoved: function( e ) {
// your stuff
},
touchesEnded: function( e ) {
// your stuff
}
});
Hope this helps.
Here's a simple workaround: drop the touchstart handler. You can reconstruct most of what's going on with just the touchmove, touchend, and touchleaving handlers.
In the browser I tested with (Chrome), scrolling happens as long as you don't have a touchstart handler; it doesn't care about the other ones. As long as you aren't actively calling ev.preventDefault in the touchmove handlers, scrolling works.
Assuming what you want to do will work fine despite only finding out about a touch when the finger starts moving, instead of when the finger initially lands, this workaround should work acceptably.
... And also I'm assuming other browsers use the same logic as Chrome.