Commit 9612f58a authored by Victor Fei's avatar Victor Fei Committed by Commit Bot

A11y:Expose labels of radio & checkbox when focused.

Currently, when a label is referenced by a control of type radio or
checkbox, we set the label as ignored in the AXTree thereby excluding
it from being perceived by ATs.
However, for the case when the control (radio/checkbox) is a
descendant of the label and label is focusable (tabindex="0") but
control is to be omitted from focus (tabindex="-1"), ATs perceive
neither the label nor the control when tabbing through the content:

    <label style="display:block" tabindex="0">
      radio button 1
      <input type="radio" id="radio1" tabindex="-1">
    </label>

    <label style="display:block" for="radio2" tabindex="0">
      radio button 2
      <input type="radio" id="radio2" tabindex="-1">
    </label>

    <label style="display:block" for="checkbox1" tabindex="0">
      checkbox 1
      <input type="checkbox" id="checkbox1" tabindex="-1">
    </label>

    <label style="display:block" for="checkbox2" tabindex="0">
      checkbox 2
      <input type="checkbox" id="checkbox2" tabindex="-1">
    </label>

This change fixes the above scenario by exposing labels (referenced by
radio or checkbox) when they are focusable. For all other cases when
labels are referenced by radio or checkbox, we keep the existing
behavior of ignoring the label.

Note: Neither Firefox nor Edge legacy hides a label from AT when the
label is referenced by a control. On Firefox and Edge legacy, label
content will be read twice (focus on label and focus on control
respectively).
WAI-ARIA spec does not specify label should be ignored when associated
with a radio/checkbox control.

Bug: 1040210
Change-Id: Ie7df5144dd2f0d285d97e6556adb46dcf2338f25
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1992017
Commit-Queue: Victor Fei <vicfei@microsoft.com>
Reviewed-by: default avatarDominic Mazzoni <dmazzoni@chromium.org>
Cr-Commit-Position: refs/heads/master@{#731820}
parent ca073774
...@@ -1661,6 +1661,11 @@ IN_PROC_BROWSER_TEST_P(DumpAccessibilityTreeTest, AccessibilityInputRadio) { ...@@ -1661,6 +1661,11 @@ IN_PROC_BROWSER_TEST_P(DumpAccessibilityTreeTest, AccessibilityInputRadio) {
RunHtmlTest(FILE_PATH_LITERAL("input-radio.html")); RunHtmlTest(FILE_PATH_LITERAL("input-radio.html"));
} }
IN_PROC_BROWSER_TEST_P(DumpAccessibilityTreeTest,
AccessibilityInputRadioCheckboxLabel) {
RunHtmlTest(FILE_PATH_LITERAL("input-radio-checkbox-label.html"));
}
IN_PROC_BROWSER_TEST_P(DumpAccessibilityTreeTest, IN_PROC_BROWSER_TEST_P(DumpAccessibilityTreeTest,
AccessibilityInputRadioInMenu) { AccessibilityInputRadioInMenu) {
RunHtmlTest(FILE_PATH_LITERAL("input-radio-in-menu.html")); RunHtmlTest(FILE_PATH_LITERAL("input-radio-in-menu.html"));
......
android.webkit.WebView focusable focused scrollable
++android.view.View
++++android.widget.RadioButton role_description='radio button' checkable clickable focusable name='label ignored for radio button'
++++android.widget.CheckBox role_description='checkbox' checkable clickable focusable name='label ignored for checkbox'
++++android.view.View focusable name='label exposed for radio button '
++++++android.view.View name='label exposed for radio button '
++++++android.widget.RadioButton role_description='radio button' checkable clickable focusable name='label exposed for radio button' item_index=1 row_index=1
++++++android.view.View name=' '
++++android.view.View focusable name='label exposed for checkbox '
++++++android.view.View name='label exposed for checkbox '
++++++android.widget.CheckBox role_description='checkbox' checkable clickable focusable name='label exposed for checkbox'
++++++android.view.View name=' '
[document web]
++[section]
++++[radio button] name='label ignored for radio button' checkable checkable:true
++++[check box] name='label ignored for checkbox' checkable checkable:true
++++[label] name='label exposed for radio button ' label-for
++++++[static] name='label exposed for radio button '
++++++[radio button] name='label exposed for radio button' checkable labelled-by checkable:true
++++++[static] name=' '
++++[label] name='label exposed for checkbox ' label-for
++++++[static] name='label exposed for checkbox '
++++++[check box] name='label exposed for checkbox' checkable labelled-by checkable:true
++++++[static] name=' '
rootWebArea
++genericContainer
++++radioButton name='label ignored for radio button' checkedState=false
++++checkBox name='label ignored for checkbox' checkedState=false
++++labelText name='label exposed for radio button '
++++++staticText name='label exposed for radio button '
++++++++inlineTextBox name='label exposed for radio button '
++++++radioButton name='label exposed for radio button' checkedState=false
++++++staticText name=' '
++++++++inlineTextBox name=' '
++++labelText name='label exposed for checkbox '
++++++staticText name='label exposed for checkbox '
++++++++inlineTextBox name='label exposed for '
++++++++inlineTextBox name='checkbox '
++++++checkBox name='label exposed for checkbox' checkedState=false
++++++staticText name=' '
AXWebArea
++AXGroup
++++AXRadioButton AXTitle='label ignored for radio button' AXValue='0'
++++AXCheckBox AXTitle='label ignored for checkbox' AXValue='0'
++++AXGroup AXTitle='label exposed for radio button '
++++++AXStaticText AXValue='label exposed for radio button '
++++++AXRadioButton AXValue='0' AXTitleUIElement='AXGroup label exposed for radio button '
++++++AXStaticText AXValue=' '
++++AXGroup AXTitle='label exposed for checkbox '
++++++AXStaticText AXValue='label exposed for checkbox '
++++++AXCheckBox AXValue='0' AXTitleUIElement='AXGroup label exposed for checkbox '
++++++AXStaticText AXValue=' '
document
++group IsControlElement=false
++++radio Name='label ignored for radio button' SelectionItem.IsSelected=false
++++checkbox Name='label ignored for checkbox' Toggle.ToggleState='Off'
++++description Name='label exposed for radio button '
++++++description Name='label exposed for radio button '
++++++radio Name='label exposed for radio button' SelectionItem.IsSelected=false
++++++description Name=' '
++++description Name='label exposed for checkbox '
++++++description Name='label exposed for checkbox '
++++++checkbox Name='label exposed for checkbox' Toggle.ToggleState='Off'
++++++description Name=' '
ROLE_SYSTEM_DOCUMENT READONLY FOCUSABLE
++IA2_ROLE_SECTION
++++ROLE_SYSTEM_RADIOBUTTON name='label ignored for radio button' FOCUSABLE IA2_STATE_CHECKABLE checkable:true
++++ROLE_SYSTEM_CHECKBUTTON name='label ignored for checkbox' FOCUSABLE IA2_STATE_CHECKABLE checkable:true
++++IA2_ROLE_LABEL name='label exposed for radio button ' FOCUSABLE
++++++ROLE_SYSTEM_STATICTEXT name='label exposed for radio button '
++++++ROLE_SYSTEM_RADIOBUTTON name='label exposed for radio button' FOCUSABLE IA2_STATE_CHECKABLE checkable:true
++++++ROLE_SYSTEM_STATICTEXT name=' '
++++IA2_ROLE_LABEL name='label exposed for checkbox ' FOCUSABLE
++++++ROLE_SYSTEM_STATICTEXT name='label exposed for checkbox '
++++++ROLE_SYSTEM_CHECKBUTTON name='label exposed for checkbox' FOCUSABLE IA2_STATE_CHECKABLE checkable:true
++++++ROLE_SYSTEM_STATICTEXT name=' '
<!DOCTYPE html>
<html>
<body>
<body>
<!-- When the radio button's associated label is not set to be
focusable, it will be ignored from the accessibility tree. -->
<label for="radio1">
label ignored for radio button
<input type="radio" id="radio1">
</label>
<!-- When the checkbox's associated label is not set to be
focusable, it will be ignored from the accessibility tree. -->
<label for="checkbox1">
label ignored for checkbox
<input type="checkbox" id="checkbox1">
</label>
<!-- When the radio button's associated label is set to be focusable,
it will be included in the accessibility tree. -->
<label for="radio2" tabindex="0">
label exposed for radio button
<input type="radio" id="radio2" tabindex="-1">
</label>
<!-- When the checkbox's associated label is set to be focusable,
it will be included in the accessibility tree. -->
<label for="checkbox2" tabindex="0">
label exposed for checkbox
<input type="checkbox" id="checkbox2" tabindex="-1">
</label>
</body>
</html>
...@@ -1742,7 +1742,8 @@ AXObject* AXLayoutObject::AccessibilityHitTest(const IntPoint& point) const { ...@@ -1742,7 +1742,8 @@ AXObject* AXLayoutObject::AccessibilityHitTest(const IntPoint& point) const {
// control. // control.
if (result->IsAXLayoutObject()) { if (result->IsAXLayoutObject()) {
AXObject* control_object = AXObject* control_object =
ToAXLayoutObject(result)->CorrespondingControlForLabelElement(); ToAXLayoutObject(result)
->CorrespondingControlAXObjectForLabelElement();
if (control_object && control_object->NameFromLabelElement()) if (control_object && control_object->NameFromLabelElement())
return control_object; return control_object;
} }
......
...@@ -178,21 +178,24 @@ AXObjectInclusion AXNodeObject::ShouldIncludeBasedOnSemantics( ...@@ -178,21 +178,24 @@ AXObjectInclusion AXNodeObject::ShouldIncludeBasedOnSemantics(
if (IsTableLikeRole() || IsTableRowLikeRole() || IsTableCellLikeRole()) if (IsTableLikeRole() || IsTableRowLikeRole() || IsTableCellLikeRole())
return kIncludeObject; return kIncludeObject;
// Ignore labels that are already referenced by a control. // Ignore labels that are already referenced by a control but are not set to
AXObject* control_object = CorrespondingControlForLabelElement(); // be focusable.
HTMLLabelElement* label = LabelElementContainer(); AXObject* control_ax_object = CorrespondingControlAXObjectForLabelElement();
if (control_object && control_object->IsCheckboxOrRadio() && if (control_ax_object && control_ax_object->IsCheckboxOrRadio() &&
control_object->NameFromLabelElement() && control_ax_object->NameFromLabelElement() &&
AccessibleNode::GetPropertyOrARIAAttribute( AccessibleNode::GetPropertyOrARIAAttribute(
label, AOMStringProperty::kRole) == g_null_atom) { LabelElementContainer(), AOMStringProperty::kRole) == g_null_atom) {
AXObject* label_ax_object = CorrespondingLabelAXObject();
// If the label is set to be focusable, we should expose it.
if (label_ax_object && label_ax_object->CanSetFocusAttribute())
return kIncludeObject;
if (ignored_reasons) { if (ignored_reasons) {
if (label && label != GetNode()) { if (label_ax_object && label_ax_object != this)
AXObject* label_ax_object = AXObjectCache().GetOrCreate(label);
ignored_reasons->push_back( ignored_reasons->push_back(
IgnoredReason(kAXLabelContainer, label_ax_object)); IgnoredReason(kAXLabelContainer, label_ax_object));
}
ignored_reasons->push_back(IgnoredReason(kAXLabelFor, control_object)); ignored_reasons->push_back(IgnoredReason(kAXLabelFor, control_ax_object));
} }
return kIgnoreObject; return kIgnoreObject;
} }
...@@ -2809,7 +2812,7 @@ void AXNodeObject::SetNode(Node* node) { ...@@ -2809,7 +2812,7 @@ void AXNodeObject::SetNode(Node* node) {
node_ = node; node_ = node;
} }
AXObject* AXNodeObject::CorrespondingControlForLabelElement() const { AXObject* AXNodeObject::CorrespondingControlAXObjectForLabelElement() const {
HTMLLabelElement* label_element = LabelElementContainer(); HTMLLabelElement* label_element = LabelElementContainer();
if (!label_element) if (!label_element)
return nullptr; return nullptr;
...@@ -2827,6 +2830,14 @@ AXObject* AXNodeObject::CorrespondingControlForLabelElement() const { ...@@ -2827,6 +2830,14 @@ AXObject* AXNodeObject::CorrespondingControlForLabelElement() const {
return AXObjectCache().GetOrCreate(corresponding_control); return AXObjectCache().GetOrCreate(corresponding_control);
} }
AXObject* AXNodeObject::CorrespondingLabelAXObject() const {
HTMLLabelElement* label_element = LabelElementContainer();
if (!label_element)
return nullptr;
return AXObjectCache().GetOrCreate(label_element);
}
HTMLLabelElement* AXNodeObject::LabelElementContainer() const { HTMLLabelElement* AXNodeObject::LabelElementContainer() const {
if (!GetNode()) if (!GetNode())
return nullptr; return nullptr;
......
...@@ -79,7 +79,8 @@ class MODULES_EXPORT AXNodeObject : public AXObject { ...@@ -79,7 +79,8 @@ class MODULES_EXPORT AXNodeObject : public AXObject {
Element* MouseButtonListener() const; Element* MouseButtonListener() const;
bool IsNativeCheckboxOrRadio() const; bool IsNativeCheckboxOrRadio() const;
void SetNode(Node*); void SetNode(Node*);
AXObject* CorrespondingControlForLabelElement() const; AXObject* CorrespondingControlAXObjectForLabelElement() const;
AXObject* CorrespondingLabelAXObject() const;
HTMLLabelElement* LabelElementContainer() const; HTMLLabelElement* LabelElementContainer() const;
// //
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment