From df739e1daf83c49eb436ae426c272938e0262f10 Mon Sep 17 00:00:00 2001 From: Eitan Isaacson Date: Mon, 16 Nov 2020 20:16:33 +0000 Subject: [PATCH] Bug 1198336 - P1: Introduce live region added/removed events. r=Jamie,morgan VoiceOver pre-caches live region data and does its own deltas to know what to parts of a subtree changed, and what to announce based on AXARIAAtomic and AXARIARelevant. I added a removed event as well. This will help us cache a "live region" flag in the main process and avoid sync round trips for attributes when not needed. Differential Revision: https://phabricator.services.mozilla.com/D96291 --- accessible/base/nsAccessibilityService.h | 2 + accessible/interfaces/nsIAccessibleEvent.idl | 12 ++- accessible/mac/AccessibleWrap.mm | 43 +++++++++- accessible/mac/DocAccessibleWrap.h | 16 +++- accessible/mac/DocAccessibleWrap.mm | 78 ++++++++++++++++++ accessible/mac/Platform.mm | 7 +- accessible/tests/browser/mac/browser.ini | 1 + .../tests/browser/mac/browser_live_regions.js | 79 +++++++++++++++++++ .../tests/mochitest/promisified-events.js | 2 + accessible/windows/msaa/nsEventMap.h | 2 + xpcom/ds/StaticAtoms.py | 1 + 11 files changed, 235 insertions(+), 8 deletions(-) create mode 100644 accessible/tests/browser/mac/browser_live_regions.js diff --git a/accessible/base/nsAccessibilityService.h b/accessible/base/nsAccessibilityService.h index 0869f76336dc..f3c88f5b20fb 100644 --- a/accessible/base/nsAccessibilityService.h +++ b/accessible/base/nsAccessibilityService.h @@ -498,6 +498,8 @@ static const char kEventTypeNames[][40] = { "text value change", // EVENT_TEXT_VALUE_CHANGE "scrolling", // EVENT_SCROLLING "announcement", // EVENT_ANNOUNCEMENT + "live region added", // EVENT_LIVE_REGION_ADDED + "live region removed", // EVENT_LIVE_REGION_REMOVED }; #endif diff --git a/accessible/interfaces/nsIAccessibleEvent.idl b/accessible/interfaces/nsIAccessibleEvent.idl index 430b169045d7..6d5e72d39920 100644 --- a/accessible/interfaces/nsIAccessibleEvent.idl +++ b/accessible/interfaces/nsIAccessibleEvent.idl @@ -428,10 +428,20 @@ interface nsIAccessibleEvent : nsISupports */ const unsigned long EVENT_ANNOUNCEMENT = 0x0059; + /** + * A live region has been introduced. Mac only. + */ + const unsigned long EVENT_LIVE_REGION_ADDED = 0x005A; + + /** + * A live region has been removed (aria-live attribute changed). Mac Only. + */ + const unsigned long EVENT_LIVE_REGION_REMOVED = 0x005B; + /** * Help make sure event map does not get out-of-line. */ - const unsigned long EVENT_LAST_ENTRY = 0x005A; + const unsigned long EVENT_LAST_ENTRY = 0x005C; /** * The type of event, based on the enumerated event values diff --git a/accessible/mac/AccessibleWrap.mm b/accessible/mac/AccessibleWrap.mm index f1ec2251754a..afcc2c2abf7d 100644 --- a/accessible/mac/AccessibleWrap.mm +++ b/accessible/mac/AccessibleWrap.mm @@ -5,7 +5,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -#include "DocAccessible.h" +#include "DocAccessibleWrap.h" #include "nsObjCExceptions.h" #include "nsCocoaUtils.h" @@ -30,7 +30,36 @@ using namespace mozilla; using namespace mozilla::a11y; AccessibleWrap::AccessibleWrap(nsIContent* aContent, DocAccessible* aDoc) - : Accessible(aContent, aDoc), mNativeObject(nil), mNativeInited(false) {} + : Accessible(aContent, aDoc), mNativeObject(nil), mNativeInited(false) { + if (aContent && aContent->IsElement() && aDoc) { + // Check if this accessible is a live region and queue it + // it for dispatching an event after it has been inserted. + DocAccessibleWrap* doc = static_cast(aDoc); + static const dom::Element::AttrValuesArray sLiveRegionValues[] = { + nsGkAtoms::OFF, nsGkAtoms::polite, nsGkAtoms::assertive, nullptr}; + int32_t attrValue = aContent->AsElement()->FindAttrValueIn( + kNameSpaceID_None, nsGkAtoms::aria_live, sLiveRegionValues, + eIgnoreCase); + if (attrValue == 0) { + // aria-live is "off", do nothing. + } else if (attrValue > 0) { + // aria-live attribute is polite or assertive. It's live! + doc->QueueNewLiveRegion(this); + } else if (const nsRoleMapEntry* roleMap = + aria::GetRoleMap(aContent->AsElement())) { + // aria role defines it as a live region. It's live! + if (roleMap->liveAttRule == ePoliteLiveAttr) { + doc->QueueNewLiveRegion(this); + } + } else if (nsStaticAtom* value = GetAccService()->MarkupAttribute( + aContent, nsGkAtoms::live)) { + // HTML element defines it as a live region. It's live! + if (value == nsGkAtoms::polite || value == nsGkAtoms::assertive) { + doc->QueueNewLiveRegion(this); + } + } + } +} AccessibleWrap::~AccessibleWrap() {} @@ -120,11 +149,17 @@ nsresult AccessibleWrap::HandleAccEvent(AccEvent* aEvent) { nsresult rv = Accessible::HandleAccEvent(aEvent); NS_ENSURE_SUCCESS(rv, rv); + uint32_t eventType = aEvent->GetEventType(); + + if (eventType == nsIAccessibleEvent::EVENT_SHOW) { + DocAccessibleWrap* doc = static_cast(Document()); + doc->ProcessNewLiveRegions(); + } + if (IPCAccessibilityActive()) { return NS_OK; } - uint32_t eventType = aEvent->GetEventType(); Accessible* eventTarget = nullptr; switch (eventType) { @@ -222,6 +257,8 @@ nsresult AccessibleWrap::HandleAccEvent(AccEvent* aEvent) { case nsIAccessibleEvent::EVENT_SELECTION: case nsIAccessibleEvent::EVENT_SELECTION_ADD: case nsIAccessibleEvent::EVENT_SELECTION_REMOVE: + case nsIAccessibleEvent::EVENT_LIVE_REGION_ADDED: + case nsIAccessibleEvent::EVENT_LIVE_REGION_REMOVED: [nativeAcc handleAccessibleEvent:eventType]; break; diff --git a/accessible/mac/DocAccessibleWrap.h b/accessible/mac/DocAccessibleWrap.h index 8c98ca09c2b4..b46fa9189a81 100644 --- a/accessible/mac/DocAccessibleWrap.h +++ b/accessible/mac/DocAccessibleWrap.h @@ -20,9 +20,23 @@ class DocAccessibleWrap : public DocAccessible { public: DocAccessibleWrap(dom::Document* aDocument, PresShell* aPresShell); + virtual ~DocAccessibleWrap(); + virtual void Shutdown() override; - virtual ~DocAccessibleWrap(); + virtual void AttributeChanged(dom::Element* aElement, int32_t aNameSpaceID, + nsAtom* aAttribute, int32_t aModType, + const nsAttrValue* aOldValue) override; + + void QueueNewLiveRegion(Accessible* aAccessible); + + void ProcessNewLiveRegions(); + + protected: + virtual void DoInitialUpdate() override; + + private: + nsTHashtable mNewLiveRegions; }; } // namespace a11y diff --git a/accessible/mac/DocAccessibleWrap.mm b/accessible/mac/DocAccessibleWrap.mm index 30a32a82cc00..9b527ee8a8d7 100644 --- a/accessible/mac/DocAccessibleWrap.mm +++ b/accessible/mac/DocAccessibleWrap.mm @@ -6,6 +6,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "DocAccessibleWrap.h" +#include "DocAccessible-inl.h" #import "mozAccessible.h" #import "MOXTextMarkerDelegate.h" @@ -23,3 +24,80 @@ void DocAccessibleWrap::Shutdown() { } DocAccessibleWrap::~DocAccessibleWrap() {} + +void DocAccessibleWrap::AttributeChanged(dom::Element* aElement, + int32_t aNameSpaceID, + nsAtom* aAttribute, int32_t aModType, + const nsAttrValue* aOldValue) { + DocAccessible::AttributeChanged(aElement, aNameSpaceID, aAttribute, aModType, + aOldValue); + if (aAttribute == nsGkAtoms::aria_live) { + Accessible* accessible = + mContent != aElement ? GetAccessible(aElement) : this; + if (!accessible) { + return; + } + + static const dom::Element::AttrValuesArray sLiveRegionValues[] = { + nsGkAtoms::OFF, nsGkAtoms::polite, nsGkAtoms::assertive, nullptr}; + int32_t attrValue = + aElement->FindAttrValueIn(kNameSpaceID_None, nsGkAtoms::aria_live, + sLiveRegionValues, eIgnoreCase); + if (attrValue > 0) { + if (!aOldValue || aOldValue->IsEmptyString() || + aOldValue->Equals(nsGkAtoms::OFF, eIgnoreCase)) { + // This element just got an active aria-live attribute value + FireDelayedEvent(nsIAccessibleEvent::EVENT_LIVE_REGION_ADDED, + accessible); + } + } else { + if (aOldValue && (aOldValue->Equals(nsGkAtoms::polite, eIgnoreCase) || + aOldValue->Equals(nsGkAtoms::assertive, eIgnoreCase))) { + // This element lost an active live region + FireDelayedEvent(nsIAccessibleEvent::EVENT_LIVE_REGION_REMOVED, + accessible); + } else if (attrValue == 0) { + // aria-live="off", check if its a role-based live region that + // needs to be removed. + if (const nsRoleMapEntry* roleMap = accessible->ARIARoleMap()) { + // aria role defines it as a live region. It's live! + if (roleMap->liveAttRule == ePoliteLiveAttr) { + FireDelayedEvent(nsIAccessibleEvent::EVENT_LIVE_REGION_REMOVED, + accessible); + } + } else if (nsStaticAtom* value = GetAccService()->MarkupAttribute( + aElement, nsGkAtoms::live)) { + // HTML element defines it as a live region. It's live! + if (value == nsGkAtoms::polite || value == nsGkAtoms::assertive) { + FireDelayedEvent(nsIAccessibleEvent::EVENT_LIVE_REGION_REMOVED, + accessible); + } + } + } + } + } +} + +void DocAccessibleWrap::QueueNewLiveRegion(Accessible* aAccessible) { + if (!aAccessible) { + return; + } + + mNewLiveRegions.PutEntry(aAccessible->UniqueID()); +} + +void DocAccessibleWrap::ProcessNewLiveRegions() { + for (auto iter = mNewLiveRegions.Iter(); !iter.Done(); iter.Next()) { + if (Accessible* liveRegion = + GetAccessibleByUniqueID(const_cast(iter.Get()->GetKey()))) { + FireDelayedEvent(nsIAccessibleEvent::EVENT_LIVE_REGION_ADDED, liveRegion); + } + } + + mNewLiveRegions.Clear(); +} + +void DocAccessibleWrap::DoInitialUpdate() { + DocAccessible::DoInitialUpdate(); + ProcessNewLiveRegions(); +} diff --git a/accessible/mac/Platform.mm b/accessible/mac/Platform.mm index 35192c66d4f3..654554fcbcdf 100644 --- a/accessible/mac/Platform.mm +++ b/accessible/mac/Platform.mm @@ -77,14 +77,15 @@ void ProxyDestroyed(ProxyAccessible* aProxy) { } void ProxyEvent(ProxyAccessible* aProxy, uint32_t aEventType) { - // ignore everything but focus-changed, value-changed, caret, - // selection, and document load complete events for now. + // Ignore event that we don't escape below, they aren't yet supported. if (aEventType != nsIAccessibleEvent::EVENT_FOCUS && aEventType != nsIAccessibleEvent::EVENT_VALUE_CHANGE && aEventType != nsIAccessibleEvent::EVENT_TEXT_VALUE_CHANGE && aEventType != nsIAccessibleEvent::EVENT_TEXT_CARET_MOVED && aEventType != nsIAccessibleEvent::EVENT_DOCUMENT_LOAD_COMPLETE && - aEventType != nsIAccessibleEvent::EVENT_REORDER) + aEventType != nsIAccessibleEvent::EVENT_REORDER && + aEventType != nsIAccessibleEvent::EVENT_LIVE_REGION_ADDED && + aEventType != nsIAccessibleEvent::EVENT_LIVE_REGION_REMOVED) return; mozAccessible* wrapper = GetNativeFromGeckoAccessible(aProxy); diff --git a/accessible/tests/browser/mac/browser.ini b/accessible/tests/browser/mac/browser.ini index 815807c1f816..4c0cc1ad7f00 100644 --- a/accessible/tests/browser/mac/browser.ini +++ b/accessible/tests/browser/mac/browser.ini @@ -39,3 +39,4 @@ skip-if = os == 'mac' && debug # Bug 1664577 [browser_navigate.js] [browser_outline.js] [browser_hierarchy.js] +[browser_live_regions.js] diff --git a/accessible/tests/browser/mac/browser_live_regions.js b/accessible/tests/browser/mac/browser_live_regions.js new file mode 100644 index 000000000000..3a624fcaf9f3 --- /dev/null +++ b/accessible/tests/browser/mac/browser_live_regions.js @@ -0,0 +1,79 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/** + * Test live region creation and removal. + */ +addAccessibleTask( + ` +
Polite region
+
Assertive region
+ `, + async (browser, accDoc) => { + let liveRegionAdded = waitForEvent(EVENT_LIVE_REGION_ADDED, "polite"); + await SpecialPowers.spawn(browser, [], () => { + content.document + .getElementById("polite") + .setAttribute("aria-atomic", "true"); + content.document + .getElementById("polite") + .setAttribute("aria-live", "polite"); + }); + await liveRegionAdded; + + let liveRegionRemoved = waitForEvent( + EVENT_LIVE_REGION_REMOVED, + "assertive" + ); + await SpecialPowers.spawn(browser, [], () => { + content.document.getElementById("assertive").removeAttribute("aria-live"); + }); + await liveRegionRemoved; + + liveRegionAdded = waitForEvent(EVENT_LIVE_REGION_ADDED, "new-region"); + await SpecialPowers.spawn(browser, [], () => { + let newRegionElm = content.document.createElement("div"); + newRegionElm.id = "new-region"; + newRegionElm.setAttribute("aria-live", "assertive"); + content.document.body.appendChild(newRegionElm); + }); + await liveRegionAdded; + + let loadComplete = Promise.all([ + waitForMacEvent("AXLoadComplete"), + waitForEvent(EVENT_LIVE_REGION_ADDED, "region-1"), + waitForEvent(EVENT_LIVE_REGION_ADDED, "region-2"), + waitForEvent(EVENT_LIVE_REGION_ADDED, "status"), + waitForEvent(EVENT_LIVE_REGION_ADDED, "output"), + ]); + + await SpecialPowers.spawn(browser, [], () => { + content.location = `data:text/html;charset=utf-8, +
+
+
+
+ `; + }); + await loadComplete; + + liveRegionRemoved = waitForEvent(EVENT_LIVE_REGION_REMOVED, "status"); + await SpecialPowers.spawn(browser, [], () => { + content.document + .getElementById("status") + .setAttribute("aria-live", "off"); + }); + await liveRegionRemoved; + + liveRegionRemoved = waitForEvent(EVENT_LIVE_REGION_REMOVED, "output"); + await SpecialPowers.spawn(browser, [], () => { + content.document + .getElementById("output") + .setAttribute("aria-live", "off"); + }); + await liveRegionRemoved; + } +); diff --git a/accessible/tests/mochitest/promisified-events.js b/accessible/tests/mochitest/promisified-events.js index ab0eca8f753f..fcea95bc636e 100644 --- a/accessible/tests/mochitest/promisified-events.js +++ b/accessible/tests/mochitest/promisified-events.js @@ -42,6 +42,8 @@ const EVENT_VIRTUALCURSOR_CHANGED = const EVENT_ALERT = nsIAccessibleEvent.EVENT_ALERT; const EVENT_TEXT_SELECTION_CHANGED = nsIAccessibleEvent.EVENT_TEXT_SELECTION_CHANGED; +const EVENT_LIVE_REGION_ADDED = nsIAccessibleEvent.EVENT_LIVE_REGION_ADDED; +const EVENT_LIVE_REGION_REMOVED = nsIAccessibleEvent.EVENT_LIVE_REGION_REMOVED; const EventsLogger = { enabled: false, diff --git a/accessible/windows/msaa/nsEventMap.h b/accessible/windows/msaa/nsEventMap.h index 49cfbbc57d7e..ee492c37e415 100644 --- a/accessible/windows/msaa/nsEventMap.h +++ b/accessible/windows/msaa/nsEventMap.h @@ -102,5 +102,7 @@ static const uint32_t gWinEventMap[] = { EVENT_OBJECT_VALUECHANGE, // nsIAccessibleEvent::EVENT_TEXT_VALUE_CHANGE kEVENT_WIN_UNKNOWN, // nsIAccessibleEvent::EVENT_SCROLLING kEVENT_WIN_UNKNOWN, // nsIAccessibleEvent::EVENT_ANNOUNCEMENT + kEVENT_WIN_UNKNOWN, // nsIAccessibleEvent::EVENT_LIVE_REGION_ADDED + kEVENT_WIN_UNKNOWN, // nsIAccessibleEvent::EVENT_LIVE_REGION_REMOVED // clang-format on }; diff --git a/xpcom/ds/StaticAtoms.py b/xpcom/ds/StaticAtoms.py index b62bcbe87f75..4738645f43c2 100644 --- a/xpcom/ds/StaticAtoms.py +++ b/xpcom/ds/StaticAtoms.py @@ -2294,6 +2294,7 @@ STATIC_ATOMS = [ Atom("aria_rowindextext", "aria-rowindextext"), Atom("aria_rowspan", "aria-rowspan"), Atom("aria_valuetext", "aria-valuetext"), + Atom("assertive", "assertive"), Atom("auto_generated", "auto-generated"), Atom("banner", "banner"), Atom("checkable", "checkable"),