Skip to content

Commit

Permalink
Merge pull request #5039 from Polymer/fix-label-tap
Browse files Browse the repository at this point in the history
Fix label tap by checking matched label pairs
  • Loading branch information
dfreedm authored Feb 22, 2018
2 parents a5cb84e + c11c99b commit 481dea6
Show file tree
Hide file tree
Showing 4 changed files with 179 additions and 0 deletions.
68 changes: 68 additions & 0 deletions lib/utils/gestures.html
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,55 @@
/** @type {(function(MouseEvent): void | undefined)} */
GestureRecognizer.prototype.click;

// keep track of any labels hit by the mouseCanceller
/** @type {!Array<!HTMLLabelElement>} */
const clickedLabels = [];

/** @type {!Map<string, boolean>} */
const labellable = {
'button': true,
'input': true,
'keygen': true,
'meter': true,
'output': true,
'textarea': true,
'progress': true,
'select': true
};

/**
* @param {HTMLElement} el Element to check labelling status
* @return {boolean} element can have labels
*/
function canBeLabelled(el) {
return labellable[el.localName] || false;
}

/**
* @param {HTMLElement} el Element that may be labelled.
* @return {!Array<!HTMLLabelElement>} Relevant label for `el`
*/
function matchingLabels(el) {
/** @type {!Array<!HTMLLabelElement>} */
let labels = el.labels;
// IE doesn't have `labels` and Safari doesn't populate `labels`
// if element is in a shadowroot.
// In this instance, finding the non-ancestor labels is enough,
// as the mouseCancellor code will handle ancstor labels
if (!labels || !labels.length) {
labels = [];
let root = el.getRootNode();
// if there is an id on `el`, check for all labels with a matching `for` attribute
if (el.id) {
let matching = root.querySelectorAll(`label[for = ${el.id}]`);
for (let i = 0; i < matching.length; i++) {
labels.push(/** @type {!HTMLLabelElement} */(matching[i]));
}
}
}
return labels;
}

// touch will make synthetic mouse events
// `preventDefault` on touchend will cancel them,
// but this breaks `<input>` focus and link clicks
Expand All @@ -115,14 +164,31 @@
mouseEvent[HANDLED_OBJ] = {skip: true};
// disable "ghost clicks"
if (mouseEvent.type === 'click') {
let clickFromLabel = false;
let path = mouseEvent.composedPath && mouseEvent.composedPath();
if (path) {
for (let i = 0; i < path.length; i++) {
if (path[i].nodeType === Node.ELEMENT_NODE) {
if (path[i].localName === 'label') {
clickedLabels.push(path[i]);
} else if (canBeLabelled(path[i])) {
let ownerLabels = matchingLabels(path[i]);
// check if one of the clicked labels is labelling this element
for (let j = 0; j < ownerLabels.length; j++) {
clickFromLabel = clickFromLabel || clickedLabels.indexOf(ownerLabels[j]) > -1;
}
}
}
if (path[i] === POINTERSTATE.mouse.target) {
return;
}
}
}
// if one of the clicked labels was labelling the target element,
// this is not a ghost click
if (clickFromLabel) {
return;
}
mouseEvent.preventDefault();
mouseEvent.stopPropagation();
}
Expand All @@ -137,6 +203,8 @@
for (let i = 0, en; i < events.length; i++) {
en = events[i];
if (setup) {
// reset clickLabels array
clickedLabels.length = 0;
document.addEventListener(en, mouseCanceller, true);
} else {
document.removeEventListener(en, mouseCanceller, true);
Expand Down
25 changes: 25 additions & 0 deletions test/smoke/label-click.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<!--
@license
Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
-->
<!DOCTYPE html>
<html>

<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<script src="../../../webcomponentsjs/webcomponents-lite.js"></script>
<link rel="import" href="../../polymer.html">
</head>

<body>
<label id="label" for="checkme">Native checkbox</label>
<input type="checkbox" id="checkme">
</body>

</html>
31 changes: 31 additions & 0 deletions test/unit/gestures-elements.html
Original file line number Diff line number Diff line change
Expand Up @@ -212,3 +212,34 @@
});
</script>
</dom-module>

<dom-module id="x-native-label">
<template>
<label id="label" for="check"></label>
<input id="check" type="checkbox">
</template>
<script>
class XNativeLabel extends Polymer.Element {
static get is() {
return 'x-native-label';
}
}
customElements.define(XNativeLabel.is, XNativeLabel);
</script>
</dom-module>

<dom-module id="x-native-label-nested">
<template>
<label id="label">
<input id="check" type="checkbox">
</label>
</template>
<script>
class XNativeLabelNested extends Polymer.Element {
static get is() {
return 'x-native-label-nested';
}
}
customElements.define(XNativeLabelNested.is, XNativeLabelNested);
</script>
</dom-module>
55 changes: 55 additions & 0 deletions test/unit/gestures.html
Original file line number Diff line number Diff line change
Expand Up @@ -559,6 +559,61 @@
assert.equal(count, 1);
Polymer.Gestures.remove(window, 'tap', increment);
});

suite('native label click', function() {

test('native label click', function() {
let el = document.createElement('x-native-label');
document.body.appendChild(el);
let target = el.$.label;
// simulate the event sequence of a touch on the screen
let touches = [{
clientX: 0,
clientY: 0,
identifier: 1,
// target is set to the element with `addEventListener`, which is `target`
target
}];
let touchstart = new CustomEvent('touchstart', {bubbles: true, composed: true});
touchstart.changedTouches = touchstart.touches = touches;
target.dispatchEvent(touchstart);
let touchend = new CustomEvent('touchend', {bubbles: true, composed: true});
touchend.touches = touchend.changedTouches = touches;
target.dispatchEvent(touchend);
// simulate a mouse click on the label
let click = new MouseEvent('click', {bubbles: true, composed: true});
target.dispatchEvent(click);
// check that the mouse click on the label will activate the checkbox
assert.equal(el.$.check.checked, true, 'checkbox should be checked');
document.body.removeChild(el);
});
});

test('label click with nested element', function() {
let el = document.createElement('x-native-label-nested');
document.body.appendChild(el);
let target = el.$.label;
// simulate the event sequence of a touch on the screen
let touches = [{
clientX: 0,
clientY: 0,
identifier: 1,
// target is set to the element with `addEventListener`, which is `target`
target
}];
let touchstart = new CustomEvent('touchstart', {bubbles: true, composed: true});
touchstart.changedTouches = touchstart.touches = touches;
target.dispatchEvent(touchstart);
let touchend = new CustomEvent('touchend', {bubbles: true, composed: true});
touchend.touches = touchend.changedTouches = touches;
target.dispatchEvent(touchend);
// simulate a mouse click on the label
let click = new MouseEvent('click', {bubbles: true, composed: true});
target.dispatchEvent(click);
// check that the mouse click on the label will activate the checkbox
assert.equal(el.$.check.checked, true, 'checkbox should be checked');
document.body.removeChild(el);
});
});
</script>

Expand Down

0 comments on commit 481dea6

Please sign in to comment.