diff --git a/lib/utils/gestures.html b/lib/utils/gestures.html
index 738940f406..ca38ec3173 100644
--- a/lib/utils/gestures.html
+++ b/lib/utils/gestures.html
@@ -97,6 +97,55 @@
/** @type {(function(MouseEvent): void | undefined)} */
GestureRecognizer.prototype.click;
+ // keep track of any labels hit by the mouseCanceller
+ /** @type {!Array} */
+ const clickedLabels = [];
+
+ /** @type {!Map} */
+ 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} Relevant label for `el`
+ */
+ function matchingLabels(el) {
+ /** @type {!Array} */
+ 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 `` focus and link clicks
@@ -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();
}
@@ -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);
diff --git a/test/smoke/label-click.html b/test/smoke/label-click.html
new file mode 100644
index 0000000000..c2ca4d3fa1
--- /dev/null
+++ b/test/smoke/label-click.html
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/unit/gestures-elements.html b/test/unit/gestures-elements.html
index 777f2de556..534b4edfbf 100644
--- a/test/unit/gestures-elements.html
+++ b/test/unit/gestures-elements.html
@@ -212,3 +212,34 @@
});
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/unit/gestures.html b/test/unit/gestures.html
index 4c8a92b4cb..30866d06fa 100644
--- a/test/unit/gestures.html
+++ b/test/unit/gestures.html
@@ -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);
+ });
});