Bug 1886711: Implement the UIA Toggle pattern. r=nlapre

Differential Revision: https://phabricator.services.mozilla.com/D205552
This commit is contained in:
James Teh 2024-03-27 00:36:29 +00:00
Родитель 2f6b638988
Коммит 2ac0262f68
5 изменённых файлов: 221 добавлений и 9 удалений

Просмотреть файл

@ -4,6 +4,12 @@
"use strict";
/* eslint-disable camelcase */
const ToggleState_Off = 0;
const ToggleState_On = 1;
const ToggleState_Indeterminate = 2;
/* eslint-enable camelcase */
/**
* Test the Invoke pattern.
*/
@ -11,6 +17,7 @@ addUiaTask(
`
<button id="button">button</button>
<p id="p">p</p>
<input id="checkbox" type="checkbox">
`,
async function testInvoke() {
await definePyVar("doc", `getDocUia()`);
@ -32,10 +39,105 @@ addUiaTask(
ok(true, "button got Invoked event");
}
let hasPattern = await runPython(`
p = findUiaByDomId(doc, "p")
return bool(getUiaPattern(p, "Invoke"))
`);
ok(!hasPattern, "p doesn't have Invoke pattern");
await testPatternAbsent("p", "Invoke");
// The Microsoft IA2 -> UIA proxy doesn't follow Microsoft's own rules.
if (gIsUiaEnabled) {
// Check boxes expose the Toggle pattern, so they should not expose the
// Invoke pattern.
await testPatternAbsent("checkbox", "Invoke");
}
}
);
/**
* Test the Toggle pattern.
*/
addUiaTask(
`
<input id="checkbox" type="checkbox" checked>
<button id="toggleButton" aria-pressed="false">toggle</button>
<button id="button">button</button>
<p id="p">p</p>
<script>
// When checkbox is clicked and it is not checked, make it indeterminate.
document.getElementById("checkbox").addEventListener("click", evt => {
// Within the event listener, .checked is reversed and you can't set
// .indeterminate. Work around this by deferring and handling the changes
// ourselves.
evt.preventDefault();
const target = evt.target;
setTimeout(() => {
if (target.checked) {
target.checked = false;
} else {
target.indeterminate = true;
}
}, 0);
});
// When toggleButton is clicked, set aria-pressed to true.
document.getElementById("toggleButton").addEventListener("click", evt => {
evt.target.ariaPressed = "true";
});
</script>
`,
async function testToggle() {
await definePyVar("doc", `getDocUia()`);
await assignPyVarToUiaWithId("checkbox");
await definePyVar("pattern", `getUiaPattern(checkbox, "Toggle")`);
ok(await runPython(`bool(pattern)`), "checkbox has Toggle pattern");
is(
await runPython(`pattern.CurrentToggleState`),
ToggleState_On,
"checkbox has ToggleState_On"
);
// The IA2 -> UIA proxy doesn't fire ToggleState prop change events.
if (gIsUiaEnabled) {
info("Calling Toggle on checkbox");
await setUpWaitForUiaPropEvent("ToggleToggleState", "checkbox");
await runPython(`pattern.Toggle()`);
await waitForUiaEvent();
ok(true, "Got ToggleState prop change event on checkbox");
is(
await runPython(`pattern.CurrentToggleState`),
ToggleState_Off,
"checkbox has ToggleState_Off"
);
info("Calling Toggle on checkbox");
await setUpWaitForUiaPropEvent("ToggleToggleState", "checkbox");
await runPython(`pattern.Toggle()`);
await waitForUiaEvent();
ok(true, "Got ToggleState prop change event on checkbox");
is(
await runPython(`pattern.CurrentToggleState`),
ToggleState_Indeterminate,
"checkbox has ToggleState_Indeterminate"
);
}
await assignPyVarToUiaWithId("toggleButton");
await definePyVar("pattern", `getUiaPattern(toggleButton, "Toggle")`);
ok(await runPython(`bool(pattern)`), "toggleButton has Toggle pattern");
is(
await runPython(`pattern.CurrentToggleState`),
ToggleState_Off,
"toggleButton has ToggleState_Off"
);
if (gIsUiaEnabled) {
info("Calling Toggle on toggleButton");
await setUpWaitForUiaPropEvent("ToggleToggleState", "toggleButton");
await runPython(`pattern.Toggle()`);
await waitForUiaEvent();
ok(true, "Got ToggleState prop change event on toggleButton");
is(
await runPython(`pattern.CurrentToggleState`),
ToggleState_On,
"toggleButton has ToggleState_Off"
);
}
await testPatternAbsent("button", "Toggle");
await testPatternAbsent("p", "Toggle");
}
);

Просмотреть файл

@ -4,7 +4,7 @@
"use strict";
/* exported gIsUiaEnabled, addUiaTask, definePyVar, assignPyVarToUiaWithId, setUpWaitForUiaEvent, setUpWaitForUiaPropEvent, waitForUiaEvent */
/* exported gIsUiaEnabled, addUiaTask, definePyVar, assignPyVarToUiaWithId, setUpWaitForUiaEvent, setUpWaitForUiaPropEvent, waitForUiaEvent, testPatternAbsent */
// Load the shared-head file first.
Services.scriptloader.loadSubScript(
@ -102,3 +102,14 @@ function waitForUiaEvent() {
onEvent.wait()
`);
}
/**
* Verify that a UIA element does *not* support the given control pattern.
*/
async function testPatternAbsent(id, patternName) {
const hasPattern = await runPython(`
el = findUiaByDomId(doc, "${id}")
return bool(getUiaPattern(el, "${patternName}"))
`);
ok(!hasPattern, `${id} doesn't have ${patternName} pattern`);
}

Просмотреть файл

@ -72,8 +72,10 @@ void a11y::PlatformEvent(Accessible* aTarget, uint32_t aEventType) {
uiaRawElmProvider::RaiseUiaEventForGeckoEvent(aTarget, aEventType);
}
void a11y::PlatformStateChangeEvent(Accessible* aTarget, uint64_t, bool) {
void a11y::PlatformStateChangeEvent(Accessible* aTarget, uint64_t aState,
bool aEnabled) {
MsaaAccessible::FireWinEvent(aTarget, nsIAccessibleEvent::EVENT_STATE_CHANGE);
uiaRawElmProvider::RaiseUiaEventForStateChange(aTarget, aState, aEnabled);
}
void a11y::PlatformFocusEvent(Accessible* aTarget,

Просмотреть файл

@ -65,6 +65,38 @@ void uiaRawElmProvider::RaiseUiaEventForGeckoEvent(Accessible* aAcc,
}
}
/* static */
void uiaRawElmProvider::RaiseUiaEventForStateChange(Accessible* aAcc,
uint64_t aState,
bool aEnabled) {
if (!StaticPrefs::accessibility_uia_enable()) {
return;
}
auto* uia = MsaaAccessible::GetFrom(aAcc);
if (!uia) {
return;
}
PROPERTYID property = 0;
_variant_t newVal;
switch (aState) {
case states::CHECKED:
case states::MIXED:
case states::PRESSED:
property = UIA_ToggleToggleStatePropertyId;
newVal.vt = VT_I4;
newVal.lVal = ToToggleState(aEnabled ? aState : 0);
break;
default:
return;
}
MOZ_ASSERT(property);
if (::UiaClientsAreListening()) {
// We can't get the old value. Thankfully, clients don't seem to need it.
_variant_t oldVal;
::UiaRaiseAutomationPropertyChangedEvent(uia, property, oldVal, newVal);
}
}
// IUnknown
STDMETHODIMP
@ -78,6 +110,8 @@ uiaRawElmProvider::QueryInterface(REFIID aIid, void** aInterface) {
*aInterface = static_cast<IRawElementProviderFragment*>(this);
} else if (aIid == IID_IInvokeProvider) {
*aInterface = static_cast<IInvokeProvider*>(this);
} else if (aIid == IID_IToggleProvider) {
*aInterface = static_cast<IToggleProvider*>(this);
} else {
return E_NOINTERFACE;
}
@ -176,11 +210,20 @@ uiaRawElmProvider::GetPatternProvider(
}
switch (aPatternId) {
case UIA_InvokePatternId:
if (acc->ActionCount() > 0) {
// Per the UIA documentation, we should only expose the Invoke pattern "if
// the same behavior is not exposed through another control pattern
// provider".
if (acc->ActionCount() > 0 && !HasTogglePattern()) {
RefPtr<IInvokeProvider> invoke = this;
invoke.forget(aPatternProvider);
}
return S_OK;
case UIA_TogglePatternId:
if (HasTogglePattern()) {
RefPtr<IToggleProvider> toggle = this;
toggle.forget(aPatternProvider);
}
return S_OK;
}
return S_OK;
}
@ -497,6 +540,31 @@ uiaRawElmProvider::Invoke() {
return S_OK;
}
// IToggleProvider methods
STDMETHODIMP
uiaRawElmProvider::Toggle() {
Accessible* acc = Acc();
if (!acc) {
return CO_E_OBJNOTCONNECTED;
}
acc->DoAction(0);
return S_OK;
}
STDMETHODIMP
uiaRawElmProvider::get_ToggleState(__RPC__out enum ToggleState* aRetVal) {
if (!aRetVal) {
return E_INVALIDARG;
}
Accessible* acc = Acc();
if (!acc) {
return CO_E_OBJNOTCONNECTED;
}
*aRetVal = ToToggleState(acc->State());
return S_OK;
}
// Private methods
bool uiaRawElmProvider::IsControl() {
@ -578,3 +646,21 @@ long uiaRawElmProvider::GetControlType() const {
MOZ_CRASH("Unknown role.");
return 0;
}
bool uiaRawElmProvider::HasTogglePattern() {
Accessible* acc = Acc();
MOZ_ASSERT(acc);
return acc->State() & states::CHECKABLE ||
acc->Role() == roles::TOGGLE_BUTTON;
}
/* static */
ToggleState uiaRawElmProvider::ToToggleState(uint64_t aState) {
if (aState & states::MIXED) {
return ToggleState_Indeterminate;
}
if (aState & (states::CHECKED | states::PRESSED)) {
return ToggleState_On;
}
return ToggleState_Off;
}

Просмотреть файл

@ -22,10 +22,13 @@ class Accessible;
class uiaRawElmProvider : public IAccessibleEx,
public IRawElementProviderSimple,
public IRawElementProviderFragment,
public IInvokeProvider {
public IInvokeProvider,
public IToggleProvider {
public:
static void RaiseUiaEventForGeckoEvent(Accessible* aAcc,
uint32_t aGeckoEvent);
static void RaiseUiaEventForStateChange(Accessible* aAcc, uint64_t aState,
bool aEnabled);
// IUnknown
STDMETHODIMP QueryInterface(REFIID aIid, void** aInterface);
@ -85,10 +88,18 @@ class uiaRawElmProvider : public IAccessibleEx,
// IInvokeProvider
virtual HRESULT STDMETHODCALLTYPE Invoke(void);
// IToggleProvider
virtual HRESULT STDMETHODCALLTYPE Toggle(void);
virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_ToggleState(
/* [retval][out] */ __RPC__out enum ToggleState* aRetVal);
private:
Accessible* Acc() const;
bool IsControl();
long GetControlType() const;
bool HasTogglePattern();
static ToggleState ToToggleState(uint64_t aState);
};
} // namespace a11y