Bug 687787: support focusin/focusout based on webkit/blink r=smaug draft
authorKevin Wern <kevin.m.wern@gmail.com>
Thu, 06 Oct 2016 21:39:53 -0400
changeset 421881 4b82765e37680bac102827107a2c933e12ef6158
parent 421880 58286ce1d3d105ba3e441ddf1e65d8682cd1c52f
child 533193 cce54bf13c5914a7f5ab3bea75b5bb1a8783990e
push id31622
push userbmo:kevin.m.wern@gmail.com
push dateFri, 07 Oct 2016 01:48:48 +0000
reviewerssmaug
bugs687787
milestone52.0a1
Bug 687787: support focusin/focusout based on webkit/blink r=smaug Blink and webkit launch focusin after focus and focusout after blur. Despite this contradiction with the spec, it is best to mirror this new way, as there is little guidance or existing code to clarify implementation amiguities that can arise from the spec. If focus/blur changes the currently focused item, do not not fire the corresponding focusin/focusout. Otherwise, always fire the corresponding event. Additionally, add a mochitest and a w3c-platform-test. MozReview-Commit-ID: AgQ8JBxKIqK
dom/base/nsFocusManager.cpp
dom/base/nsFocusManager.h
dom/base/nsGkAtomList.h
dom/events/EventNameList.h
dom/events/test/mochitest.ini
dom/events/test/test_bug687787.html
dom/webidl/EventHandler.webidl
testing/web-platform/meta/MANIFEST.json
testing/web-platform/tests/uievents/order-of-events/focus-events/focus-automated-blink-webkit.html
widget/EventMessageList.h
widget/nsBaseWidget.cpp
--- a/dom/base/nsFocusManager.cpp
+++ b/dom/base/nsFocusManager.cpp
@@ -2016,44 +2016,90 @@ public:
   nsCOMPtr<nsISupports>   mTarget;
   RefPtr<nsPresContext> mContext;
   EventMessage            mEventMessage;
   bool                    mWindowRaised;
   bool                    mIsRefocus;
   nsCOMPtr<EventTarget>   mRelatedTarget;
 };
 
+class FocusInOutEvent : public Runnable
+{
+public:
+  FocusInOutEvent(nsISupports* aTarget, EventMessage aEventMessage,
+                 nsPresContext* aContext, EventTarget* aRelatedTarget)
+    : mTarget(aTarget)
+    , mContext(aContext)
+    , mEventMessage(aEventMessage)
+    , mRelatedTarget(aRelatedTarget)
+  {
+  }
+
+  NS_IMETHOD Run()
+  {
+    InternalFocusEvent event(true, mEventMessage);
+    event.mFlags.mBubbles = true;
+    event.mFlags.mCancelable = false;
+    event.mRelatedTarget = mRelatedTarget;
+    return EventDispatcher::Dispatch(mTarget, mContext, &event);
+  }
+
+  nsCOMPtr<nsISupports>   mTarget;
+  RefPtr<nsPresContext> mContext;
+  EventMessage            mEventMessage;
+  nsCOMPtr<EventTarget>   mRelatedTarget;
+};
+
 static nsIDocument*
 GetDocumentHelper(EventTarget* aTarget)
 {
   nsCOMPtr<nsINode> node = do_QueryInterface(aTarget);
   if (!node) {
     nsCOMPtr<nsPIDOMWindowInner> win = do_QueryInterface(aTarget);
     return win ? win->GetExtantDoc() : nullptr;
   }
 
   return node->OwnerDoc();
 }
 
+void nsFocusManager::SendFocusInOrOutEvent(EventMessage aEventMessage,
+                                     nsIPresShell* aPresShell,
+                                     nsISupports* aTarget,
+                                     EventTarget* aRelatedTarget)
+{
+  NS_ASSERTION(aEventMessage == eFocusIn || aEventMessage == eFocusOut,
+      "Wrong event type for SendFocusInOrOutEvent");
+
+  nsContentUtils::AddScriptRunner(
+      new FocusInOutEvent(
+        aTarget,
+        aEventMessage,
+        aPresShell->GetPresContext(),
+        aRelatedTarget));
+}
+
 void
 nsFocusManager::SendFocusOrBlurEvent(EventMessage aEventMessage,
                                      nsIPresShell* aPresShell,
                                      nsIDocument* aDocument,
                                      nsISupports* aTarget,
                                      uint32_t aFocusMethod,
                                      bool aWindowRaised,
                                      bool aIsRefocus,
                                      EventTarget* aRelatedTarget)
 {
   NS_ASSERTION(aEventMessage == eFocus || aEventMessage == eBlur,
                "Wrong event type for SendFocusOrBlurEvent");
 
   nsCOMPtr<EventTarget> eventTarget = do_QueryInterface(aTarget);
   nsCOMPtr<nsIDocument> eventTargetDoc = GetDocumentHelper(eventTarget);
   nsCOMPtr<nsIDocument> relatedTargetDoc = GetDocumentHelper(aRelatedTarget);
+  nsCOMPtr<nsPIDOMWindowOuter> currentWindow = mFocusedWindow;
+  nsCOMPtr<nsIContent> currentFocusedContent = currentWindow ?
+      currentWindow->GetFocusedNode() : nullptr;
 
   // set aRelatedTarget to null if it's not in the same document as eventTarget
   if (eventTargetDoc != relatedTargetDoc) {
     aRelatedTarget = nullptr;
   }
 
   bool dontDispatchEvent =
     eventTargetDoc && nsContentUtils::IsUserFocusIgnored(eventTargetDoc);
@@ -2094,16 +2140,30 @@ nsFocusManager::SendFocusOrBlurEvent(Eve
     }
   }
 #endif
 
   if (!dontDispatchEvent) {
     nsContentUtils::AddScriptRunner(
       new FocusBlurEvent(aTarget, aEventMessage, aPresShell->GetPresContext(),
                          aWindowRaised, aIsRefocus, aRelatedTarget));
+      nsCOMPtr<nsIContent> newFocus = currentWindow ? currentWindow->GetFocusedNode() : nullptr;
+
+      if (currentFocusedContent == newFocus) {
+        EventMessage focusInOrOutMessage;
+
+        if (aEventMessage == eFocus) {
+          focusInOrOutMessage = eFocusIn;
+        }
+        else {
+          focusInOrOutMessage = eFocusOut;
+        }
+        SendFocusInOrOutEvent(focusInOrOutMessage, aPresShell, aTarget,
+            aRelatedTarget);
+      }
   }
 }
 
 void
 nsFocusManager::ScrollIntoView(nsIPresShell* aPresShell,
                                nsIContent* aContent,
                                uint32_t aFlags)
 {
--- a/dom/base/nsFocusManager.h
+++ b/dom/base/nsFocusManager.h
@@ -292,16 +292,32 @@ protected:
                             nsIDocument* aDocument,
                             nsISupports* aTarget,
                             uint32_t aFocusMethod,
                             bool aWindowRaised,
                             bool aIsRefocus = false,
                             mozilla::dom::EventTarget* aRelatedTarget = nullptr);
 
   /**
+   *  Send a focusin or focusout event
+   *
+   *  aEventMessage should be either eFocusIn or eFocusOut.
+   *
+   *  aTarget is the content the event will fire on (the object to be
+   *  focused for focusin, the object to be blurred for focusout).
+   *
+   *  aRelatedTarget is the content related to the event (the object
+   *  losing focus for focusin, the object getting focus for focusout).
+   */
+  void SendFocusInOrOutEvent(mozilla::EventMessage aEventMessage,
+                             nsIPresShell* aPresShell,
+                             nsISupports* aTarget,
+                             mozilla::dom::EventTarget* aRelatedTarget = nullptr);
+
+  /**
    * Scrolls aContent into view unless the FLAG_NOSCROLL flag is set.
    */
   void ScrollIntoView(nsIPresShell* aPresShell,
                       nsIContent* aContent,
                       uint32_t aFlags);
 
   /**
    * Raises the top-level window aWindow at the widget level.
--- a/dom/base/nsGkAtomList.h
+++ b/dom/base/nsGkAtomList.h
@@ -790,16 +790,18 @@ GK_ATOM(onenterpincodereq, "onenterpinco
 GK_ATOM(onemergencycbmodechange, "onemergencycbmodechange")
 GK_ATOM(onerror, "onerror")
 GK_ATOM(onevicted, "onevicted")
 GK_ATOM(onfacesdetected, "onfacesdetected")
 GK_ATOM(onfailed, "onfailed")
 GK_ATOM(onfetch, "onfetch")
 GK_ATOM(onfinish, "onfinish")
 GK_ATOM(onfocus, "onfocus")
+GK_ATOM(onfocusin, "onfocusin")
+GK_ATOM(onfocusout, "onfocusout")
 GK_ATOM(onfrequencychange, "onfrequencychange")
 GK_ATOM(onfullscreenchange, "onfullscreenchange")
 GK_ATOM(onfullscreenerror, "onfullscreenerror")
 GK_ATOM(onspeakerforcedchange, "onspeakerforcedchange")
 GK_ATOM(onget, "onget")
 GK_ATOM(ongroupchange, "ongroupchange")
 GK_ATOM(onhashchange, "onhashchange")
 GK_ATOM(onheadphoneschange, "onheadphoneschange")
--- a/dom/events/EventNameList.h
+++ b/dom/events/EventNameList.h
@@ -494,16 +494,24 @@ FORWARDED_EVENT(blur,
 ERROR_EVENT(error,
             eLoadError,
             EventNameType_All,
             eBasicEventClass)
 FORWARDED_EVENT(focus,
                 eFocus,
                 EventNameType_HTMLXUL,
                 eFocusEventClass)
+FORWARDED_EVENT(focusin,
+                eFocusIn,
+                EventNameType_HTMLXUL,
+                eFocusEventClass)
+FORWARDED_EVENT(focusout,
+                eFocusOut,
+                EventNameType_HTMLXUL,
+                eFocusEventClass)
 FORWARDED_EVENT(load,
                 eLoad,
                 EventNameType_All,
                 eBasicEventClass)
 FORWARDED_EVENT(resize,
                 eResize,
                 EventNameType_All,
                 eBasicEventClass)
--- a/dom/events/test/mochitest.ini
+++ b/dom/events/test/mochitest.ini
@@ -196,8 +196,9 @@ support-files =
 [test_eventhandler_scoping.html]
 [test_bug1013412.html]
 skip-if = buildapp == 'b2g' # no wheel events on b2g
 [test_dom_activate_event.html]
 [test_bug1264380.html]
 run-if = (e10s && os != "win") # Bug 1270043, crash at windows platforms; Bug1264380 comment 20, nsDragService::InvokeDragSessionImpl behaves differently among platform implementations in non-e10s mode which prevents us to check the validity of nsIDragService::getCurrentSession() consistently via synthesize mouse clicks in non-e10s mode.
 [test_passive_listeners.html]
 [test_paste_image.html]
+[test_bug687787.html]
new file mode 100644
--- /dev/null
+++ b/dom/events/test/test_bug687787.html
@@ -0,0 +1,636 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=687787
+-->
+<head>
+  <title>Test for Bug 687787</title>
+  <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=687787">Mozilla Bug 687787</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var content = document.getElementById('content');
+var eventStack = [];
+
+function _doc_callback(e){
+    var event = {'type' : e.type, 'target' : e.target, 'relatedTarget' : e.relatedTarget }
+    eventStack.push(event);
+}
+
+function clearEventStack(){
+    eventStack = [];
+}
+
+document.addEventListener("focus", _doc_callback, true);
+document.addEventListener("focusin", _doc_callback, true);
+document.addEventListener("focusout", _doc_callback, true);
+document.addEventListener("blur", _doc_callback, true);
+
+function CompareEventToExpected(e, expected) {
+    if (expected == null || e == null)
+        return false;
+    if (e.type == expected.type && e.target == expected.target && e.relatedTarget == expected.relatedTarget)
+        return true;
+    return false;
+}
+
+function TestEventOrderNormal() {
+
+    var input1 = document.createElement('input');
+    var input2 = document.createElement('input');
+    var input3 = document.createElement('input');
+    var content = document.getElementById('content');
+
+    input1.setAttribute('id', 'input1');
+    input2.setAttribute('id', 'input2');
+    input3.setAttribute('id', 'input3');
+    input1.setAttribute('type', 'text');
+    input2.setAttribute('type', 'text');
+    input3.setAttribute('type', 'text');
+
+    content.appendChild(input1);
+    content.appendChild(input2);
+    content.appendChild(input3);
+    content.style.display = 'block'
+
+    expectedEventOrder = [
+        {'type' : 'blur',
+        'target' : input1,
+        'relatedTarget' : input2},
+        {'type' : 'focusout',
+        'target' : input1,
+        'relatedTarget' : input2},
+        {'type' : 'focus',
+        'target' : input2,
+        'relatedTarget' : input1},
+        {'type' : 'focusin',
+        'target' : input2,
+        'relatedTarget' : input1},
+        {'type' : 'blur',
+        'target' : input2,
+        'relatedTarget' : input3},
+        {'type' : 'focusout',
+        'target' : input2,
+        'relatedTarget' : input3},
+        {'type' : 'focus',
+        'target' : input3,
+        'relatedTarget' : input2},
+        {'type' : 'focusin',
+        'target' : input3,
+        'relatedTarget' : input2},
+    ]
+
+    input1.focus();
+    clearEventStack();
+
+    input2.focus();
+    input3.focus();
+
+    for (var i = 0; i < expectedEventOrder.length || i < eventStack.length ; i++) {
+        ok(CompareEventToExpected(expectedEventOrder[i], eventStack[i]), 'Normal event order is correct: Event ' + i + ': '
+                + 'Expected ('
+                    + expectedEventOrder[i].type + ','
+                    + (expectedEventOrder[i].target ? expectedEventOrder[i].target.id : null) + ','
+                    + (expectedEventOrder[i].relatedTarget ? expectedEventOrder[i].relatedTarget.id : null) + '), '
+                + 'Actual ('
+                    + eventStack[i].type + ','
+                    + (eventStack[i].target ? eventStack[i].target.id : null) + ','
+                    + (eventStack[i].relatedTarget ? eventStack[i].relatedTarget.id : null) + ')');
+    }
+
+    while (content.firstChild) {
+        content.removeChild(content.firstChild);
+    }
+}
+
+function TestEventOrderNormalFiresAtRightTime() {
+
+    var input1 = document.createElement('input');
+    var input2 = document.createElement('input');
+    var input3 = document.createElement('input');
+    var content = document.getElementById('content');
+
+    input1.setAttribute('id', 'input1');
+    input2.setAttribute('id', 'input2');
+    input3.setAttribute('id', 'input3');
+    input1.setAttribute('type', 'text');
+    input2.setAttribute('type', 'text');
+    input3.setAttribute('type', 'text');
+
+    input1.onblur = function(e)
+    {
+        ok(document.activeElement == document.body, 'input1: not focused when blur fires')
+    }
+
+    input1.onfocusout = function(e)
+    {
+        ok(document.activeElement == document.body, 'input1: not focused when focusout fires')
+    }
+
+    input2.onfocus = function(e)
+    {
+        ok(document.activeElement == input2, 'input2: focused when focus fires')
+    }
+
+    input2.onfocusin = function(e)
+    {
+        ok(document.activeElement == input2, 'input2: focused when focusin fires')
+    }
+
+    content.appendChild(input1);
+    content.appendChild(input2);
+    content.style.display = 'block'
+
+    expectedEventOrder = [
+        {'type' : 'blur',
+        'target' : input1,
+        'relatedTarget' : input2},
+        {'type' : 'focusout',
+        'target' : input1,
+        'relatedTarget' : input2},
+        {'type' : 'focus',
+        'target' : input2,
+        'relatedTarget' : input1},
+        {'type' : 'focusin',
+        'target' : input2,
+        'relatedTarget' : input1},
+    ]
+
+    input1.focus();
+    clearEventStack();
+
+    input2.focus();
+
+    for (var i = 0; i < expectedEventOrder.length || i < eventStack.length ; i++) {
+        ok(CompareEventToExpected(expectedEventOrder[i], eventStack[i]), 'Normal event order is correct: Event ' + i + ': '
+                + 'Expected ('
+                    + expectedEventOrder[i].type + ','
+                    + (expectedEventOrder[i].target ? expectedEventOrder[i].target.id : null) + ','
+                    + (expectedEventOrder[i].relatedTarget ? expectedEventOrder[i].relatedTarget.id : null) + '), '
+                + 'Actual ('
+                    + eventStack[i].type + ','
+                    + (eventStack[i].target ? eventStack[i].target.id : null) + ','
+                    + (eventStack[i].relatedTarget ? eventStack[i].relatedTarget.id : null) + ')');
+    }
+
+    while (content.firstChild) {
+        content.removeChild(content.firstChild);
+    }
+}
+
+function TestFocusOutRedirectsFocus() {
+
+    var input1 = document.createElement('input');
+    var input2 = document.createElement('input');
+    var input3 = document.createElement('input');
+    var content = document.getElementById('content');
+
+    input1.setAttribute('id', 'input1');
+    input2.setAttribute('id', 'input2');
+    input3.setAttribute('id', 'input3');
+    input1.setAttribute('type', 'text');
+    input2.setAttribute('type', 'text');
+    input3.setAttribute('type', 'text');
+    input1.onfocusout = function () {
+        input3.focus();
+    }
+
+    content.appendChild(input1);
+    content.appendChild(input2);
+    content.appendChild(input3);
+    content.style.display = 'block'
+
+    expectedEventOrder = [
+        {'type' : 'blur',
+        'target' : input1,
+        'relatedTarget' : input2},
+        {'type' : 'focusout',
+        'target' : input1,
+        'relatedTarget' : input2},
+        {'type' : 'focus',
+        'target' : input3,
+        'relatedTarget' : null},
+        {'type' : 'focusin',
+        'target' : input3,
+        'relatedTarget' : null},
+    ]
+
+    input1.focus();
+    clearEventStack();
+    input2.focus();
+
+    for (var i = 0; i < expectedEventOrder.length || i < eventStack.length; i++) {
+        ok(CompareEventToExpected(expectedEventOrder[i], eventStack[i]), 'Normal event order is correct: Event ' + i + ': '
+                + 'Expected ('
+                    + expectedEventOrder[i].type + ','
+                    + (expectedEventOrder[i].target ? expectedEventOrder[i].target.id : null) + ','
+                    + (expectedEventOrder[i].relatedTarget ? expectedEventOrder[i].relatedTarget.id : null) + '), '
+                + 'Actual ('
+                    + eventStack[i].type + ','
+                    + (eventStack[i].target ? eventStack[i].target.id : null) + ','
+                    + (eventStack[i].relatedTarget ? eventStack[i].relatedTarget.id : null) + ')');
+    }
+
+    while (content.firstChild) {
+        content.removeChild(content.firstChild);
+    }
+}
+
+function TestFocusInRedirectsFocus() {
+
+    var input1 = document.createElement('input');
+    var input2 = document.createElement('input');
+    var input3 = document.createElement('input');
+    var content = document.getElementById('content');
+
+    input1.setAttribute('id', 'input1');
+    input2.setAttribute('id', 'input2');
+    input3.setAttribute('id', 'input3');
+    input1.setAttribute('type', 'text');
+    input2.setAttribute('type', 'text');
+    input3.setAttribute('type', 'text');
+    input2.onfocusin = function () {
+        input3.focus();
+    }
+
+    content.appendChild(input1);
+    content.appendChild(input2);
+    content.appendChild(input3);
+    content.style.display = 'block'
+
+    expectedEventOrder = [
+        {'type' : 'blur',
+        'target' : input1,
+        'relatedTarget' : input2},
+        {'type' : 'focusout',
+        'target' : input1,
+        'relatedTarget' : input2},
+        {'type' : 'focus',
+        'target' : input2,
+        'relatedTarget' : input1},
+        {'type' : 'focusin',
+        'target' : input2,
+        'relatedTarget' : input1},
+        {'type' : 'blur',
+        'target' : input2,
+        'relatedTarget' : input3},
+        {'type' : 'focusout',
+        'target' : input2,
+        'relatedTarget' : input3},
+        {'type' : 'focus',
+        'target' : input3,
+        'relatedTarget' : input2},
+        {'type' : 'focusin',
+        'target' : input3,
+        'relatedTarget' : input2},
+    ]
+
+    input1.focus();
+    clearEventStack();
+    input2.focus();
+
+    for (var i = 0; i < expectedEventOrder.length || i < eventStack.length; i++) {
+        ok(CompareEventToExpected(expectedEventOrder[i], eventStack[i]), 'Normal event order is correct: Event ' + i + ': '
+                + 'Expected ('
+                    + expectedEventOrder[i].type + ','
+                    + (expectedEventOrder[i].target ? expectedEventOrder[i].target.id : null) + ','
+                    + (expectedEventOrder[i].relatedTarget ? expectedEventOrder[i].relatedTarget.id : null) + '), '
+                + 'Actual ('
+                    + eventStack[i].type + ','
+                    + (eventStack[i].target ? eventStack[i].target.id : null) + ','
+                    + (eventStack[i].relatedTarget ? eventStack[i].relatedTarget.id : null) + ')');
+    }
+
+    while (content.firstChild) {
+        content.removeChild(content.firstChild);
+    }
+}
+
+function TestBlurRedirectsFocus() {
+
+    var input1 = document.createElement('input');
+    var input2 = document.createElement('input');
+    var input3 = document.createElement('input');
+    var content = document.getElementById('content');
+
+    input1.setAttribute('id', 'input1');
+    input2.setAttribute('id', 'input2');
+    input3.setAttribute('id', 'input3');
+    input1.setAttribute('type', 'text');
+    input2.setAttribute('type', 'text');
+    input3.setAttribute('type', 'text');
+    input1.onblur = function () {
+        input3.focus();
+    }
+
+    content.appendChild(input1);
+    content.appendChild(input2);
+    content.appendChild(input3);
+    content.style.display = 'block'
+
+    expectedEventOrder = [
+        {'type' : 'blur',
+        'target' : input1,
+        'relatedTarget' : input2},
+        {'type' : 'focus',
+        'target' : input3,
+        'relatedTarget' : null},
+        {'type' : 'focusin',
+        'target' : input3,
+        'relatedTarget' : null},
+    ]
+
+    input1.focus();
+    clearEventStack();
+    input2.focus();
+
+    for (var i = 0; i < expectedEventOrder.length || i < eventStack.length; i++) {
+        ok(CompareEventToExpected(expectedEventOrder[i], eventStack[i]), 'Normal event order is correct: Event ' + i + ': '
+                + 'Expected ('
+                    + expectedEventOrder[i].type + ','
+                    + (expectedEventOrder[i].target ? expectedEventOrder[i].target.id : null) + ','
+                    + (expectedEventOrder[i].relatedTarget ? expectedEventOrder[i].relatedTarget.id : null) + '), '
+                + 'Actual ('
+                    + eventStack[i].type + ','
+                    + (eventStack[i].target ? eventStack[i].target.id : null) + ','
+                    + (eventStack[i].relatedTarget ? eventStack[i].relatedTarget.id : null) + ')');
+    }
+
+    while (content.firstChild) {
+        content.removeChild(content.firstChild);
+    }
+}
+
+function TestFocusRedirectsFocus() {
+
+    var input1 = document.createElement('input');
+    var input2 = document.createElement('input');
+    var input3 = document.createElement('input');
+    var content = document.getElementById('content');
+
+    input1.setAttribute('id', 'input1');
+    input2.setAttribute('id', 'input2');
+    input3.setAttribute('id', 'input3');
+    input1.setAttribute('type', 'text');
+    input2.setAttribute('type', 'text');
+    input3.setAttribute('type', 'text');
+    input2.onfocus = function () {
+        input3.focus();
+    }
+
+    content.appendChild(input1);
+    content.appendChild(input2);
+    content.appendChild(input3);
+    content.style.display = 'block'
+
+    expectedEventOrder = [
+        {'type' : 'blur',
+        'target' : input1,
+        'relatedTarget' : input2},
+        {'type' : 'focusout',
+        'target' : input1,
+        'relatedTarget' : input2},
+        {'type' : 'focus',
+        'target' : input2,
+        'relatedTarget' : input1},
+        {'type' : 'blur',
+        'target' : input2,
+        'relatedTarget' : input3},
+        {'type' : 'focusout',
+        'target' : input2,
+        'relatedTarget' : input3},
+        {'type' : 'focus',
+        'target' : input3,
+        'relatedTarget' : input2},
+        {'type' : 'focusin',
+        'target' : input3,
+        'relatedTarget' : input2},
+    ]
+
+    input1.focus();
+    clearEventStack();
+    input2.focus();
+
+    for (var i = 0; i < expectedEventOrder.length || i < eventStack.length; i++) {
+        ok(CompareEventToExpected(expectedEventOrder[i], eventStack[i]), 'Normal event order is correct: Event ' + i + ': '
+                + 'Expected ('
+                    + expectedEventOrder[i].type + ','
+                    + (expectedEventOrder[i].target ? expectedEventOrder[i].target.id : null) + ','
+                    + (expectedEventOrder[i].relatedTarget ? expectedEventOrder[i].relatedTarget.id : null) + '), '
+                + 'Actual ('
+                    + eventStack[i].type + ','
+                    + (eventStack[i].target ? eventStack[i].target.id : null) + ','
+                    + (eventStack[i].relatedTarget ? eventStack[i].relatedTarget.id : null) + ')');
+    }
+
+    while (content.firstChild) {
+        content.removeChild(content.firstChild);
+    }
+}
+
+function TestEventOrderDifferentDocument() {
+
+    var input1 = document.createElement('input');
+    var input2 = document.createElement('input');
+    var iframe1 = document.createElement('iframe');
+    var content = document.getElementById('content');
+
+    input1.setAttribute('id', 'input1');
+    input2.setAttribute('id', 'input2');
+    iframe1.setAttribute('id', 'iframe1');
+    input1.setAttribute('type', 'text');
+    input2.setAttribute('type', 'text');
+
+    content.appendChild(input1);
+    content.appendChild(iframe1);
+    iframe1.contentDocument.body.appendChild(input2);
+    content.style.display = 'block'
+
+    iframe1.contentDocument.addEventListener("focus", _doc_callback, true);
+    iframe1.contentDocument.addEventListener("focusin", _doc_callback, true);
+    iframe1.contentDocument.addEventListener("focusout", _doc_callback, true);
+    iframe1.contentDocument.addEventListener("blur", _doc_callback, true);
+
+    expectedEventOrder = [
+        {'type' : 'blur',
+        'target' : input1,
+        'relatedTarget' : null},
+        {'type' : 'focusout',
+        'target' : input1,
+        'relatedTarget' : null},
+        {'type' : 'blur',
+        'target' : document,
+        'relatedTarget' : null},
+        {'type' : 'focusout',
+        'target' : document,
+        'relatedTarget' : null},
+        {'type' : 'focus',
+        'target' : iframe1.contentDocument,
+        'relatedTarget' : null},
+        {'type' : 'focusin',
+        'target' : iframe1.contentDocument,
+        'relatedTarget' : null},
+        {'type' : 'focus',
+        'target' : input2,
+        'relatedTarget' : null},
+        {'type' : 'focusin',
+        'target' : input2,
+        'relatedTarget' : null},
+    ]
+
+    input1.focus();
+    clearEventStack();
+    input2.focus();
+
+    for (var i = 0; i < expectedEventOrder.length || i < eventStack.length; i++) {
+        ok(CompareEventToExpected(expectedEventOrder[i], eventStack[i]), 'Normal event order is correct: Event ' + i + ': '
+                + 'Expected ('
+                    + expectedEventOrder[i].type + ','
+                    + (expectedEventOrder[i].target ? expectedEventOrder[i].target.id : null) + ','
+                    + (expectedEventOrder[i].relatedTarget ? expectedEventOrder[i].relatedTarget.id : null) + '), '
+                    + expectedEventOrder[i].target
+                + 'Actual ('
+                    + eventStack[i].type + ','
+                    + (eventStack[i].target ? eventStack[i].target.id : null) + ','
+                    + (eventStack[i].relatedTarget ? eventStack[i].relatedTarget.id : null) + ')');
+    }
+
+    while (content.firstChild) {
+        content.removeChild(content.firstChild);
+    }
+}
+
+
+function TestFocusOutMovesTarget() {
+
+    var input1 = document.createElement('input');
+    var input2 = document.createElement('input');
+    var iframe1 = document.createElement('iframe');
+    var content = document.getElementById('content');
+
+    input1.setAttribute('id', 'input1');
+    input2.setAttribute('id', 'input2');
+    iframe1.setAttribute('id', 'iframe1');
+    input1.setAttribute('type', 'text');
+    input2.setAttribute('type', 'text');
+
+    input1.onfocusout = function () {
+        iframe1.contentDocument.body.appendChild(input2);
+    }
+
+    content.appendChild(input1);
+    content.appendChild(input2);
+    content.appendChild(iframe1);
+    content.style.display = 'block'
+
+    iframe1.contentDocument.addEventListener("focus", _doc_callback, true);
+    iframe1.contentDocument.addEventListener("focusin", _doc_callback, true);
+    iframe1.contentDocument.addEventListener("focusout", _doc_callback, true);
+    iframe1.contentDocument.addEventListener("blur", _doc_callback, true);
+
+    expectedEventOrder = [
+        {'type' : 'blur',
+        'target' : input1,
+        'relatedTarget' : input2},
+        {'type' : 'focusout',
+        'target' : input1,
+        'relatedTarget' : input2},
+        {'type' : 'focus',
+        'target' : input2,
+        'relatedTarget' : null},
+        {'type' : 'focusin',
+        'target' : input2,
+        'relatedTarget' : null},
+    ]
+
+    input1.focus();
+    clearEventStack();
+    input2.focus();
+
+    for (var i = 0; i < expectedEventOrder.length || i < eventStack.length; i++) {
+        ok(CompareEventToExpected(expectedEventOrder[i], eventStack[i]), 'Normal event order is correct: Event ' + i + ': '
+                + 'Expected ('
+                    + expectedEventOrder[i].type + ','
+                    + (expectedEventOrder[i].target ? expectedEventOrder[i].target.id : null) + ','
+                    + (expectedEventOrder[i].relatedTarget ? expectedEventOrder[i].relatedTarget.id : null) + '), '
+                + 'Actual ('
+                    + eventStack[i].type + ','
+                    + (eventStack[i].target ? eventStack[i].target.id : null) + ','
+                    + (eventStack[i].relatedTarget ? eventStack[i].relatedTarget.id : null) + ')');
+    }
+
+    while (content.firstChild) {
+        content.removeChild(content.firstChild);
+    }
+}
+
+function TestFocusFromNothingFiresFocusIn() {
+
+    var input1 = document.createElement('input');
+    var content = document.getElementById('content');
+
+    input1.setAttribute('id', 'input1');
+    input1.setAttribute('type', 'text');
+
+    content.appendChild(input1);
+
+    expectedEventOrder = [
+        {'type' : 'focus',
+        'target' : document,
+        'relatedTarget' : null},
+        {'type' : 'focusin',
+        'target' : document,
+        'relatedTarget' : null},
+        {'type' : 'focus',
+        'target' : input1,
+        'relatedTarget' : null},
+        {'type' : 'focusin',
+        'target' : input1,
+        'relatedTarget' : null},
+    ]
+
+    window.blur();
+    clearEventStack();
+    input1.focus();
+
+    for (var i = 0; i < expectedEventOrder.length || i < eventStack.length; i++) {
+        ok(CompareEventToExpected(expectedEventOrder[i], eventStack[i]), 'Normal event order is correct: Event ' + i + ': '
+                + 'Expected ('
+                    + expectedEventOrder[i].type + ','
+                    + (expectedEventOrder[i].target ? expectedEventOrder[i].target.id : null) + ','
+                    + (expectedEventOrder[i].relatedTarget ? expectedEventOrder[i].relatedTarget.id : null) + '), '
+                + 'Actual ('
+                    + eventStack[i].type + ','
+                    + (eventStack[i].target ? eventStack[i].target.id : null) + ','
+                    + (eventStack[i].relatedTarget ? eventStack[i].relatedTarget.id : null) + ')');
+    }
+
+    while (content.firstChild) {
+        content.removeChild(content.firstChild);
+    }
+}
+
+TestEventOrderNormal();
+TestEventOrderNormalFiresAtRightTime();
+TestFocusOutRedirectsFocus();
+TestFocusInRedirectsFocus();
+TestBlurRedirectsFocus();
+TestFocusRedirectsFocus();
+TestFocusOutMovesTarget();
+TestEventOrderDifferentDocument();
+TestFocusFromNothingFiresFocusIn();
+
+</script>
+</pre>
+</body>
+</html>
--- a/dom/webidl/EventHandler.webidl
+++ b/dom/webidl/EventHandler.webidl
@@ -27,16 +27,18 @@ typedef OnErrorEventHandlerNonNull? OnEr
 [NoInterfaceObject]
 interface GlobalEventHandlers {
            attribute EventHandler onabort;
            attribute EventHandler onblur;
 // We think the spec is wrong here. See OnErrorEventHandlerForNodes/Window
 // below.
 //         attribute OnErrorEventHandler onerror;
            attribute EventHandler onfocus;
+           attribute EventHandler onfocusin;
+           attribute EventHandler onfocusout;
            //(Not implemented)attribute EventHandler oncancel;
            attribute EventHandler oncanplay;
            attribute EventHandler oncanplaythrough;
            attribute EventHandler onchange;
            attribute EventHandler onclick;
            //(Not implemented)attribute EventHandler onclose;
            attribute EventHandler oncontextmenu;
            //(Not implemented)attribute EventHandler oncuechange;
--- a/testing/web-platform/meta/MANIFEST.json
+++ b/testing/web-platform/meta/MANIFEST.json
@@ -37533,16 +37533,22 @@
           }
         ],
         "svg/linking/scripted/href-script-element.html": [
           {
             "path": "svg/linking/scripted/href-script-element.html",
             "url": "/svg/linking/scripted/href-script-element.html"
           }
         ],
+        "uievents/order-of-events/focus-events/focus-automated-blink-webkit.html": [
+          {
+            "path": "uievents/order-of-events/focus-events/focus-automated-blink-webkit.html",
+            "url": "/uievents/order-of-events/focus-events/focus-automated-blink-webkit.html"
+          }
+        ],
         "web-animations/interfaces/Animation/effect.html": [
           {
             "path": "web-animations/interfaces/Animation/effect.html",
             "url": "/web-animations/interfaces/Animation/effect.html"
           }
         ],
         "web-animations/interfaces/KeyframeEffect/iterationComposite.html": [
           {
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/uievents/order-of-events/focus-events/focus-automated-blink-webkit.html
@@ -0,0 +1,171 @@
+<!DOCTYPE html>
+<!-- Modified from Chris Rebert's manual version -->
+<!-- This documents the behavior according to blink's implementation -->
+<html>
+  <head>
+    <meta charset="utf-8">
+    <title>Focus-related events should fire in the correct order</title>
+    <link rel="help" href="https://w3c.github.io/uievents/#events-focusevent-event-order">
+    <meta name="flags" content="interact">
+  <script src="/resources/testharness.js"></script>
+  <script src="/resources/testharnessreport.js"></script>
+  </head>
+  <body id="body">
+    <input type="text" id="a" value="First">
+    <input type="text" id="b" value="Second">
+    <br>
+    <input type="text" id="c" value="Third">
+    <iframe id="iframe">
+    </iframe>
+    <br>
+    <script>
+
+    var test_id = 0;
+    var tests = ['normal', 'iframe']
+
+    function record(evt) {
+      if (done && (evt.type == 'focusin' || evt.type == 'focus') && (evt.target == c)) {
+          startNext();
+      }
+      if (!done) {
+          var activeElement = document.activeElement ?
+            (document.activeElement.tagName === 'IFRAME' ?
+            document.activeElement.contentDocument.activeElement.id :
+            document.activeElement.id) : null;
+          events[tests[test_id]].push(evt.type);
+          targets[tests[test_id]].push(evt.target.id);
+          focusedElements[tests[test_id]].push(activeElement);
+          relatedTargets[tests[test_id]].push(evt.relatedTarget ? evt.relatedTarget.id : null);
+      }
+    }
+    function startNext() {
+        done = false;
+        test_id++;
+    }
+    function finish() {
+        done = true;
+    }
+    var relevantEvents = [
+      'focus',
+      'blur',
+      'focusin',
+      'focusout'
+    ];
+
+    var iframe = document.getElementById('iframe');
+    var a = document.getElementById('a');
+    var b = document.getElementById('b');
+    var c = document.getElementById('c');
+    var d = document.createElement('input');
+
+    d.setAttribute('id', 'd');
+    d.setAttribute('type', 'text');
+    d.setAttribute('value', 'Fourth');
+
+    var events = {'normal': [], 'iframe': []};
+    var targets = {'normal': [], 'iframe': []};
+    var focusedElements = {'normal': [], 'iframe': []};
+    var relatedTargets = {'normal': [], 'iframe': []};
+    var done = false;
+
+    var async_test_normal = async_test('Focus-related events should fire in the correct order (same DocumentOwner)');
+    var async_test_iframe_static = async_test('Focus-related events should fire in the correct order (different DocumentOwner)');
+
+    window.onload = function(evt) {
+
+        iframe.contentDocument.body.appendChild(d);
+
+        var inputs = [a, b, c, d];
+
+        for (var i = 0; i < inputs.length; i++) {
+          for (var k = 0; k < relevantEvents.length; k++) {
+            inputs[i].addEventListener(relevantEvents[k], record, false);
+          }
+        }
+
+        a.addEventListener('focusin', function() { b.focus(); }, false);
+        b.addEventListener('focusin', function() {
+            console.log(events['normal']);
+            console.log(targets['normal']);
+            console.log(relatedTargets['normal']);
+            console.log(focusedElements['normal']);
+
+            async_test_normal.step( function() {
+                assert_array_equals(
+                  events['normal'],
+                  ['focus', 'focusin', 'blur', 'focusout', 'focus', 'focusin'],
+                  'Focus-related events should fire in this order: focusin, focus, focusout, focusin, blur, focus'
+                );
+
+                assert_array_equals(
+                  targets['normal'],
+                  [      'a',     'a',        'a',       'a',    'b',     'b'],
+                  'Focus-related events should fire at the correct targets'
+                );
+
+                assert_array_equals(
+                  relatedTargets['normal'],
+                  [    null,    null,         'b',       'b',    'a',     'a'],
+                  'Focus-related events should reference correct relatedTargets'
+                );
+
+                assert_array_equals(
+                  focusedElements['normal'],
+                  [   'a',     'a',        'body',    'body',    'b',     'b'],
+                  'Focus-related events should fire at the correct time relative to actual focus changes'
+                );
+
+                async_test_normal.done();
+                });
+
+            b.addEventListener('focusout', function() { finish(); c.focus(); });
+            b.blur();
+
+            }, false);
+
+        c.addEventListener('focusin', function() {d.focus();});
+        d.addEventListener('focusin', function() {
+            console.log(events['iframe']);
+            console.log(targets['iframe']);
+            console.log(relatedTargets['iframe']);
+            console.log(focusedElements['iframe']);
+
+            async_test_iframe_static.step(function() {
+                assert_array_equals(
+                  events['iframe'],
+                  ['focus', 'focusin', 'blur', 'focusout', 'focus', 'focusin'],
+                  'Focus-related events should fire in this order: focusin, focus, focusout, focusin, blur, focus'
+                );
+
+                assert_array_equals(
+                  targets['iframe'],
+                  [      'c',     'c',        'c',       'c',    'd',     'd'],
+                  'Focus-related events should fire at the correct targets'
+                );
+
+                assert_array_equals(
+                  relatedTargets['iframe'],
+                  [    null,    null,        null,      null,   null,    null],
+                  'Focus-related events should reference correct relatedTargets'
+                );
+
+                assert_array_equals(
+                  focusedElements['iframe'],
+                  [   'c',     'c',        'body',      'body', 'd',     'd'],
+                  'Focus-related events should fire at the correct time relative to actual focus changes'
+                );
+
+                async_test_iframe_static.done();
+                });
+
+            d.addEventListener('focusout', function() { finish();});
+
+          }, false);
+
+        a.focus();
+
+    }
+
+    </script>
+  </body>
+</html>
--- a/widget/EventMessageList.h
+++ b/widget/EventMessageList.h
@@ -123,16 +123,18 @@ NS_EVENT_MESSAGE(eFormSubmit)
 NS_EVENT_MESSAGE(eFormReset)
 NS_EVENT_MESSAGE(eFormChange)
 NS_EVENT_MESSAGE(eFormSelect)
 NS_EVENT_MESSAGE(eFormInvalid)
 
 //Need separate focus/blur notifications for non-native widgets
 NS_EVENT_MESSAGE(eFocus)
 NS_EVENT_MESSAGE(eBlur)
+NS_EVENT_MESSAGE(eFocusIn)
+NS_EVENT_MESSAGE(eFocusOut)
 
 NS_EVENT_MESSAGE(eDragEnter)
 NS_EVENT_MESSAGE(eDragOver)
 NS_EVENT_MESSAGE(eDragExit)
 NS_EVENT_MESSAGE(eDrag)
 NS_EVENT_MESSAGE(eDragEnd)
 NS_EVENT_MESSAGE(eDragStart)
 NS_EVENT_MESSAGE(eDrop)
--- a/widget/nsBaseWidget.cpp
+++ b/widget/nsBaseWidget.cpp
@@ -3046,16 +3046,18 @@ case _value: eventName.AssignLiteral(_na
   {
     _ASSIGN_eventName(eBlur,"eBlur");
     _ASSIGN_eventName(eDrop,"eDrop");
     _ASSIGN_eventName(eDragEnter,"eDragEnter");
     _ASSIGN_eventName(eDragExit,"eDragExit");
     _ASSIGN_eventName(eDragOver,"eDragOver");
     _ASSIGN_eventName(eEditorInput,"eEditorInput");
     _ASSIGN_eventName(eFocus,"eFocus");
+    _ASSIGN_eventName(FocusIn,"eFocusIn");
+    _ASSIGN_eventName(FocusOut,"eFocusOut");
     _ASSIGN_eventName(eFormSelect,"eFormSelect");
     _ASSIGN_eventName(eFormChange,"eFormChange");
     _ASSIGN_eventName(eFormReset,"eFormReset");
     _ASSIGN_eventName(eFormSubmit,"eFormSubmit");
     _ASSIGN_eventName(eImageAbort,"eImageAbort");
     _ASSIGN_eventName(eLoadError,"eLoadError");
     _ASSIGN_eventName(eKeyDown,"eKeyDown");
     _ASSIGN_eventName(eKeyPress,"eKeyPress");