-
-
Save NickGard/43328a4e223698e6a63cbff410e35342 to your computer and use it in GitHub Desktop.
(function() { | |
var capturedEvents = []; | |
var capturing = false; | |
function getEventTarget(event) { | |
return event.composedPath()[0] || event.target; | |
} | |
function captureEvent(e) { | |
if (capturing) { | |
e.preventDefault(); | |
e.stopPropagation(); | |
e.stopImmediatePropagation(); | |
capturedEvents.unshift(e); | |
} | |
} | |
function hiddenOrInert(element) { | |
var el = element; | |
while (el) { | |
if (el.style.display === 'none' || el.hasAttribute('inert')) return true; | |
el = el.parentElement; | |
} | |
return false; | |
} | |
/* | |
* The only mousedown events we care about here are ones emanating from | |
* (A) anchor links with href attribute, | |
* (B) non-disabled buttons, | |
* (C) non-disabled textarea, | |
* (D) non-disabled inputs of type "button", "reset", "checkbox", "radio", "submit" | |
* (E) non-interactive elements (button, a, input, textarea, select) that have a tabindex with a numeric value | |
* (F) audio elements | |
* (G) video elements with controls attribute | |
* (H) any element with the contenteditable attribute | |
*/ | |
function isFocusableElement(el) { | |
var tag = el.tagName; | |
return ( | |
!hiddenOrInert(el) && ( | |
(/^a$/i.test(tag) && el.href != null) || // (A) | |
(/^(button|textarea)$/i.test(tag) && el.disabled !== true) || // (B) (C) | |
(/^input$/i.test(tag) && | |
/^(button|reset|submit|radio|checkbox)$/i.test(el.type) && | |
!el.disabled) || // (D) | |
(!/^(button|input|textarea|select|a)$/i.test(tag) && | |
!Number.isNaN(Number.parseFloat(el.getAttribute('tabindex')))) || // (E) | |
/^audio$/i.test(tag) || // (F) | |
(/^video$/i.test(tag) && el.controls === true) || // (G) | |
el.getAttribute('contenteditable') != null // (H) | |
) | |
); | |
} | |
function getLabelledElement(labelElement) { | |
var forId = labelElement.getAttribute('for'); | |
return forId | |
? document.querySelector('#'+forId) | |
: labelElement.querySelector('button, input, keygen, select, textarea'); | |
} | |
function getFocusableElement(e) { | |
var currentElement = getEventTarget(e); | |
var focusableElement; | |
while (!focusableElement && currentElement !== null && currentElement.nodeType === 1) { | |
if (isFocusableElement(currentElement)) { | |
focusableElement = currentElement; | |
} else if (/^label$/i.test(currentElement.tagName)) { | |
var labelledElement = getLabelledElement(currentElement); | |
if (isFocusableElement(labelledElement)) { | |
focusableElement = labelledElement; | |
} | |
} | |
currentElement = currentElement.parentElement || currentElement.parentNode | |
} | |
return focusableElement; | |
} | |
function handler(e) { | |
var focusableElement = getFocusableElement(e); | |
if (focusableElement) { | |
if (focusableElement === document.activeElement) { | |
// mousedown is happening on the currently focused element | |
// do not fire the 'focus' event in this case AND | |
// call preventDefault() to stop the browser from transferring | |
// focus to the body element | |
e.preventDefault(); | |
} else { | |
// start capturing possible out-of-order mouse events | |
capturing = true; | |
/* | |
* enqueue the focus event _after_ the current batch of events, which | |
* includes any blur events but before the mouseup and click events. | |
* The correct order of events is: | |
* | |
* [this element] MOUSEDOWN <-- this event | |
* [previously active element] BLUR | |
* [previously active element] FOCUSOUT | |
* [this element] FOCUS <-- forced event | |
* [this element] FOCUSIN <-- triggered by forced event | |
* [this element] MOUSEUP <-- possibly captured event (it may have fired _before_ the FOCUS event) | |
* [this element] CLICK <-- possibly captured event (it may have fired _before_ the FOCUS event) | |
*/ | |
setTimeout(() => { | |
// stop capturing possible out-of-order mouse events | |
capturing = false; | |
// trigger focus event | |
focusableElement.focus(); | |
// re-dispatch captured mouse events in order | |
while (capturedEvents.length > 0) { | |
var event = capturedEvents.pop(); | |
getEventTarget(event).dispatchEvent(new MouseEvent(event.type, event)); | |
} | |
}, 0); | |
} | |
} | |
} | |
if (/apple/i.test(navigator.vendor)) { | |
window.addEventListener('mousedown', handler, {capture: true}); | |
window.addEventListener('mouseup', captureEvent, {capture: true}); | |
window.addEventListener('click', captureEvent, {capture: true}); | |
} | |
})(); |
There's an issue with textarea
on iOS Safari using this polyfill. Focusing an textarea won't trigger displaying a keyboard. I couldn't find a proper fix, so I removed textarea from RegEx in isFocusableElement
function as a temporary workaround.
Thank you all for your comments, bug reports, and improvement suggestions! I just got around to testing and fixing things:
@aercolino, using event.composedPath()
is an awesome improvement 👍
@borm, this error is fixed now. It was caused by some events not having a composed path, so I re-added event.target
as a fallback. 🔧
@sewerynkalemba, pointerup
, pointerdown
, touchstart
, and touchend
do not fire out of order for iOS or iPadOS, so I never included those. By calling preventDefault()
on the touch*
events, you stop the mouse*
and click
events from firing, which led to the bug you later reported about the virtual keyboard not opening. Unfortunately, only click
events are allowed to be re-dispatched and still engage the default actions. All other events that are dispatched programmatically do not trigger default actions.
@laurent-d I could not reproduce this bug. If you (or anyone) sees something like this again, please leave me a message here and I'll try to reproduce and fix it.
Hi, selecting text on safari (Desktop) in a textarea does not work propperly (Both firefox an chrome are not affected)
The only way to select any text is clicking in an unfocused textarea and selecting the text before releasing the mouse button.
An already focused textarea will not accept click&select, double-click(singleword), or tripple-click(whole line select).
Might be related to @sewerynkalemba's report
@NickGard have you ever considered releasing this polyfill as an npm
package? It would be extremely useful
There was a type error in isFocusableElement.
TypeError: null is not an object (evaluating 'e.tagName')
There should be a check to make sure el is not null or undefined.
function isFocusableElement(el) {
if (!el) return false;
var tag = el.tagName;
There's a new, more complete version of this polyfill at https://gist.github.com/NickGard/343da4185ed244b9c98aaad267362066. The version here didn't fix all of the test cases on https://nickgard.github.io/click-focus-testing/.
There's a new, more complete version of this polyfill at https://gist.github.com/NickGard/343da4185ed244b9c98aaad267362066. The version here didn't fix all of the test cases on https://nickgard.github.io/click-focus-testing/.
Thank you @NickGard 🙏
thanks @sewerynkalemba for your input...
All actions needed double click on iOs without your fix