Skip to content

Commit

Permalink
Notify user of HTML validation error messages
Browse files Browse the repository at this point in the history
Make HTML native validation of form controls accessible

A simple example is @required on a form control. Blink provides a
helpful message box if the user attempts to submit a form with a
required field left empty. The control is also focused.

This CL produces an accessible alert for the notification when it appears.

Bug: 897039
Change-Id: Ib78ce09821d794c8b35ca151c1f6a153adff9414
Reviewed-on: https://chromium-review.googlesource.com/c/1403980
Reviewed-by: Kent Tamura <tkent@chromium.org>
Reviewed-by: Dominic Mazzoni <dmazzoni@chromium.org>
Commit-Queue: Aaron Leventhal <aleventhal@chromium.org>
Cr-Commit-Position: refs/heads/master@{#625551}
  • Loading branch information
aleventhal authored and Commit Bot committed Jan 24, 2019
1 parent 3344a6d commit 50f6ce8
Show file tree
Hide file tree
Showing 26 changed files with 445 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -1936,3 +1936,28 @@ TEST_F('ChromeVoxBackgroundTest', 'AriaSliderWithValueText', function() {
.replay();
});
});

TEST_F('ChromeVoxBackgroundTest', 'ValidationTest', function() {
var mockFeedback = this.createMockFeedback();
this.runWithLoadedTree(function() {/*
<label for="in1">Name:</label>
<input id="in1" required autofocus>
<script>
const in1 = document.querySelector('input');
in1.addEventListener('focus', () => {
setTimeout(() => {
in1.setCustomValidity('Please enter name');
in1.reportValidity();
}, 500);
});
</script>
*/}, function(root) {
mockFeedback
.expectSpeech('Name:')
.expectSpeech('Edit text')
.expectSpeech('Required')
.expectSpeech('Alert')
.expectSpeech('Please enter name')
.replay();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,11 @@ IN_PROC_BROWSER_TEST_F(DumpAccessibilityEventsTest,
RunEventTest(FILE_PATH_LITERAL("remove-hidden-attribute.html"));
}

IN_PROC_BROWSER_TEST_F(DumpAccessibilityEventsTest,
AccessibilityEventsReportValidityInvalidField) {
RunEventTest(FILE_PATH_LITERAL("report-validity-invalid-field.html"));
}

IN_PROC_BROWSER_TEST_F(
DumpAccessibilityEventsTest,
AccessibilityEventsRemoveHiddenAttributeSubtree) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,7 @@ void DumpAccessibilityTreeTest::AddDefaultFilters(
AddFilter(filters, "invalidState=false",
Filter::DENY); // Don't show false value
AddFilter(filters, "roleDescription=*");
AddFilter(filters, "errormessageId=*");

//
// OS X
Expand Down Expand Up @@ -1273,6 +1274,11 @@ IN_PROC_BROWSER_TEST_F(DumpAccessibilityTreeTest, AccessibilityForm) {
RunHtmlTest(FILE_PATH_LITERAL("form.html"));
}

IN_PROC_BROWSER_TEST_F(DumpAccessibilityTreeTest,
AccessibilityFormValidationMessage) {
RunHtmlTest(FILE_PATH_LITERAL("form-validation-message.html"));
}

IN_PROC_BROWSER_TEST_F(DumpAccessibilityTreeTest, AccessibilityFrameset) {
RunHtmlTest(FILE_PATH_LITERAL("frameset.html"));
}
Expand Down
10 changes: 8 additions & 2 deletions content/renderer/accessibility/blink_ax_tree_source.cc
Original file line number Diff line number Diff line change
Expand Up @@ -115,14 +115,15 @@ class AXContentNodeDataSparseAttributeAdapter
case WebAXObjectAttribute::kAriaActiveDescendant:
// TODO(dmazzoni): WebAXObject::ActiveDescendant currently returns
// more information than the sparse interface does.
// ******** Why is this a TODO? ********
break;
case WebAXObjectAttribute::kAriaDetails:
dst_->AddIntAttribute(ax::mojom::IntAttribute::kDetailsId,
value.AxID());
break;
case WebAXObjectAttribute::kAriaErrorMessage:
dst_->AddIntAttribute(ax::mojom::IntAttribute::kErrormessageId,
value.AxID());
// Use WebAXObject::ErrorMessage(), which provides both ARIA error
// messages as well as built-in HTML form validation messages.
break;
default:
NOTREACHED();
Expand Down Expand Up @@ -693,6 +694,11 @@ void BlinkAXTreeSource::SerializeNode(WebAXObject src,
src.AriaActiveDescendant().AxID());
}

if (!src.ErrorMessage().IsDetached()) {
dst->AddIntAttribute(ax::mojom::IntAttribute::kErrormessageId,
src.ErrorMessage().AxID());
}

if (ui::IsHeading(dst->role) && src.HeadingLevel()) {
dst->AddIntAttribute(ax::mojom::IntAttribute::kHierarchicalLevel,
src.HeadingLevel());
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
AXWebArea
++AXGroup AXRoleDescription='alert' AXARIAAtomic='1'
++AXGroup AXSubrole=AXApplicationAlert AXRoleDescription='alert' AXARIAAtomic='1'
++++AXStaticText AXValue='This test is for aria role="alert"' AXARIAAtomic='0'
1 change: 1 addition & 0 deletions content/test/data/accessibility/aria/aria-alert.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<!DOCTYPE html>
<!--
@MAC-ALLOW:AXRoleDescription='alert'
@MAC-ALLOW:AXSubrole=AXApplicationAlert
@MAC-ALLOW:AXARIAAtomic=*
@WIN-ALLOW:atomic:*
@WIN-ALLOW:container-atomic:*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
AXFocusedUIElementChanged on AXTextField AXTitle="Pet name:"
AXLiveRegionChanged on AXGroup AXDescription="Please enter pet name"
AXLiveRegionCreated on AXGroup AXDescription="Please enter pet name"
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
EVENT_OBJECT_FOCUS on <input#in1> role=ROLE_SYSTEM_TEXT name="Pet name:" FOCUSED,FOCUSABLE IA2_STATE_EDITABLE,IA2_STATE_INVALID_ENTRY,IA2_STATE_REQUIRED,IA2_STATE_SELECTABLE_TEXT,IA2_STATE_SINGLE_LINE
EVENT_SYSTEM_ALERT on role=ROLE_SYSTEM_ALERT name="Please enter pet name"
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<!--
@WIN-DENY:*
@WIN-ALLOW:EVENT_OBJECT_FOCUS*
@WIN-ALLOW:EVENT_SYSTEM_ALERT*
@WIN-ALLOW:EVENT_OBJECT_LIVE*
@MAC-DENY:*
@MAC-ALLOW:AXFocusedUIElementChanged*
@MAC-ALLOW:AXFocusedUIElementChanged*
@MAC-ALLOW:AXLiveRegion*
-->
<!DOCTYPE html>
<form>
<label for="in1">Pet name:</label><input id="in1" required>
</form>
<script>
function go() {
const in1 = document.querySelector('input');
in1.setCustomValidity('Please enter pet name');
in1.reportValidity();
}
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
rootWebArea
++form
++++labelText
++++++staticText name='Pet name:'
++++++++inlineTextBox name='Pet name:'
++++textField required name='Pet name:' errormessageId=alert invalidState=true
++++++genericContainer
++alert containerLiveRelevant='additions' containerLiveStatus='assertive' name='Please enter pet name' liveRelevant='additions' liveStatus='assertive' containerLiveAtomic=true containerLiveBusy=false liveAtomic=true
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
AXWebArea
++AXGroup
++++AXGroup
++++++AXStaticText AXValue='Pet name:'
++++AXTextField AXTitle='Pet name:'
++AXGroup AXSubrole=AXApplicationAlert AXDescription='Please enter pet name'
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
ROLE_SYSTEM_DOCUMENT READONLY FOCUSABLE
++IA2_ROLE_FORM
++++IA2_ROLE_LABEL
++++++ROLE_SYSTEM_STATICTEXT name='Pet name:'
++++ROLE_SYSTEM_TEXT name='Pet name:' FOCUSABLE IA2_STATE_INVALID_ENTRY IA2_STATE_REQUIRED
++ROLE_SYSTEM_ALERT name='Please enter pet name'
14 changes: 14 additions & 0 deletions content/test/data/accessibility/html/form-validation-message.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<!--
@BLINK-ALLOW:live*
@BLINK-ALLOW:container*
@MAC-ALLOW:AXSubrole=AXApplicationAlert
-->
<!DOCTYPE html>
<form>
<label for="in1">Pet name:</label><input id="in1" required>
</form>
<script>
const in1 = document.querySelector('input');
in1.setCustomValidity('Please enter pet name');
in1.reportValidity();
</script>
1 change: 1 addition & 0 deletions third_party/blink/public/web/web_ax_object.h
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ class WebAXObject {
BLINK_EXPORT WebString FontFamily() const;
BLINK_EXPORT float FontSize() const;
BLINK_EXPORT bool CanvasHasFallbackContent() const;
BLINK_EXPORT WebAXObject ErrorMessage() const;
// If this is an image, returns the image (scaled to maxSize) as a data url.
BLINK_EXPORT WebString ImageDataUrl(const WebSize& max_size) const;
BLINK_EXPORT WebAXRestriction Restriction() const;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,8 @@ class CORE_EXPORT AXObjectCache
virtual void HandleLayoutComplete(Document*) = 0;
virtual void HandleClicked(Node*) = 0;
virtual void HandleAutofillStateChanged(Element*, bool) = 0;
virtual void HandleValidationMessageVisibilityChanged(
const Element* form_control) = 0;

// Handle any notifications which arrived while layout was dirty.
virtual void ProcessUpdatesAfterLayout(Document&) = 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
#include "cc/layers/picture_layer.h"
#include "third_party/blink/public/platform/task_type.h"
#include "third_party/blink/public/web/web_text_direction.h"
#include "third_party/blink/renderer/core/accessibility/ax_object_cache.h"
#include "third_party/blink/renderer/core/dom/element.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/frame/local_frame_view.h"
Expand Down Expand Up @@ -88,6 +89,8 @@ void ValidationMessageClientImpl::ShowValidationMessage(
overlay_ = FrameOverlay::Create(target_frame, std::move(delegate));
bool success =
target_frame->View()->UpdateLifecycleToCompositingCleanPlusScrolling();
ValidationMessageVisibilityChanged(anchor);

// The lifecycle update should always succeed, because this is not inside
// of a throttling scope.
DCHECK(success);
Expand Down Expand Up @@ -124,13 +127,23 @@ void ValidationMessageClientImpl::HideValidationMessageImmediately(
}

void ValidationMessageClientImpl::Reset(TimerBase*) {
const Element& anchor = *current_anchor_;

timer_ = nullptr;
current_anchor_ = nullptr;
message_ = String();
finish_time_ = TimeTicks();
overlay_ = nullptr;
overlay_delegate_ = nullptr;
page_->GetChromeClient().UnregisterPopupOpeningObserver(this);
ValidationMessageVisibilityChanged(anchor);
}

void ValidationMessageClientImpl::ValidationMessageVisibilityChanged(
const Element& element) {
Document& document = element.GetDocument();
if (AXObjectCache* cache = document.ExistingAXObjectCache())
cache->HandleValidationMessageVisibilityChanged(&element);
}

bool ValidationMessageClientImpl::IsValidationMessageVisible(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ class ValidationMessageClientImpl final
LocalFrameView* CurrentView();
void HideValidationMessageImmediately(const Element& anchor);
void Reset(TimerBase*);
void ValidationMessageVisibilityChanged(const Element& anchor);

void ShowValidationMessage(const Element& anchor,
const String& message,
Expand Down
2 changes: 2 additions & 0 deletions third_party/blink/renderer/modules/accessibility/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ blink_modules_sources("accessibility") {
"ax_sparse_attribute_setter.h",
"ax_svg_root.cc",
"ax_svg_root.h",
"ax_validation_message.cc",
"ax_validation_message.h",
"ax_virtual_object.cc",
"ax_virtual_object.h",
"inspector_accessibility_agent.cc",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2091,6 +2091,7 @@ void AXLayoutObject::AddChildren() {
AddRemoteSVGChildren();
AddTableChildren();
AddInlineTextBoxChildren(false);
AddValidationMessageChild();
AddAccessibleNodeChildren();

for (const auto& child : children_) {
Expand Down Expand Up @@ -2781,6 +2782,30 @@ void AXLayoutObject::AddInlineTextBoxChildren(bool force) {
}
}

void AXLayoutObject::AddValidationMessageChild() {
if (!IsWebArea())
return;
AXObject* ax_object = AXObjectCache().ValidationMessageObjectIfVisible();
if (ax_object)
children_.push_back(ax_object);
}

AXObject* AXLayoutObject::ErrorMessage() const {
// Check for aria-errormessage.
Element* existing_error_message =
GetAOMPropertyOrARIAAttribute(AOMRelationProperty::kErrorMessage);
if (existing_error_message)
return AXObjectCache().GetOrCreate(existing_error_message);

// Check for visible validationMessage. This can only be visible for a focused
// control. Corollary: if there is a visible validationMessage alert box, then
// it is related to the current focus.
if (this != AXObjectCache().FocusedObject())
return nullptr;

return AXObjectCache().ValidationMessageObjectIfVisible();
}

void AXLayoutObject::LineBreaks(Vector<int>& line_breaks) const {
if (!IsTextControl())
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,10 @@ class MODULES_EXPORT AXLayoutObject : public AXNodeObject {
// For a table row or column.
AXObject* HeaderObject() const override;

// The aria-errormessage object or native object from a validationMessage
// alert.
AXObject* ErrorMessage() const override;

private:
bool IsTabItemSelected() const;
bool IsValidSelectionBound(const AXObject*) const;
Expand All @@ -224,6 +228,7 @@ class MODULES_EXPORT AXLayoutObject : public AXNodeObject {
void AddRemoteSVGChildren();
void AddTableChildren();
void AddInlineTextBoxChildren(bool force);
void AddValidationMessageChild();
ax::mojom::Role DetermineTableCellRole() const;
ax::mojom::Role DetermineTableRowRole() const;
bool FindAllTableCellsWithRole(ax::mojom::Role, AXObjectVector&) const;
Expand Down
2 changes: 2 additions & 0 deletions third_party/blink/renderer/modules/accessibility/ax_object.h
Original file line number Diff line number Diff line change
Expand Up @@ -528,6 +528,7 @@ class MODULES_EXPORT AXObject : public GarbageCollectedFinalized<AXObject> {
virtual bool IsTextControl() const { return false; }
bool IsTextObject() const;
bool IsTree() const { return RoleValue() == ax::mojom::Role::kTree; }
virtual bool IsValidationMessage() const { return false; }
virtual bool IsVirtualObject() const { return false; }
bool IsWebArea() const {
return RoleValue() == ax::mojom::Role::kRootWebArea;
Expand Down Expand Up @@ -744,6 +745,7 @@ class MODULES_EXPORT AXObject : public GarbageCollectedFinalized<AXObject> {
virtual String AriaAutoComplete() const { return String(); }
virtual void AriaOwnsElements(AXObjectVector& owns) const {}
virtual void AriaDescribedbyElements(AXObjectVector&) const {}
virtual AXObject* ErrorMessage() const { return nullptr; }
virtual ax::mojom::HasPopup HasPopup() const {
return ax::mojom::HasPopup::kFalse;
}
Expand Down
Loading

0 comments on commit 50f6ce8

Please sign in to comment.