diff options
author | Michał Gołębiowski-Owczarek <m.goleb@gmail.com> | 2024-05-20 18:05:19 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-05-20 18:05:19 +0200 |
commit | 527fb3dcf0dcde69302a741dfc61cbfa58e99eb0 (patch) | |
tree | 1e3a6845a1c5cb3f70cc80584579c19f992149be /src/event.js | |
parent | 5880e02707dcefc4ec527bd1c56f64b8b0eba391 (diff) | |
download | jquery-527fb3dcf0dcde69302a741dfc61cbfa58e99eb0.tar.gz jquery-527fb3dcf0dcde69302a741dfc61cbfa58e99eb0.zip |
Event: Increase robustness of an inner native event in leverageNative
In Firefox, alert displayed just before blurring an element dispatches
the native blur event twice which tripped the jQuery logic if a jQuery blur
handler was not attached before the trigger call.
This was because the `leverageNative` logic part for triggering first checked if
setup was done before (which, for example, is done if a jQuery handler was
registered before for this element+event pair) and - if it was not - added
a dummy handler that just returned `true`. The `leverageNative` logic made that
`true` then saved into private data, replacing the previous `saved` array. Since
`true` passed the truthy check, the second native inner handler treated `true`
as an array, crashing on the `slice` call.
The same issue could happen if a handler returning `true` is attached before
triggering. A bare `length` check would not be enough as the user handler may
return an array-like as well. To remove this potential data shape clash, capture
the inner result in an object with a `value` property instead of saving it
directly.
Since it's impossible to call `alert()` in unit tests, simulate the issue by
replacing the `addEventListener` method on a test button with a version that
calls attached blur handlers twice.
Fixes gh-5459
Closes gh-5466
Ref gh-5236
Diffstat (limited to 'src/event.js')
-rw-r--r-- | src/event.js | 57 |
1 files changed, 39 insertions, 18 deletions
diff --git a/src/event.js b/src/event.js index 0112264a4..a216f9a8c 100644 --- a/src/event.js +++ b/src/event.js @@ -519,14 +519,29 @@ function leverageNative( el, type, isSetup ) { var result, saved = dataPriv.get( this, type ); + // This controller function is invoked under multiple circumstances, + // differentiated by the stored value in `saved`: + // 1. For an outer synthetic `.trigger()`ed event (detected by + // `event.isTrigger & 1` and non-array `saved`), it records arguments + // as an array and fires an [inner] native event to prompt state + // changes that should be observed by registered listeners (such as + // checkbox toggling and focus updating), then clears the stored value. + // 2. For an [inner] native event (detected by `saved` being + // an array), it triggers an inner synthetic event, records the + // result, and preempts propagation to further jQuery listeners. + // 3. For an inner synthetic event (detected by `event.isTrigger & 1` and + // array `saved`), it prevents double-propagation of surrogate events + // but otherwise allows everything to proceed (particularly including + // further listeners). + // Possible `saved` data shapes: `[...], `{ value }`, `false`. if ( ( event.isTrigger & 1 ) && this[ type ] ) { // Interrupt processing of the outer synthetic .trigger()ed event - if ( !saved ) { + if ( !saved.length ) { // Store arguments for use when handling the inner native event - // There will always be at least one argument (an event object), so this array - // will not be confused with a leftover capture object. + // There will always be at least one argument (an event object), + // so this array will not be confused with a leftover capture object. saved = slice.call( arguments ); dataPriv.set( this, type, saved ); @@ -541,29 +556,35 @@ function leverageNative( el, type, isSetup ) { event.stopImmediatePropagation(); event.preventDefault(); - return result; + // 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; } - // If this is an inner synthetic event for an event with a bubbling surrogate - // (focus or blur), assume that the surrogate already propagated from triggering - // the native event and prevent that from happening again here. - // This technically gets the ordering wrong w.r.t. to `.trigger()` (in which the - // bubbling surrogate propagates *after* the non-bubbling base), but that seems - // less bad than duplication. + // If this is an inner synthetic event for an event with a bubbling + // surrogate (focus or blur), assume that the surrogate already + // propagated from triggering the native event and prevent that + // from happening again here. } else if ( ( jQuery.event.special[ type ] || {} ).delegateType ) { event.stopPropagation(); } - // If this is a native event triggered above, everything is now in order - // Fire an inner synthetic event with the original arguments - } else if ( saved ) { + // If this is a native event triggered above, everything is now in order. + // Fire an inner synthetic event with the original arguments. + } else if ( saved.length ) { // ...and capture the result - dataPriv.set( this, type, jQuery.event.trigger( - saved[ 0 ], - saved.slice( 1 ), - this - ) ); + dataPriv.set( this, type, { + value: jQuery.event.trigger( + saved[ 0 ], + saved.slice( 1 ), + this + ) + } ); // Abort handling of the native event by all jQuery handlers while allowing // native handlers on the same element to run. On target, this is achieved |