diff --git a/lib/utils/gestures.html b/lib/utils/gestures.html
index b29a7a4d4e..94cd964dbd 100644
--- a/lib/utils/gestures.html
+++ b/lib/utils/gestures.html
@@ -97,10 +97,59 @@
/** @type {(function(MouseEvent): void | undefined)} */
GestureRecognizer.prototype.click;
- // keep track of any labels hit by tghe mouseCanceller
+ // 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.
+ if (!labels || !labels.length) {
+ labels = [];
+ let root = el.getRootNode();
+ // find all labels that are ancestors
+ let cur = el.parentNode;
+ while (cur !== root) {
+ if (cur.localName === 'label') {
+ labels.push(/** @type {!HTMLLabelElement} */(cur));
+ }
+ }
+ // 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
@@ -127,8 +176,8 @@
if (path[i].nodeType === Node.ELEMENT_NODE) {
if (path[i].localName === 'label') {
labels.push(path[i]);
- } else if (path[i].labels) {
- let ownerLabels = path[i].labels;
+ } else if (canBeLabelled(path[i])) {
+ let ownerLabels = matchingLabels(path[i]);
for (let j = 0; j < ownerLabels.length; j++) {
clickFromLabel = clickFromLabel || clickedLabels.indexOf(ownerLabels[j]) > -1;
}
diff --git a/test/unit/gestures-elements.html b/test/unit/gestures-elements.html
index 777f2de556..ccf03f36ea 100644
--- a/test/unit/gestures-elements.html
+++ b/test/unit/gestures-elements.html
@@ -212,3 +212,18 @@
});
+
+
+
+
+
+
+
+
diff --git a/test/unit/gestures.html b/test/unit/gestures.html
index 4c8a92b4cb..b3ad058953 100644
--- a/test/unit/gestures.html
+++ b/test/unit/gestures.html
@@ -559,6 +559,29 @@
assert.equal(count, 1);
Polymer.Gestures.remove(window, 'tap', increment);
});
+
+ test('native label click', function() {
+ let el = document.createElement('x-native-label');
+ document.body.appendChild(el);
+ let target = el.$.label;
+ let touches = [{
+ clientX: 0,
+ clientY: 0,
+ identifier: 1,
+ // target is set to the element with `addEventListener`, which is app
+ 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);
+ let click = new MouseEvent('click', {bubbles: true, composed: true});
+ target.dispatchEvent(click);
+ assert.equal(el.$.check.checked, true);
+ document.body.removeChild(el);
+ });
});