Skip to content

Commit

Permalink
Event: Simulate focus/blur in IE via focusin/focusout (3.x version)
Browse files Browse the repository at this point in the history
In IE (all versions), `focus` & `blur` handlers are fired asynchronously
but `focusin` & `focusout` are run synchronously. In other browsers, all
those handlers are fired synchronously. Asynchronous behavior of these
handlers in IE caused issues for IE (gh-4856, gh-4859).

We now simulate `focus` via `focusin` & `blur` via `focusout` in IE to avoid
these issues. This also let us simplify some tests.

This commit also simplifies `leverageNative` - with IE now using `focusin`
to simulate `focus` and `focusout` to simulate `blur`, we don't have to deal
with async events in `leverageNative`. This also fixes broken `focus` triggers
after first triggering it on a hidden element - previously, `leverageNative`
assumed that the native `focus` handler not firing after calling the native 
`focus` method meant it would be handled later, asynchronously, which
was not the case (gh-4950).

To preserve relative `focusin`/`focus` & `focusout`/`blur` event order
guaranteed on the 3.x branch, attach a single handler for both events in IE.

A side effect of this is that to reduce size the `event/focusin` module
no longer exists and it's impossible to disable the `focusin` patch
in modern browsers via the jQuery custom build system.

Fixes gh-4856
Fixes gh-4859
Fixes gh-4950
Ref gh-5223
Closes gh-5224

Co-authored-by: Richard Gibson <richard.gibson@gmail.com>
  • Loading branch information
mgol and gibson042 committed Mar 27, 2023
1 parent 4837a95 commit 59f7b55
Show file tree
Hide file tree
Showing 7 changed files with 258 additions and 162 deletions.
1 change: 0 additions & 1 deletion README.md
Expand Up @@ -95,7 +95,6 @@ Some example modules that can be excluded are:
- **dimensions**: The `.width()` and `.height()` methods, including `inner-` and `outer-` variations.
- **effects**: The `.animate()` method and its shorthands such as `.slideUp()` or `.hide("slow")`.
- **event**: The `.on()` and `.off()` methods and all event functionality.
- **event/focusin**: Cross-browser support for the focusin and focusout events.
- **event/trigger**: The `.trigger()` and `.triggerHandler()` methods.
- **offset**: The `.offset()`, `.position()`, `.offsetParent()`, `.scrollLeft()`, and `.scrollTop()` methods.
- **wrap**: The `.wrap()`, `.wrapAll()`, `.wrapInner()`, and `.unwrap()` methods.
Expand Down
186 changes: 137 additions & 49 deletions src/event.js
Expand Up @@ -27,25 +27,6 @@ function returnFalse() {
return false;
}

// Support: IE <=9 - 11+
// focus() and blur() are asynchronous, except when they are no-op.
// So expect focus to be synchronous when the element is already active,
// and blur to be synchronous when the element is not already active.
// (focus and blur are always synchronous in other supported browsers,
// this just defines when we can count on it).
function expectSync( elem, type ) {
return ( elem === safeActiveElement() ) === ( type === "focus" );
}

// Support: IE <=9 only
// Accessing document.activeElement can throw unexpectedly
// https://bugs.jquery.com/ticket/13393
function safeActiveElement() {
try {
return document.activeElement;
} catch ( err ) { }
}

function on( elem, types, selector, data, fn, one ) {
var origFn, type;

Expand Down Expand Up @@ -483,7 +464,7 @@ jQuery.event = {
el.click && nodeName( el, "input" ) ) {

// dataPriv.set( el, "click", ... )
leverageNative( el, "click", returnTrue );
leverageNative( el, "click", true );
}

// Return false to allow normal processing in the caller
Expand Down Expand Up @@ -534,10 +515,10 @@ jQuery.event = {
// synthetic events by interrupting progress until reinvoked in response to
// *native* events that it fires directly, ensuring that state changes have
// already occurred before other listeners are invoked.
function leverageNative( el, type, expectSync ) {
function leverageNative( el, type, isSetup ) {

// Missing expectSync indicates a trigger call, which must force setup through jQuery.event.add
if ( !expectSync ) {
// Missing `isSetup` indicates a trigger call, which must force setup through jQuery.event.add
if ( !isSetup ) {
if ( dataPriv.get( el, type ) === undefined ) {
jQuery.event.add( el, type, returnTrue );
}
Expand All @@ -549,15 +530,13 @@ function leverageNative( el, type, expectSync ) {
jQuery.event.add( el, type, {
namespace: false,
handler: function( event ) {
var notAsync, result,
var result,
saved = dataPriv.get( this, type );

if ( ( event.isTrigger & 1 ) && this[ type ] ) {

// Interrupt processing of the outer synthetic .trigger()ed event
// Saved data should be false in such cases, but might be a leftover capture object
// from an async native handler (gh-4350)
if ( !saved.length ) {
if ( !saved ) {

// Store arguments for use when handling the inner native event
// There will always be at least one argument (an event object), so this array
Expand All @@ -566,28 +545,17 @@ function leverageNative( el, type, expectSync ) {
dataPriv.set( this, type, saved );

// Trigger the native event and capture its result
// Support: IE <=9 - 11+
// focus() and blur() are asynchronous
notAsync = expectSync( this, type );
this[ type ]();
result = dataPriv.get( this, type );
if ( saved !== result || notAsync ) {
dataPriv.set( this, type, false );
} else {
result = {};
}

if ( saved !== result ) {

// Cancel the outer synthetic event
event.stopImmediatePropagation();
event.preventDefault();

// Support: Chrome 86+
// In Chrome, if an element having a focusout handler is blurred by
// clicking outside of it, it invokes the handler synchronously. If
// that handler calls `.remove()` on the element, the data is cleared,
// leaving `result` undefined. We need to guard against this.
return result && result.value;
return result;
}

// If this is an inner synthetic event for an event with a bubbling surrogate
Expand All @@ -605,16 +573,11 @@ function leverageNative( el, type, expectSync ) {
} else if ( saved.length ) {

// ...and capture the result
dataPriv.set( this, type, {
value: jQuery.event.trigger(

// Support: IE <=9 - 11+
// Extend with the prototype to reset the above stopImmediatePropagation()
jQuery.extend( saved[ 0 ], jQuery.Event.prototype ),
dataPriv.set( this, type, jQuery.event.trigger(
saved[ 0 ],
saved.slice( 1 ),
this
)
} );
) );

// Abort handling of the native event
event.stopImmediatePropagation();
Expand Down Expand Up @@ -756,18 +719,73 @@ jQuery.each( {
}, jQuery.event.addProp );

jQuery.each( { focus: "focusin", blur: "focusout" }, function( type, delegateType ) {

function focusMappedHandler( nativeEvent ) {
if ( document.documentMode ) {

// Support: IE 11+
// Attach a single focusin/focusout handler on the document while someone wants
// focus/blur. This is because the former are synchronous in IE while the latter
// are async. In other browsers, all those handlers are invoked synchronously.

// `handle` from private data would already wrap the event, but we need
// to change the `type` here.
var handle = dataPriv.get( this, "handle" ),
event = jQuery.event.fix( nativeEvent );
event.type = nativeEvent.type === "focusin" ? "focus" : "blur";
event.isSimulated = true;

// First, handle focusin/focusout
handle( nativeEvent );

// ...then, handle focus/blur
//
// focus/blur don't bubble while focusin/focusout do; simulate the former by only
// invoking the handler at the lower level.
if ( event.target === event.currentTarget ) {

// The setup part calls `leverageNative`, which, in turn, calls
// `jQuery.event.add`, so event handle will already have been set
// by this point.
handle( event );
}
} else {

// For non-IE browsers, attach a single capturing handler on the document
// while someone wants focusin/focusout.
jQuery.event.simulate( delegateType, nativeEvent.target,
jQuery.event.fix( nativeEvent ) );
}
}

jQuery.event.special[ type ] = {

// Utilize native event if possible so blur/focus sequence is correct
setup: function() {

var attaches;

// Claim the first handler
// dataPriv.set( this, "focus", ... )
// dataPriv.set( this, "blur", ... )
leverageNative( this, type, expectSync );
leverageNative( this, type, true );

if ( document.documentMode ) {

// Support: IE 9 - 11+
// We use the same native handler for focusin & focus (and focusout & blur)
// so we need to coordinate setup & teardown parts between those events.
// Use `delegateType` as the key as `type` is already used by `leverageNative`.
attaches = dataPriv.get( this, delegateType );
if ( !attaches ) {
this.addEventListener( delegateType, focusMappedHandler );
}
dataPriv.set( this, delegateType, ( attaches || 0 ) + 1 );
} else {

// Return false to allow normal processing in the caller
return false;
}
},
trigger: function() {

Expand All @@ -778,6 +796,24 @@ jQuery.each( { focus: "focusin", blur: "focusout" }, function( type, delegateTyp
return true;
},

teardown: function() {
var attaches;

if ( document.documentMode ) {
attaches = dataPriv.get( this, delegateType ) - 1;
if ( !attaches ) {
this.removeEventListener( delegateType, focusMappedHandler );
dataPriv.remove( this, delegateType );
} else {
dataPriv.set( this, delegateType, attaches );
}
} else {

// Return false to indicate standard teardown should be applied
return false;
}
},

// Suppress native focus or blur if we're currently inside
// a leveraged native-event stack
_default: function( event ) {
Expand All @@ -786,6 +822,58 @@ jQuery.each( { focus: "focusin", blur: "focusout" }, function( type, delegateTyp

delegateType: delegateType
};

// Support: Firefox <=44
// Firefox doesn't have focus(in | out) events
// Related ticket - https://bugzilla.mozilla.org/show_bug.cgi?id=687787
//
// Support: Chrome <=48 - 49, Safari <=9.0 - 9.1
// focus(in | out) events fire after focus & blur events,
// which is spec violation - http://www.w3.org/TR/DOM-Level-3-Events/#events-focusevent-event-order
// Related ticket - https://bugs.chromium.org/p/chromium/issues/detail?id=449857
//
// Support: IE 9 - 11+
// To preserve relative focusin/focus & focusout/blur event order guaranteed on the 3.x branch,
// attach a single handler for both events in IE.
jQuery.event.special[ delegateType ] = {
setup: function() {

// Handle: regular nodes (via `this.ownerDocument`), window
// (via `this.document`) & document (via `this`).
var doc = this.ownerDocument || this.document || this,
dataHolder = document.documentMode ? this : doc,
attaches = dataPriv.get( dataHolder, delegateType );

// Support: IE 9 - 11+
// We use the same native handler for focusin & focus (and focusout & blur)
// so we need to coordinate setup & teardown parts between those events.
// Use `delegateType` as the key as `type` is already used by `leverageNative`.
if ( !attaches ) {
if ( document.documentMode ) {
this.addEventListener( delegateType, focusMappedHandler );
} else {
doc.addEventListener( type, focusMappedHandler, true );
}
}
dataPriv.set( dataHolder, delegateType, ( attaches || 0 ) + 1 );
},
teardown: function() {
var doc = this.ownerDocument || this.document || this,
dataHolder = document.documentMode ? this : doc,
attaches = dataPriv.get( dataHolder, delegateType ) - 1;

if ( !attaches ) {
if ( document.documentMode ) {
this.removeEventListener( delegateType, focusMappedHandler );
} else {
doc.removeEventListener( type, focusMappedHandler, true );
}
dataPriv.remove( dataHolder, delegateType );
} else {
dataPriv.set( dataHolder, delegateType, attaches );
}
}
};
} );

// Create mouseenter/leave events using mouseover/out and event-time checks
Expand Down
58 changes: 0 additions & 58 deletions src/event/focusin.js

This file was deleted.

11 changes: 0 additions & 11 deletions src/event/support.js

This file was deleted.

1 change: 0 additions & 1 deletion src/jquery.js
Expand Up @@ -11,7 +11,6 @@ define( [
"./queue/delay",
"./attributes",
"./event",
"./event/focusin",
"./manipulation",
"./manipulation/_evalUrl",
"./wrap",
Expand Down

0 comments on commit 59f7b55

Please sign in to comment.