From 6ad3651dbfea9e9bb56e608f72b4ef2f97bd4e70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Go=C5=82=C4=99biowski-Owczarek?= Date: Mon, 27 Mar 2023 21:47:01 +0200 Subject: [PATCH] Event: Make trigger(focus/blur/click) work with native handlers In `leverageNative`, instead of calling `event.stopImmediatePropagation()` which would abort both native & jQuery handlers, set the wrapper's `isImmediatePropagationStopped` property to a function returning `true`. Since for each element + type pair jQuery attaches only one native handler, there is also only one wrapper jQuery event so this achieves the goal: on the target element jQuery handlers don't fire but native ones do. Unfortunately, this workaround doesn't work for handlers on ancestors - since the native event is re-wrapped by a jQuery one on each level of the propagation, the only way to stop it for jQuery was to stop it for everyone via native `stopPropagation()`. This is not a problem for `focus`/`blur` which don't bubble, but it does also stop `click` on checkboxes and radios. We accept this limitation. Fixes gh-5015 Closes gh-5228 --- src/event.js | 16 +++++++++--- test/unit/event.js | 63 ++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 73 insertions(+), 6 deletions(-) diff --git a/src/event.js b/src/event.js index 95221f475..c9a397c90 100644 --- a/src/event.js +++ b/src/event.js @@ -545,8 +545,8 @@ function leverageNative( el, type, isSetup ) { } // 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. + // (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. @@ -565,8 +565,16 @@ function leverageNative( el, type, isSetup ) { this ) ); - // Abort handling of the native event - event.stopImmediatePropagation(); + // 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 + // by stopping immediate propagation just on the jQuery event. However, + // the native event is re-wrapped by a jQuery one on each level of the + // propagation so the only way to stop it for jQuery is to stop it for + // everyone via native `stopPropagation()`. This is not a problem for + // focus/blur which don't bubble, but it does also stop click on checkboxes + // and radios. We accept this limitation. + event.stopPropagation(); + event.isImmediatePropagationStopped = returnTrue; } } } ); diff --git a/test/unit/event.js b/test/unit/event.js index aa55c06a1..79057f191 100644 --- a/test/unit/event.js +++ b/test/unit/event.js @@ -3423,6 +3423,36 @@ QUnit.test( "trigger(focus) works after focusing when hidden (gh-4950)", functio assert.equal( document.activeElement, input[ 0 ], "input has focus" ); } ); +QUnit.test( "trigger(focus) fires native & jQuery handlers (gh-5015)", function( assert ) { + assert.expect( 3 ); + + var input = jQuery( "" ), + + // Support: IE 9 - 11+ + // focus is async in IE; we now emulate it via sync focusin in jQuery + // but this test also attaches native handlers. + done = assert.async( 3 ); + + input.appendTo( "#qunit-fixture" ); + + input[ 0 ].addEventListener( "focus", function() { + assert.ok( true, "1st native handler fired" ); + done(); + } ); + + input.on( "focus", function() { + assert.ok( true, "jQuery handler fired" ); + done(); + } ); + + input[ 0 ].addEventListener( "focus", function() { + assert.ok( true, "2nd native handler fired" ); + done(); + } ); + + input.trigger( "focus" ); +} ); + // TODO replace with an adaptation of // https://github.com/jquery/jquery/pull/1367/files#diff-a215316abbaabdf71857809e8673ea28R2464 ( function() { @@ -3431,10 +3461,13 @@ QUnit.test( "trigger(focus) works after focusing when hidden (gh-4950)", functio checkbox: "", radio: "" }, - makeTestFor3751 + function( type, html ) { + makeTestForGh3751( type, html ); + makeTestForGh5015( type, html ); + } ); - function makeTestFor3751( type, html ) { + function makeTestForGh3751( type, html ) { var testName = "native-backed namespaced clicks are handled correctly (gh-3751) - " + type; QUnit.test( testName, function( assert ) { assert.expect( 2 ); @@ -3461,4 +3494,30 @@ QUnit.test( "trigger(focus) works after focusing when hidden (gh-4950)", functio target.trigger( "click.fired" ); } ); } + + function makeTestForGh5015( type, html ) { + var testName = "trigger(click) fires native & jQuery handlers (gh-5015) - " + type; + QUnit.test( testName, function( assert ) { + assert.expect( 3 ); + + var parent = supportjQuery( "
" + html + "
" ), + input = jQuery( parent[ 0 ].firstChild ); + + parent.appendTo( "#qunit-fixture" ); + + input[ 0 ].addEventListener( "click", function() { + assert.ok( true, "1st native handler fired" ); + } ); + + input.on( "click", function() { + assert.ok( true, "jQuery handler fired" ); + } ); + + input[ 0 ].addEventListener( "click", function() { + assert.ok( true, "2nd native handler fired" ); + } ); + + input.trigger( "click" ); + } ); + } } )();