Skip to content

Instantly share code, notes, and snippets.

@NickGard
Last active January 29, 2024 10:24
Show Gist options
  • Save NickGard/43328a4e223698e6a63cbff410e35342 to your computer and use it in GitHub Desktop.
Save NickGard/43328a4e223698e6a63cbff410e35342 to your computer and use it in GitHub Desktop.
Safari Focus Polyfill
(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});
}
})();
@PitWenkin
Copy link

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

@Maximaximum
Copy link

@NickGard have you ever considered releasing this polyfill as an npm package? It would be extremely useful

@kimskovhusandersen
Copy link

kimskovhusandersen commented Jan 25, 2024

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;

@NickGard
Copy link
Author

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/.

@kimskovhusandersen
Copy link

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 🙏

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment