diff options
-rw-r--r-- | src/event.js | 102 | ||||
-rw-r--r-- | test/unit/event.js | 322 |
2 files changed, 258 insertions, 166 deletions
diff --git a/src/event.js b/src/event.js index 0c66d3856..95221f475 100644 --- a/src/event.js +++ b/src/event.js @@ -1,9 +1,9 @@ import jQuery from "./core.js"; -import document from "./var/document.js"; import documentElement from "./var/documentElement.js"; import rnothtmlwhite from "./var/rnothtmlwhite.js"; import rcheckableType from "./var/rcheckableType.js"; import slice from "./var/slice.js"; +import isIE from "./var/isIE.js"; import acceptData from "./data/var/acceptData.js"; import dataPriv from "./data/var/dataPriv.js"; import nodeName from "./core/nodeName.js"; @@ -21,16 +21,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 === document.activeElement ) === ( type === "focus" ); -} - function on( elem, types, selector, data, fn, one ) { var origFn, type; @@ -459,7 +449,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 @@ -511,10 +501,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 ); } @@ -526,15 +516,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 @@ -543,28 +531,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 = {}; - } + dataPriv.set( this, type, false ); + 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 @@ -582,16 +559,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 ), - saved.slice( 1 ), - this - ) - } ); + dataPriv.set( this, type, jQuery.event.trigger( + saved[ 0 ], + saved.slice( 1 ), + this + ) ); // Abort handling of the native event event.stopImmediatePropagation(); @@ -724,6 +696,29 @@ jQuery.each( { }, jQuery.event.addProp ); jQuery.each( { focus: "focusin", blur: "focusout" }, function( type, delegateType ) { + + // 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. + function focusMappedHandler( nativeEvent ) { + + // `eventHandle` would already wrap the event, but we need to change the `type` here. + var event = jQuery.event.fix( nativeEvent ); + event.type = nativeEvent.type === "focusin" ? "focus" : "blur"; + event.isSimulated = true; + + // 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. + dataPriv.get( this, "handle" )( event ); + } + } + jQuery.event.special[ type ] = { // Utilize native event if possible so blur/focus sequence is correct @@ -732,10 +727,15 @@ jQuery.each( { focus: "focusin", blur: "focusout" }, function( type, delegateTyp // Claim the first handler // dataPriv.set( this, "focus", ... ) // dataPriv.set( this, "blur", ... ) - leverageNative( this, type, expectSync ); + leverageNative( this, type, true ); + + if ( isIE ) { + this.addEventListener( delegateType, focusMappedHandler ); + } else { - // Return false to allow normal processing in the caller - return false; + // Return false to allow normal processing in the caller + return false; + } }, trigger: function() { @@ -746,6 +746,16 @@ jQuery.each( { focus: "focusin", blur: "focusout" }, function( type, delegateTyp return true; }, + teardown: function() { + if ( isIE ) { + this.removeEventListener( delegateType, focusMappedHandler ); + } 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 ) { diff --git a/test/unit/event.js b/test/unit/event.js index 8ca06ced8..aa55c06a1 100644 --- a/test/unit/event.js +++ b/test/unit/event.js @@ -2172,12 +2172,12 @@ QUnit.test( "focusin bubbles", function( assert ) { // Removed since DOM focus is unreliable on test swarm // DOM focus method -// input[0].focus(); +// input[ 0 ].focus(); // To make the next focus test work, we need to take focus off the input. // This will fire another focusin event, so set order to reflect that. // order = 1; -// jQuery("#text1")[0].focus(); +// jQuery( "#text1" )[ 0 ].focus(); // jQuery trigger, which calls DOM focus order = 0; @@ -2187,6 +2187,42 @@ QUnit.test( "focusin bubbles", function( assert ) { jQuery( "body" ).off( "focusin.focusinBubblesTest" ); } ); +QUnit.test( "focus does not bubble", function( assert ) { + assert.expect( 1 ); + + var done = assert.async(), + input = jQuery( "<input type='text' />" ).prependTo( "body" ); + + // focus the element so DOM focus won't fire + input[ 0 ].focus(); + + jQuery( "body" ).on( "focus.focusDoesNotBubbleTest", function() { + assert.ok( false, "focus doesn't fire on body" ); + } ); + + input.on( "focus.focusDoesNotBubbleTest", function() { + assert.ok( true, "focus on the element" ); + } ); + +// Removed since DOM focus is unreliable on test swarm + // DOM focus method +// input[ 0 ].focus(); + + // To make the next focus test work, we need to take focus off the input. + // This will fire another focusin event, so set order to reflect that. +// jQuery( "#text1" )[ 0 ].focus(); + + // jQuery trigger, which calls DOM focus + input.trigger( "focus" ); + + input.remove(); + jQuery( "body" ).off( "focus.focusDoesNotBubbleTest" ); + + setTimeout( function() { + done(); + }, 50 ); +} ); + QUnit.test( "custom events with colons (trac-3533, trac-8272)", function( assert ) { assert.expect( 1 ); @@ -2652,6 +2688,10 @@ QUnit.test( "element removed during focusout (gh-4417)", function( assert ) { button[ 0 ].blur = function() { jQuery.cleanData( [ this ] ); this.parentNode.removeChild( this ); + + // Redefine `blur` to avoid a hard crash in Karma tests that stop + // the test runner in case this test fails. + this.blur = jQuery.noop; }; button[ 0 ].click(); @@ -3067,12 +3107,7 @@ QUnit.test( "focusout/focusin support", function( assert ) { var focus, parent = jQuery( "<div>" ), input = jQuery( "<input>" ), - inputExternal = jQuery( "<input>" ), - - // 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(); + inputExternal = jQuery( "<input>" ); parent.append( input ); jQuery( "#qunit-fixture" ).append( parent ).append( inputExternal ); @@ -3080,61 +3115,54 @@ QUnit.test( "focusout/focusin support", function( assert ) { // initially, lose focus inputExternal[ 0 ].focus(); - setTimeout( function() { - parent - .on( "focus", function() { - assert.ok( false, "parent: focus not fired" ); - } ) - .on( "focusin", function() { - assert.ok( true, "parent: focusin fired" ); - } ) - .on( "blur", function() { - assert.ok( false, "parent: blur not fired" ); - } ) - .on( "focusout", function() { - assert.ok( true, "parent: focusout fired" ); - } ); - - input - .on( "focus", function() { - assert.ok( true, "element: focus fired" ); - } ) - .on( "focusin", function() { - assert.ok( true, "element: focusin fired" ); - focus = true; - } ) - .on( "blur", function() { - assert.ok( true, "parent: blur fired" ); - } ) - .on( "focusout", function() { - assert.ok( true, "element: focusout fired" ); - } ); - - // gain focus - input[ 0 ].focus(); + parent + .on( "focus", function() { + assert.ok( false, "parent: focus not fired" ); + } ) + .on( "focusin", function() { + assert.ok( true, "parent: focusin fired" ); + } ) + .on( "blur", function() { + assert.ok( false, "parent: blur not fired" ); + } ) + .on( "focusout", function() { + assert.ok( true, "parent: focusout fired" ); + } ); - // then lose it - inputExternal[ 0 ].focus(); + input + .on( "focus", function() { + assert.ok( true, "element: focus fired" ); + } ) + .on( "focusin", function() { + assert.ok( true, "element: focusin fired" ); + focus = true; + } ) + .on( "blur", function() { + assert.ok( true, "parent: blur fired" ); + } ) + .on( "focusout", function() { + assert.ok( true, "element: focusout fired" ); + } ); - setTimeout( function() { + // gain focus + input[ 0 ].focus(); - // DOM focus is unreliable in TestSwarm - if ( QUnit.isSwarm && !focus ) { - assert.ok( true, "GAP: Could not observe focus change" ); - assert.ok( true, "GAP: Could not observe focus change" ); - assert.ok( true, "GAP: Could not observe focus change" ); - assert.ok( true, "GAP: Could not observe focus change" ); - assert.ok( true, "GAP: Could not observe focus change" ); - assert.ok( true, "GAP: Could not observe focus change" ); - } + // then lose it + inputExternal[ 0 ].focus(); - // cleanup - parent.off(); - input.off(); + // DOM focus is unreliable in TestSwarm + if ( QUnit.isSwarm && !focus ) { + assert.ok( true, "GAP: Could not observe focus change" ); + assert.ok( true, "GAP: Could not observe focus change" ); + assert.ok( true, "GAP: Could not observe focus change" ); + assert.ok( true, "GAP: Could not observe focus change" ); + assert.ok( true, "GAP: Could not observe focus change" ); + assert.ok( true, "GAP: Could not observe focus change" ); + } - done(); - }, 50 ); - }, 50 ); + // cleanup + parent.off(); + input.off(); } ); QUnit.test( "focus-blur order (trac-12868)", function( assert ) { @@ -3142,56 +3170,45 @@ QUnit.test( "focus-blur order (trac-12868)", function( assert ) { var order, $text = jQuery( "#text1" ), - $radio = jQuery( "#radio1" ), - - // 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(); + $radio = jQuery( "#radio1" ); $radio[ 0 ].focus(); - setTimeout( function() { - - $text - .on( "focus", function() { - assert.equal( order++, 1, "text focus" ); - } ) - .on( "blur", function() { - assert.equal( order++, 0, "text blur" ); - } ); - $radio - .on( "focus", function() { - assert.equal( order++, 1, "radio focus" ); - } ) - .on( "blur", function() { - assert.equal( order++, 0, "radio blur" ); - } ); + $text + .on( "focus", function() { + assert.equal( order++, 1, "text focus" ); + } ) + .on( "blur", function() { + assert.equal( order++, 0, "text blur" ); + } ); + $radio + .on( "focus", function() { + assert.equal( order++, 1, "radio focus" ); + } ) + .on( "blur", function() { + assert.equal( order++, 0, "radio blur" ); + } ); - // Enabled input getting focus - order = 0; - assert.equal( document.activeElement, $radio[ 0 ], "radio has focus" ); - $text.trigger( "focus" ); - setTimeout( function() { + // Enabled input getting focus + order = 0; + assert.equal( document.activeElement, $radio[ 0 ], "radio has focus" ); + $text.trigger( "focus" ); - // DOM focus is unreliable in TestSwarm - if ( QUnit.isSwarm && order === 0 ) { - assert.ok( true, "GAP: Could not observe focus change" ); - assert.ok( true, "GAP: Could not observe focus change" ); - } + // DOM focus is unreliable in TestSwarm + if ( QUnit.isSwarm && order === 0 ) { + assert.ok( true, "GAP: Could not observe focus change" ); + assert.ok( true, "GAP: Could not observe focus change" ); + } - assert.equal( document.activeElement, $text[ 0 ], "text has focus" ); + assert.equal( document.activeElement, $text[ 0 ], "text has focus" ); - // Run handlers without native method on an input - order = 1; - $radio.triggerHandler( "focus" ); + // Run handlers without native method on an input + order = 1; + $radio.triggerHandler( "focus" ); - // Clean up - $text.off(); - $radio.off(); - done(); - }, 50 ); - }, 50 ); + // Clean up + $text.off(); + $radio.off(); } ); QUnit.test( "Event handling works with multiple async focus events (gh-4350)", function( assert ) { @@ -3199,10 +3216,6 @@ QUnit.test( "Event handling works with multiple async focus events (gh-4350)", f var remaining = 3, input = jQuery( "#name" ), - - // 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(); input @@ -3212,6 +3225,17 @@ QUnit.test( "Event handling works with multiple async focus events (gh-4350)", f if ( remaining > 0 ) { input.trigger( "blur" ); } else { + + if ( QUnit.isIE ) { + + // Support: <=IE 11+ + // In IE, one of the blurs sometimes triggers a focus on body + // which in turn restores focus to the input, leading to 4 assertions + // firing instead of three. This only happens if other tests are + // running on the same test page. Avoid this issue in tests by removing + // the handler early. + input.off( "focus" ); + } done(); } } ) @@ -3237,6 +3261,45 @@ QUnit.test( "Event handling works with multiple async focus events (gh-4350)", f } ); } ); +// Support: IE <=9 - 11+ +// focus and blur events are asynchronous. +// The browser window must be topmost for this to work properly!! +QUnit.test( "async focus queues properly (gh-4859)", function( assert ) { + assert.expect( 1 ); + + var $text = jQuery( "#text1" ), + $radio = jQuery( "#radio1" ), + done = assert.async(); + + $text.trigger( "focus" ); + $radio.trigger( "focus" ); + $text.trigger( "focus" ); + + setTimeout( function() { + assert.equal( document.activeElement, $text[ 0 ], "focus follows the last trigger" ); + done(); + }, 500 ); +} ); + +// Support: IE <=9 - 11+ +// focus and blur events are asynchronous. +// The browser window must be topmost for this to work properly!! +QUnit.test( "async focus queues properly with blur (gh-4856)", function( assert ) { + assert.expect( 1 ); + + var $text = jQuery( "#text1" ), + done = assert.async(); + + $text.trigger( "focus" ); + $text.trigger( "blur" ); + $text.trigger( "focus" ); + + setTimeout( function() { + assert.equal( document.activeElement, $text[ 0 ], "focus-after-blur is respected" ); + done(); + }, 500 ); +} ); + QUnit.test( "native-backed events preserve trigger data (gh-1741, gh-4139)", function( assert ) { assert.expect( 17 ); @@ -3246,12 +3309,7 @@ QUnit.test( "native-backed events preserve trigger data (gh-1741, gh-4139)", fun 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(); + slice = data.slice; // click (gh-4139) assert.strictEqual( targets[ 0 ].checked, false, "checkbox unchecked before click" ); @@ -3277,18 +3335,26 @@ QUnit.test( "native-backed events preserve trigger data (gh-1741, gh-4139)", fun var type = event.type; assert.deepEqual( slice.call( arguments, 1 ), data, type + " handler received correct data" ); + + if ( QUnit.isIE && type === "focus" ) { + + // Support: <=IE 11+ + // In IE, one of the blurs sometimes triggers a focus on body + // which in turn restores focus to the input, leading to 4 assertions + // firing instead of three. This only happens if other tests are + // running on the same test page. Avoid this issue in tests by removing + // the handler early. + checkbox.off( "focus" ); + } } ); 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 ); + + assert.strictEqual( document.activeElement, checkbox[ 0 ], + "element focused after focus event (default action)" ); + checkbox.trigger( "blur", data ); + + assert.notEqual( document.activeElement, checkbox[ 0 ], + "element not focused after blur event (default action)" ); } ); QUnit.test( "focus change during a focus handler (gh-4382)", function( assert ) { @@ -3341,6 +3407,22 @@ QUnit.test( "trigger(focus) works after .on(focus).off(focus) (gh-4867)", functi assert.equal( document.activeElement, input[ 0 ], "input has focus" ); } ); +QUnit.test( "trigger(focus) works after focusing when hidden (gh-4950)", function( assert ) { + assert.expect( 1 ); + + var input = jQuery( "<input />" ); + + input.appendTo( "#qunit-fixture" ); + + input + .css( "display", "none" ) + .trigger( "focus" ) + .css( "display", "" ) + .trigger( "focus" ); + + assert.equal( document.activeElement, input[ 0 ], "input has focus" ); +} ); + // TODO replace with an adaptation of // https://github.com/jquery/jquery/pull/1367/files#diff-a215316abbaabdf71857809e8673ea28R2464 ( function() { |