dequelabs / dequelabs/axe-core
Accessible name calculation should not allow fallback from empty <label>s
- Dominant language
- JavaScript
- Stars
- 7.5k
- Forks
- 933
- Avg merge
- 2d 23h
- Merged PRs (30d)
- 17
Description
### Product
axe-core
### Product Version
4.10.1
### Latest Version
- [x] I have tested the issue with the latest version of the product
### Issue Description
The following checks:
- `non-empty-placeholder` (used by the `label` rule)
- `non-empty-value` (used by the `input-button-name` rule)
- `non-empty-title` (used by many `*-name` rules)
- `button-has-visible-text` (used by the `button-name` rule)
...currently work by just checking the specific attribute/content they're associated with, ignoring other context. However, it turns out that in some browsers (notably Chrome), the presence of an empty `` (either implicitly or explicitly linked) will result in these cases being ignored during accessible name calculations. We think this is likely an HTML-AAM violation on Chrome and Safari's parts, but for accessibility-supported purposes, axe-core should respect the least-functional of the fallback behaviors (Chrome's).
I verified that this applies to at least the `label`, `button-name`, and `input-button-name` rules, but note that this probably applies to the other rules that rely on `non-empty-title` as well (we'll need to add integration tests of that as part of this update)
**Note that we'll also want to make corresponding updates to axe's own accessible name calculations, in addition to those specific checks.**
#### Standard interpretation
[HTML-AAM §4.1.1](https://w3c.github.io/html-aam/#input-type-text-input-type-password-input-type-number-input-type-search-input-type-tel-input-type-email-input-type-url-and-textarea-element-accessible-name-computation) step 3 is consistent with this for `title` attributes:
> 1. If the control has an [aria-label](https://www.w3.org/TR/wai-aria-1.2/#aria-label) or an [aria-labelledby](https://www.w3.org/TR/wai-aria-1.2/#aria-labelledby) attribute the [accessible name](https://www.w3.org/TR/accname-1.2/#dfn-accessible-name) is to be calculated using the algorithm defined in [Accessible Name and Description: Computation and API Mappings](https://w3c.github.io/accname/).
> 2. Otherwise use the associated label element or elements [accessible name(s)](https://www.w3.org/TR/accname-1.2/#dfn-accessible-name) - if more than one label is associated; concatenate by DOM order, delimited by spaces.
> 3. If the [accessible name](https://www.w3.org/TR/accname-1.2/#dfn-accessible-name) is still empty, then: use the control's title attribute.
> 4. Otherwise use the control's [placeholder](https://w3c.github.io/html-aam/#att-placeholder) value.
> 5. If none of the above yield a usable text string there is no [accessible name](https://www.w3.org/TR/accname-1.2/#dfn-accessible-name).
The word "Otherwise" in step 4 is a bit ambiguous, but based on [the PR introducing the language](https://github.com/w3c/html-aam/issues/167), I think it was probably meant to be read similarly to step 3 (ie, as if "Otherwise" were replaced with "If the [accessible name](https://www.w3.org/TR/accname-1.2/#dfn-accessible-name) is still empty, then: "), which would also be consistent with the current `label` behavior.
#### Accessibility Supported status
Unfortunately, the accessibility supported state isn't really consistent with any reasonable interpretation of the standard. Firefox falls back like the `label` rule expects, but Chrome never fallsand Safari will treat elements with empty ``s as having empty accessible names in most cases, even if a non-empty placeholder or title is present:
```html
content
content
```
The accessible names as reported by browsers are:
| Case | Chrome 130.0.6723.59 | Safari 17.6 (19618.3.11.11.5) | Firefox 131.0.3 |
| - | - | - | - |
| label `fail15` | `""` | `""` | `"placeholder value"` |
| label `fail16` | `""` | `"placeholder value"` | `"placeholder value"` |
| label `fail17` | `""` | `""` | `"title value"` |
| label `fail18` | `""` | `""` | `"title value"` |
| button-name `fail5` | `""` | `""` | `"title value"` |
| button-name `fail6` | `""` | `""` | `"title value"` |
| button-name `fail7` | `""` | `""` | `"content"` |
| button-name `fail8` | `""` | `"content"` | `"content"` |
| input-button-name `fail5` | `""` | `""` | `"value attr"` |
| input-button-name `fail6` | `""` | `"value attr"` | `"value attr"` |
#### Expectation
All 10 of the cases above should be violations, since at least one of our accessibility-supported browser combos report empty accessible names for them.
#### Actual
All 10 of those cases pass
#### How to Reproduce
See above
#### Additional context
##### Original report
See [this axe-community slack thread](https://axecommunity.slack.com/archives/C01BSF8J6LS/p1728999543482409). Thanks @pattonwebz for the original report!
##### `aria-label`/`aria-labelledby` out-of-scope
Note that the lack of fallback here *only applies to empty `` elements*! All 3 browsers *do* fall back from empty `aria-label` and `aria-labelledby` cases to titles and placeholders:
```html
```
| Case | Chrome | Safari | Firefox |
| - | - | - | - |
| label `pass18` | `"placeholder value"` | `"placeholder value"` | `"placeholder value"` |
| label `pass19` | `"placeholder value"` | `"placeholder value"` | `"placeholder value"` |
| label `pass20` | `"title value"` | `"title value"` | `"title value"` |
| label `pass21` | `"title value"` | `"title value"` | `"title value"` |
##### Suggested check fixes
At the check level, I think the simplest fix would probably be to update the 4 checks in question to return `false` if an implicit or explicit label is present, which a custom check message explaining why they're inapplicable.
If we write the integration tests and find that actually there are *some* cases where the current behavior with `non-empty-title` is correct, we could instead consider something similar to the exiting `"none": ["hidden-explicit-label"]` check config in the `label` rule, adding a special check that just reports on this specific incompatibility. I think that'd probably result in worse check messaging in the unaffected cases, though; I think it'd be better to avoid adding more verbosity to those rules' remediation messages, they're already a bit dense.
##### Suggested accessible name calculation fixes
I think for this it'd be cleaner to do it a bit differently from the checks and closer to how it seems like Chrome is operating in practice. Currently, `accessible-text-virtual.js` and `native-text-alternative.js` both contain `reduce` calls that iterate over the name calculation steps and treat cases where a step returns `''` as meaning "fall back to the next step". Since it seems like the chrome behavior really boils down to "if the name from `` is empty, treat it as a special halting empty name", I'd lean towards updating those core loops to support steps indicating a similar concept, maybe something like a `Symbol('halting-empty-string')` that is understood to exit-fast from the steps, and update the `labelText` naming method implementation to return that if it sees a present-but-empty case.
##### Other updates
We'll want to audit everywhere else our `label`, `labelVirtual`, and `labelText` functions are called to double check if there are any other cases repeating name calculation logic that need similar updates
Contributor guide
Assessment
This issue has not been assessed yet.