-
Notifications
You must be signed in to change notification settings - Fork 783
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
fix(region): Decorative images ignored by region rule #4412
Conversation
if ( | ||
getRole(virtualNode) === 'button' || | ||
isRegion(virtualNode, options) || | ||
['iframe', 'frame'].includes(virtualNode.props.nodeName) || | ||
(dom.isSkipLink(virtualNode.actualNode) && | ||
dom.getElementByReference(virtualNode.actualNode, 'href')) || | ||
!dom.isVisibleToScreenReaders(node) | ||
!dom.isVisibleToScreenReaders(node) || |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
if ( | ||
getRole(virtualNode) === 'button' || | ||
isRegion(virtualNode, options) || | ||
['iframe', 'frame'].includes(virtualNode.props.nodeName) || | ||
(dom.isSkipLink(virtualNode.actualNode) && | ||
dom.getElementByReference(virtualNode.actualNode, 'href')) || | ||
!dom.isVisibleToScreenReaders(node) | ||
!dom.isVisibleToScreenReaders(node) || |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
@@ -86,6 +91,10 @@ function findRegionlessElms(virtualNode, options) { | |||
} | |||
} | |||
|
|||
function isPresentationGraphic(virtualNode) { | |||
return virtualNode.props.nodeName === 'img' && virtualNode.attr('alt') === ''; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 mainreduce
initializes 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 mainreduce
also initializes the value as''
- ^'s uses several functions in its reduce including
findTextMethods
finds and usesnative-text-methods.js
'sattrText
function 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 img
s 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
role
attribute 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you add an integration test as well?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you add an integration test for this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added :)
test/checks/navigation/region.js
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We intentionally allow svg's outside of regions fab58d4bf#diff-b11ab1ec32c4f5c0e17de0c9a1f3c5a9835bcc133f673b5da3ecaaf66a932077
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added the other tests as both unit and integration tests
Co-authored-by: Wilco Fiers <WilcoFiers@users.noreply.github.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Almost. Just accept the suggestion and we're done.
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/1
check which currently only handlesalt=''
and ignores them for the region ruletreats svg elements as regions
so the new function doesn't check for svg'sfix: #4145