Bug 1625915 - Expose validation error message via AXValidationError. r=morgan
Differential Revision: https://phabricator.services.mozilla.com/D214679
This commit is contained in:
committed by
mreschenberg@mozilla.com
parent
2ea61132e6
commit
921153b34f
@@ -551,6 +551,7 @@ static const char kEventTypeNames[][40] = {
|
||||
"live region removed", // EVENT_LIVE_REGION_REMOVED
|
||||
"inner reorder", // EVENT_INNER_REORDER
|
||||
"live region changed", // EVENT_LIVE_REGION_CHANGED
|
||||
"errormessage changed", // EVENT_ERRORMESSAGE_CHANGED
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
@@ -218,10 +218,15 @@ interface nsIAccessibleEvent : nsISupports
|
||||
*/
|
||||
const unsigned long EVENT_LIVE_REGION_CHANGED = 0x0029;
|
||||
|
||||
/**
|
||||
* A value of aria-errormessage was changed.
|
||||
*/
|
||||
const unsigned long EVENT_ERRORMESSAGE_CHANGED = 0x002a;
|
||||
|
||||
/**
|
||||
* Help make sure event map does not get out-of-line.
|
||||
*/
|
||||
const unsigned long EVENT_LAST_ENTRY = 0x002a;
|
||||
const unsigned long EVENT_LAST_ENTRY = 0x002b;
|
||||
|
||||
/**
|
||||
* The type of event, based on the enumerated event values
|
||||
|
||||
@@ -33,6 +33,16 @@ void DocAccessibleWrap::AttributeChanged(dom::Element* aElement,
|
||||
const nsAttrValue* aOldValue) {
|
||||
DocAccessible::AttributeChanged(aElement, aNameSpaceID, aAttribute, aModType,
|
||||
aOldValue);
|
||||
if (aAttribute == nsGkAtoms::aria_errormessage) {
|
||||
LocalAccessible* accessible =
|
||||
mContent != aElement ? GetAccessible(aElement) : this;
|
||||
if (!accessible) {
|
||||
return;
|
||||
}
|
||||
FireDelayedEvent(nsIAccessibleEvent::EVENT_ERRORMESSAGE_CHANGED,
|
||||
accessible);
|
||||
}
|
||||
|
||||
if (aAttribute == nsGkAtoms::aria_live) {
|
||||
LocalAccessible* accessible =
|
||||
mContent != aElement ? GetAccessible(aElement) : this;
|
||||
|
||||
@@ -109,6 +109,9 @@
|
||||
// AXEnabled
|
||||
- (NSNumber* _Nullable)moxEnabled;
|
||||
|
||||
// AXErrorMessageElements
|
||||
- (NSArray* _Nullable)moxErrorMessageElements;
|
||||
|
||||
// AXFocused
|
||||
- (NSNumber* _Nullable)moxFocused;
|
||||
|
||||
|
||||
@@ -101,7 +101,8 @@ void PlatformEvent(Accessible* aTarget, uint32_t aEventType) {
|
||||
aEventType != nsIAccessibleEvent::EVENT_LIVE_REGION_REMOVED &&
|
||||
aEventType != nsIAccessibleEvent::EVENT_LIVE_REGION_CHANGED &&
|
||||
aEventType != nsIAccessibleEvent::EVENT_NAME_CHANGE &&
|
||||
aEventType != nsIAccessibleEvent::EVENT_OBJECT_ATTRIBUTE_CHANGED) {
|
||||
aEventType != nsIAccessibleEvent::EVENT_OBJECT_ATTRIBUTE_CHANGED &&
|
||||
aEventType != nsIAccessibleEvent::EVENT_ERRORMESSAGE_CHANGED) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -93,6 +93,8 @@ enum CheckedState {
|
||||
inContainer:(mozilla::a11y::Accessible*)container
|
||||
at:(int32_t)start;
|
||||
|
||||
- (void)maybePostValidationErrorChanged;
|
||||
|
||||
// internal method to retrieve a child at a given index.
|
||||
- (id)childAt:(uint32_t)i;
|
||||
|
||||
@@ -188,6 +190,9 @@ enum CheckedState {
|
||||
// override
|
||||
- (NSString*)moxInvalid;
|
||||
|
||||
// override
|
||||
- (NSString*)moxErrorMessageElements;
|
||||
|
||||
// override
|
||||
- (NSNumber*)moxFocused;
|
||||
|
||||
|
||||
@@ -602,6 +602,17 @@ struct RoleDescrComparator {
|
||||
return ([self stateWithMask:states::INVALID] != 0) ? @"true" : @"false";
|
||||
}
|
||||
|
||||
- (NSArray*)moxErrorMessageElements {
|
||||
if (![[self moxInvalid] isEqualToString:@"false"]) {
|
||||
NSArray* relations = [self getRelationsByType:RelationType::ERRORMSG];
|
||||
if ([relations count] > 0) {
|
||||
return relations;
|
||||
}
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (NSNumber*)moxFocused {
|
||||
return @([self stateWithMask:states::FOCUSED] != 0);
|
||||
}
|
||||
@@ -966,6 +977,7 @@ struct RoleDescrComparator {
|
||||
inserted:(BOOL)isInserted
|
||||
inContainer:(Accessible*)container
|
||||
at:(int32_t)start {
|
||||
[self maybePostValidationErrorChanged];
|
||||
}
|
||||
|
||||
- (void)handleAccessibleEvent:(uint32_t)eventType {
|
||||
@@ -1027,6 +1039,32 @@ struct RoleDescrComparator {
|
||||
MOZ_ASSERT(mIsLiveRegion);
|
||||
[self moxPostNotification:@"AXLiveRegionChanged"];
|
||||
break;
|
||||
case nsIAccessibleEvent::EVENT_ERRORMESSAGE_CHANGED: {
|
||||
// aria-errormessage was changed. If aria-invalid != "true", it means that
|
||||
// VoiceOver should (a) expose a new message or (b) remove an
|
||||
// old message
|
||||
if (![[self moxInvalid] isEqualToString:@"false"]) {
|
||||
[self moxPostNotification:@"AXValidationErrorChanged"];
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)maybePostValidationErrorChanged {
|
||||
NSArray* relations =
|
||||
[self getRelationsByType:(mozilla::a11y::RelationType::ERRORMSG_FOR)];
|
||||
if ([relations count] > 0) {
|
||||
// only fire AXValidationErrorChanged if related node is not
|
||||
// `aria-invalid="false"`
|
||||
for (uint32_t relIdx = 0; relIdx <= [relations count]; relIdx++) {
|
||||
NSString* invalidStr = [relations[relIdx] moxInvalid];
|
||||
if (![invalidStr isEqualToString:@"false"]) {
|
||||
[self moxPostNotification:@"AXValidationErrorChanged"];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,8 @@ https_first_disabled = true
|
||||
["browser_aria_current.js"]
|
||||
skip-if = ["os == 'mac' && os_version == '15.30' && arch == 'aarch64' && opt"] # Bug 1802555
|
||||
|
||||
["browser_aria_errormessage.js"]
|
||||
|
||||
["browser_aria_expanded.js"]
|
||||
|
||||
["browser_aria_haspopup.js"]
|
||||
|
||||
268
accessible/tests/browser/mac/browser_aria_errormessage.js
Normal file
268
accessible/tests/browser/mac/browser_aria_errormessage.js
Normal file
@@ -0,0 +1,268 @@
|
||||
/* 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";
|
||||
|
||||
/**
|
||||
* This test verifies that an input with aria-invalid="true/grammar/spelling" exposes the MOX accessible for
|
||||
* its error message via AXErrorMessageElements.
|
||||
*/
|
||||
addAccessibleTask(
|
||||
`
|
||||
<input id="input-invalid" aria-invalid="true" aria-errormessage="error-msg">
|
||||
<input id="input-invalid-grammar" aria-invalid="grammar" aria-errormessage="error-msg">
|
||||
<input id="input-invalid-spelling" aria-invalid="spelling" aria-errormessage="error-msg">
|
||||
<div id="error-msg">Field validation failed</div>
|
||||
`,
|
||||
(_browser, accDoc) => {
|
||||
const messagesInvalid = getNativeInterface(accDoc, "input-invalid")
|
||||
.getAttributeValue("AXErrorMessageElements")
|
||||
.map(e => e.getAttributeValue("AXDOMIdentifier"));
|
||||
|
||||
is(
|
||||
messagesInvalid.length,
|
||||
1,
|
||||
"Only one element referenced via 'aria-errormessage'"
|
||||
);
|
||||
is(
|
||||
messagesInvalid[0],
|
||||
"error-msg",
|
||||
"input#input-invalid refers 'error-msg' in the 'aria-errormessage'"
|
||||
);
|
||||
|
||||
const messagesInvalidGrammar = getNativeInterface(
|
||||
accDoc,
|
||||
"input-invalid-grammar"
|
||||
)
|
||||
.getAttributeValue("AXErrorMessageElements")
|
||||
.map(e => e.getAttributeValue("AXDOMIdentifier"));
|
||||
|
||||
is(
|
||||
messagesInvalidGrammar.length,
|
||||
1,
|
||||
"Only one element referenced via 'aria-errormessage'"
|
||||
);
|
||||
is(
|
||||
messagesInvalidGrammar[0],
|
||||
"error-msg",
|
||||
"input#input-invalid-grammar refers 'error-msg' in the 'aria-errormessage'"
|
||||
);
|
||||
|
||||
const messagesInvalidSpelling = getNativeInterface(
|
||||
accDoc,
|
||||
"input-invalid-spelling"
|
||||
)
|
||||
.getAttributeValue("AXErrorMessageElements")
|
||||
.map(e => e.getAttributeValue("AXDOMIdentifier"));
|
||||
|
||||
is(
|
||||
messagesInvalidSpelling.length,
|
||||
1,
|
||||
"Only one element referenced via 'aria-errormessage'"
|
||||
);
|
||||
is(
|
||||
messagesInvalidSpelling[0],
|
||||
"error-msg",
|
||||
"input#input-invalid-spelling refers 'error-msg' in the 'aria-errormessage'"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* This test verifies that an input with aria-invalid=true exposes all the MOX accessibles defined through `aria-errormessage`
|
||||
* via AXErrorMessageElements
|
||||
*/
|
||||
addAccessibleTask(
|
||||
`
|
||||
<label for="input">Field with error</label><input id="input" aria-invalid="true" aria-errormessage="error-msg-specialchar error-msg-10charlong">
|
||||
<div id="error-msg-specialchar">Field must contain special characters</div>
|
||||
<div id="error-msg-10charlong">Field must contain more than 10 characters</div>
|
||||
`,
|
||||
(_browser, accDoc) => {
|
||||
let input = getNativeInterface(accDoc, "input");
|
||||
const errorMessageList = input.getAttributeValue("AXErrorMessageElements");
|
||||
|
||||
let messages = errorMessageList.map(e =>
|
||||
e.getAttributeValue("AXDOMIdentifier")
|
||||
);
|
||||
messages.sort();
|
||||
|
||||
is(
|
||||
messages.length,
|
||||
2,
|
||||
"input#input references two elements via 'aria-errormessage'"
|
||||
);
|
||||
is(
|
||||
messages[0],
|
||||
"error-msg-10charlong",
|
||||
"We expect all elements listed in 'aria-errormessage'"
|
||||
);
|
||||
is(
|
||||
messages[1],
|
||||
"error-msg-specialchar",
|
||||
"We expect all elements listed in 'aria-errormessage'"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* When aria-invalid is set to "false", attribute is missing or without a value, AXErrorMessageElements should
|
||||
* not return associated error messages.
|
||||
* This test verifies that in this cases, AXErrorMessageElements returns `null`.
|
||||
*/
|
||||
addAccessibleTask(
|
||||
`
|
||||
<label for="input">Field with error</label><input id="input-invalid-false" aria-invalid="false" aria-errormessage="error-msg-specialchar error-msg-10charlong">
|
||||
<label for="input">Field with error</label><input id="input-invalid-missing" aria-errormessage="error-msg-specialchar error-msg-10charlong">
|
||||
<label for="input">Field with error</label><input id="input-invalid-spelling-error" aria-invalid aria-errormessage="error-msg-specialchar error-msg-10charlong">
|
||||
<div id="error-msg-specialchar">Field must contain special characters</div>
|
||||
<div id="error-msg-10charlong">Field must contain more than 10 characters</div>
|
||||
`,
|
||||
(_browser, accDoc) => {
|
||||
const errorsForInvalidFalse = getNativeInterface(
|
||||
accDoc,
|
||||
"input-invalid-false"
|
||||
).getAttributeValue("AXErrorMessageElements");
|
||||
|
||||
is(
|
||||
errorsForInvalidFalse,
|
||||
null,
|
||||
"When aria-invalid is 'false', [AXErrorMessageElements] should return null"
|
||||
);
|
||||
|
||||
const errorsForInvalidMissing = getNativeInterface(
|
||||
accDoc,
|
||||
"input-invalid-missing"
|
||||
).getAttributeValue("AXErrorMessageElements");
|
||||
|
||||
is(
|
||||
errorsForInvalidMissing,
|
||||
null,
|
||||
"When aria-invalid is missing, [AXErrorMessageElements] should return null"
|
||||
);
|
||||
|
||||
const errorsForSpellingError = getNativeInterface(
|
||||
accDoc,
|
||||
"input-invalid-spelling-error"
|
||||
).getAttributeValue("AXErrorMessageElements");
|
||||
|
||||
is(
|
||||
errorsForSpellingError,
|
||||
null,
|
||||
"When aria-invalid is provided without value, [AXErrorMessageElements] should return null"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* This test modifies the innerText of an associated error message and verifies the correct event AXValidationErrorChagned is fired.
|
||||
*/
|
||||
addAccessibleTask(
|
||||
`
|
||||
<label for="input">Field with error</label><input id="input" aria-invalid="true" aria-errormessage="error-msg">
|
||||
<div id="error-msg">Field validation failed</div>
|
||||
`,
|
||||
async (browser, _accDoc) => {
|
||||
let validationErrorChanged = waitForMacEvent("AXValidationErrorChanged");
|
||||
await SpecialPowers.spawn(browser, [], () => {
|
||||
content.document.getElementById("error-msg").innerText =
|
||||
"new error message";
|
||||
});
|
||||
await validationErrorChanged;
|
||||
info("validationErrorChanged: event has arrived");
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* This test modifies the inner tree of an associated error message and verifies the correct event AXValidationErrorChagned is fired.
|
||||
*/
|
||||
addAccessibleTask(
|
||||
`
|
||||
<label for="input">Field with error</label><input id="input" aria-invalid="true" aria-errormessage="error-msg">
|
||||
<div id="error-msg">Field validation failed <span id="inner-error-msg"></span></div>
|
||||
`,
|
||||
async (browser, _accDoc) => {
|
||||
let validationErrorChanged = waitForMacEvent("AXValidationErrorChanged");
|
||||
|
||||
info("validationErrorChanged: changing inner element");
|
||||
await SpecialPowers.spawn(browser, [], () => {
|
||||
content.document.getElementById("inner-error-msg").innerText =
|
||||
"detailed error message";
|
||||
});
|
||||
|
||||
await validationErrorChanged;
|
||||
|
||||
info("validationErrorChanged: event has arrived");
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* When the value of `aria-errormessage` is changed, AXValidationErrorChanged should be triggered.
|
||||
* The test removes the element id from `aria-errormessage` and checks that:
|
||||
* - the event was fired
|
||||
* - AXErrorMessageElements does not return error messages
|
||||
*
|
||||
* Then, the test inserts element id back to `aria-errormessage` and checks that:
|
||||
* - the event AXValidationErrorChanged was fired
|
||||
* - AXErrorMessageElements contain our error message
|
||||
*/
|
||||
addAccessibleTask(
|
||||
`
|
||||
<label for="input">Field with error</label><input id="input" aria-invalid="true" aria-errormessage="error-msg">
|
||||
<div id="error-msg">Field validation failed</div>
|
||||
`,
|
||||
async (browser, accDoc) => {
|
||||
let validationErrorChanged = waitForMacEvent("AXValidationErrorChanged");
|
||||
|
||||
info("validationErrorChanged: removing reference to error");
|
||||
await SpecialPowers.spawn(browser, [], () => {
|
||||
content.document
|
||||
.getElementById("input")
|
||||
.setAttribute("aria-errormessage", "");
|
||||
});
|
||||
|
||||
await validationErrorChanged;
|
||||
|
||||
info("validationErrorChanged: event has arrived");
|
||||
|
||||
let validationErrors = getNativeInterface(
|
||||
accDoc,
|
||||
"input"
|
||||
).getAttributeValue("AXErrorMessageElements");
|
||||
|
||||
is(
|
||||
validationErrors,
|
||||
null,
|
||||
"We have removed reference to error message, AXErrorMessageElements should now contain nothing"
|
||||
);
|
||||
|
||||
info("validationErrorChanged: adding the reference back");
|
||||
validationErrorChanged = waitForMacEvent("AXValidationErrorChanged");
|
||||
|
||||
await SpecialPowers.spawn(browser, [], () => {
|
||||
content.document
|
||||
.getElementById("input")
|
||||
.setAttribute("aria-errormessage", "error-msg");
|
||||
});
|
||||
|
||||
await validationErrorChanged;
|
||||
|
||||
validationErrors = getNativeInterface(accDoc, "input")
|
||||
.getAttributeValue("AXErrorMessageElements")
|
||||
.map(e => e.getAttributeValue("AXDOMIdentifier"));
|
||||
|
||||
info("validation errors: " + JSON.stringify(validationErrors));
|
||||
|
||||
is(
|
||||
validationErrors.length,
|
||||
1,
|
||||
"Reference to 'error-msg' was returned back"
|
||||
);
|
||||
is(
|
||||
validationErrors[0],
|
||||
"error-msg",
|
||||
"Reference to 'error-msg' was returned back"
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -54,5 +54,6 @@ static const uint32_t gWinEventMap[] = {
|
||||
kEVENT_WIN_UNKNOWN, // nsIAccessibleEvent::EVENT_LIVE_REGION_REMOVED
|
||||
kEVENT_WIN_UNKNOWN, // nsIAccessibleEvent::EVENT_INNER_REORDER
|
||||
kEVENT_WIN_UNKNOWN, // nsIAccessibleEvent::EVENT_LIVE_REGION_CHANGED
|
||||
kEVENT_WIN_UNKNOWN, // nsIAccessibleEvent::EVENT_ERRORMESSAGE_CHANGED
|
||||
// clang-format on
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user