Bug 1733248 - Fire state change events for autocomplete changes. r=morgan

Trickier than just listening for attributes on inputs since a form
can toggle autocomplete too.

Also fixed To32States to work on the 32nd bit.

Differential Revision: https://phabricator.services.mozilla.com/D128913
This commit is contained in:
Eitan Isaacson 2021-10-21 22:47:43 +00:00
parent 9ea8dc7379
commit 254970745b
4 changed files with 136 additions and 7 deletions

View File

@ -220,7 +220,7 @@ class nsAccUtils {
static uint32_t To32States(uint64_t aState, bool* aIsExtra) {
uint32_t extraState = aState >> 31;
*aIsExtra = !!extraState;
return aState | extraState;
return extraState ? extraState : aState;
}
/**

View File

@ -54,6 +54,35 @@ nsAtom* HTMLFormAccessible::LandmarkRole() const {
: nsGkAtoms::form;
}
void HTMLFormAccessible::DOMAttributeChanged(int32_t aNameSpaceID,
nsAtom* aAttribute,
int32_t aModType,
const nsAttrValue* aOldValue,
uint64_t aOldState) {
HyperTextAccessibleWrap::DOMAttributeChanged(aNameSpaceID, aAttribute,
aModType, aOldValue, aOldState);
if (aAttribute == nsGkAtoms::autocomplete) {
dom::HTMLFormElement* formEl = dom::HTMLFormElement::FromNode(mContent);
nsIHTMLCollection* controls = formEl->Elements();
uint32_t length = controls->Length();
for (uint32_t i = 0; i < length; i++) {
if (LocalAccessible* acc = mDoc->GetAccessible(controls->Item(i))) {
if (acc->IsTextField() && !acc->IsPassword()) {
if (!acc->Elm()->HasAttr(nsGkAtoms::list_) &&
!acc->Elm()->AttrValueIs(kNameSpaceID_None,
nsGkAtoms::autocomplete, nsGkAtoms::OFF,
eIgnoreCase)) {
RefPtr<AccEvent> stateChangeEvent =
new AccStateChangeEvent(acc, states::SUPPORTS_AUTOCOMPLETION);
mDoc->FireDelayedEvent(stateChangeEvent);
}
}
}
}
}
}
////////////////////////////////////////////////////////////////////////////////
// HTMLRadioButtonAccessible
////////////////////////////////////////////////////////////////////////////////
@ -319,7 +348,8 @@ void HTMLTextFieldAccessible::Value(nsString& aValue) const {
}
bool HTMLTextFieldAccessible::AttributeChangesState(nsAtom* aAttribute) {
if (aAttribute == nsGkAtoms::readonly) {
if (aAttribute == nsGkAtoms::readonly || aAttribute == nsGkAtoms::list_ ||
aAttribute == nsGkAtoms::autocomplete) {
return true;
}

View File

@ -247,6 +247,11 @@ class HTMLFormAccessible : public HyperTextAccessibleWrap {
virtual a11y::role NativeRole() const override;
protected:
virtual void DOMAttributeChanged(int32_t aNameSpaceID, nsAtom* aAttribute,
int32_t aModType,
const nsAttrValue* aOldValue,
uint64_t aOldState) override;
virtual ~HTMLFormAccessible() = default;
};

View File

@ -297,6 +297,92 @@
`${aID} should not be multiselectable`);
}
async function testAutocomplete() {
// A text input will have autocomplete via browser's form autofill...
testStates("input",
0, EXT_STATE_SUPPORTS_AUTOCOMPLETION,
0, 0,
"input supports autocompletion");
// unless it is explicitly turned off.
testStates("input-autocomplete-off",
0, 0,
0, EXT_STATE_SUPPORTS_AUTOCOMPLETION,
"input-autocomplete-off does not support autocompletion");
// An input with a datalist will always have autocomplete.
testStates("input-list",
0, EXT_STATE_SUPPORTS_AUTOCOMPLETION,
0, 0,
"input-list supports autocompletion");
// password fields don't get autocomplete.
testStates("input-password",
0, 0,
0, EXT_STATE_SUPPORTS_AUTOCOMPLETION,
"input-autocomplete-off does not support autocompletion");
let p = waitForEvents({
expected: [
// Setting the form's autocomplete attribute to "off" will cause
// "input" to lost its autocomplete state.
stateChange(EXT_STATE_SUPPORTS_AUTOCOMPLETION, true, false, "input")
],
unexpected: [
// "input-list" should preserve its autocomplete state regardless of
// forms "autocomplete" attribute
[EVENT_STATE_CHANGE, "input-list"],
// "input-autocomplete-off" already has its autocomplte off, so no state
// change here.
[EVENT_STATE_CHANGE, "input-autocomplete-off"],
// passwords never get autocomplete
[EVENT_STATE_CHANGE, "input-password"],
]
});
getNode("form").setAttribute("autocomplete", "off");
await p;
// Same when we remove the form's autocomplete attribute.
p = waitForEvents({
expected: [stateChange(EXT_STATE_SUPPORTS_AUTOCOMPLETION, true, true, "input")],
unexpected: [
[EVENT_STATE_CHANGE, "input-list"],
[EVENT_STATE_CHANGE, "input-autocomplete-off"],
[EVENT_STATE_CHANGE, "input-password"],
]
});
getNode("form").removeAttribute("autocomplete");
await p;
p = waitForEvents({
expected: [
// Forcing autocomplete off on an input will cause a state change
stateChange(EXT_STATE_SUPPORTS_AUTOCOMPLETION, true, false, "input"),
// Associating a datalist with an autocomplete=off input
// will give it an autocomplete state, regardless.
stateChange(EXT_STATE_SUPPORTS_AUTOCOMPLETION, true, true, "input-autocomplete-off"),
// XXX: datalist inputs also get a HASPOPUP state, the inconsistent
// use of that state is inexplicable, but lets make sure we fire state
// change events for it anyway.
stateChange(STATE_HASPOPUP, false, true, "input-autocomplete-off"),
],
unexpected: [
// Forcing autocomplete off with a dataset input does nothing.
[EVENT_STATE_CHANGE, "input-list"],
// passwords never get autocomplete
[EVENT_STATE_CHANGE, "input-password"],
]
});
getNode("input").setAttribute("autocomplete", "off");
getNode("input-list").setAttribute("autocomplete", "off");
getNode("input-autocomplete-off").setAttribute("list", "browsers");
getNode("input-password").setAttribute("autocomplete", "off");
await p;
}
async function doTests() {
// Test opening details objects
await openNode("detailsOpen", "summaryOpen", true);
@ -362,6 +448,8 @@
await testMultiSelectable("select", "multiple");
await testAutocomplete();
SimpleTest.finish();
}
@ -457,15 +545,21 @@
<form id="form">
<button id="default-button">hello</button>
<button>world</button>
<input id="input">
<input id="input-autocomplete-off" autocomplete="off">
<input id="input-list" list="browsers">
<input id="input-password" type="password">
<datalist id="browsers">
<option value="Internet Explorer">
<option value="Firefox">
<option value="Google Chrome">
<option value="Opera">
<option value="Safari">
</datalist>
</form>
<div id="article" role="article">hello</div>
<form id="form">
<button id="default-button">hello</button>
<button>world</button>
</form>
<img id="animated-image" src="../animated-gif.gif">
<ul id="listbox" role="listbox">