fix(region): Decorative images ignored by region rule#4412
fix(region): Decorative images ignored by region rule#4412gaiety-deque merged 11 commits intodevelopfrom
Conversation
| (dom.isSkipLink(virtualNode.actualNode) && | ||
| dom.getElementByReference(virtualNode.actualNode, 'href')) || | ||
| !dom.isVisibleToScreenReaders(node) | ||
| !dom.isVisibleToScreenReaders(node) || |
There was a problem hiding this comment.
This brings up an interesting question. Should a presentational img using alt="" be considered not visible to screen readers? @WilcoFiers is the better change to update isVisibleToScreenReaders to return false for an img with alt=""? Right now isVisibleToScreenReaders returns true for an img with empty alt.
There was a problem hiding this comment.
You know I was wondering that, it was a path I started diving down and then uncertainty got the best of me. It's a function leveraged in a lot of places.
If we think that's the right path I'm happy to try it out and see if any other test failures come up along the way that make us second guess it.
There was a problem hiding this comment.
Fair question, but no I don't think so. Conceptually, there's a difference between a hidden element and an element that's ignored. It matters for example on <img role="button" alt="">. That should fail for having an inappropriate role, whereas <img role="button" aria-hidden="true" alt=""> should be incomplete.
| (dom.isSkipLink(virtualNode.actualNode) && | ||
| dom.getElementByReference(virtualNode.actualNode, 'href')) || | ||
| !dom.isVisibleToScreenReaders(node) | ||
| !dom.isVisibleToScreenReaders(node) || |
There was a problem hiding this comment.
Fair question, but no I don't think so. Conceptually, there's a difference between a hidden element and an element that's ignored. It matters for example on <img role="button" alt="">. That should fail for having an inappropriate role, whereas <img role="button" aria-hidden="true" alt=""> should be incomplete.
| } | ||
|
|
||
| function isPresentationGraphic(virtualNode) { | ||
| return virtualNode.props.nodeName === 'img' && virtualNode.attr('alt') === ''; |
There was a problem hiding this comment.
This feels a little incomplete. There are other graphics, and there are more ways for an image to be decorative. We have methods we can use to figure this stuff out:
| return virtualNode.props.nodeName === 'img' && virtualNode.attr('alt') === ''; | |
| return aria.isVisualContent(virtualNode.actualNode) && text.accessibleTextVirtual(virtualNode) === ''; |
There was a problem hiding this comment.
Hmm I agree, alas I'm a bit unsure which existing methods are the most appropriate.
Your suggestion is close, however Edit: nevermind, it doesaccessibleTextVirtual never actually checks for an alt attribute in its computational steps.
The closest I've found is Edit: Nevermind I misunderstood what this was forhasAltEvaluate in shared but that isn't actually used anywhere and only returns a boolean.
However, I think Edit: Nevermind againfindTextMethods should work as it uses nativeTextMethods which checks for alt text - but that gives me an empty string even without an alt attribute on the image.
Think I'll post asking for pairing assistance tomorrow to learn how to fish for the right answer on this one :)
There was a problem hiding this comment.
@WilcoFiers I paired with Steve and we generally agree that it'd be nice to make this check more broad, however isVisualContent is perhaps too broad for what it checks for. We currently seem to lack a shared utility for checking if something is a graphic (as far as we could tell) so one path forward may be writing one.
We would both love to circle back with you on Monday @WilcoFiers about what we'd like to do
There was a problem hiding this comment.
In a call we discussed we wanted to check for the tags img, svg, object and canvas and to then use accessibleTextVirtual but that function does not work for our needs, as it seems to return an empty string regardless of if an img tag has no alt attribute (like the original issue) or an empty alt attribute.
My findings copied from Slack below:
It (accessibleTextVirtual) seems to be written deeply to rely on empty strings all over the place as far as I can tell.
accessibleTextVirtual's mainreduceinitializes the value as''- ^ it also tries to sanitize the value, which if the value is falsey at all returns an empty string
- ^ the reduce returns immediately if the value is anything that isn't an empty string
nativeTextAlternative's mainreducealso initializes the value as''- ^'s uses several functions in its reduce including
findTextMethodsfinds and usesnative-text-methods.js'sattrTextfunction which returns the attribute or an empty string meaning it'll never returnnull
I tried adjusting all of these places but it got pretty hairy and makes me think this was all set up intentionally the way it was, and it may be the wrong thing to use.
So this PR is stuck assuming on this comment thread currently without a clear path forward. I believe Steve and Wilco are busy so, hi @dbjorge any thoughts?
There was a problem hiding this comment.
I think the real question is the following:
What we need to determine is if the region rule should ignore both an image with an empty alt='' attribute and if an image is entirely lacking an alt attribute
There was a problem hiding this comment.
My thoughts on the problem
What we need to determine is if the region rule should ignore both an image with an empty alt='' attribute and if an image is entirely lacking an alt attribute
I think this ought to come down to "is the image in question going to be exposed to ATs or not"; I think the result of that is that region should ignore most cases with empty alt and should not ignore cases with no alt, but note that there are some edge cases where the alt attribute isn't the whole story.
I agree with the other comment chain that it's confusing that the use of isVisibleToScreenReaders doesn't already cover "is the image going to be exposed to ATs or not". Even if we do decide to keep isVisibleToScreenReaders as-is, I think it'd be a good idea for us to at least update the comment on isVisibleToScreenReaders to explain this gotcha.
The difficulty with updating isVisibleToScreenReaders for this case is that many places that use it (including-but-not-limited-to its own recursive implementation and also region-evaluate here) do so with the understanding that if it returns false for a given element, it implies that the element and all its descendants are going to be invisible to screen readers.
However, that's not the case here!
The mechanism by which imgs with alt="" get hidden from ATs is that the implicit aria role for such an element is role="none"/role="presentation" rather than role="img"1,2.
role="none"/role="presentation" semantics are different from isVisibleToScreenReaders' semantics in that they don't hide the element's descendants from the accessibility tree; they only hide the semantics of the element itself. A basic <img> tag with a resolved role of none will be omitted from the accessibility tree because its semantics as an image covers the only information about it that would appear in the tree. But in something like <h1 role="none">content</h1>, for example, the content text still appears in the accessibility tree (just as a plain text node, not treated as a heading).
What I suggest to resolve it
I lean towards updating findRegionlessElms to calculate an isShallowlyHidden condition for candidate elements, something like this:
const role = getRole(virtualNode, { noPresentational: true });
const isPresentational = role === null
// The element itself is not visible to screen readers, but its descendants might be
const isShallowlyHidden = isPresentational && !hasChildTextNodes(elm)...and then update the conditional that checks for dom.hasContent(node, true) (L76 of the original) to treat !isShallowlyHidden as an additional requirement to return [virtualNode];.
You could make a reasonable argument that the better fix would be to add presentational-role as an additional any check on the region rule. I think that (+ a change to that check's messages to special case the messaging for the alt="" case) would likely be a better result message in this specific motivating repro case, but I recommend against it because of the way the region rule currently tries to only emit failures for the topmost ancestor in a tree of regionless elements; it'll give a misleading "how to fix" recommendation for region violations on elements with text descendants, where a presentational role wouldn't be enough to hide that element's descendant tree as a whole.
Footnotes
1: This has a few caveats:
- If the author assigns a
roleattribute explicitly, it will take precedence over implicit role selection - If the element is focusable or has a global aria attribute, its implicit role will be treated as
role="img"even withalt=""- this also overrules an explicit
role="none"/role="presentation"attribute
- this also overrules an explicit
2: axe-core has a few places where this logic comes up, including the caveats from note 1; see presentational-role-evalutate.js and implicit-html-roles.js's img entry
There was a problem hiding this comment.
Possibly addressed in 524f0be
I played with adding some manual checks for the caveats of global aria attributes and focusable, but it didn't seem to need any additional code. Writing the tests leads me to believe the caveats are handled as expected.
524f0be to
f914665
Compare
WilcoFiers
left a comment
There was a problem hiding this comment.
Can you add an integration test as well?
There was a problem hiding this comment.
Can you add an integration test for this.
There was a problem hiding this comment.
Can you add a few more tests here about other elements:
<canvas role="none">passes<object aria-label="foo">fails<svg role="none" aria-label="foo">fails<div role="none">foo</div>fails-
<div role="none"></div>passes
There was a problem hiding this comment.
We intentionally allow svg's outside of regions fab58d4bf#diff-b11ab1ec32c4f5c0e17de0c9a1f3c5a9835bcc133f673b5da3ecaaf66a932077
There was a problem hiding this comment.
Added the other tests as both unit and integration tests
Co-authored-by: Wilco Fiers <WilcoFiers@users.noreply.github.com>
Co-authored-by: Wilco Fiers <WilcoFiers@users.noreply.github.com>
### [4.9.1](v4.9.0...v4.9.1) (2024-05-06) ### Bug Fixes - Prevent errors when loading axe in a page with prototype.js - **aria-allowed-attr:** allow meter role allowed aria-\* attributes on meter element ([#4435](#4435)) ([7ac6392](7ac6392)) - **aria-allowed-role:** add gridcell, separator, slider and treeitem to allowed roles of button element ([#4398](#4398)) ([4788bf8](4788bf8)) - **aria-roles:** correct abstract roles (types) for aria-roles([#4421](#4421)) - **aria-valid-attr-value:** aria-controls & aria-haspopup incomplete ([#4418](#4418)) - fix building axe-core translation files with region locales ([#4396](#4396)) ([5c318f3](5c318f3)), closes [#4388](#4388) - **invalidrole:** allow upper and mixed case role names ([#4358](#4358)) ([105016c](105016c)), closes [#2695](#2695) - **isVisibleOnScreen:** account for position: absolute elements inside overflow container ([#4405](#4405)) ([2940f6e](2940f6e)), closes [#4016](#4016) - **label-content-name-mismatch:** better dismiss and wysiwyg symbolic text characters ([#4402](#4402)) - **region:** Decorative images ignored by region rule ([#4412](#4412)) - **target-size:** ignore descendant elements in shadow dom ([#4410](#4410)) ([6091367](6091367)) - **target-size:** pass for element that has nearby elements that are obscured ([#4422](#4422)) ([3a90bb7](3a90bb7)), closes [#4387](#4387) This PR was opened by a robot 🤖 🎉 (And updated by @WilcoFiers )
Issue was around decorative images, specifically 1 pixel wide/tall marketing tracking images, that get added outside of regions failing the region rule. An easy and fairly robust solution the issue opener agreed with was ignoring images with
alt=''and so that's what this PR implements.Dev notes:
isPresentationGraphic/1check which currently only handlesalt=''and ignores them for the region ruletreats svg elements as regionsso the new function doesn't check for svg'sfix: #4145