Bug 1473234: make a11y listen to DOM events from iframes and shadow DOM. r=eeejay
authorJames Teh <jteh@mozilla.com>
Mon, 25 Mar 2019 05:04:36 +0000
changeset 466033 5697e0aa6e26882f0bca8216687c642f458c7e91
parent 466032 e712ac9dac7d364e3c7ffcab7a631ad395de47ce
child 466034 67cc4d399e8a5080c55ba428942e61613b6ba44b
push id35758
push userrgurzau@mozilla.com
push dateTue, 26 Mar 2019 09:51:47 +0000
treeherdermozilla-central@4572f6055a6a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerseeejay
bugs1473234
milestone68.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1473234: make a11y listen to DOM events from iframes and shadow DOM. r=eeejay 1. Register with the root document window's parent target, since this receives events for iframes and shadow DOM. (The root document itself doesn't.) 2. Hold onto the target node when scheduling processing of the DOM event, as GetOriginalTarget returns null when we process shadow DOM events async. Depends on D21349 Differential Revision: https://phabricator.services.mozilla.com/D21350
accessible/base/NotificationController.h
accessible/generic/DocAccessible-inl.h
accessible/generic/DocAccessible.h
accessible/generic/RootAccessible.cpp
accessible/generic/RootAccessible.h
accessible/tests/mochitest/events/test_valuechange.html
--- a/accessible/base/NotificationController.h
+++ b/accessible/base/NotificationController.h
@@ -209,32 +209,33 @@ class NotificationController final : pub
   /**
    * Process the generic notification synchronously if there are no pending
    * layout changes and no notifications are pending or being processed right
    * now. Otherwise, queue it up to process asynchronously.
    *
    * @note  The caller must guarantee that the given instance still exists when
    *        the notification is processed.
    */
-  template <class Class, class Arg>
+  template <class Class, class... Args>
   inline void HandleNotification(
-      Class* aInstance, typename TNotification<Class, Arg>::Callback aMethod,
-      Arg* aArg) {
+      Class* aInstance,
+      typename TNotification<Class, Args...>::Callback aMethod,
+      Args*... aArgs) {
     if (!IsUpdatePending()) {
 #ifdef A11Y_LOG
       if (mozilla::a11y::logging::IsEnabled(
               mozilla::a11y::logging::eNotifications))
         mozilla::a11y::logging::Text("sync notification processing");
 #endif
-      (aInstance->*aMethod)(aArg);
+      (aInstance->*aMethod)(aArgs...);
       return;
     }
 
     RefPtr<Notification> notification =
-        new TNotification<Class, Arg>(aInstance, aMethod, aArg);
+        new TNotification<Class, Args...>(aInstance, aMethod, aArgs...);
     if (notification && mNotifications.AppendElement(notification))
       ScheduleProcessing();
   }
 
   /**
    * Schedule the generic notification to process asynchronously.
    *
    * @note  The caller must guarantee that the given instance still exists when
--- a/accessible/generic/DocAccessible-inl.h
+++ b/accessible/generic/DocAccessible-inl.h
@@ -54,23 +54,23 @@ inline void DocAccessible::FireDelayedEv
   RefPtr<AccEvent> event = new AccEvent(aEventType, aTarget);
   FireDelayedEvent(event);
 }
 
 inline void DocAccessible::BindChildDocument(DocAccessible* aDocument) {
   mNotificationController->ScheduleChildDocBinding(aDocument);
 }
 
-template <class Class, class Arg>
+template <class Class, class... Args>
 inline void DocAccessible::HandleNotification(
-    Class* aInstance, typename TNotification<Class, Arg>::Callback aMethod,
-    Arg* aArg) {
+    Class* aInstance, typename TNotification<Class, Args...>::Callback aMethod,
+    Args*... aArgs) {
   if (mNotificationController) {
-    mNotificationController->HandleNotification<Class, Arg>(aInstance, aMethod,
-                                                            aArg);
+    mNotificationController->HandleNotification<Class, Args...>(
+        aInstance, aMethod, aArgs...);
   }
 }
 
 inline void DocAccessible::UpdateText(nsIContent* aTextNode) {
   NS_ASSERTION(mNotificationController, "The document was shut down!");
 
   // Ignore the notification if initial tree construction hasn't been done yet.
   if (mNotificationController && HasLoadState(eTreeConstructed))
--- a/accessible/generic/DocAccessible.h
+++ b/accessible/generic/DocAccessible.h
@@ -221,20 +221,20 @@ class DocAccessible : public HyperTextAc
 
   /**
    * Process the generic notification.
    *
    * @note  The caller must guarantee that the given instance still exists when
    *          notification is processed.
    * @see   NotificationController::HandleNotification
    */
-  template <class Class, class Arg>
-  void HandleNotification(Class* aInstance,
-                          typename TNotification<Class, Arg>::Callback aMethod,
-                          Arg* aArg);
+  template <class Class, class... Args>
+  void HandleNotification(
+      Class* aInstance,
+      typename TNotification<Class, Args...>::Callback aMethod, Args*... aArgs);
 
   /**
    * Return the cached accessible by the given DOM node if it's in subtree of
    * this document accessible or the document accessible itself, otherwise null.
    *
    * @return the accessible object
    */
   Accessible* GetAccessible(nsINode* aNode) const {
--- a/accessible/generic/RootAccessible.cpp
+++ b/accessible/generic/RootAccessible.cpp
@@ -156,33 +156,38 @@ const char* const kEventTypes[] = {
     "DOMMenuItemActive", "DOMMenuItemInactive", "DOMMenuBarActive",
     "DOMMenuBarInactive"};
 
 nsresult RootAccessible::AddEventListeners() {
   // EventTarget interface allows to register event listeners to
   // receive untrusted events (synthetic events generated by untrusted code).
   // For example, XBL bindings implementations for elements that are hosted in
   // non chrome document fire untrusted events.
-  nsCOMPtr<EventTarget> nstarget = mDocumentNode;
+  // We must use the window's parent target in order to receive events from
+  // iframes and shadow DOM; e.g. ValueChange events from a <select> in an
+  // iframe or shadow DOM. The root document itself doesn't receive these.
+  nsPIDOMWindowOuter* window = mDocumentNode->GetWindow();
+  nsCOMPtr<EventTarget> nstarget = window ? window->GetParentTarget() : nullptr;
 
   if (nstarget) {
     for (const char *const *e = kEventTypes, *const *e_end =
                                                  ArrayEnd(kEventTypes);
          e < e_end; ++e) {
       nsresult rv = nstarget->AddEventListener(NS_ConvertASCIItoUTF16(*e), this,
                                                true, true);
       NS_ENSURE_SUCCESS(rv, rv);
     }
   }
 
   return DocAccessible::AddEventListeners();
 }
 
 nsresult RootAccessible::RemoveEventListeners() {
-  nsCOMPtr<EventTarget> target = mDocumentNode;
+  nsPIDOMWindowOuter* window = mDocumentNode->GetWindow();
+  nsCOMPtr<EventTarget> target = window ? window->GetParentTarget() : nullptr;
   if (target) {
     for (const char *const *e = kEventTypes, *const *e_end =
                                                  ArrayEnd(kEventTypes);
          e < e_end; ++e) {
       target->RemoveEventListener(NS_ConvertASCIItoUTF16(*e), this, true);
     }
   }
 
@@ -198,16 +203,24 @@ nsresult RootAccessible::RemoveEventList
 void RootAccessible::DocumentActivated(DocAccessible* aDocument) {}
 
 ////////////////////////////////////////////////////////////////////////////////
 // nsIDOMEventListener
 
 NS_IMETHODIMP
 RootAccessible::HandleEvent(Event* aDOMEvent) {
   MOZ_ASSERT(aDOMEvent);
+  if (IsDefunct()) {
+    // Even though we've been shut down, RemoveEventListeners might not have
+    // removed the event handlers on the window's parent target if GetWindow
+    // returned null, so we might still get events here in this case. We should
+    // just ignore these events.
+    return NS_OK;
+  }
+
   nsCOMPtr<nsINode> origTargetNode =
       do_QueryInterface(aDOMEvent->GetOriginalTarget());
   if (!origTargetNode) return NS_OK;
 
 #ifdef A11Y_LOG
   if (logging::IsEnabled(logging::eDOMEvents)) {
     nsAutoString eventType;
     aDOMEvent->GetType(eventType);
@@ -217,56 +230,51 @@ RootAccessible::HandleEvent(Event* aDOME
 
   DocAccessible* document =
       GetAccService()->GetDocAccessible(origTargetNode->OwnerDoc());
 
   if (document) {
     // Root accessible exists longer than any of its descendant documents so
     // that we are guaranteed notification is processed before root accessible
     // is destroyed.
-    document->HandleNotification<RootAccessible, Event>(
-        this, &RootAccessible::ProcessDOMEvent, aDOMEvent);
+    // For shadow DOM, GetOriginalTarget on the Event returns null if we
+    // process the event async, so we must pass the target node as well.
+    document->HandleNotification<RootAccessible, Event, nsINode>(
+        this, &RootAccessible::ProcessDOMEvent, aDOMEvent, origTargetNode);
   }
 
   return NS_OK;
 }
 
 // RootAccessible protected
-void RootAccessible::ProcessDOMEvent(Event* aDOMEvent) {
+void RootAccessible::ProcessDOMEvent(Event* aDOMEvent, nsINode* aTarget) {
   MOZ_ASSERT(aDOMEvent);
-  nsCOMPtr<nsINode> origTargetNode =
-      do_QueryInterface(aDOMEvent->GetOriginalTarget());
+  MOZ_ASSERT(aTarget);
 
   nsAutoString eventType;
   aDOMEvent->GetType(eventType);
 
 #ifdef A11Y_LOG
   if (logging::IsEnabled(logging::eDOMEvents))
-    logging::DOMEvent("processed", origTargetNode, eventType);
+    logging::DOMEvent("processed", aTarget, eventType);
 #endif
 
-  if (!origTargetNode) {
-    // Original target has ceased to exist.
-    return;
-  }
-
   if (eventType.EqualsLiteral("popuphiding")) {
-    HandlePopupHidingEvent(origTargetNode);
+    HandlePopupHidingEvent(aTarget);
     return;
   }
 
   DocAccessible* targetDocument =
-      GetAccService()->GetDocAccessible(origTargetNode->OwnerDoc());
+      GetAccService()->GetDocAccessible(aTarget->OwnerDoc());
   if (!targetDocument) {
     // Document has ceased to exist.
     return;
   }
 
-  Accessible* accessible =
-      targetDocument->GetAccessibleOrContainer(origTargetNode);
+  Accessible* accessible = targetDocument->GetAccessibleOrContainer(aTarget);
   if (!accessible) return;
 
 #ifdef MOZ_XUL
   XULTreeAccessible* treeAcc = accessible->AsXULTree();
   if (treeAcc) {
     if (eventType.EqualsLiteral("TreeRowCountChanged")) {
       HandleTreeRowCountChangedEvent(aDOMEvent, treeAcc);
       return;
--- a/accessible/generic/RootAccessible.h
+++ b/accessible/generic/RootAccessible.h
@@ -49,17 +49,17 @@ class RootAccessible : public DocAccessi
    * Add/remove DOM event listeners.
    */
   virtual nsresult AddEventListeners() override;
   virtual nsresult RemoveEventListeners() override;
 
   /**
    * Process the DOM event.
    */
-  void ProcessDOMEvent(dom::Event* aEvent);
+  void ProcessDOMEvent(dom::Event* aDOMEvent, nsINode* aTarget);
 
   /**
    * Process "popupshown" event. Used by HandleEvent().
    */
   void HandlePopupShownEvent(Accessible* aAccessible);
 
   /*
    * Process "popuphiding" event. Used by HandleEvent().
--- a/accessible/tests/mochitest/events/test_valuechange.html
+++ b/accessible/tests/mochitest/events/test_valuechange.html
@@ -116,17 +116,17 @@
       };
     }
 
     function changeSelectValue(aID, aKey, aValue) {
       this.eventSeq =
         [ new invokerChecker(EVENT_TEXT_VALUE_CHANGE, getAccessible(aID)) ];
 
       this.invoke = function changeSelectValue_invoke() {
-        getNode(aID).focus();
+        getAccessible(aID).takeFocus();
         synthesizeKey(aKey, {}, window);
       };
 
       this.finalCheck = function changeSelectValue_finalCheck() {
         is(getAccessible(aID).value, aValue, "Wrong value for " + prettyName(aID));
       };
 
       this.getID = function changeSelectValue_getID() {
@@ -162,16 +162,22 @@
       gQueue.push(new changeValue("combobox", "hello"));
 
       gQueue.push(new changeProgressValue("progress", "50"));
       gQueue.push(new changeRangeValue("range"));
 
       gQueue.push(new changeSelectValue("select", "VK_DOWN", "2nd"));
       gQueue.push(new changeSelectValue("select", "3", "3rd"));
 
+      let iframeSelect = getAccessible("selectIframe").firstChild.firstChild;
+      gQueue.push(new changeSelectValue(iframeSelect, "VK_DOWN", "2"));
+
+      let shadowSelect = getAccessible("selectShadow").firstChild;
+      gQueue.push(new changeSelectValue(shadowSelect, "VK_DOWN", "2"));
+
       gQueue.invoke(); // Will call SimpleTest.finish();
     }
 
     SimpleTest.waitForExplicitFinish();
     addA11yLoadEvent(doTests);
   </script>
 </head>
 
@@ -241,10 +247,29 @@
   <!-- input@type="range" -->
   <input type="range" id="range" min="0" max="10" value="6">
 
   <select id="select">
     <option>1st</option>
     <option>2nd</option>
     <option>3rd</option>
   </select>
+
+  <iframe id="selectIframe"
+    src="data:text/html,<select id='iframeSelect'><option>1</option><option>2</option></select>">
+  </iframe>
+
+  <div id="selectShadow"></div>
+  <script>
+    let host = document.getElementById("selectShadow");
+    let shadow = host.attachShadow({mode: "open"});
+    let select = document.createElement("select");
+    select.id = "shadowSelect";
+    let option = document.createElement("option");
+    option.textContent = "1";
+    select.appendChild(option);
+    option = document.createElement("option");
+    option.textContent = "2";
+    select.appendChild(option);
+    shadow.appendChild(select);
+  </script>
 </body>
 </html>