Neutralize FastClick
Categories
(Web Compatibility :: Interventions, enhancement, P2)
Tracking
(Not tracked)
People
(Reporter: karlcow, Unassigned, NeedInfo)
References
()
Details
Updated•7 years ago
|
Comment 1•2 years ago
|
||
I'm revisiting this, as we still fairly regularly discover sites that are using FastClick. I don't think there's a tenable way to detect FastClick on all sites automatically, but given that the list of known sites with this breakage seems manageable, I think it should be viable to maintain a JS injection like this on the known sites for now:
(function() {
const proto = CSS2Properties.prototype;
const descriptor = Object.getOwnPropertyDescriptor(proto, "touchAction");
const { get } = descriptor;
descriptor.get = function() {
try {
throw Error()
} catch (e) {
if (e.stack?.includes("notNeeded")) {
return "none";
}
}
return get.call(this);
};
Object.defineProperty(proto, "touchAction", descriptor);
})();
This is based on the code seen at https://github.com/ftlabs/fastclick/blob/main/lib/fastclick.js#L810.
It might be possible to simplify it, especially on select sites (such as simply having msTouchAction's getter always return "none"
), but this is the safest more generalized method I can see which makes sure that if a site otherwise checks the values of touchAction
, msTouchAction
, or the other things that the notNeeded
function of FastClick checks, that we aren't likely to step on their toes as well.
It's likely that there will be sites which uglify the code further, breaking the check for notNeeded
, and if I find a site like that I'll probably just use a simplified version on those sites, as there are limits to what we can do to fix websites' bugs for them.
Updated•2 years ago
|
Updated•2 years ago
|
Updated•2 years ago
|
Updated•2 years ago
|
Updated•2 years ago
|
Comment 2•2 years ago
•
|
||
This was shipped in bug1805409.
A complete list of sites that received the sitepatch: https://github.com/webcompat/web-bugs/issues?q=is%3Aissue+label%3Atype-fastclick+label%3Asitepatch-applied+
Comment 3•2 years ago
|
||
For the record, the reason FastClick breaks selects on Fenix is because it preventDefault
s the mousedown
and touchend
events when you tap on a select box. It doesn't do this on Chrome/Safari because there are user-agent string sniffs specifically to avoid doing so, so sadly the only way to fix this bug is to override the UA string on affected sites so FastClick passes its notNeeded
test.
Comment 4•2 years ago
|
||
Actually, there is a possibility here for us to maybe work around FastClick, but it would require Gecko to be crafty. While FastClick cancels the touchend
event when a user taps on a select
element, it also creates a mousedown
event on the same select
at the same coordinates, and sets forwardedTouchEvent=true
on the JS event object just before dispatching it. So I wonder if we could do something like this during the dispatchEvent
, specifically for mousedown
events (and only if touch events are enabled):
- check if the event is dispatched on a
select
, hasforwardedTouchEvent==true
and is untrusted. - check if the same
select
isn't open, because it has had atouchend
event canceled withpreventDefault
just now, sharing the same client/screen coordinates (copied fromchangedTouches[0]
, which is what FastClick is doing) - go ahead and open the select box (ie, trigger the default, just-prevented action).
@Emilio, do you think something like this might be remotely feasible? We know there is a long tail of sites so affected by FastClick, since the list we're aware of keeps growing and rarely shrinks. If a more general fix like this might work, then I think it's worth considering (otherwise the best we can do is wait for people to discover the broken sites and make site-specific interventions).
Comment 5•2 years ago
|
||
(In reply to Thomas Wisniewski [:twisniewski] from comment #4)
Actually, there is a possibility here for us to maybe work around FastClick, but it would require Gecko to be crafty. While FastClick cancels the
touchend
event when a user taps on aselect
element, it also creates amousedown
event on the sameselect
at the same coordinates, and setsforwardedTouchEvent=true
on the JS event object just before dispatching it. So I wonder if we could do something like this during thedispatchEvent
, specifically formousedown
events (and only if touch events are enabled):
- check if the event is dispatched on a
select
, hasforwardedTouchEvent==true
and is untrusted.
The <select>
event listener is here. What we do is listening to the mousedown triggered by touchdown.
We could consider changing or hacking around that code, seems doable... I assume forwardedTouchEvent
would be the event.detail
? I think we'd need to be careful to avoid triggering getters etc on the object, but assuming there's a mechanism for getting that without side effects it seems doable, would be a matter of hacking around that code and see what fastclick is doing...
Comment 6•2 years ago
|
||
Sure, this is what FastClick is basically doing (one source is here):
e.prototype.sendClick = function (e, t) {
var n,
r;
if (document.activeElement && document.activeElement !== e) {
document.activeElement.blur()
}
r = t.changedTouches[0];
n = document.createEvent('MouseEvents');
n.initMouseEvent(
this.determineEventType(e),
true,
true,
window,
1,
r.screenX,
r.screenY,
r.clientX,
r.clientY,
false,
false,
false,
false,
0,
null
);
n.forwardedTouchEvent = true;
e.dispatchEvent(n)
};
Where sendClick
is called with e
being the select
, and r
being a touchend
event. It's called during the touchend handler, which also prevents the default:
e.prototype.onTouchEnd = function (e) {
// snip a bunch of code
if (!this.needsClick(l)) { // where `l` is of course the select element
e.preventDefault();
this.sendClick(l, e)
}
And for completeness, this is needsClick
:
e.prototype.needsClick = function (e) {
switch (e.nodeName.toLowerCase()) {
case 'button':
case 'select':
case 'textarea':
if (e.disabled) {
return true
}
break;
case 'input':
if (r && e.type === 'file' || e.disabled) {
return true
}
break;
case 'label':
case 'iframe':
case 'video':
return true
}
return /\bneedsclick\b/.test(e.className)
};
So basically it prevents the touchend
, blurs the select
, and fires a mousedown
on it instead in lieu of the touchend
.
Comment 7•2 years ago
|
||
Oh, so forwardedTouchEvent is on the object itself. Does something like this work?
diff --git a/layout/forms/HTMLSelectEventListener.cpp b/layout/forms/HTMLSelectEventListener.cpp
index dffabe1bc7451..72b6159f38adc 100644
--- a/layout/forms/HTMLSelectEventListener.cpp
+++ b/layout/forms/HTMLSelectEventListener.cpp
@@ -402,8 +402,32 @@ static void FireDropDownEvent(HTMLSelectElement* aElement, bool aShow,
CanBubble::eYes, Cancelable::eNo);
}
+static bool IsFastClickMouseDownEvent(dom::Event* aMouseEvent) {
+ if (aMouseEvent->IsTrusted()) {
+ return false;
+ }
+
+ JS::Rooted<JSObject*> wrapper(aMouseEvent->GetWrapper());
+ if (!wrapper) {
+ return false;
+ }
+
+ AutoJSAPI jsapi;
+ if (!jsapi.Init(aMouseEvent->GetParentObject())) {
+ return false;
+ }
+
+ JSContext* cx = jsapi.cx();
+ bool found = false;
+ return JS_HasProperty(cx, wrapper, "forwardedTouchEvent", &found) && found;
+}
+
nsresult HTMLSelectEventListener::MouseDown(dom::Event* aMouseEvent) {
- NS_ASSERTION(aMouseEvent != nullptr, "aMouseEvent is null.");
+ NS_ASSERTION(aMouseEvent, "aMouseEvent is null.");
+
+ if (mIsCombobox && IsFastClickMouseDownEvent(aMouseEvent)) {
+ // Hackety hack?
+ }
MouseEvent* mouseEvent = aMouseEvent->AsMouseEvent();
NS_ENSURE_TRUE(mouseEvent, NS_ERROR_FAILURE);
Comment 8•2 years ago
|
||
I think so, though of course it might be safer to only do the "hackety hack" part if we just prevented default on a trusted touchend with the select box as its target.
Also I've found one more wrinkle: it's not just select boxes which are affected, but also <a> and buttons which submit forms (or any other preventable default, really). And in the case of non-selects, FastClick fires a click
instead of a mousedown
. But the principle is the same, so it might still be doable.
Comment 9•2 years ago
|
||
Are you able to try to poke at it and get it to work? I'm a bit swamped with other stuff so it might be a bit until I get to this. Just want to make sure it doesn't fall through the cracks.
Comment 10•2 years ago
|
||
I don't have much time now either, but I'll keep it on my radar and try to poke at it, yes. As long as there's a reasonable chance we'd accept something like this, then I feel it's justifiable effort :)
Description
•