Skip to content
Merged
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 @@ -6,6 +6,7 @@ This page contains documentation for all Herb Linter rules.

#### Accessibility

- [`a11y-disabled-attribute`](./a11y-disabled-attribute.md) - Prevent usage of the `disabled` attribute on unsupported elements
- [`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
Expand Down
47 changes: 47 additions & 0 deletions javascript/packages/linter/docs/rules/a11y-disabled-attribute.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Linter Rule: Disabled Attribute

**Rule:** `a11y-disabled-attribute`

## Description

The `disabled` attribute is only valid on certain HTML elements. Using it on other elements has no effect and may confuse users or assistive technologies.

## Rationale

The HTML `disabled` attribute is a boolean attribute that only applies to form-related elements: `button`, `input`, `textarea`, `option`, `select`, `fieldset`, `optgroup`, and `task-lists`. Applying it to other elements like `<a>`, `<div>`, or `<span>` has no native browser behavior and can be misleading.

## Examples

### ✅ Good

```erb
<button disabled>Continue</button>
```

```erb
<input type="text" disabled>
```

```erb
<select disabled><option>A</option></select>
```

### 🚫 Bad

```erb
<a href="https://github.com/" disabled>Go to GitHub</a>
```

```erb
<div disabled>Content</div>
```

```erb
<span disabled>Text</span>
```

## References

- [erblint-github: `DisabledAttribute`](https://github.com/github/erblint-github/blob/main/lib/erblint-github/linters/github/accessibility/disabled_attribute.rb)
- [erblint-github docs](https://github.com/github/erblint-github/blob/main/docs/rules/accessibility/disabled-attribute.md)
- [MDN: HTML attribute `disabled`](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/disabled)
2 changes: 2 additions & 0 deletions javascript/packages/linter/src/rules.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { RuleClass } from "./types.js"

import { A11yDisabledAttributeRule } from "./rules/a11y-disabled-attribute.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"
Expand Down Expand Up @@ -103,6 +104,7 @@ import { SVGTagNameCapitalizationRule } from "./rules/svg-tag-name-capitalizatio
import { TurboPermanentRequireIdRule } from "./rules/turbo-permanent-require-id.js"

export const rules: RuleClass[] = [
A11yDisabledAttributeRule,
A11yNoAccesskeyAttributeRule,
A11yNoAriaUnsupportedElementsRule,
A11yNoAutofocusAttributeRule,
Expand Down
50 changes: 50 additions & 0 deletions javascript/packages/linter/src/rules/a11y-disabled-attribute.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { hasAttribute, getTagLocalName } from "@herb-tools/core"

import { BaseRuleVisitor } from "./rule-utils.js"
import { ParserRule } from "../types.js"
import type { UnboundLintOffense, LintContext, FullRuleConfig } from "../types.js"
import type { HTMLOpenTagNode, ParseResult } from "@herb-tools/core"

const VALID_DISABLED_TAGS = new Set([
"button", "fieldset", "input", "optgroup", "option", "select", "textarea", "task-lists"
])

class DisabledAttributeVisitor extends BaseRuleVisitor {
visitHTMLOpenTagNode(node: HTMLOpenTagNode): void {
this.checkDisabledAttribute(node)
super.visitHTMLOpenTagNode(node)
}

private checkDisabledAttribute(node: HTMLOpenTagNode): void {
if (!hasAttribute(node, "disabled")) return

const tagName = getTagLocalName(node)
if (!tagName) return
if (VALID_DISABLED_TAGS.has(tagName)) return

this.addOffense(
`The \`disabled\` attribute is only valid on ${[...VALID_DISABLED_TAGS].join(", ")}.`,
node.tag_name!.location,
)
}
}

export class A11yDisabledAttributeRule extends ParserRule {
static ruleName = "a11y-disabled-attribute"
static introducedIn = this.version("unreleased")

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

check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense[] {
const visitor = new DisabledAttributeVisitor(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,3 +1,4 @@
export * from "./a11y-disabled-attribute.js"
export * from "./a11y-no-accesskey-attribute.js"
export * from "./a11y-no-aria-unsupported-elements.js"
export * from "./a11y-no-autofocus-attribute.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