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); + }); });