зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1886711: Implement the UIA Toggle pattern. r=nlapre
Differential Revision: https://phabricator.services.mozilla.com/D205552
This commit is contained in:
Родитель
2f6b638988
Коммит
2ac0262f68
|
@ -4,6 +4,12 @@
|
||||||
|
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
|
/* eslint-disable camelcase */
|
||||||
|
const ToggleState_Off = 0;
|
||||||
|
const ToggleState_On = 1;
|
||||||
|
const ToggleState_Indeterminate = 2;
|
||||||
|
/* eslint-enable camelcase */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test the Invoke pattern.
|
* Test the Invoke pattern.
|
||||||
*/
|
*/
|
||||||
|
@ -11,6 +17,7 @@ addUiaTask(
|
||||||
`
|
`
|
||||||
<button id="button">button</button>
|
<button id="button">button</button>
|
||||||
<p id="p">p</p>
|
<p id="p">p</p>
|
||||||
|
<input id="checkbox" type="checkbox">
|
||||||
`,
|
`,
|
||||||
async function testInvoke() {
|
async function testInvoke() {
|
||||||
await definePyVar("doc", `getDocUia()`);
|
await definePyVar("doc", `getDocUia()`);
|
||||||
|
@ -32,10 +39,105 @@ addUiaTask(
|
||||||
ok(true, "button got Invoked event");
|
ok(true, "button got Invoked event");
|
||||||
}
|
}
|
||||||
|
|
||||||
let hasPattern = await runPython(`
|
await testPatternAbsent("p", "Invoke");
|
||||||
p = findUiaByDomId(doc, "p")
|
// The Microsoft IA2 -> UIA proxy doesn't follow Microsoft's own rules.
|
||||||
return bool(getUiaPattern(p, "Invoke"))
|
if (gIsUiaEnabled) {
|
||||||
`);
|
// Check boxes expose the Toggle pattern, so they should not expose the
|
||||||
ok(!hasPattern, "p doesn't have Invoke pattern");
|
// 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";
|
"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.
|
// Load the shared-head file first.
|
||||||
Services.scriptloader.loadSubScript(
|
Services.scriptloader.loadSubScript(
|
||||||
|
@ -102,3 +102,14 @@ function waitForUiaEvent() {
|
||||||
onEvent.wait()
|
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);
|
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);
|
MsaaAccessible::FireWinEvent(aTarget, nsIAccessibleEvent::EVENT_STATE_CHANGE);
|
||||||
|
uiaRawElmProvider::RaiseUiaEventForStateChange(aTarget, aState, aEnabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
void a11y::PlatformFocusEvent(Accessible* aTarget,
|
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
|
// IUnknown
|
||||||
|
|
||||||
STDMETHODIMP
|
STDMETHODIMP
|
||||||
|
@ -78,6 +110,8 @@ uiaRawElmProvider::QueryInterface(REFIID aIid, void** aInterface) {
|
||||||
*aInterface = static_cast<IRawElementProviderFragment*>(this);
|
*aInterface = static_cast<IRawElementProviderFragment*>(this);
|
||||||
} else if (aIid == IID_IInvokeProvider) {
|
} else if (aIid == IID_IInvokeProvider) {
|
||||||
*aInterface = static_cast<IInvokeProvider*>(this);
|
*aInterface = static_cast<IInvokeProvider*>(this);
|
||||||
|
} else if (aIid == IID_IToggleProvider) {
|
||||||
|
*aInterface = static_cast<IToggleProvider*>(this);
|
||||||
} else {
|
} else {
|
||||||
return E_NOINTERFACE;
|
return E_NOINTERFACE;
|
||||||
}
|
}
|
||||||
|
@ -176,11 +210,20 @@ uiaRawElmProvider::GetPatternProvider(
|
||||||
}
|
}
|
||||||
switch (aPatternId) {
|
switch (aPatternId) {
|
||||||
case UIA_InvokePatternId:
|
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;
|
RefPtr<IInvokeProvider> invoke = this;
|
||||||
invoke.forget(aPatternProvider);
|
invoke.forget(aPatternProvider);
|
||||||
}
|
}
|
||||||
return S_OK;
|
return S_OK;
|
||||||
|
case UIA_TogglePatternId:
|
||||||
|
if (HasTogglePattern()) {
|
||||||
|
RefPtr<IToggleProvider> toggle = this;
|
||||||
|
toggle.forget(aPatternProvider);
|
||||||
|
}
|
||||||
|
return S_OK;
|
||||||
}
|
}
|
||||||
return S_OK;
|
return S_OK;
|
||||||
}
|
}
|
||||||
|
@ -497,6 +540,31 @@ uiaRawElmProvider::Invoke() {
|
||||||
return S_OK;
|
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
|
// Private methods
|
||||||
|
|
||||||
bool uiaRawElmProvider::IsControl() {
|
bool uiaRawElmProvider::IsControl() {
|
||||||
|
@ -578,3 +646,21 @@ long uiaRawElmProvider::GetControlType() const {
|
||||||
MOZ_CRASH("Unknown role.");
|
MOZ_CRASH("Unknown role.");
|
||||||
return 0;
|
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,
|
class uiaRawElmProvider : public IAccessibleEx,
|
||||||
public IRawElementProviderSimple,
|
public IRawElementProviderSimple,
|
||||||
public IRawElementProviderFragment,
|
public IRawElementProviderFragment,
|
||||||
public IInvokeProvider {
|
public IInvokeProvider,
|
||||||
|
public IToggleProvider {
|
||||||
public:
|
public:
|
||||||
static void RaiseUiaEventForGeckoEvent(Accessible* aAcc,
|
static void RaiseUiaEventForGeckoEvent(Accessible* aAcc,
|
||||||
uint32_t aGeckoEvent);
|
uint32_t aGeckoEvent);
|
||||||
|
static void RaiseUiaEventForStateChange(Accessible* aAcc, uint64_t aState,
|
||||||
|
bool aEnabled);
|
||||||
|
|
||||||
// IUnknown
|
// IUnknown
|
||||||
STDMETHODIMP QueryInterface(REFIID aIid, void** aInterface);
|
STDMETHODIMP QueryInterface(REFIID aIid, void** aInterface);
|
||||||
|
@ -85,10 +88,18 @@ class uiaRawElmProvider : public IAccessibleEx,
|
||||||
// IInvokeProvider
|
// IInvokeProvider
|
||||||
virtual HRESULT STDMETHODCALLTYPE Invoke(void);
|
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:
|
private:
|
||||||
Accessible* Acc() const;
|
Accessible* Acc() const;
|
||||||
bool IsControl();
|
bool IsControl();
|
||||||
long GetControlType() const;
|
long GetControlType() const;
|
||||||
|
bool HasTogglePattern();
|
||||||
|
static ToggleState ToToggleState(uint64_t aState);
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace a11y
|
} // namespace a11y
|
||||||
|
|
Загрузка…
Ссылка в новой задаче