Linter: Implement a11y-no-visually-hidden-interactive-elements rule#1671
Linter: Implement a11y-no-visually-hidden-interactive-elements rule#1671joelhawksley wants to merge 3 commits into
a11y-no-visually-hidden-interactive-elements rule#1671Conversation
|
Wouldn't this rule incorrectly flag things like |
|
@brunoprietog that's a good point! The original rule from rubocop-github did not account for those patterns. I went ahead and added tests and a fix and put your name on the commit ❤️ |
|
Great! ❤️ I'm not sure if it should be enabled by default, since we're assuming you'd be using Tailwind here, right? Generally, when I define the class in my projects, I do it like this: .visually-hidden:not(:focus):not(:active):not(:focus-within) {
clip-path: inset(50%);
height: 1px;
overflow: clip;
position: absolute;
white-space: nowrap;
width: 1px;
}And that way, you always avoid the problem. You could call it sr-only, too. But if you use Bootstrap, it could also apply to Or maybe make the class name configurable in some way? |
|
@brunoprietog forgive me if I'm not following, but I think we can the accommodation for Tailwind and not worry whether folks are using it or not. As for whether to allow for configuring other classes, I think that's a good idea but maybe @marcoroth can decide? I assume we'd just allow a list to be passed to the rule configuration. |
|
Oh I'm sorry, I think I didn't explain myself clearly. What I meant was that this actually depends a lot on whether you're using Tailwind or not, in the sense that you could create your own Similarly, if we added the visually-hidden Bootstrap class to the list along with |
Implements the `a11y-no-visually-hidden-interactive-elements` rule from erblint-github's `NoVisuallyHiddenInteractiveElements`. This rule flags interactive elements (a, button, summary, select, option, textarea) that have the `sr-only` CSS class, which visually hides them. Sighted keyboard users navigating to a visually hidden interactive element may become confused, thinking keyboard focus has been lost. Note: `input` elements are intentionally not flagged to avoid false positives (e.g. file inputs). Closes marcoroth#1225
Avoid flagging interactive elements that use sr-only alongside focus:not-sr-only or focus-within:not-sr-only, since these elements become visible on focus (e.g. skip-to-content links). Co-authored-by: Bruno Prieto <brunoprietog@users.noreply.github.com>
|
@brunoprietog I see what you mean now. Yes, this rule depends on what the class actually does in your application. I think it's fine to leave this as disabled by default, but maybe we should add some docs to clarify your concern. Would you be up for suggesting changes on this PR? |
a11y-no-visually-hidden-interactive-elements linter rulea11y-no-visually-hidden-interactive-elements rule
brunoprietog
left a comment
There was a problem hiding this comment.
I have been thinking about this more and I have some doubts I wanted to share.
Whether sr-only is a problem depends on the CSS, not the HTML. If the class is defined to reveal the element on focus (like the example I shared before, with :not(:focus):not(:active):not(:focus-within)), then using it on interactive elements is never a problem, no matter where it is used. So the real fix is to define the class correctly once in CSS, instead of checking every usage in the templates. If you control the CSS, this rule does not add much.
However, Tailwind is very widely used, and there sr-only keeps the element permanently hidden, and you have to reveal it per element (focus:not-sr-only). In that case the fix really is per usage, so the rule is useful.
But this also means it is a Tailwind-specific rule, not a general a11y one. It will not catch Bootstrap, plain CSS, or a custom sr-only that reveals on focus, while the name a11y-no-visually-hidden-interactive-elements looks general.
So I have two questions:
- Is it worth keeping, considering the CSS fix is better when you control the CSS? I think yes, mainly because Tailwind is so widely used.
- If we keep it, should it be scoped and named to make clear that it is really about Tailwind's
sr-only, instead of looking like a general rule? There is already precedent for context-specific rules (actionview-*).
What do you think?
|
|
||
| Visually hiding interactive elements can be confusing to sighted keyboard users as it appears their focus has been lost when they navigate to the hidden element. | ||
|
|
||
| Note: `input` elements are not flagged at this time as some visually hidden inputs might cause false positives (e.g. file inputs). |
There was a problem hiding this comment.
| Note: `input` elements are not flagged at this time as some visually hidden inputs might cause false positives (e.g. file inputs). | |
| Note: `input` elements are not flagged at this time as some visually hidden inputs might cause false positives (e.g. file inputs). | |
| ## When to enable | |
| This rule is specific to Tailwind in practice: it detects Tailwind's `sr-only` class and its `*:not-sr-only` focus-reveal variants. It is disabled by default because whether `sr-only` is a problem depends on how the class is defined in CSS. | |
| In Tailwind, `sr-only` keeps the element permanently hidden, so revealing it on focus has to be added per element (`focus:not-sr-only`), which is what this rule checks for. If instead you define your own `sr-only` that reveals on focus (e.g. `.sr-only:not(:focus):not(:active):not(:focus-within)`), then using it on interactive elements is not a problem and you can keep the rule disabled. | |
| Class names also differ across frameworks (e.g. Bootstrap's `visually-hidden` / `visually-hidden-focusable`), which this rule does not currently handle. |
| const INTERACTIVE_ELEMENTS = ["a", "button", "summary", "select", "option", "textarea"] | ||
|
|
||
| const VISUALLY_HIDDEN_CLASSES = ["sr-only"] | ||
| const VISUALLY_HIDDEN_UNDO_CLASSES = ["not-sr-only", "focus:not-sr-only", "focus-within:not-sr-only"] |
There was a problem hiding this comment.
Since the rule already works on Tailwind's conventions, it might be better to handle its focus variants more completely. Instead of an explicit list, matching the semantics (any focus-triggered not-sr-only) would be more robust:
const VISUALLY_SHOWN_ON_FOCUS = /(?:^|:)(?:group-)?focus(?:-visible|-within)?:not-sr-only$/
const isUndoClass = (cls: string) => cls === "not-sr-only" || VISUALLY_SHOWN_ON_FOCUS.test(cls)And then !classes.some(isUndoClass) below. This covers focus, focus-visible, focus-within, group-focus-within, and responsive prefixes like md:focus:not-sr-only at once. It still excludes hover: and active:, because those do not reveal the element for keyboard users, so the offense should still fire.
Funny you should mention this re: Tailwind, when we don't use it at GitHub! It just so happens that our implementation is similar in this regard. What do you think about defaulting to sr-only and allowing the class to be configured? |
|
Yes I suppose so, but is that possible? Just out of curiosity, is there a reason why you haven't fixed that at the source in the CSS on GitHub instead of using the rule? Or maybe there's something I'm not taking into account |
I'm on paternity leave at the moment, so I can't answer that question 😄 @marcoroth how do you think we should proceed here? |
Implements the
a11y-no-visually-hidden-interactive-elementsrule from erblint-github'sNoVisuallyHiddenInteractiveElements.This rule flags interactive elements (
a,button,summary,select,option,textarea) that have thesr-onlyCSS class, which visually hides them. Sighted keyboard users navigating to a visually hidden interactive element may become confused, thinking keyboard focus has been lost.Note:
inputelements are intentionally not flagged to avoid false positives (e.g. file inputs).Closes #1225