Skip to content

Roles fixes#49

Merged
drwpow merged 13 commits intodrwpow:mainfrom
jlp-craigmorten:roles-fixes
Apr 13, 2025
Merged

Roles fixes#49
drwpow merged 13 commits intodrwpow:mainfrom
jlp-craigmorten:roles-fixes

Conversation

@jlp-craigmorten
Copy link
Contributor

@jlp-craigmorten jlp-craigmorten commented Mar 23, 2025

Changes

Result of hacking a script to compare roles defined in html-aria to aria-query and then fact checking differences against appropriate specs.

Notes:

  • We have interpreted the spec such that classes do not inherit their superclass(es) implicit attribute value(s) for the role.
  • The defaultAttributeValues has been updated to reflect the union of implicit attribute value(s) for the role (for supported attributes) as well as fallback attribute value(s) for the role (for required attribute author error handling). They do not reflect attribute default values - this detail is captured in src/lib/aria-attributes.ts instead.

How to Review

Comparison against relevant specs:

Specifically the role tables as well as the states and properties sections of WAI-ARIA.

Comparison against aria-query and/or user-agents (though for the prior it isn't formally coded against 1.3 and known to have inaccuracies).

Checklist

  • Unit tests updated

@changeset-bot
Copy link

changeset-bot bot commented Mar 23, 2025

🦋 Changeset detected

Latest commit: e219d79

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
html-aria Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@drwpow
Copy link
Owner

drwpow commented Mar 29, 2025

  • Trying to work out the behaviour of inheritance when it comes to default/implicit state/property values
    • The spec doesn't explicitly state that implicit state/property values for a role are inherited

Well it gets confusing! In ARIA 1.3, I think it does (link):

Superclass. This extension causes all the states and properties of the superclass role to propagate to the subclass role.

HOWEVER, poking at, say, alertdialog vs alert for aria-live defaults:

Browser alert alertdialog
Chrome "assertive" undefined ("off")
Firefox "assertive" undefined ("off")
Safari "assertive" "assertive"

So do we go with the spec, and assume defaults are inherited? Or do we go with browsers, which seem to imply they aren’t? I’ll see if I can dig up if the aria-query behavior is intentional or not. If it is, I wouldn’t mind comforming to it re: default inheritance (I’m most inclined to follow the browser trend here, and my personal bias is to trust Chrome/Firefox over Safari at the moment re: a11y).

Assume corrections to superclasses means may need corrections to subclasses? Are these just the inverse relationship? (it's not something the spec defines!)

Sorry I’m not sure I understand if this is in relation to the previous point or a new question. If it’s just the question “is this an inverse relationship?” IMO the 1.3 spec does strongly imply that: for each role, superclasses and subclasses are mirrored (I haven’t mapped them out but I’m not aware of any asymmetric relationships).

@cmorten
Copy link
Collaborator

cmorten commented Mar 29, 2025

So do we go with the spec, and assume defaults are inherited?

I think the spec is ambiguous here… there is room for interpretation of the quoted section either way - if inheritance is simply of required, prohibited, and supported states and properties then it would explain why browsers (I agree with the Safari skepticism) have implemented alertdialog to suit.

Perhaps worthy of an issue search and/or new issue on wai-aria to confirm?

Sorry I’m not sure I understand if this is in relation to the previous point or a new question.

New question and more note to myself to check what the spec says and what you’ve implemented here.

From what you’ve clarified sounds like need to ensure we mirror!

@cmorten
Copy link
Collaborator

cmorten commented Mar 29, 2025

w3c/aria#1096 implies default values are not inherited, although the comment isn’t baked explicitly into the spec, they simply amended wording around “all constraints”.

@drwpow
Copy link
Owner

drwpow commented Mar 29, 2025

w3c/aria#1096 implies default values are not inherited, although the comment isn’t baked explicitly into the spec, they simply amended wording around “all constraints”.

Yeah that’s good enough for me. Since it seems like aria-query, Chrome/Firefox, and some issues on wai-aria are in agreement, that’s enough of a consensus to say this library is out of alignment and we should change behavior to conform to this.

Reviewing your list vs aria-query (very helpful, thank you! 🙏), this is what I’m seeing revisiting ARIA 1.3:

  • alertdialog: this has no defaults! we incorrectly inherited from alert.

  • combobox: I don’t see aria-expanded with a default value. The spec reads:

    Authors MUST set aria-expanded

    But since it requires manual setting, though, I don’t really care if we set a default value or not (I wouldn’t mind to just conform with aria-query)

  • heading: this seems correct—there shouldn’t be any defaults? Not sure where 2 comes from 🤔

  • option: since aria-selected MAY be undefined, it seems like that should be the default if we’re being pedantic about it, not false which is technically different

  • progressbar: this one’s pretty clear in 1.3 these are explicit defaults (i.e. not inherited)

  • spinbutton: this one’s also pretty clear in 1.3 there are NO defaults (it’s unusual to see them explicate the default is not set)

  • timer: aria-live="off" is also correct in 1.3, and not inherited

  • treegrid: we incorrectly inherited aria-orientation when we shouldn’t

  • treeitem: I think we’re correct in NOT setting this default, since the spec says:

    Authors SHOULD NOT specify both aria-selected and aria-checked on treeitem elements contained by the same tree

    meaning if aria-checked is set, it would be a minor violation

  • doc-pagebreak: I’m less confident about this one but it seems like there’s no defaults?

@jlp-craigmorten
Copy link
Contributor Author

jlp-craigmorten commented Mar 30, 2025

✅ heading: this seems correct—there shouldn’t be any defaults? Not sure where 2 comes from 🤔

This one has bugged me as I was so sure this was the case as well... so not sure if there was a spec revision at some point or mandela effect, but the aria-level default of 2 must come from somewhere... 🤔

Update: found it, see https://www.w3.org/TR/wai-aria-1.3/#document-handling_author-errors_states-properties

If a required WAI-ARIA attribute for a given role is missing, user agents SHOULD process the attribute as if the values given in the following table were provided.

WAI-ARIA role Required Attribute Fallback value
... ... ...
heading aria-level 2
... ... ...

Note

Implicit values for non-required states and properties appear in the characteristics table for each role. These are not considered fallback values so are not included here.

So it (and other values) are user-agent fallbacks to compensate for author errors for required property and states.

Could go either way whether you include these in defaultAttributeValues for this library - gut feel is that the library is a stand-in for a user-agent so should capture these fallbacks.

@jlp-craigmorten jlp-craigmorten marked this pull request as ready for review March 30, 2025 11:19
@drwpow
Copy link
Owner

drwpow commented Mar 30, 2025

Update: found it, see https://www.w3.org/TR/wai-aria-1.3/#document-handling_author-errors_states-properties

Aha! Good find! So that’s why some of the roles explicate no defaults—it’s because of this table.

Would be good to double-check this isn’t completely off from current browser behavior but if not, this seems like the missing piece to explain aria-query’s behavior and there’s evidence to match it a little closer.

@jlp-craigmorten
Copy link
Contributor Author

jlp-craigmorten commented Apr 11, 2025

Would be good to double-check this isn’t completely off from current browser behavior

Setup

Created a partial test page for these (got lazy doing all of them, can if think worth it):

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>ARIA Fallback Values</title>
</head>
<body>
  <!-- Fallback for checkbox aria-checked -->
  <div role="checkbox" aria-checked="true">Checked Checkbox</div>
  <div role="checkbox" aria-checked="false">Unchecked Checkbox</div>
  <div role="checkbox">Checkbox with fallback (false)</div>

  <!-- Fallback for combobox aria-controls and aria-expanded -->
  <div role="combobox" aria-controls="combobox-controlled" aria-expanded="true">Expanded Combobox with controls</div>
  <div role="combobox" aria-controls="combobox-controlled" aria-expanded="false">Collapsed Combobox with controls</div>
  <div role="combobox" aria-controls="combobox-controlled">Combobox with controls and aria-expanded fallback (false)</div>
  <div role="combobox" aria-expanded="true">Expanded Combobox with aria-controls fallback (no mapping)</div>
  <div role="combobox" aria-expanded="false">Collapsed Combobox with aria-controls fallback (no mapping)</div>
  <div role="combobox">Combobox with aria-expanded fallback (false) and aria-controls fallback (no mapping)</div>
  <div id="combobox-controlled"></div>

  <!-- Fallback for heading aria-level -->
  <div role="heading" aria-level="1">Level 1 Heading</div>
  <div role="heading">Heading with fallback (level 2)</div>

  <!-- Fallback for menuitemcheckbox aria-checked -->
  <div role="menuitemcheckbox" aria-checked="true">Checked Menuitemcheckbox</div>
  <div role="menuitemcheckbox" aria-checked="false">Unchecked Menuitemcheckbox</div>
  <div role="menuitemcheckbox">Menuitemcheckbox with fallback (false)</div>

  <!-- Fallback for menuitemradio aria-checked -->
  <div role="menuitemradio" aria-checked="true">Checked Menuitemradio</div>
  <div role="menuitemradio" aria-checked="false">Unchecked Menuitemradio</div>
  <div role="menuitemradio">Menuitemradio with fallback (false)</div>

  <!-- Fallback for radio aria-checked -->
  <div role="radio" aria-checked="true">Checked Radio</div>
  <div role="radio" aria-checked="false">Unchecked Radio</div>
  <div role="radio">Radio with fallback (false)</div>

  <!-- Fallback for switch aria-checked -->
  <div role="switch" aria-checked="true">Checked Switch</div>
  <div role="switch" aria-checked="false">Unchecked Switch</div>
  <div role="switch">Switch with fallback (false)</div>

  <div>End</div>
</body>
</html></body></html>

And setup a @guidepup/playwright test for it:

const { expect } = require("@playwright/test");
const { voiceOverTest: test } = require("@guidepup/playwright");

test.use({ voiceOverStartOptions: { capture: "initial" } });

test.describe("Playwright VoiceOver custom iframe page", () => {
  test("open test page and navigate through to the end", async ({ page, voiceOver }) => {
    // Visit the test page, wait for contents to load, and then navigate to the main content.
    await page.goto("http://localhost:8081/fallbacks.html", {
      waitUntil: "load",
    });

    await expect(
      page.getByRole("heading", { level: 1, name: "Level 1 Heading" })
    ).toBeVisible();

    await voiceOver.navigateToWebContent();

    // Navigate to the end of the test page.
    while (!(await voiceOver.lastSpokenPhrase()).includes("End")) {
      await voiceOver.next();
    }

    console.log(await voiceOver.spokenPhraseLog());
  });
});

Results

Safari Chrome Notes
Checked Checkbox ticked tickbox Checked Checkbox ticked Tick box  
Unchecked Checkbox unticked tickbox Unchecked Checkbox unticked Tick box  
Checkbox with fallback (false) unticked tickbox Checkbox with fallback (false) unticked Tick box Browsers align with spec
Expanded Combobox with controls Expanded Combobox with controls  
Collapsed Combobox with controls Collapsed Combobox with controls  
Combobox with controls and aria-expanded fallback (false) Combobox with controls and aria-expanded fallback (false) No mention of expanded state, could just be VO being rubbish
Expanded Combobox with aria-controls fallback (no mapping) Expanded Combobox with aria-controls fallback (no mapping)  
Collapsed Combobox with aria-controls fallback (no mapping) Collapsed Combobox with aria-controls fallback (no mapping)  
Combobox with aria-expanded fallback (false) and aria-controls fallback (no mapping) Combobox with aria-expanded fallback (false) and aria-controls fallback (no mapping) No mention of expanded state, could just be VO being rubbish
heading level 1 Level 1 Heading heading level 1 Level 1 Heading  
heading Heading with fallback (level 2) heading level 2 Heading with fallback (level 2) Chrome aligns with spec with a default of 2, Safari does not
Checked Menuitemcheckbox ticked menu item Checked Menuitemcheckbox ticked menu item  
Unchecked Menuitemcheckbox menu item Unchecked Menuitemcheckbox menu item  
Menuitemcheckbox with fallback (false) menu item Menuitemcheckbox with fallback (false) menu item No mention of Unchecked state
Checked Menuitemradio ticked menu item Checked Menuitemradio ticked menu item  
Unchecked Menuitemradio menu item Unchecked Menuitemradio menu item  
Menuitemradio with fallback (false) menu item Menuitemradio with fallback (false) menu item No mention of Unchecked state
Checked Radio selected radio button Checked Radio selected radio button  
Unchecked Radio radio button Unchecked Radio radio button  
Radio with fallback (false) radio button Radio with fallback (false) radio button No mention of Unchecked state
Checked Switch on switch Checked Switch on switch  
Unchecked Switch off switch Unchecked Switch off switch  
Switch with fallback (false) off switch Switch with fallback (false) off switch No mention of Unchecked state

So it's a mixed bag of browsers/VO exposing this. Haven't checked windows + Narrator/NVDA or any mobile combinations.

@jlp-craigmorten
Copy link
Contributor Author

Also worth noting that there are WPT tests covering some of this fallback behaviour, e.g. https://github.com/web-platform-tests/wpt/blob/master/core-aam/heading-no-level-manual.html asserts that browsers should fallback to a level 2 heading when no aria-level is provided on role="heading"

@jlp-craigmorten
Copy link
Contributor Author

Checking Firefox (IMO has the nicest a11y devtools), also looks to be very similar to Chrome in it's adherence to the fallback spec - some stuff, but not all.

Firefox with accessibility inspector devtools open with a heading element highlighted that has an attribute of level 2 (but that element didn't specify an aria-level)

Copy link
Owner

@drwpow drwpow left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This all looks great! Thanks for all the great research and thought put into this. I’m aligned on all the changes here.

For this many role changes I think this would probably be closer to a breaking change. If we just changed it to minor I’d be happy to merge and release

@drwpow drwpow merged commit 1f90a50 into drwpow:main Apr 13, 2025
6 checks passed
@github-actions github-actions bot mentioned this pull request Apr 13, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants