Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unclear what counts as interactive in Presentational Roles Conflict Resolution #1270

Open
dd8 opened this issue May 14, 2020 · 11 comments
Open
Assignees
Labels
clarification clarifying or correcting language that is either confusing, misleading or under-specified
Projects
Milestone

Comments

@dd8
Copy link

dd8 commented May 14, 2020

It's not clear what "interactive" means in the following sentence:

If an element is focusable, or otherwise interactive, user agents MUST ignore the presentation role and expose the element with its implicit role, in order to ensure that the element is operable.

https://w3c.github.io/aria/#conflict_resolution_presentation_none

Does it mean something that currently accepts user input (e.g. not disabled or hidden) or does it mean interactive elements (which may be hidden or disabled) such as those listed in https://html.spec.whatwg.org/multipage/dom.html#interactive-content

  1. Role resolution on a visible enabled control is pretty clear - this should be exposed as role=button:
<button role="none">Next</button>
  1. Role resolution on a visible disabled control is unclear - the user can't focus it or interact with it. Should this be exposed as role=button or role=none:
<button role="none" disabled>Next</button>

Edit: If the disabled attribute blocks conflict resolution, then adding/removing the disabled attribute changes the role. This may be undesirable.

  1. Role resolution on a hidden control is also unclear - the user can't interact with it. Should this be exposed as role=button or role=none:
<button role="none" style="display:none">Next</button>

Edit: If the hiding the element blocks conflict resolution, then showing/hiding the element also changes the role. This starts getting difficult when you account for things obscuring the element like modal boxes coded in HTML.

@carmacleod
Copy link
Contributor

There's some related discussion in #1192.

@dd8
Copy link
Author

dd8 commented May 14, 2020

There's some related discussion in #1192.

@carmacleod Thanks - I added a couple of edits above.

One thing that seems potentially confusing is toggling between role=none and the native role depending on other attributes or CSS styles (although this happens with the current definition if you add a global ARIa attribute)

@scottaohara
Copy link
Member

I wouldn’t expect any of these examples to be valid.

A button in a disabled state is still a button, and you can’t have something be both exposed as presentational while also being exposed as disabled.

Hiding an element from the accessibility tree / from being visible again doesn’t change its role. It also doesn't exactly make it non-interactive either.

For instance, the following hidden button can still be "clicked" by another button to produce the alert.

<button class=one>try me</button>
<button hidden>Hidden</button>

<script>
var f = document.querySelector('[hidden]');
var b = document.querySelector('.one')

f.addEventListener('click', function () {
  alert('I am hiden')
})

b.addEventListener('click', function () {
  f.click()
})
</script>

@dd8
Copy link
Author

dd8 commented May 14, 2020

@scottaohara

I wouldn’t expect any of these examples to be valid.

Yep, I think any examples showing Presentational Roles Conflict should be invalid

A button in a disabled state is still a button, and you can’t have something
be both exposed as presentational while also being exposed as disabled.

That's how I expected it to work, but that's not how it works in the Chrome or Firefox accessibility inspectors:

Code Chrome Firefox Safari
button role="none" role=button role=pushbutton role=AXButton
button role="none" disabled role=ignored role=text leaf role=AXStaticText
button role="none" disabled aria-disabled='true' role=button role=pushbutton role=AXButton
button role="none" disabled title='foo' role=ignored role=pushbutton role=AXStaticText
button role="none" disabled id='labelledby-target' role=ignored role=pushbutton role=AXStaticText
button role="none" style="display:none" not in a11y tree not in a11y tree not in a11y tree

Edit added Safari and more attribute tests

Hiding an element from the accessibility tree / from being visible again
doesn’t change its role. It also doesn't exactly make it non-interactive either.

It usually doesn't matter for hidden elements since they're not in the a11y tree. It might matter for hidden elements referenced by aria-labelledby since step 2E in accname takes role into account.

@scottaohara
Copy link
Member

what's interesting about that is those results don't hold true for aria-disabled

so the following are both still exposed as buttons

<button aria-disabled=true role=none>
<button aria-disabled=true disabled role=none>

@dd8
Copy link
Author

dd8 commented May 15, 2020

Did a bit of digging through WebKit browser code. WebKit never overrides role=none on natively disabled form controls :

Edit: also overrides role=presentational / role=none if global ARIA attributes present

https://github.com/WebKit/webkit/blob/master/Source/WebCore/accessibility/AccessibilityNodeObject.cpp#L2192

AccessibilityRole AccessibilityNodeObject::determineAriaRoleAttribute() const
{
    const AtomicString& ariaRole = getAttribute(roleAttr);
    if (ariaRole.isNull() || ariaRole.isEmpty())
        return AccessibilityRole::Unknown;
    
    AccessibilityRole role = ariaRoleToWebCoreRole(ariaRole);

    // ARIA states if an item can get focus, it should not be presentational.
    if (role == AccessibilityRole::Presentational && canSetFocusAttribute())
        return AccessibilityRole::Unknown;

    if (role == AccessibilityRole::Button)
        role = buttonRoleType();

    if (role == AccessibilityRole::TextArea && !ariaIsMultiline())
        role = AccessibilityRole::TextField;

    role = remapAriaRoleDueToParent(role);
    
    // Presentational roles are invalidated by the presence of ARIA attributes.
    if (role == AccessibilityRole::Presentational && supportsARIAAttributes())
        role = AccessibilityRole::Unknown;
    
    // The ARIA spec states, "Authors must give each element with role region a brief label that
    // describes the purpose of the content in the region." The Core AAM states, "Special case:
    // if the region does not have an accessible name, do not expose the element as a landmark.
    // Use the native host language role of the element instead."
    if (role == AccessibilityRole::LandmarkRegion && !hasAttribute(aria_labelAttr) && !hasAttribute(aria_labelledbyAttr))
        role = AccessibilityRole::Unknown;

    if (static_cast<int>(role))
        return role;

    return AccessibilityRole::Unknown;
}

bool AccessibilityNodeObject::canSetFocusAttribute() const
{
   /// snipped setup code ///

    if (element.isDisabledFormControl())
        return false;

    return element.supportsFocus();
}

https://github.com/WebKit/webkit/blob/master/Source/WebCore/html/HTMLFormControlElement.cpp#L355

bool HTMLFormControlElement::isDisabledFormControl() const
{
    return m_disabled || m_disabledByAncestorFieldset;
}

@dd8
Copy link
Author

dd8 commented May 15, 2020

Chromium works similarly but also takes global ARIA attributes into account:

https://chromium.googlesource.com/chromium/src/+/master/third_party/blink/renderer/modules/accessibility/ax_object.cc#2443

ax::mojom::blink::Role AXObject::DetermineAriaRoleAttribute() const {

/// snipped setup code ///

// ARIA states if an item can get focus, it should not be presentational.
// It also states user agents should ignore the presentational role if
// the element has global ARIA states and properties.
 if ((role == ax::mojom::blink::Role::kNone  ||
   role == ax::mojom::blink::Role::kPresentational) &&
  (CanSetFocusAttribute() || HasGlobalARIAAttribute()))
    return ax::mojom::blink::Role::kUnknown;

 /// snipped code to handle other role types ///

}


@scottaohara
Copy link
Member

Yeh, this confirms my suspicion

It also states user agents should ignore the presentational role if the element has global ARIA states and properties.

Chrome is ignoring the presentation role because of the aria attribute itself, and not because of what the aria attribute represents.

@dd8
Copy link
Author

dd8 commented May 15, 2020

And Firefox ignores role=presentation / role=none if:

  • element is focusable (and not disabled)
  • element has a title attribute
  • element has global ARIA attribute
  • element has ID referenced by aria-labelledby, aria-describedby, aria-details etc

See comment from James Teh #1192 (comment)

https://dxr.mozilla.org/mozilla-central/source/accessible/base/nsAccessibilityService.cpp#1036

  // If the element is focusable or global ARIA attribute is applied to it or
  // it is referenced by ARIA relationship then treat role="presentation" on
  // the element as the role is not there.
  if (roleMapEntry && (roleMapEntry->Is(nsGkAtoms::presentation) ||
                       roleMapEntry->Is(nsGkAtoms::none))) {
    if (!MustBeAccessible(content, document)) return nullptr;

    roleMapEntry = nullptr;
  }

static bool MustBeAccessible(nsIContent* aContent, DocAccessible* aDocument) {
  if (aContent->GetPrimaryFrame()->IsFocusable()) return true;

  if (aContent->IsElement()) {
    uint32_t attrCount = aContent->AsElement()->GetAttrCount();
    for (uint32_t attrIdx = 0; attrIdx < attrCount; attrIdx++) {
      const nsAttrName* attr = aContent->AsElement()->GetAttrNameAt(attrIdx);
      if (attr->NamespaceEquals(kNameSpaceID_None)) {
        nsAtom* attrAtom = attr->Atom();
        if (attrAtom == nsGkAtoms::title && aContent->IsHTMLElement()) {
          // If the author provided a title on an element that would not
          // be accessible normally, assume an intent and make it accessible.
          return true;
        }

        nsDependentAtomString attrStr(attrAtom);
        if (!StringBeginsWith(attrStr, NS_LITERAL_STRING("aria-")))
          continue;  // not ARIA

        // A global state or a property and in case of token defined.
        uint8_t attrFlags = aria::AttrCharacteristicsFor(attrAtom);
        if ((attrFlags & ATTR_GLOBAL) &&
            (!(attrFlags & ATTR_VALTOKEN) ||
             nsAccUtils::HasDefinedARIAToken(aContent, attrAtom))) {
          return true;
        }
      }
    }

    // If the given ID is referred by relation attribute then create an
    // accessible for it.
    nsAutoString id;
    if (nsCoreUtils::GetID(aContent, id) && !id.IsEmpty()) {
      return aDocument->IsDependentID(aContent->AsElement(), id);
    }
  }

  return false;
}

@dd8
Copy link
Author

dd8 commented May 16, 2020

In current implementations disabling a button with role=none blocks presentation role conflict resolution because the button isn't focusable.

<!-- computed role is button -->
<button role='none'> 

<!-- computed role is none -->
<button role='none' disabled> 

This causes the button role to change as controls are enabled/disabled. This is confusing for the user and causes weird authoring inconsistencies like this:

<!-- computed role is none because control is non-focusable-->
<button role='none' disabled> 

<!-- computed role is button due to global aria attribute although control is non-focusable-->
<button role='none' disabled aria-disabled='true'> 

In the worst case some buttons disappear completely from the a11y tree in Chrome/Safari when disabled:

<!-- computed role=button, name=Save -->
<button role='none' title='Save'><img src='save.png' alt=''/></button> 

<!-- computed role=none, no text voiced (in Safari/Chrome) -->
<button role='none' title='Save' disabled><img src='save.png' alt=''/></button> 

This doesn't happen in Firefox because it uses the title attribute to trigger role conflict resolution, although this isn't in the current spec.

Edit: I think it would be better if the spec was changed to ensure role doesn't change when control state changes. Something like this might work:

If an element is focusable, or categorized by the host language as interactive content (including disabled elements), user agents MUST ignore the presentation role and expose the element with its implicit role, in order to ensure that the element is operable.

@pkra pkra added the clarification clarifying or correcting language that is either confusing, misleading or under-specified label Jun 27, 2023
@pkra pkra added this to the ARIA 1.3 milestone Jun 27, 2023
@pkra
Copy link
Member

pkra commented Jun 27, 2023

I've matched this issue with #1192 so that they hopefully get resolved together.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
clarification clarifying or correcting language that is either confusing, misleading or under-specified
Projects
ARIA 1.3
Awaiting triage
Development

No branches or pull requests

5 participants