From 3c047cfc64b1df3cc026d369cfa2e60102361adb Mon Sep 17 00:00:00 2001 From: Morgan Rae Reschenberg Date: Tue, 21 May 2024 22:11:05 +0000 Subject: [PATCH] Bug 1895408: Add support for exposing critical, supoptimal, optimal meter values via AXValueDescription r=Jamie Differential Revision: https://phabricator.services.mozilla.com/D209615 --- accessible/base/CacheConstants.h | 2 + accessible/generic/LocalAccessible.cpp | 8 +++ accessible/html/HTMLFormControlAccessible.cpp | 42 +++++++++++++++ accessible/html/HTMLFormControlAccessible.h | 11 ++++ accessible/ipc/RemoteAccessible.cpp | 13 +++++ accessible/ipc/RemoteAccessible.h | 3 ++ accessible/mac/AccessibleWrap.mm | 3 ++ accessible/mac/mozActionElements.h | 7 +++ accessible/mac/mozActionElements.mm | 42 +++++++++++++++ accessible/tests/browser/mac/browser_range.js | 51 +++++++++++++++++++ 10 files changed, 182 insertions(+) diff --git a/accessible/base/CacheConstants.h b/accessible/base/CacheConstants.h index eb5cc79f5e89..984b950128e9 100644 --- a/accessible/base/CacheConstants.h +++ b/accessible/base/CacheConstants.h @@ -243,6 +243,8 @@ class CacheKey { static constexpr nsStaticAtom* TextValue = nsGkAtoms::aria_valuetext; // gfx::Matrix4x4, CacheDomain::TransformMatrix static constexpr nsStaticAtom* TransformMatrix = nsGkAtoms::transform; + // int32_t, CacheDomain::Value + static constexpr nsStaticAtom* ValueRegion = nsGkAtoms::valuetype; // nsTArray, CacheDomain::Viewport // The list of Accessibles in the viewport used for hit testing and on-screen // determination. diff --git a/accessible/generic/LocalAccessible.cpp b/accessible/generic/LocalAccessible.cpp index 8668679bab39..7ecf5b711480 100644 --- a/accessible/generic/LocalAccessible.cpp +++ b/accessible/generic/LocalAccessible.cpp @@ -3452,6 +3452,14 @@ already_AddRefed LocalAccessible::BundleFieldsForCache( fields->SetAttribute(CacheKey::SrcURL, DeleteEntry()); } } + + if (TagName() == nsGkAtoms::meter) { + // We should only cache value region for HTML meter elements. A meter + // should always have a value region, so this attribute should never + // be empty (i.e. there is no DeleteEntry() clause here). + HTMLMeterAccessible* meter = static_cast(this); + fields->SetAttribute(CacheKey::ValueRegion, meter->ValueRegion()); + } } if (aCacheDomain & CacheDomain::Viewport && IsDoc()) { diff --git a/accessible/html/HTMLFormControlAccessible.cpp b/accessible/html/HTMLFormControlAccessible.cpp index 602998dbb4a2..d525551e4ef2 100644 --- a/accessible/html/HTMLFormControlAccessible.cpp +++ b/accessible/html/HTMLFormControlAccessible.cpp @@ -18,6 +18,7 @@ #include "nsContentList.h" #include "mozilla/dom/HTMLInputElement.h" +#include "mozilla/dom/HTMLMeterElement.h" #include "mozilla/dom/HTMLTextAreaElement.h" #include "mozilla/dom/HTMLFormControlsCollection.h" #include "nsIFormControl.h" @@ -965,6 +966,39 @@ bool HTMLMeterAccessible::SetCurValue(double aValue) { return false; // meters are readonly. } +int32_t HTMLMeterAccessible::ValueRegion() const { + dom::HTMLMeterElement* elm = dom::HTMLMeterElement::FromNode(mContent); + if (!elm) { + return -1; + } + double high = elm->High(); + double low = elm->Low(); + double optimum = elm->Optimum(); + double value = elm->Value(); + // For more information on how these regions are defined, see + // "UA requirements for regions of the gauge" + // https://html.spec.whatwg.org/multipage/form-elements.html#the-meter-element + if (optimum > high) { + if (value > high) { + return 1; + } + return value > low ? 0 : -1; + } + if (optimum < low) { + if (value < low) { + return 1; + } + return value < high ? 0 : -1; + } + // optimum is between low and high, inclusive + if (value >= low && value <= high) { + return 1; + } + // Both upper and lower regions are considered equally + // non-optimal. + return 0; +} + void HTMLMeterAccessible::DOMAttributeChanged(int32_t aNameSpaceID, nsAtom* aAttribute, int32_t aModType, @@ -976,4 +1010,12 @@ void HTMLMeterAccessible::DOMAttributeChanged(int32_t aNameSpaceID, if (aAttribute == nsGkAtoms::value) { mDoc->FireDelayedEvent(nsIAccessibleEvent::EVENT_VALUE_CHANGE, this); } + + if (aAttribute == nsGkAtoms::high || aAttribute == nsGkAtoms::low || + aAttribute == nsGkAtoms::optimum) { + // Our meter's value region may have changed, queue an update for + // the value domain. + mDoc->QueueCacheUpdate(this, CacheDomain::Value); + return; + } } diff --git a/accessible/html/HTMLFormControlAccessible.h b/accessible/html/HTMLFormControlAccessible.h index 1e00bd4e4098..9a01167efd34 100644 --- a/accessible/html/HTMLFormControlAccessible.h +++ b/accessible/html/HTMLFormControlAccessible.h @@ -335,6 +335,17 @@ class HTMLMeterAccessible : public LeafAccessible { // Widgets virtual bool IsWidget() const override; + // HTMLMeterAccessible + + /** + * Given the low, high, and optimum attrs from DOM, return an int + * that indicates which region the current value falls in: + * - Optimal (1) + * - Suboptimal (0) + * - Critical, or "even less good" by the spec (-1) + */ + int32_t ValueRegion() const; + protected: virtual ~HTMLMeterAccessible() {} diff --git a/accessible/ipc/RemoteAccessible.cpp b/accessible/ipc/RemoteAccessible.cpp index 0077750ed3c3..24501dd6c808 100644 --- a/accessible/ipc/RemoteAccessible.cpp +++ b/accessible/ipc/RemoteAccessible.cpp @@ -1348,6 +1348,19 @@ void RemoteAccessible::Announce(const nsString& aAnnouncement, } #endif // !defined(XP_WIN) +int32_t RemoteAccessible::ValueRegion() const { + MOZ_ASSERT(TagName() == nsGkAtoms::meter, + "Accessing value region on non-meter element?"); + if (mCachedFields) { + if (auto region = + mCachedFields->GetAttribute(CacheKey::ValueRegion)) { + return *region; + } + } + // Expose sub-optimal (but not critical) as the value region, as a fallback. + return 0; +} + void RemoteAccessible::ScrollSubstringToPoint(int32_t aStartOffset, int32_t aEndOffset, uint32_t aCoordinateType, diff --git a/accessible/ipc/RemoteAccessible.h b/accessible/ipc/RemoteAccessible.h index 45f41b8fb56d..6d98841ae88c 100644 --- a/accessible/ipc/RemoteAccessible.h +++ b/accessible/ipc/RemoteAccessible.h @@ -374,6 +374,9 @@ class RemoteAccessible : public Accessible, public HyperTextAccessibleBase { void Announce(const nsString& aAnnouncement, uint16_t aPriority); #endif // !defined(XP_WIN) + // HTMLMeterAccessible + int32_t ValueRegion() const; + // HyperTextAccessibleBase virtual already_AddRefed DefaultTextAttributes() override; diff --git a/accessible/mac/AccessibleWrap.mm b/accessible/mac/AccessibleWrap.mm index e8107b1690ec..651a2fdf5582 100644 --- a/accessible/mac/AccessibleWrap.mm +++ b/accessible/mac/AccessibleWrap.mm @@ -195,6 +195,9 @@ Class a11y::GetTypeFromRole(roles::Role aRole) { case roles::PROGRESSBAR: return [mozRangeAccessible class]; + case roles::METER: + return [mozMeterAccessible class]; + case roles::SPINBUTTON: case roles::SLIDER: return [mozIncrementableAccessible class]; diff --git a/accessible/mac/mozActionElements.h b/accessible/mac/mozActionElements.h index d5b286ff97e0..6518eb4596ec 100644 --- a/accessible/mac/mozActionElements.h +++ b/accessible/mac/mozActionElements.h @@ -88,6 +88,13 @@ @end +@interface mozMeterAccessible : mozRangeAccessible + +// override +- (NSString*)moxValueDescription; + +@end + /** * Base accessible for an incrementable, a settable range */ diff --git a/accessible/mac/mozActionElements.mm b/accessible/mac/mozActionElements.mm index e3a2ff959840..1d0d97bf8350 100644 --- a/accessible/mac/mozActionElements.mm +++ b/accessible/mac/mozActionElements.mm @@ -177,6 +177,48 @@ using namespace mozilla::a11y; @end +@implementation mozMeterAccessible + +- (NSString*)moxValueDescription { + nsAutoString valueDesc; + mGeckoAccessible->Value(valueDesc); + if (mGeckoAccessible->TagName() != nsGkAtoms::meter) { + // We're dealing with an aria meter, which shouldn't get + // a value region. + return nsCocoaUtils::ToNSString(valueDesc); + } + + if (!valueDesc.IsEmpty()) { + // Append a comma to separate the existing value description + // from the value region. + valueDesc.Append(u", "_ns); + } + // We need to concat the given value description + // with a description of the value as either optimal, + // suboptimal, or critical. + int32_t region; + if (mGeckoAccessible->IsRemote()) { + region = mGeckoAccessible->AsRemote()->ValueRegion(); + } else { + HTMLMeterAccessible* localMeter = + static_cast(mGeckoAccessible->AsLocal()); + region = localMeter->ValueRegion(); + } + + if (region == 1) { + valueDesc.Append(u"Optimal value"_ns); + } else if (region == 0) { + valueDesc.Append(u"Suboptimal value"_ns); + } else { + MOZ_ASSERT(region == -1); + valueDesc.Append(u"Critical value"_ns); + } + + return nsCocoaUtils::ToNSString(valueDesc); +} + +@end + @implementation mozIncrementableAccessible - (NSString*)moxValueDescription { diff --git a/accessible/tests/browser/mac/browser_range.js b/accessible/tests/browser/mac/browser_range.js index 8a5bafba5095..a6c3378a04ac 100644 --- a/accessible/tests/browser/mac/browser_range.js +++ b/accessible/tests/browser/mac/browser_range.js @@ -242,3 +242,54 @@ addAccessibleTask( is(progress.getAttributeValue("AXValue"), 90, "Correct updated value"); } ); + +/** + * Verify meter HTML elements expose the value region as part of their value + * description. + */ +addAccessibleTask( + ``, + async (browser, accDoc) => { + const meter = getNativeInterface(accDoc, "fuel"); + is(meter.getAttributeValue("AXValue"), 50, "Correct value"); + is( + meter.getAttributeValue("AXValueDescription"), + "50, Suboptimal value", + "Value description contains appropriate value region" + ); + + let evt = waitForMacEvent("AXValueChanged"); + await invokeContentTask(browser, [], () => { + const f = content.document.getElementById("fuel"); + f.setAttribute("value", "90"); + }); + await evt; + + is( + meter.getAttributeValue("AXValueDescription"), + "90, Optimal value", + "Value description updated to optimal" + ); + + await invokeContentTask(browser, [], () => { + const f = content.document.getElementById("fuel"); + f.setAttribute("optimum", "20"); + }); + await untilCacheIs( + () => meter.getAttributeValue("AXValueDescription"), + "90, Critical value", + "Value description updated to critical." + ); + + // XXX bug 1895627: + // await invokeContentTask(browser, [], () => { + // const f = content.document.getElementById("fuel"); + // f.textContent = "at 90/100"; + // }); + // await untilCacheIs( + // () => meter.getAttributeValue("AXValueDescription"), + // "at 90/100, Critical value", + // "Value description updated to include inner text." + // ); + } +);