Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions javascript/packages/linter/docs/rules/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ This page contains documentation for all Herb Linter rules.
- [`a11y-no-accesskey-attribute`](./a11y-no-accesskey-attribute.md) - Prevent usage of the `accesskey` attribute
- [`a11y-no-aria-unsupported-elements`](./a11y-no-aria-unsupported-elements.md) - Prevent usage of ARIA on unsupported elements
- [`a11y-no-autofocus-attribute`](./a11y-no-autofocus-attribute.md) - Prevent usage of the `autofocus` attribute
- [`a11y-no-visually-hidden-interactive-elements`](./a11y-no-visually-hidden-interactive-elements.md) - Prevent visually hidden interactive elements


#### Action View
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Linter Rule: No visually hidden interactive elements

**Rule:** `a11y-no-visually-hidden-interactive-elements`

## Description

Enforce that interactive elements are not visually hidden using classes like `sr-only`.

## Rationale

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).

## Examples

### ✅ Good

```erb
<h2 class="sr-only">Welcome to GitHub</h2>
```

```erb
<span class="sr-only">Visually hidden text</span>
```

```erb
<button class="btn">Submit</button>
```

### 🚫 Bad

```erb
<button class="sr-only">Submit</button>
```

```erb
<a class="sr-only" href="/about">About</a>
```

```erb
<summary class="sr-only">Details</summary>
```

```erb
<select class="sr-only"><option>A</option></select>
```

```erb
<textarea class="sr-only"></textarea>
```

## References

- [erblint-github: NoVisuallyHiddenInteractiveElements](https://github.com/github/erblint-github/blob/main/lib/erblint-github/linters/github/accessibility/no_visually_hidden_interactive_elements.rb)
- [erblint-github docs](https://github.com/github/erblint-github/blob/main/docs/rules/accessibility/no-visually-hidden-interactive-elements.md)
2 changes: 2 additions & 0 deletions javascript/packages/linter/src/rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { RuleClass } from "./types.js"
import { A11yNoAccesskeyAttributeRule } from "./rules/a11y-no-accesskey-attribute.js"
import { A11yNoAriaUnsupportedElementsRule } from "./rules/a11y-no-aria-unsupported-elements.js"
import { A11yNoAutofocusAttributeRule } from "./rules/a11y-no-autofocus-attribute.js"
import { A11yNoVisuallyHiddenInteractiveElementsRule } from "./rules/a11y-no-visually-hidden-interactive-elements.js"

import { ActionViewNoSilentHelperRule } from "./rules/actionview-no-silent-helper.js"
import { ActionViewNoSilentRenderRule } from "./rules/actionview-no-silent-render.js"
Expand Down Expand Up @@ -106,6 +107,7 @@ export const rules: RuleClass[] = [
A11yNoAccesskeyAttributeRule,
A11yNoAriaUnsupportedElementsRule,
A11yNoAutofocusAttributeRule,
A11yNoVisuallyHiddenInteractiveElementsRule,

ActionViewNoSilentHelperRule,
ActionViewNoSilentRenderRule,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { type ParseResult, type ParserOptions, type HTMLElementNode, getTagLocalName, getStaticAttributeValue } from "@herb-tools/core"

import { BaseRuleVisitor } from "./rule-utils.js"
import { ParserRule } from "../types.js"
import type { UnboundLintOffense, LintContext, FullRuleConfig } from "../types.js"

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"]

class NoVisuallyHiddenInteractiveElementsVisitor extends BaseRuleVisitor {
visitHTMLElementNode(node: HTMLElementNode): void {
const tagName = getTagLocalName(node)

if (tagName && INTERACTIVE_ELEMENTS.includes(tagName)) {
const classValue = getStaticAttributeValue(node, "class")

if (classValue) {
const classes = classValue.split(/\s+/)

if (VISUALLY_HIDDEN_CLASSES.some((cls) => classes.includes(cls)) && !VISUALLY_HIDDEN_UNDO_CLASSES.some((cls) => classes.includes(cls))) {
this.addOffense(
"Avoid visually hiding interactive elements. 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.",
node.tag_name!.location,
)
}
}
}

super.visitHTMLElementNode(node)
}
}

export class A11yNoVisuallyHiddenInteractiveElementsRule extends ParserRule {
static ruleName = "a11y-no-visually-hidden-interactive-elements"
static introducedIn = this.version("0.9.4")

get defaultConfig(): FullRuleConfig {
return {
enabled: false,
severity: "warning",
}
}

get parserOptions(): Partial<ParserOptions> {
return {
action_view_helpers: true,
}
}

check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense[] {
const visitor = new NoVisuallyHiddenInteractiveElementsVisitor(this.ruleName, context)

visitor.visit(result.value)

return visitor.offenses
}
}
1 change: 1 addition & 0 deletions javascript/packages/linter/src/rules/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export * from "./a11y-no-accesskey-attribute.js"
export * from "./a11y-no-aria-unsupported-elements.js"
export * from "./a11y-no-autofocus-attribute.js"
export * from "./a11y-no-visually-hidden-interactive-elements.js"

export * from "./rule-utils.js"
export * from "./prism-rule-utils.js"
Expand Down
44 changes: 22 additions & 22 deletions javascript/packages/linter/test/__snapshots__/cli.test.ts.snap

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading