Everything you (n)ever wanted to know about touch and pointer events
Everything you (n)ever wanted to know about touch and pointer events
Patrick H. Lauke / Last major changes: 2 May 2022
github.com/patrickhlauke/getting-touchy-presentation
"evergreen" expanded version of this presentation
(and branches for specific conferences)
my JavaScript sucks...
(but will hopefully convey the right concepts)
“how can I make my website
work on touch devices?”
you don't need touch events
browsers emulate regular mouse events
(mouseenter) > mouseover > mousemove* > mousedown >
(focus) > mouseup > click
* only a single “sacrificial” mousemove
event fired
(mouseenter) > mouseover > mousemove >
mousedown > (focus) > mouseup > click
mousemove > mousedown > mouseup > click
(mouseout) > (blur)
focus
/blur
only on focusable elements; subtle differences between browsers
Mobile/tablet touchscreen activation/tap event order
emulation works,
but is limiting/problematic
patrickhlauke.github.io/touch/tests/event-listener_show-delay.html
less of a problem in modern browsers, but for iOS < 9.3 or older Androids still relevant
“we need to go deeper...”
introduced by Apple, adopted in Chrome/Firefox/Opera
(and belatedly IE/Edge – more on that later)
touchstart
touchmove
touchend
touchcancel
touchstart > [touchmove]+ > touchend >
(mouseenter) > mouseover > mousemove > mousedown >
(focus) > mouseup > click
(mouse events only fired for single-finger tap)
touchstart > [touchmove]+ > touchend >
(mouseenter) > mouseover > mousemove > mousedown >
(focus) > mouseup > click
touchstart > [touchmove]+ > touchend >
mousemove > mousedown > mouseup > click
mouseout > (mouseleave) > (blur)
touchmove
events prevent mouse compatibility events after touchend
(not considered a "clean" tap)touchmove
events on activatable elements can lead to touchcancel
(in old Chrome/Browser versions)touchmove
focus
/ blur
and some mouse compatibility events (e.g. mouseenter
/ mouseleave
)blur
)
some browsers outright weird...
mouseover > mousemove > touchstart > touchend >
mousedown > mouseup > click
touchstart > mouseover > mousemove > mousedown >
touchend > mouseup > click
mouseover > mousemove > touchstart > (touchmove)+ > touchend >
mousedown > focus > mouseup > click
shouldn't affect your code, unless you're expecting a very specific sequence...
<interlude >
simple feature detection for touch events
/* feature detection for touch events */
if ('ontouchstart' in window) {
/* some clever stuff here */
}
/* older browsers have flaky support so more
hacky tests needed...use Modernizr.touch or similar */
/* conditional "touch OR mouse/keyboard" event binding */
if ('ontouchstart' in window) {
// set up event listeners for touch
...
} else {
// set up event listeners for mouse/keyboard
...
}
don't make it touch-exclusive
hybrid devices
touch + mouse + keyboard
@patrick_h_lauke showing a naive "touch or mouse" issue on Flickr
(which has since been fixed)
Chrome now returns window.TouchEvent
on non-touch devices
patrickhlauke.github.io/touch/tests/touch-feature-detect.html
even on "mobile" we can have multiple inputs...
HTC Wildfire with optical trackball sends mouse events...
Windows 10 "Continuum" (mobile device acting as desktop)
Samsung DeX (mobile device acting as desktop)
Android + mouse – like touch (mouse emulating touch)
touchstart > touchend > mouseover > mousemove >
mousedown > (focus) > mouseup > click
Android + Chrome 58 + mouse – like desktop
mouse events (+ pointer events) + click
Blackberry PlayBook 2.0 + mouse – like desktop
mouseover > mousedown > (mousemove)+ > mouseup > click
Blackberry Leap (BBOS 10.1) + mouse – like desktop
mouseover > mousedown > (mousemove)+ > mouseup > click
Android + keyboard – like desktop
focus / click / blur
iOS keyboard by default only works in same situations as on-screen keyboard
(e.g. text inputs, URL entry)
iOS13+ enable full keyboard access (equivalent to desktop keyboard support)
Settings > Accessibility > Keyboard > Full Keyboard Access
iOS + keyboard – keyboard, touch, pointer, and mouse events
focus > pointerover > pointerenter > touchstart > mouseover > mouseenter > gotpointercapture > pointerup > lostpointercapture > mouseout > mouseleave > pointerout > pointerleave > touchend > mousedown > mouseup > click
mobile Assistive Technologies
(e.g. screen readers on touchscreen devices)
iOS + VoiceOver (with/without keyboard) – similar to touch
focus / touchstart > touchend > (mouseenter) > mouseover > mousemove > mousedown > blur > mouseup > click
Android 4.3/Chrome + TalkBack – keyboard/mouse hybrid
focus / blur > mousedown > mouseup > click > focus
Android 6.1/Chrome + TalkBack – like touch
touchstart > touchend > mouseover > mouseenter > mousemove > mousedown > focus > mouseup > click
Android 6.1/Firefox + TalkBack – similar to touch
touchstart > mousedown > focus > touchend > mouseup > click
gesture*
events (for pinch-to-zoom, rotation)no way to detect these cases...
/* feature detection for touch events */
if ('ontouchstart' in window) {
/* browser supports touch events but user is
not necessarily using touch (exclusively) */
/* it could be a mobile, tablet, desktop, fridge ... */
}
touch or mouse or keyboard
touch and mouse and keyboard
what about
CSS4 Media Queries?
pointer
/ hover
relate to “primary” pointing deviceany-pointer
/ any-hover
relate to all pointing devices
/* the primary input is ... */
@media (pointer: fine) { /* a mouse, stylus, ... */ }
@media (pointer: coarse) { /* a touchscreen, ... */ }
@media (pointer: none) { /* not a pointer (e.g. d-pad) */ }
@media (hover: hover) { /* hover-capable */ }
@media (hover: none) { /* not hover-capable */ }
hover: on-demand
/ any-hover: on-demand
removed in recent drafts
pointer
/ hover
relate to “primary” pointing deviceany-pointer
/ any-hover
relate to all pointing devices
/* across all detected pointing devices at least one is ... */
@media (any-pointer: fine) { /* a mouse, stylus, ... */ }
@media (any-pointer: coarse) { /* a touchscreen, ... */ }
@media (any-pointer: none) { /* none of them are pointers */ }
@media (any-hover: hover) { /* hover-capable */ }
@media (any-hover: none) { /* none of them are hover-capable */ }
hover: on-demand
/ any-hover: on-demand
removed in recent drafts
if (window.matchMedia("(any-pointer:coarse)").matches) { ... }
/* listen for dynamic changes */
window.matchMedia("(any-pointer:coarse)")↵
.onchange = function(e) { ... }
window.matchMedia("(any-pointer:coarse)")↵
.addEventListener("change", function(e) { ... } , false }
window.matchMedia("(any-pointer:coarse)")↵
.removeEventListener("change", function(e) { ... } , false }
patrickhlauke.github.io/touch/pointer-hover-any-pointer-any-hover
primary input is unchanged (and Chrome/Android required full restart – Issue 442418)
/* Naive uses of Interaction Media Features */
@media (pointer: fine) {
/* primary input has fine pointer precision...
so make all buttons/controls small? */
}
@media (hover: hover) {
/* primary input has hover...so we can rely on it? */
}
/* pointer and hover only check "primary" input, but
what if there's a secondary input that lacks capabilities? */
/* Better uses of Interaction Media Features */
@media (any-pointer: coarse) {
/* at least one input has coarse pointer precision...
provide larger buttons/controls for touch */
}
/* test for *any* "least capable" inputs (primary or not) */
@media (any-hover: none) {
/* at least one input lacks hover capability...
don't rely on it or avoid altogether */
}
Limitation: [mediaqueries-4] any-hover can't be used to detect mixed hover and non-hover capable pointers. Also, pointer
/hover
/any-pointer
/any-hover
don't cover non-pointer inputs (e.g. keyboards) – always assume non-hover-capable inputs
detect which input the users is using right now...
if in doubt, offer a way to switch interfaces...
or just always use a touch-optimised interface
</ interlude >
touch events
vs
limitations/problems
patrickhlauke.github.io/touch/tests/event-listener_show-delay.html
less of a problem in modern browsers, but for iOS < 9.3 or older Androids still relevant
why the delay?
double-tap to zoom
(mostly anyway)
when does the delay happen?
touchstart > [touchmove]+ > touchend >
[300ms delay]
(mouseenter) > mouseover > mousemove > mousedown >
(focus) > mouseup > click
“how can we make it feel responsive like a native app?”
react to events fired before the 300ms delay...
touchstart
for an “immediate” control
(e.g. fire/jump button on a game)
touchend
for a control that fires after finger lifted
(but this can result in events firing after zoom/scroll)
don't make it touch-exclusive
/* DON'T DO THIS:
conditional "touch OR mouse/keyboard" event binding */
if ('ontouchstart' in window) {
// set up event listeners for touch
foo.addEventListener('touchend', ...);
...
} else {
// set up event listeners for mouse/keyboard
foo.addEventListener('click', ...);
...
}
/* DO THIS:
doubled-up "touch AND mouse/keyboard" event binding */
// set up event listeners for touch
foo.addEventListener('touchend', ...);
// set up event listeners for mouse/keyboard
foo.addEventListener('click', ...);
/* but this would fire our function twice for touch? */
patrickhlauke.github.io/touch/tests/event-listener_naive-event-doubling.html
/* DO THIS:
doubled-up "touch AND mouse/keyboard" event binding */
// set up event listeners for touch
foo.addEventListener('touchend', function(e) {
// prevent compatibility mouse events and click
e.preventDefault();
...
});
// set up event listeners for mouse/keyboard
foo.addEventListener('click', ...);
patrickhlauke.github.io/touch/tests/event-listener_naive-event-doubling-fixed.html
preventDefault()
kills
scrolling, pinch/zoom, etc
apply preventDefault()
carefully
(just on buttons/links, not entire page)
browsers working to remove double-tap to zoom delay
non-scalable/zoomable viewport and
"double-tap to zoom"
<meta name="viewport" content="user-scalable=no">
patrickhlauke.github.io/touch/tests/event-listener_user-scalable-no.html
<meta name="viewport" content="user-scalable=no">
patrickhlauke.github.io/touch/tests/event-listener_user-scalable-no.html
... content="minimum-scale=1, maximum-scale=1"
patrickhlauke.github.io/touch/tests/event-listener_minimum-maximum-scale.html
... content="minimum-scale=1, maximum-scale=1"
patrickhlauke.github.io/touch/tests/event-listener_minimum-maximum-scale.html
Changeset 191072 - Web pages with unscalable viewports shouldn't have a single tap delay
(from iOS 9.3 onwards)
what about accessibility?
Chrome: Settings > Accessibility > Force enable zoom
Opera: Settings > Force enable zoom
Firefox: Settings > Accessibility > Always enable zoom
Samsung Internet: Internet settings > Manual zoom
"Force enable zoom" reintroduces delay – a fair compromise?
patrickhlauke.github.io/touch/tests/event-listener_user-scalable-no.html
@thomasfuchs - Safari/iOS10beta3 ignores unscalable viewports
(no official Apple documentation about the change...but the 300ms delay is back)
"mobile optimised" viewport and
"double-tap to zoom"
Chrome 32+ / Android: content="width=device-width"
suppresses double-tap-to-zoom, still allows pinch zoom
Bug 941995 - Remove 300ms [...] on "responsive" pages
[RESOLVED FIXED]
Bug 150604 - Implement viewport-width-based fast-click heuristic
(from iOS 9.3 onwards)
if your code does rely on handling click
/ mouse events:
<meta name="viewport" content="width=device-width">
touch-action:manipulation
in CSS (part of Pointer Events)touchstart > [touchmove]+ > touchend >
(mouseenter) > mouseover >
mousemove* > mousedown > (focus) >
mouseup > click
* mouse event emulation fires only a single mousemove
too many touchmove
events prevent mouse compatibility events after touchend
doubling up handling of mousemove
and touchmove
var posX, posY;
function positionHandler(e) {
posX = e.clientX;
posY = e.clientY;
}
canvas.addEventListener('mousemove', positionHandler, false);
var posX, posY;
function positionHandler(e) {
posX = e.clientX;
posY = e.clientY;
}
canvas.addEventListener('mousemove', positionHandler, false);
canvas.addEventListener('touchmove', positionHandler, false);
/* but this won't work for touch... */
the anatomy of Touch Events
interface MouseEvent : UIEvent {
readonly attribute long screenX;
readonly attribute long screenY;
readonly attribute long clientX;
readonly attribute long clientY;
readonly attribute boolean ctrlKey;
readonly attribute boolean shiftKey;
readonly attribute boolean altKey;
readonly attribute boolean metaKey;
readonly attribute unsigned short button;
readonly attribute EventTarget relatedTarget;
// [DOM4] UI Events
readonly attribute unsigned short buttons;
};
www.w3.org/TR/DOM-Level-2-Events/events.html#Events-MouseEvent
partial interface MouseEvent {
readonly attribute double screenX;
readonly attribute double screenY;
readonly attribute double pageX;
readonly attribute double pageY;
readonly attribute double clientX;
readonly attribute double clientY;
readonly attribute double x;
readonly attribute double y;
readonly attribute double offsetX;
readonly attribute double offsetY;
};
www.w3.org/TR/cssom-view/#extensions-to-the-mouseevent-interface
interface TouchEvent : UIEvent {
readonly attribute TouchList touches;
readonly attribute TouchList targetTouches;
readonly attribute TouchList changedTouches;
readonly attribute boolean altKey;
readonly attribute boolean metaKey;
readonly attribute boolean ctrlKey;
readonly attribute boolean shiftKey;
};
interface Touch {
readonly attribute long identifier;
readonly attribute EventTarget target;
readonly attribute long screenX;
readonly attribute long screenY;
readonly attribute long clientX;
readonly attribute long clientY;
readonly attribute long pageX;
readonly attribute long pageY;
};
www.w3.org/TR/touch-events/#touch-interface
(this has been amended/extended in later implementations – more on that later)
TouchList
differencestouches
targetTouches
changedTouches
changedTouches
depending on event:
touchstart
, all new touch points that became activetouchmove
, all touch points that changed/moved since last eventtouchend
/ touchcancel
, touch points that were removedvar posX, posY;
function positionHandler(e) {
if ((e.clientX)&&(e.clientY)) {
posX = e.clientX; posY = e.clientY;
} else if (e.targetTouches) {
posX = e.targetTouches[0].clientX;
posY = e.targetTouches[0].clientY;
e.preventDefault();
}
}
canvas.addEventListener('mousemove', positionHandler, false );
canvas.addEventListener('touchmove', positionHandler, false );
TouchList
collections orderTouch
objects in a TouchList
can changetargetTouches[0]
not guaranteed to always be the same finger when dealing with multitouchidentifier
property for each Touch
to explicitly track a particular touch point / finger in multitouch(3) Naive mouse-driven fake slider (doesn't "capture")
Demo: without tricks, slider won't work when moving mouse outside
(4) Basic mouse-driven fake slider
Exercise/demo: expand mouse-driven code to also work with touch
tracking finger movement over time ... swipe gestures
/* Swipe detection from basic principles */
Δt = end.time - start.time;
Δx = end.x - start.x;
Δy = end.y - start.y;
if ((Δt > timeThreshold) || ((Δx + Δy) < distanceThreshold)) {
/* too slow or movement too small */
} else {
/* it's a swipe!
- use Δx and Δy to determine direction
- pythagoras √( Δx² + Δy² ) for distance
- distance/Δt to determine speed */
}
don't forget mouse/keyboard!
touchmove
fires...a lot!
do absolute minimum on each touchmove
(usually: store coordinates)
do heavy lifting (drawing etc.) separately, avoid queueing
(e.g. using setTimeout
and/or requestAnimationFrame
)
debounce / throttle events
Function.prototype.debounce = function (milliseconds, context) {
var baseFunction = this, timer = null, wait = milliseconds;
return function () {
var self = context || this, args = arguments;
function complete() {
baseFunction.apply(self, args);
timer = null;
}
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(complete, wait);
};
};
window.addEventListener('touchmove', myFunction.debounce(250));
Function.prototype.throttle = function (milliseconds, context) {
var baseFunction = this, lastEventTimestamp = null,
limit = milliseconds;
return function () {
var self = context || this, args = arguments, now = Date.now();
if (!lastEventTimestamp || now - lastEventTimestamp >= limit) {
lastEventTimestamp = now;
baseFunction.apply(self, args);
}
};
};
window.addEventListener('touchmove', myFunction.throttle(250));
why stop at a single point?
multitouch support
interface TouchEvent : UIEvent {
readonly attribute TouchList touches;
readonly attribute TouchList targetTouches;
readonly attribute TouchList changedTouches;
readonly attribute boolean altKey;
readonly attribute boolean metaKey;
readonly attribute boolean ctrlKey;
readonly attribute boolean shiftKey;
};
/* iterate over relevant TouchList */
for (i=0; i<e.targetTouches.length; i++) {
...
posX = e.targetTouches[i].clientX;
posY = e.targetTouches[i].clientY;
...
}
iOS/iPad preventDefault()
can't override 4-finger gestures
iOS7+ Safari/WebView preventDefault()
can't override edge swipes
Chrome/iOS (WebView) custom swipe, also can't be prevented
patrickhlauke.github.io/touch/tracker/multi-touch-tracker.html
multitouch gestures
/* iOS/Safari/WebView has gesture events for size/rotation,
not part of the W3C Touch Events spec */
/* gesturestart / gesturechange / gestureend */
foo.addEventListener('gesturechange', function(e) {
/* e.scale
e.rotation */
/* values can be plugged directly into
CSS transforms etc */
});
/* not supported in other browsers, but potentially
useful for iOS exclusive content (hybrid apps/webviews) */
iOS Developer Library - Safari Web Content Guide - Handling gesture events
/* Pinch/rotation detection from basic principles */
Δx = x2 - x1;
Δy = y2 - y1;
/* pythagoras √( Δx² + Δy² ) to calculate distance */
Δd = Math.sqrt( (Δx * Δx) + (Δy * Δy) );
/* trigonometry to calculate angle */
α = Math.atan2( Δy, Δx );
not all old/cheap devices/OSs support multitouch!
HTC Hero – Android 2.1
LG Optimus 2X – Android 2.3.7
ZTE Open – Firefox OS 1.1
Touch Events specification
grey areas...
do touch events fire during scroll/zoom?
different models and behaviours, particularly in old browsers
e.g. older versions of Chrome fire touchcancel
on scroll/zoom
YouTube: Google Developers - Mobile Web Thursdays: Performance on Mobile
not defined in spec (yet), but de facto
yes in most modern browsers
touchmove
and scrollnon-standard iOS quirks...
making generic elements clickable...
Historically (until end of 2016?), Apple suggested adding dummy onclick=""
handlers
iOS Developer Library - Safari Web Content Guide - Handling Events
dummy onclick=""
handlers are unnecessary (at least since iOS6)
Apple quietly dropped this from their documentation...
mouse + click
event bubbling on touch
(important when using event delegation)
click
bubbling in iOSbody
) has explicit mouse or click
handler (even if only empty function)document
) has cursor:pointer
Quirksmode: Mouse event bubbling in iOS
WebKit Bug 171105 - Normalizing touch events mouse compatibility event bubbling
real-world example: force bubbling by adding/removing noop mouseover
event handlers
if your code does rely on handling mouse events:
click
handlers to make arbitrary (non-interactive) elements react to mouse events (such as mouseover
, mousedown
, mouseup
...)click
)Touch Events extensions...
/* Extension to touch objects */
partial interface Touch {
readonly attribute float radiusX;
readonly attribute float radiusY;
readonly attribute float rotationAngle;
readonly attribute float force;
};
/* Touch Events – contact geometry */
partial interface Touch {
readonly attribute float radiusX;
readonly attribute float radiusY;
readonly attribute float rotationAngle;
readonly attribute float force;
};
/* Touch Events – force */
partial interface Touch {
readonly attribute float radiusX;
readonly attribute float radiusY;
readonly attribute float rotationAngle;
readonly attribute float force;
};
force
: value in range 0
– 1
. if no hardware support 0
(some devices, e.g. Nexus 10, "fake" force
based on radiusX
/ radiusY
)
patrickhlauke.github.io/touch/tracker/tracker-force-pressure.html
iPhone 6s with "3D Touch" supports force
...
(Safari and WKWebView, e.g. Chrome/iOS, but not UIWebView, e.g. Firefox/iOS)
patrickhlauke.github.io/touch/tracker/tracker-force-pressure.html
...while in non-3D Touch models (e.g. iPhone 5c) force == 0
could easily be emulated using pressure ("3D touch") information
bit retro – Apple abandoned it for long-press ("haptic touch") in iOS13
(incidentally, why does Apple keep making up fancy/confusing terms?)
webkitmouseforcewillbegin
, webkitmouseforcedown
, webkitmouseforceup
, webkitmouseforcechanged
webkitForce
propertyinterface Touch {
readonly attribute long identifier;
readonly attribute EventTarget target;
readonly attribute long float screenX;
readonly attribute long float screenY;
readonly attribute long float clientX;
readonly attribute long float clientY;
readonly attribute long float pageX;
readonly attribute long float pageY;
};
W3C Web Events WG - Touch Events errata
(early effort to clarify grey areas, simplify language used)
W3C Touch Events Level 2 (Editor's Draft)
(merges errata, touch events extensions, fractional touch coordinates)
up to IE9 (Win7 / WinPhone7.5) only mouse events
in IE10 Microsoft introduced Pointer Events
unifies mouse, touch and pen input into a single event model
...and some new inputs (though currently mapped to mouse)
not just some
“not invented here”
technology
submitted by Microsoft as W3C Candidate REC 09 May 2013
work now continues to enhance/expand Pointer Events...
about:config / dom.w3c_pointer_events.enabled
...what about Apple?
Microsoft submitted a proof of concept WebKit patch
Bug 105463 - Implement pointer events RESOLVED WONTFIX
Apple Pencil currently indistinguishable from touch in browser / JavaScript
(no tilt information, fires touch events)
Apple Pencil currently indistinguishable from touch in browser / JavaScript
(no tilt information, fires touch events)
enum TouchType {
"direct",
"stylus"
};
interface Touch {
...
readonly attribute float altitudeAngle;
readonly attribute float azimuthAngle;
readonly attribute TouchType touchType;
};
Safari/iOS 10.3 exposes tilt and distinguishes touch and stylus
Safari 13 Release Notes
(September 2019)
New Webkit features in Safari 13.1
To ensure the best experience, web developers can use feature detection and adopt Pointer Events...
the anatomy of Pointer Events
(sequence, event object, ...)
mousemove* >
pointerover > mouseover >
pointerenter > mouseenter >
pointerdown > mousedown >
focus
gotpointercapture >
pointermove > mousemove >
pointerup > mouseup >
lostpointercapture >
click >
pointerout > mouseout >
pointerleave > mouseleave
mouse events fired “inline” with pointer events
(for a primary pointer, e.g. first finger on screen)
Chrome, Edge (on mobile), IE11 (Windows Phone 8.1update1) support
both Touch Events and Pointer Events
w3c.github.io/pointerevents/#mapping-for-devices-that-do-not-support-hover
about:flags
in Microsoft Edge to turn on touch events on desktop
(e.g. touch-enabled laptops) – off by default for site compatibility
pointerover > pointerenter > pointerdown >
touchstart >
pointerup >
touchend >
mouseover > mouseenter > mousemove > mousedown >
focus >
mouseup >
click >
pointerout > pointerleave
Specific order of events is not defined in the spec in this case – will vary between browsers...
only problem if handling both pointer events and mouse events (which is nonsensical)
/* Pointer Events extend Mouse Events
vs Touch Events and their new objects / TouchLists / Touches */
interface PointerEvent : MouseEvent {
readonly attribute long pointerId;
readonly attribute long width;
readonly attribute long height;
readonly attribute float pressure;
readonly attribute float tangentialPressure; /* Level 2 */
readonly attribute long tiltX;
readonly attribute long tiltY;
readonly attribute long twist; /* Level 2 */
readonly attribute DOMString pointerType;
readonly attribute boolean isPrimary;
}
/* plus all MouseEvent attributes: clientX, clientY, etc */
handling mouse input is exactly the same as traditional mouse events
(only change pointer*
instead of mouse*
event names)
handling touch or stylus is also exactly the same as traditional mouse events
(mouse code can be adapted to touch/stylus quickly)
simple feature detection for pointer events
/* detecting pointer events support */
if (window.PointerEvent) {
/* some clever stuff here but this covers
touch, stylus, mouse, etc */
}
/* still listen to click for keyboard! */
"don't forget about keyboard users" note in Pointer Events spec
/* detect maximum number of touch points */
if (navigator.maxTouchPoints > 0) {
/* device with a touchscreen */
}
if (navigator.maxTouchPoints > 1) {
/* multitouch-capable device */
}
"you can detect a touchscreen"
...but user may still use other input mechanisms
Chromebook Pixel: navigator.maxTouchPoints == 16
patrickhlauke.github.io/touch/touchscreen-detection/
...the presence of a touchscreen still does not guarantee that a user will actually use the touchscreen...
do pointer events fire during scroll/zoom?
once a browser handles scrolling, it sends pointercancel
patrickhlauke.github.io/touch/gesture-touch/pointerevents.html
pointer events
vs
limitations/problems of mouse event emulation
patrickhlauke.github.io/touch/tests/event-listener.html
(IE11/WinPhone 8.1 Update no optimization forwidth=device-width
)
patrickhlauke.github.io/touch/tests/event-listener.html
(IE/Win8 has double-tap to zoom, so problem on desktop too)
patrickhlauke.github.io/touch/tests/event-listener.html
(Microsoft Edge/Win10 has double-tap to zoom, so problem on desktop too)
...
[300ms delay]
click
...
300ms delay just before click
event
“how can we make it feel responsive like a native app?”
we could try a similar approach to touch events...
pointerup
and click
listeners?preventDefault
?won't work: preventDefault()
stops mouse compatibility events, but click
is not considered mouse compatibility event
a more declarative approach
with touch-action
touch-action
what action should the browser handle?
touch-action: auto /* default */
touch-action: none
touch-action: pan-x
touch-action: pan-y
touch-action: manipulation /* pan/zoom */
touch-action: pan-x pan-y /* can be combined */
www.w3.org/TR/pointerevents/#the-touch-action-css-property
only determines default touch action, does not stop compatibility mouse events nor click
expanded set of values (useful for pull-to-refresh, carousels, etc)
touch-action: pan-left
touch-action: pan-right
touch-action: pan-up
touch-action: pan-down
touch-action: pan-left pan-down /* can be combined */
touch-action: pan-x pan-down /* can be combined */
w3c.github.io/pointerevents/#the-touch-action-css-property
new values only determine allowed pan direction at the start of the interaction;
once panning starts, user can also move in opposite direction along same axis
compat.spec.whatwg.org adds extra value pinch-zoom
although it's called "touch-action", it applies to any pointer type that pans/zooms
(e.g. Samsung Note + stylus, Chrome <58/Android + mouse)
w3c.github.io/pointerevents/#declaring-candidate-regions-for-default-touch-behaviors
touch-action:none
(suppress all default browser behaviour)
touch-action:none
kills scrolling, long-press, pinch/zoom
touch-action:manipulation
(suppress double-tap-to-zoom)
browsers working to remove double-tap to zoom delay
(when page not zoomable)
<meta name="viewport" content="user-scalable=no">
patrickhlauke.github.io/touch/tests/event-listener_user-scalable-no.html
"Allow zooming on all web content" reintroduces delay
patrickhlauke.github.io/touch/tests/event-listener_user-scalable-no.html
Windows 10 build 15007 on Mobile ignores unscalable viewports
no delay until user zooms for first time...
patrickhlauke.github.io/touch/particle/2
mousemove
/pointermove
fire, but browser scroll action takes over
patrickhlauke.github.io/touch/particle/3b
add touch-action:none
- and just listen to pointermove
...
no need for separate mouse or touch event listeners
/* touch events: separate handling */
foo.addEventListener('touchmove', ... , false);
foo.addEventListener('mousemove', ... , false);
/* pointer events: single listener for mouse, stylus, touch */
foo.addEventListener('pointermove', ... , false);
no need for separate mouse or touch code to get x / y coords
/* Reminder: Touch Events need separate code for x / y coords */
function positionHandler(e) {
if ((e.clientX)&&(e.clientY)) {
posX = e.clientX; posY = e.clientY;
} else if (e.targetTouches) {
posX = e.targetTouches[0].clientX;
posY = e.targetTouches[0].clientY;
...
}
}
canvas.addEventListener('mousemove', positionHandler, false );
canvas.addEventListener('touchmove', positionHandler, false );
/* Pointer Events extend Mouse Events */
foo.addEventListener('pointermove', function(e) {
...
posX = e.clientX;
posY = e.clientY;
...
}, false);
coded to only use mouse events
3D Rotator modified to use Pointer Events
minimal code changes, as Pointer Events extend mouse events
(4) Basic mouse-driven fake slider
Exercise/demo: convert mouse-driven code to use Pointer Events
but you can distinguish
mouse or touch or stylus
foo.addEventListener('pointermove', function(e) {
...
switch(e.pointerType) {
case 'mouse':
...
break;
case 'pen':
...
break;
case 'touch':
...
break;
default: /* future-proof */
}
...
} , false);
for single pointer interactions, check isPrimary
/* only do something if it's a primary pointer
(e.g. the first finger on the touchscreen) */
foo.addEventListener('pointermove', function(e) {
if (e.isPrimary) {
...
}
} , false);
what about multitouch?
/* PointerEvents don't have the handy TouchList objects,
so we have to replicate something similar... */
var points = [];
switch (e.type) {
case 'pointerdown':
/* add to the array */
break;
case 'pointermove':
/* update the relevant array entry's x and y */
break;
case 'pointerup':
case 'pointercancel':
/* remove the relevant array entry */
break;
}
patrickhlauke.github.io/touch/tracker/multi-touch-tracker-pointer.html
(note multiple isPrimary
pointers, per pointer type)
simultaneous use of inputs is hardware-dependent
(e.g. Surface "palm blocking" prevents concurrent touch/stylus/mouse, but not
touch/external mouse/external stylus)
extended capabilities
(if supported by hardware)
/* Pointer Events - pressure */
interface PointerEvent : MouseEvent {
readonly attribute long pointerId;
readonly attribute long width;
readonly attribute long height;
readonly attribute float pressure;
readonly attribute float tangentialPressure;
readonly attribute long tiltX;
readonly attribute long tiltY;
readonly attribute long twist;
readonly attribute DOMString pointerType;
readonly attribute boolean isPrimary;
}
pressure
: value in range 0
– 1
. if no hardware support,0.5
in active button state, 0
otherwise
pointermove
firespressure == 0
(non-active button state)pointerdown
/pointerup
to be safe/* Pointer Events - contact geometry */
interface PointerEvent : MouseEvent {
readonly attribute long pointerId;
readonly attribute long width;
readonly attribute long height;
readonly attribute float pressure;
readonly attribute float tangentialPressure;
readonly attribute long tiltX;
readonly attribute long tiltY;
readonly attribute long twist;
readonly attribute DOMString pointerType;
readonly attribute boolean isPrimary;
}
if hardware can't detect contact geometry, either 0
or "best guess"
(e.g. for mouse/stylus, return width
/ height
of 1
)
patrickhlauke.github.io/touch/tracker/multi-touch-tracker-pointer-hud.html
(Nokia Lumia 520: pressure
is 0.5
- active state - and width
/ height
is 0
)
/* Pointer Events - tilt */
interface PointerEvent : MouseEvent {
readonly attribute long pointerId;
readonly attribute long width;
readonly attribute long height;
readonly attribute float pressure;
readonly attribute float tangentialPressure;
readonly attribute long tiltX;
readonly attribute long tiltY;
readonly attribute long twist;
readonly attribute DOMString pointerType;
readonly attribute boolean isPrimary;
}
tiltX
/tiltY
: value in degrees -90
– 90
.
returns 0
if hardware does not support tilt
pointermove
fires if any property changes,
not just x/y position
(width
, height
, tiltX
, tiltY
, pressure
)
pointer capture
(implicit vs explicit)
/* events related to pointer capture */
foo.addEventListener("gotpointercapture", ... , false)
foo.addEventListener("lostpointercapture", ... , false)
/* methods related to pointer capture */
element.setPointerCapture(pointerId)
element.releasePointerCapture(pointerId)
if (element.hasPointerCapture(pointerId)) { ... }
/* example of how to capture a pointer explicitly */
element.addEventListener('pointerdown', function(e) {
this.setPointerCapture(e.pointerId);
}, false }
/* capture automatically released on pointerup / pointercancel
or explicitly with releasePointerCapture() */
setPointerCapture
can simplify mouse input
e.g. draggable interfaces/sliders, use instead of
"bind mousemove
to document
/body
on mousedown
"
(6) Basic pointer events-driven fake slider
Exercise/demo: simplify code to use pointer capture
pointer events as the future?
transitional event handling
(until all browsers support pointer events)
/* cover all cases (hat-tip Stu Cox) */
if (window.PointerEvent) {
/* bind to Pointer Events: pointerdown, pointerup, etc */
} else {
/* bind to mouse events: mousedown, mouseup, etc */
if ('ontouchstart' in window) {
/* bind to Touch Events: touchstart, touchend, etc */
}
}
/* bind to keyboard / click */
polyfills for pointer events
(code for tomorrow, today)
/* adding PEP from the jQuery CDN */
<script src="https://code.jquery.com/pep/0.4.3/pep.js"></script>
/* doesn't parse CSS, needs custom touch-action attribute
(could inject this via custom JavaScript on load/mutation?) */
<button touch-action="none">...</div>
/* navigator.maxTouchPoints always returns 0
(real value would require hardware integration) */
pointerover > pointerenter > pointerdown >
touchstart >
pointerup > pointerout > pointerleave >
touchend >
mouseover > mouseenter > mousemove >
mousedown > mouseup >
click
essentially, relevant Pointer Events before touchstart
and touchend
3D Rotator modified to use Pointer Events
minimal code changes, as Pointer Events extend mouse events
(7) Basic pointer events-driven fake slider with setPointerCapture
Exercise/demo: make slider work in non-Pointer-Events browsers
utility libraries
(because life is too short to hand-code gesture support)
/* Hammer's high-level events example */
var element = document.getElementById('test_el');
var hammertime = new Hammer(element);
hammertime.on("swipe",
function(event) {
/* handle horizontal swipes */
});
debugging/testing
nothing beats having real devices...
Chrome DevTools / Device Mode & Mobile Emulation
correctly emulates touch and pointer events
Chrome DevTools / Device Mode & Mobile Emulation
correctly emulates pointer events (with pointerType=="touch"
)
emulate a touch interface (even without mobile emulation)
Firefox Developer Tools > Responsive Design Mode
correctly emulates touch events
no touch emulation in Safari "Responsive Design Mode"
further reading...