Event: Leverage native events for focus/blur/click; propagate additional data

Summary of the changes/fixes:
1. Trigger checkbox and radio click events identically (cherry-picked from
   b442abacbb that was reverted before).
2. Manually trigger a native event before checkbox/radio handlers.
3. Add test coverage for triggering namespaced native-backed events.
4. Propagate extra parameters passed when triggering the click event to
   the handlers.
5. Intercept and preserve namespaced native-backed events.
6. Leverage native events for focus and blur.
7. Accept that focusin handlers may fire more than once for now.

Fixes gh-1741
Fixes gh-3423
Fixes gh-3751
Fixes gh-4139
Closes gh-4279
Ref gh-1367
Ref gh-3494
This commit is contained in:
Richard Gibson 2017-01-11 15:19:30 -07:00 committed by Michał Gołębiowski-Owczarek
parent a0abd15b9e
commit 669f720edc
5 changed files with 296 additions and 56 deletions

View File

@ -4,6 +4,7 @@ define( [
"./var/documentElement", "./var/documentElement",
"./var/isFunction", "./var/isFunction",
"./var/rnothtmlwhite", "./var/rnothtmlwhite",
"./var/rcheckableType",
"./var/slice", "./var/slice",
"./data/var/dataPriv", "./data/var/dataPriv",
"./core/nodeName", "./core/nodeName",
@ -11,7 +12,7 @@ define( [
"./core/init", "./core/init",
"./selector" "./selector"
], function( jQuery, document, documentElement, isFunction, rnothtmlwhite, ], function( jQuery, document, documentElement, isFunction, rnothtmlwhite,
slice, dataPriv, nodeName ) { rcheckableType, slice, dataPriv, nodeName ) {
"use strict"; "use strict";
@ -329,9 +330,10 @@ jQuery.event = {
while ( ( handleObj = matched.handlers[ j++ ] ) && while ( ( handleObj = matched.handlers[ j++ ] ) &&
!event.isImmediatePropagationStopped() ) { !event.isImmediatePropagationStopped() ) {
// Triggered event must either 1) have no namespace, or 2) have namespace(s) // If the event is namespaced, then each handler is only invoked if it is
// a subset or equal to those in the bound event (both can have no namespace). // specially universal or its namespaces are a superset of the event's.
if ( !event.rnamespace || event.rnamespace.test( handleObj.namespace ) ) { if ( !event.rnamespace || handleObj.namespace === false ||
event.rnamespace.test( handleObj.namespace ) ) {
event.handleObj = handleObj; event.handleObj = handleObj;
event.data = handleObj.data; event.data = handleObj.data;
@ -457,37 +459,101 @@ jQuery.event = {
}, },
focus: { focus: {
// Fire native event if possible so blur/focus sequence is correct // Utilize native event if possible so blur/focus sequence is correct
trigger: function() { setup: function() {
if ( this !== safeActiveElement() && this.focus ) {
this.focus(); // Claim the first handler
return false; // dataPriv.set( this, "focus", ... )
} leverageNative( this, "focus", false, function( el ) {
return el !== safeActiveElement();
} );
// Return false to allow normal processing in the caller
return false;
}, },
trigger: function() {
// Force setup before trigger
leverageNative( this, "focus", returnTrue );
// Return non-false to allow normal event-path propagation
return true;
},
delegateType: "focusin" delegateType: "focusin"
}, },
blur: { blur: {
trigger: function() {
if ( this === safeActiveElement() && this.blur ) { // Utilize native event if possible so blur/focus sequence is correct
this.blur(); setup: function() {
return false;
} // Claim the first handler
// dataPriv.set( this, "blur", ... )
leverageNative( this, "blur", false, function( el ) {
return el === safeActiveElement();
} );
// Return false to allow normal processing in the caller
return false;
}, },
trigger: function() {
// Force setup before trigger
leverageNative( this, "blur", returnTrue );
// Return non-false to allow normal event-path propagation
return true;
},
delegateType: "focusout" delegateType: "focusout"
}, },
click: { click: {
// For checkbox, fire native event so checked state will be right // Utilize native event to ensure correct state for checkable inputs
trigger: function() { setup: function( data ) {
if ( this.type === "checkbox" && this.click && nodeName( this, "input" ) ) {
this.click(); // For mutual compressibility with _default, replace `this` access with a local var.
return false; // `|| data` is dead code meant only to preserve the variable through minification.
var el = this || data;
// Claim the first handler
if ( rcheckableType.test( el.type ) &&
el.click && nodeName( el, "input" ) &&
dataPriv.get( el, "click" ) === undefined ) {
// dataPriv.set( el, "click", ... )
leverageNative( el, "click", false, returnFalse );
} }
// Return false to allow normal processing in the caller
return false;
},
trigger: function( data ) {
// For mutual compressibility with _default, replace `this` access with a local var.
// `|| data` is dead code meant only to preserve the variable through minification.
var el = this || data;
// Force setup before triggering a click
if ( rcheckableType.test( el.type ) &&
el.click && nodeName( el, "input" ) &&
dataPriv.get( el, "click" ) === undefined ) {
leverageNative( el, "click", returnTrue );
}
// Return non-false to allow normal event-path propagation
return true;
}, },
// For cross-browser consistency, don't fire native .click() on links // For cross-browser consistency, suppress native .click() on links
// Also prevent it if we're currently inside a leveraged native-event stack
_default: function( event ) { _default: function( event ) {
return nodeName( event.target, "a" ); var target = event.target;
return rcheckableType.test( target.type ) &&
target.click && nodeName( target, "input" ) &&
dataPriv.get( target, "click" ) ||
nodeName( target, "a" );
} }
}, },
@ -504,6 +570,77 @@ jQuery.event = {
} }
}; };
// Ensure the presence of an event listener that handles manually-triggered
// 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, forceAdd, allowAsync ) {
// Setup must go through jQuery.event.add
if ( forceAdd ) {
jQuery.event.add( el, type, forceAdd );
return;
}
// Register the controller as a special universal handler for all event namespaces
dataPriv.set( el, type, forceAdd );
jQuery.event.add( el, type, {
namespace: false,
handler: function( event ) {
var maybeAsync, result,
saved = dataPriv.get( this, type );
// Interrupt processing of the outer synthetic .trigger()ed event
if ( ( event.isTrigger & 1 ) && this[ type ] && !saved ) {
// Store arguments for use when handling the inner native event
saved = slice.call( arguments );
dataPriv.set( this, type, saved );
// Trigger the native event and capture its result
// Support: IE <=9 - 11+
// focus() and blur() are asynchronous
maybeAsync = allowAsync( this, type );
this[ type ]();
result = dataPriv.get( this, type );
if ( result !== saved ) {
dataPriv.set( this, type, false );
// Cancel the outer synthetic event
event.stopImmediatePropagation();
event.preventDefault();
return result;
} else if ( maybeAsync ) {
// Cancel the outer synthetic event in expectation of a followup
event.stopImmediatePropagation();
event.preventDefault();
return;
} else {
dataPriv.set( this, type, false );
}
// If this is a native event triggered above, everything is now in order
// Fire an inner synthetic event with the original arguments
} else if ( !event.isTrigger && saved ) {
// ...and capture the result
dataPriv.set( this, type, jQuery.event.trigger(
// Support: IE <=9 - 11+
// Extend with the prototype to reset the above stopImmediatePropagation()
jQuery.extend( saved.shift(), jQuery.Event.prototype ),
saved,
this
) );
// Abort handling of the native event
event.stopImmediatePropagation();
}
}
} );
}
jQuery.removeEvent = function( elem, type, handle ) { jQuery.removeEvent = function( elem, type, handle ) {
// This "if" is needed for plain objects // This "if" is needed for plain objects

View File

@ -4,8 +4,8 @@ define( [
"./var/concat", "./var/concat",
"./var/isFunction", "./var/isFunction",
"./var/push", "./var/push",
"./var/rcheckableType",
"./core/access", "./core/access",
"./manipulation/var/rcheckableType",
"./manipulation/var/rtagName", "./manipulation/var/rtagName",
"./manipulation/var/rscriptType", "./manipulation/var/rscriptType",
"./manipulation/wrapMap", "./manipulation/wrapMap",
@ -24,8 +24,8 @@ define( [
"./traversing", "./traversing",
"./selector", "./selector",
"./event" "./event"
], function( jQuery, isAttached, concat, isFunction, push, access, ], function( jQuery, isAttached, concat, isFunction, push, rcheckableType,
rcheckableType, rtagName, rscriptType, access, rtagName, rscriptType,
wrapMap, getAll, setGlobalEval, buildFragment, support, wrapMap, getAll, setGlobalEval, buildFragment, support,
dataPriv, dataUser, acceptData, DOMEval, nodeName ) { dataPriv, dataUser, acceptData, DOMEval, nodeName ) {

View File

@ -1,7 +1,7 @@
define( [ define( [
"./core", "./core",
"./core/toType", "./core/toType",
"./manipulation/var/rcheckableType", "./var/rcheckableType",
"./var/isFunction", "./var/isFunction",
"./core/init", "./core/init",
"./traversing", // filter "./traversing", // filter

View File

@ -2383,12 +2383,14 @@ QUnit.test( "clone() delegated events (#11076)", function( assert ) {
clone.remove(); clone.remove();
} ); } );
QUnit.test( "checkbox state (#3827)", function( assert ) { QUnit.test( "checkbox state (trac-3827)", function( assert ) {
assert.expect( 9 ); assert.expect( 16 );
var markup = jQuery( "<div><input type=checkbox><div>" ).appendTo( "#qunit-fixture" ), var markup = jQuery( "<div class='parent'><input type=checkbox><div>" ),
cb = markup.find( "input" )[ 0 ]; cb = markup.find( "input" )[ 0 ];
markup.appendTo( "#qunit-fixture" );
jQuery( cb ).on( "click", function() { jQuery( cb ).on( "click", function() {
assert.equal( this.checked, false, "just-clicked checkbox is not checked" ); assert.equal( this.checked, false, "just-clicked checkbox is not checked" );
} ); } );
@ -2398,18 +2400,31 @@ QUnit.test( "checkbox state (#3827)", function( assert ) {
// Native click // Native click
cb.checked = true; cb.checked = true;
assert.equal( cb.checked, true, "native - checkbox is initially checked" ); assert.equal( cb.checked, true, "native event - checkbox is initially checked" );
cb.click(); cb.click();
assert.equal( cb.checked, false, "native - checkbox is no longer checked" ); assert.equal( cb.checked, false, "native event - checkbox is no longer checked" );
// jQuery click // jQuery click
cb.checked = true; cb.checked = true;
assert.equal( cb.checked, true, "jQuery - checkbox is initially checked" ); assert.equal( cb.checked, true, "jQuery event - checkbox is initially checked" );
jQuery( cb ).trigger( "click" ); jQuery( cb ).trigger( "click" );
assert.equal( cb.checked, false, "jQuery - checkbox is no longer checked" ); assert.equal( cb.checked, false, "jQuery event - checkbox is no longer checked" );
// Handlers only; checkbox state remains false // Handlers only; checkbox state remains false
jQuery( cb ).triggerHandler( "click" ); jQuery( cb ).triggerHandler( "click" );
assert.equal( cb.checked, false, "handlers only - checkbox is still unchecked" );
// Trigger parameters are preserved (trac-13353, gh-4139)
cb.checked = true;
assert.equal( cb.checked, true, "jQuery event with data - checkbox is initially checked" );
jQuery( cb ).on( "click", function( e, data ) {
assert.equal( data, "clicked", "trigger data passed to handler" );
} );
markup.on( "click", function( e, data ) {
assert.equal( data, "clicked", "trigger data passed to bubbled handler" );
} );
jQuery( cb ).trigger( "click", [ "clicked" ] );
assert.equal( cb.checked, false, "jQuery event with data - checkbox is no longer checked" );
} ); } );
QUnit.test( "event object properties on natively-triggered event", function( assert ) { QUnit.test( "event object properties on natively-triggered event", function( assert ) {
@ -2876,18 +2891,16 @@ QUnit.test(
QUnit.test( "originalEvent type of simulated event", function( assert ) { QUnit.test( "originalEvent type of simulated event", function( assert ) {
assert.expect( 2 ); assert.expect( 2 );
var done = assert.async(), var outer = jQuery(
outer = jQuery(
"<div id='donor-outer'>" + "<div id='donor-outer'>" +
"<form id='donor-form'>" + "<form id='donor-form'>" +
"<input id='donor-input' type='checkbox' />" + "<input id='donor-input' type='text' />" +
"</form>" + "</form>" +
"</div>" "</div>"
).appendTo( "#qunit-fixture" ), ).appendTo( "#qunit-fixture" ),
input = jQuery( "#donor-input" ), input = jQuery( "#donor-input" ),
expectedType = jQuery.support.focusin ? "focusin" : "focus", done = assert.async(),
finish = function() { finish = function() {
finish = null;
// Remove jQuery handlers to ensure removal of capturing handlers on the document // Remove jQuery handlers to ensure removal of capturing handlers on the document
outer.off( "focusin" ); outer.off( "focusin" );
@ -2897,23 +2910,15 @@ QUnit.test( "originalEvent type of simulated event", function( assert ) {
outer.on( "focusin", function( event ) { outer.on( "focusin", function( event ) {
assert.equal( event.type, "focusin", "focusin event at ancestor" ); assert.equal( event.type, "focusin", "focusin event at ancestor" );
assert.equal( event.originalEvent.type, expectedType, assert.equal( event.originalEvent.type, "click",
"focus event at ancestor has correct originalEvent type" ); "focus event at ancestor has correct originalEvent type" );
setTimeout( finish ); setTimeout( finish );
} ); } );
input.trigger( "focus" );
// DOM focus is unreliable in TestSwarm; set a simulated event workaround timeout input[ 0 ].addEventListener( "click", function( nativeEvent ) {
setTimeout( function() { jQuery.event.simulate( "focusin", this, jQuery.event.fix( nativeEvent ) );
if ( !finish ) { } );
return; input[ 0 ].click();
}
input[ 0 ].addEventListener( "click", function( nativeEvent ) {
expectedType = nativeEvent.type;
jQuery.event.simulate( "focusin", this, jQuery.event.fix( nativeEvent ) );
} );
input[ 0 ].click();
}, QUnit.config.testTimeout / 4 || 1000 );
} ); } );
QUnit.test( "trigger('click') on radio passes extra params", function( assert ) { QUnit.test( "trigger('click') on radio passes extra params", function( assert ) {
@ -2951,7 +2956,11 @@ QUnit.test( "Check order of focusin/focusout events", function( assert ) {
.on( "focus", function() { .on( "focus", function() {
focus = true; focus = true;
} ) } )
.on( "focusin", function() {
// PR gh-4279 fixed a lot of `focus`-related issues but made `focusin` fire twice.
// We've decided to accept this drawback for now. If it's fixed, change `one` to `on`
// in the following line:
.one( "focusin", function() {
assert.ok( !focus, "Focusin event should fire before focus does" ); assert.ok( !focus, "Focusin event should fire before focus does" );
focus = true; focus = true;
} ) } )
@ -2984,11 +2993,11 @@ QUnit.test( "focus-blur order (#12868)", function( assert ) {
var order, var order,
$text = jQuery( "#text1" ), $text = jQuery( "#text1" ),
$radio = jQuery( "#radio1" ).trigger( "focus" ), $radio = jQuery( "#radio1" ),
// Support: IE <=10 only // Support: IE <=9 - 11+
// IE8-10 fire focus/blur events asynchronously; this is the resulting mess. // focus and blur events are asynchronous; this is the resulting mess.
// IE's browser window must be topmost for this to work properly!! // The browser window must be topmost for this to work properly!!
done = assert.async(); done = assert.async();
$radio[ 0 ].focus(); $radio[ 0 ].focus();
@ -3035,3 +3044,97 @@ QUnit.test( "focus-blur order (#12868)", function( assert ) {
}, 50 ); }, 50 );
}, 50 ); }, 50 );
} ); } );
QUnit.test( "native-backed events preserve trigger data (gh-1741, gh-4139)", function( assert ) {
assert.expect( 17 );
var parent = supportjQuery(
"<div class='parent'><input type='checkbox'><input type='radio'></div>"
).appendTo( "#qunit-fixture" ),
targets = jQuery( parent[ 0 ].childNodes ),
checkbox = jQuery( targets[ 0 ] ),
data = [ "arg1", "arg2" ],
slice = data.slice,
// Support: IE <=9 - 11+
// focus and blur events are asynchronous; this is the resulting mess.
// The browser window must be topmost for this to work properly!!
done = assert.async();
// click (gh-4139)
assert.strictEqual( targets[ 0 ].checked, false, "checkbox unchecked before click" );
assert.strictEqual( targets[ 1 ].checked, false, "radio unchecked before click" );
targets.add( parent ).on( "click", function( event ) {
var type = event.target.type,
level = event.currentTarget === parent[ 0 ] ? "parent" : "";
assert.strictEqual( event.target.checked, true,
type + " toggled before invoking " + level + " handler" );
assert.deepEqual( slice.call( arguments, 1 ), data,
type + " " + level + " handler received correct data" );
} );
targets.trigger( "click", data );
assert.strictEqual( targets[ 0 ].checked, true,
"checkbox toggled after click (default action)" );
assert.strictEqual( targets[ 1 ].checked, true,
"radio toggled after event (default action)" );
// focus (gh-1741)
assert.notEqual( document.activeElement, checkbox[ 0 ],
"element not focused before focus event" );
checkbox.on( "focus blur", function( event ) {
var type = event.type;
assert.deepEqual( slice.call( arguments, 1 ), data,
type + " handler received correct data" );
} );
checkbox.trigger( "focus", data );
setTimeout( function() {
assert.strictEqual( document.activeElement, checkbox[ 0 ],
"element focused after focus event (default action)" );
checkbox.trigger( "blur", data );
setTimeout( function() {
assert.notEqual( document.activeElement, checkbox[ 0 ],
"element not focused after blur event (default action)" );
done();
}, 50 );
}, 50 );
} );
// TODO replace with an adaptation of
// https://github.com/jquery/jquery/pull/1367/files#diff-a215316abbaabdf71857809e8673ea28R2464
( function() {
supportjQuery.each(
{
checkbox: "<input type='checkbox'>",
radio: "<input type='radio'>"
},
makeTestFor3751
);
function makeTestFor3751( type, html ) {
var testName = "native-backed namespaced clicks are handled correctly (gh-3751) - " + type;
QUnit.test( testName, function( assert ) {
assert.expect( 2 );
var parent = supportjQuery( "<div class='parent'>" + html + "</div>" ),
target = jQuery( parent[ 0 ].firstChild );
parent.appendTo( "#qunit-fixture" );
target.add( parent )
.on( "click.notFired", function( event ) {
assert.ok( false, "namespaced event should not be received" +
" by wrong-namespace listener at " + event.currentTarget.nodeName );
} )
.on( "click.fired", function( event ) {
assert.equal( event.target.checked, true,
"toggled before invoking handler at " + event.currentTarget.nodeName );
} )
.on( "click", function( event ) {
assert.ok( false, "namespaced event should not be received" +
" by non-namespaced listener at " + event.currentTarget.nodeName );
} );
target.trigger( "click.fired" );
} );
}
} )();