Bug 687787: support focusin/focusout based on webkit/blink r=smaug
authorKevin Wern <kevin.m.wern@gmail.com>
Thu, 06 Oct 2016 21:39:53 -0400
changeset 432792 aa39c1a962ffa63c1c5017b8c824663dea930771
parent 432791 01927ad414ea0925f976b191c2db41a0d0152ae9
child 432793 c7ae634cfdf2d493085bbb6d7c552dff03856270
push id34424
push userbmo:jmaher@mozilla.com
push dateWed, 02 Nov 2016 19:59:36 +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 is fired on a window or document, or the event triggers a change of focus, do 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
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,112 @@ 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,
+                 nsPIDOMWindowOuter* aOriginalFocusedWindow,
+                 nsIContent* aOriginalFocusedContent,
+                 EventTarget* aRelatedTarget)
+    : mTarget(aTarget)
+    , mContext(aContext)
+    , mEventMessage(aEventMessage)
+    , mOriginalFocusedWindow(aOriginalFocusedWindow)
+    , mOriginalFocusedContent(aOriginalFocusedContent)
+    , mRelatedTarget(aRelatedTarget)
+  {
+  }
+
+  NS_IMETHOD Run() override
+  {
+    nsCOMPtr<nsIContent> originalWindowFocus = mOriginalFocusedWindow ?
+        mOriginalFocusedWindow->GetFocusedNode() :
+        nullptr;
+    // Blink does not check that focus is the same after blur, but WebKit does.
+    // Opt to follow Blink's behavior (see bug 687787).
+    if (mEventMessage == eFocusOut ||
+        originalWindowFocus == mOriginalFocusedContent) {
+      InternalFocusEvent event(true, mEventMessage);
+      event.mFlags.mBubbles = true;
+      event.mFlags.mCancelable = false;
+      event.mRelatedTarget = mRelatedTarget;
+      return EventDispatcher::Dispatch(mTarget, mContext, &event);
+    }
+    return NS_OK;
+  }
+
+  nsCOMPtr<nsISupports>        mTarget;
+  RefPtr<nsPresContext>        mContext;
+  EventMessage                 mEventMessage;
+  nsCOMPtr<nsPIDOMWindowOuter> mOriginalFocusedWindow;
+  nsCOMPtr<nsIContent>         mOriginalFocusedContent;
+  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,
+                                     nsPIDOMWindowOuter* aCurrentFocusedWindow,
+                                     nsIContent* aCurrentFocusedContent,
+                                     EventTarget* aRelatedTarget)
+{
+  NS_ASSERTION(aEventMessage == eFocusIn || aEventMessage == eFocusOut,
+      "Wrong event type for SendFocusInOrOutEvent");
+
+  nsContentUtils::AddScriptRunner(
+      new FocusInOutEvent(
+        aTarget,
+        aEventMessage,
+        aPresShell->GetPresContext(),
+        aCurrentFocusedWindow,
+        aCurrentFocusedContent,
+        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<nsPIDOMWindowInner> targetWindow = do_QueryInterface(aTarget);
+  nsCOMPtr<nsIDocument> targetDocument = do_QueryInterface(aTarget);
+  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 +2162,29 @@ nsFocusManager::SendFocusOrBlurEvent(Eve
     }
   }
 #endif
 
   if (!dontDispatchEvent) {
     nsContentUtils::AddScriptRunner(
       new FocusBlurEvent(aTarget, aEventMessage, aPresShell->GetPresContext(),
                          aWindowRaised, aIsRefocus, aRelatedTarget));
+
+    // Check that the target is not a window or document before firing
+    // focusin/focusout. Other browsers do not fire focusin/focusout on window,
+    // despite being required in the spec, so follow their behavior.
+    //
+    // As for document, we should not even fire focus/blur, but until then, we
+    // need this check. targetDocument should be removed once bug 1228802 is
+    // resolved.
+    if (!targetWindow && !targetDocument) {
+      EventMessage focusInOrOutMessage = aEventMessage == eFocus ? eFocusIn : eFocusOut;
+      SendFocusInOrOutEvent(focusInOrOutMessage, aPresShell, aTarget,
+          currentWindow, currentFocusedContent, aRelatedTarget);
+    }
   }
 }
 
 void
 nsFocusManager::ScrollIntoView(nsIPresShell* aPresShell,
                                nsIContent* aContent,
                                uint32_t aFlags)
 {
--- a/dom/base/nsFocusManager.h
+++ b/dom/base/nsFocusManager.h
@@ -292,16 +292,40 @@ 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 that gained
+   *  focus for focusin, the object blurred for focusout).
+   *
+   *  aCurrentFocusedWindow is the window focused before the focus/blur event
+   *  was fired.
+   *
+   *  aCurrentFocusedContent is the content focused before the focus/blur event
+   *  was fired.
+   *
+   *  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,
+                             nsPIDOMWindowOuter* aCurrentFocusedWindow,
+                             nsIContent* aCurrentFocusedContent,
+                             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
@@ -789,16 +789,18 @@ GK_ATOM(onenabled, "onenabled")
 GK_ATOM(onenterpincodereq, "onenterpincodereq")
 GK_ATOM(onemergencycbmodechange, "onemergencycbmodechange")
 GK_ATOM(onerror, "onerror")
 GK_ATOM(onevicted, "onevicted")
 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
@@ -498,16 +498,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
@@ -197,8 +197,9 @@ support-files =
 [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_messageEvent_init.html]
+[test_bug687787.html]
new file mode 100644
--- /dev/null
+++ b/dom/events/test/test_bug687787.html
@@ -0,0 +1,617 @@
+<!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 _callback(e){
+    var event = {'type' : e.type, 'target' : e.target, 'relatedTarget' : e.relatedTarget }
+    eventStack.push(event);
+}
+
+function clearEventStack(){
+    eventStack = [];
+}
+
+window.addEventListener("focus", _callback, true);
+window.addEventListener("focusin", _callback, true);
+window.addEventListener("focusout", _callback, true);
+window.addEventListener("blur", _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) + ')');
+    }
+
+    content.innerHTML = '';
+}
+
+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.addEventListener('focusout', 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.addEventListener('focusin', 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) + ')');
+    }
+
+    content.innerHTML = '';
+}
+
+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.addEventListener('focusout', 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) + ')');
+    }
+
+    content.innerHTML = '';
+}
+
+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.addEventListener('focusin', 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) + ')');
+    }
+
+    content.innerHTML = '';
+}
+
+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},
+        {'type' : 'focusout',
+        'target' : input1,
+        '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) + ')');
+    }
+
+    content.innerHTML = '';
+}
+
+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) + ')');
+    }
+
+    content.innerHTML = '';
+}
+
+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", _callback, true);
+    iframe1.contentDocument.addEventListener("focusin", _callback, true);
+    iframe1.contentDocument.addEventListener("focusout", _callback, true);
+    iframe1.contentDocument.addEventListener("blur", _callback, true);
+
+    expectedEventOrder = [
+        {'type' : 'blur',
+        'target' : input1,
+        'relatedTarget' : null},
+        {'type' : 'focusout',
+        'target' : input1,
+        'relatedTarget' : null},
+        {'type' : 'blur',
+        'target' : document,
+        'relatedTarget' : null},
+        {'type' : 'blur',
+        'target' : window,
+        'relatedTarget' : null},
+        {'type' : 'focus',
+        '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) + '), '
+                + 'Actual ('
+                    + eventStack[i].type + ','
+                    + (eventStack[i].target ? eventStack[i].target.id : null) + ','
+                    + (eventStack[i].relatedTarget ? eventStack[i].relatedTarget.id : null) + ')');
+    }
+
+    content.innerHTML = '';
+}
+
+
+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.addEventListener('focusout', function () {
+        iframe1.contentDocument.body.appendChild(input2);
+    });
+
+    content.appendChild(input1);
+    content.appendChild(input2);
+    content.appendChild(iframe1);
+    content.style.display = 'block'
+
+    iframe1.contentDocument.addEventListener("focus", _callback, true);
+    iframe1.contentDocument.addEventListener("focusin", _callback, true);
+    iframe1.contentDocument.addEventListener("focusout", _callback, true);
+    iframe1.contentDocument.addEventListener("blur", _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) + ')');
+    }
+
+    content.innerHTML = '';
+}
+
+function TestBlurWindowAndRefocusInputOnlyFiresFocusInOnInput() {
+
+    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' : 'focus',
+        'target' : window,
+        '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) + ')');
+    }
+
+    content.innerHTML = '';
+}
+
+TestEventOrderNormal();
+TestEventOrderNormalFiresAtRightTime();
+TestFocusOutRedirectsFocus();
+TestFocusInRedirectsFocus();
+TestBlurRedirectsFocus();
+TestFocusRedirectsFocus();
+TestFocusOutMovesTarget();
+TestEventOrderDifferentDocument();
+TestBlurWindowAndRefocusInputOnlyFiresFocusInOnInput();
+
+</script>
+</pre>
+</body>
+</html>
--- a/testing/web-platform/meta/MANIFEST.json
+++ b/testing/web-platform/meta/MANIFEST.json
@@ -38688,16 +38688,22 @@
           }
         ],
         "secure-contexts/shared-worker-secure-first.https.html": [
           {
             "path": "secure-contexts/shared-worker-secure-first.https.html",
             "url": "/secure-contexts/shared-worker-secure-first.https.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"
+          }
+        ],
         "webaudio/the-audio-api/the-constantsourcenode-interface/test-constantsourcenode.html": [
           {
             "path": "webaudio/the-audio-api/the-constantsourcenode-interface/test-constantsourcenode.html",
             "url": "/webaudio/the-audio-api/the-constantsourcenode-interface/test-constantsourcenode.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,170 @@
+<!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
@@ -124,16 +124,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
@@ -3042,16 +3042,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(eFocusIn,"eFocusIn");
+    _ASSIGN_eventName(eFocusOut,"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");