a11y-lint 0.13.0 → 0.14.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.claude/commands/task.md +37 -0
- data/CHANGELOG.md +12 -0
- data/docs/CONTRIBUTING.md +35 -0
- data/docs/src/_components/shared/code_tabs.css +57 -0
- data/docs/src/_components/shared/code_tabs.erb +30 -0
- data/docs/src/_components/shared/code_tabs.js +54 -0
- data/docs/src/_components/shared/code_tabs.rb +36 -0
- data/docs/src/rules/form-controls-need-accessible-names.md +20 -9
- data/docs/src/rules/images-need-alt-text.md +22 -6
- data/lib/a11y/lint/phlex_runner.rb +14 -1
- data/lib/a11y/lint/rules/robust/{simple_form_select_missing_accessible_name.rb → simple_form_input_missing_accessible_name.rb} +8 -9
- data/lib/a11y/lint/version.rb +1 -1
- metadata +8 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 17ce590d5fc702bdcf3feb01606767c2aaa0938ca6a40b3f456737d7e9f519f2
|
|
4
|
+
data.tar.gz: b1ec19b360ccbc592fd42a9848cfdb351b924f3752b3041afa063f64bfd3e380
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fa1db2cd985c446515c75808512e3663859af93d254949e51fb3638e0ee1fe7cf2172a55686f66b937476e8739a9f24910b8662ef03e285445dfab9079983be3
|
|
7
|
+
data.tar.gz: 045c3ec7a7e4442a47517020aa2cf3da15496a3e304a1768e8e067d7767f2cd0693db025711f25c05e02736c6d3f0095378e9dfb68ed1a3b19ebcb3e8232a49e
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Capture a free-form idea as a GitHub issue in the current repo
|
|
3
|
+
argument-hint: <idea>
|
|
4
|
+
allowed-tools: Bash(gh issue create:*), Bash(gh repo view:*), Bash(grep:*), Bash(rg:*), Bash(ls:*), Bash(find:*), Read, Glob, Grep
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Take the free-form idea below and turn it into a well-formed GitHub issue in the current repo.
|
|
8
|
+
|
|
9
|
+
Idea: $ARGUMENTS
|
|
10
|
+
|
|
11
|
+
## What to do
|
|
12
|
+
|
|
13
|
+
1. **Refine** the idea into:
|
|
14
|
+
- A clear **title** (≤ ~70 chars, sentence case, no trailing period)
|
|
15
|
+
- A **body** with these sections (omit any that don't apply):
|
|
16
|
+
- **Problem / idea** — restate the idea clearly in 1–3 sentences
|
|
17
|
+
- **Proposed approach** — only include if the approach is obvious from the idea
|
|
18
|
+
- **Files likely involved** — only include if you can quickly identify them from the repo (e.g. a new rule idea should point at the right WCAG principle directory under `lib/a11y/lint/rules/`)
|
|
19
|
+
|
|
20
|
+
2. **Clarify only if necessary** — one round, max. Voice input drops context, but too many questions defeat frictionless capture. Skip clarification when the idea is unambiguous. Typical clarifications: bug vs. feature, which rule/file, which template pipeline (Slim/ERB/Phlex).
|
|
21
|
+
|
|
22
|
+
3. **Create the issue** with `gh issue create --title "..." --body "$(cat <<'EOF' ... EOF)"` against the current repo. Do **not** pass `--repo`. Do **not** add labels.
|
|
23
|
+
|
|
24
|
+
4. **Return the issue URL** that `gh issue create` prints. Keep your final reply to one line — just the URL (and a one-line summary if helpful).
|
|
25
|
+
|
|
26
|
+
## Out of scope
|
|
27
|
+
|
|
28
|
+
- Auto-suggesting labels
|
|
29
|
+
- Cross-repo support
|
|
30
|
+
- Duplicate detection / linking related issues
|
|
31
|
+
- Editing or closing existing issues
|
|
32
|
+
|
|
33
|
+
## Style notes
|
|
34
|
+
|
|
35
|
+
- Bias toward **fast capture** over thoroughness — the issue can always be edited on GitHub.
|
|
36
|
+
- Don't pad the body with boilerplate. If "Proposed approach" or "Files likely involved" isn't obvious, leave it out.
|
|
37
|
+
- Match the tone of recent issues in this repo if you can quickly check one.
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.14.1] - 2026-05-03
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- `AnchorMissingAccessibleName` and `ButtonMissingAccessibleName` no longer report Phlex `a`/`button` tags whose accessible text comes from a phlex-rails value helper (`t`, `translate`, `l`, `localize`, `pluralize`, `truncate`, the `number_to_*` / `number_with_*` helpers, `highlight`, `excerpt`) or Phlex's built-in `text`. Previously only `plain` was recognized
|
|
15
|
+
|
|
16
|
+
## [0.14.0] - 2026-04-28
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- **Breaking:** `SimpleFormSelectMissingAccessibleName` is now `SimpleFormInputMissingAccessibleName` and flags any Simple Form `form.input` with `label: false`/`label: ""` that has no `aria-label` or `aria-labelledby` in `input_html`, regardless of input type (WCAG 4.1.2). `as: :hidden` is skipped. Projects that pinned the rule by name in `.a11y-lint.yml` need to update the key
|
|
21
|
+
|
|
10
22
|
## [0.13.0] - 2026-04-28
|
|
11
23
|
|
|
12
24
|
### Added
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Contributing to the a11y-lint docs site
|
|
2
|
+
|
|
3
|
+
## Authoring rule reference pages
|
|
4
|
+
|
|
5
|
+
Rule pages live in `src/rules/`, one Markdown file per WCAG concept. Each rule class is an anchor section within that page.
|
|
6
|
+
|
|
7
|
+
### Showing example code in multiple template languages
|
|
8
|
+
|
|
9
|
+
a11y-lint supports ERB, Slim, and Phlex. When a rule page shows example code, render it through the `Shared::CodeTabs` component so all three pipelines collapse into one tabbed block instead of three stacked code blocks.
|
|
10
|
+
|
|
11
|
+
ERB is required and is the default tab. Slim and Phlex are optional — omit either kwarg and the tab won't render.
|
|
12
|
+
|
|
13
|
+
```erb
|
|
14
|
+
<%= render Shared::CodeTabs.new(
|
|
15
|
+
erb: <<~ERB,
|
|
16
|
+
<img src="hero.jpg" alt="Team celebrating after a product launch">
|
|
17
|
+
ERB
|
|
18
|
+
slim: <<~SLIM,
|
|
19
|
+
img src="hero.jpg" alt="Team celebrating after a product launch"
|
|
20
|
+
SLIM
|
|
21
|
+
phlex: <<~PHLEX,
|
|
22
|
+
img(src: "hero.jpg", alt: "Team celebrating after a product launch")
|
|
23
|
+
PHLEX
|
|
24
|
+
) %>
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The component takes care of:
|
|
28
|
+
|
|
29
|
+
- Rouge syntax highlighting per tab (using the `erb`, `slim`, and `ruby` lexers).
|
|
30
|
+
- WAI-ARIA tab pattern: `role="tablist"` / `role="tab"` / `role="tabpanel"` with full `aria-*` wiring.
|
|
31
|
+
- Keyboard navigation: Left/Right arrows move between tabs, Home/End jump to the first/last tab, Enter/Space activate, Tab moves into the panel.
|
|
32
|
+
- A no-JavaScript fallback that shows the ERB panel only.
|
|
33
|
+
- `prefers-reduced-motion`.
|
|
34
|
+
|
|
35
|
+
Multiple instances on the same page get unique IDs automatically.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
.code-tabs {
|
|
2
|
+
margin: 1.5rem 0;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
.code-tabs__tablist {
|
|
6
|
+
display: none;
|
|
7
|
+
gap: 0.25rem;
|
|
8
|
+
border-bottom: 1px solid #ddd;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.code-tabs.is-enhanced .code-tabs__tablist {
|
|
12
|
+
display: flex;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.code-tabs__tab {
|
|
16
|
+
background: transparent;
|
|
17
|
+
border: 1px solid transparent;
|
|
18
|
+
border-bottom: none;
|
|
19
|
+
border-radius: 4px 4px 0 0;
|
|
20
|
+
padding: 0.5rem 1rem;
|
|
21
|
+
font: inherit;
|
|
22
|
+
font-weight: 600;
|
|
23
|
+
color: var(--body-color);
|
|
24
|
+
cursor: pointer;
|
|
25
|
+
margin-bottom: -1px;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.code-tabs__tab[aria-selected="true"] {
|
|
29
|
+
background: white;
|
|
30
|
+
border-color: #ddd;
|
|
31
|
+
color: var(--heading-color);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.code-tabs__tab:focus-visible,
|
|
35
|
+
.code-tabs__panel:focus-visible {
|
|
36
|
+
outline: 2px solid var(--action-color);
|
|
37
|
+
outline-offset: 2px;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.code-tabs__panel {
|
|
41
|
+
border: 1px solid #ddd;
|
|
42
|
+
border-radius: 0 4px 4px 4px;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.code-tabs.is-enhanced .code-tabs__panel {
|
|
46
|
+
border-top-left-radius: 0;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.code-tabs__panel pre {
|
|
50
|
+
margin: 0;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
@media (prefers-reduced-motion: reduce) {
|
|
54
|
+
.code-tabs__tab {
|
|
55
|
+
transition: none;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<div class="code-tabs">
|
|
2
|
+
<div class="code-tabs__tablist" role="tablist" aria-label="Template language">
|
|
3
|
+
<% tabs.each_with_index do |tab, i| %>
|
|
4
|
+
<button
|
|
5
|
+
type="button"
|
|
6
|
+
role="tab"
|
|
7
|
+
id="<%= tab_id(tab[:key]) %>"
|
|
8
|
+
aria-controls="<%= panel_id(tab[:key]) %>"
|
|
9
|
+
aria-selected="<%= i.zero? %>"
|
|
10
|
+
tabindex="<%= i.zero? ? 0 : -1 %>"
|
|
11
|
+
class="code-tabs__tab"
|
|
12
|
+
>
|
|
13
|
+
<%= tab[:label] %>
|
|
14
|
+
</button>
|
|
15
|
+
<% end %>
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
<% tabs.each_with_index do |tab, i| %>
|
|
19
|
+
<div
|
|
20
|
+
role="tabpanel"
|
|
21
|
+
id="<%= panel_id(tab[:key]) %>"
|
|
22
|
+
aria-labelledby="<%= tab_id(tab[:key]) %>"
|
|
23
|
+
class="code-tabs__panel"
|
|
24
|
+
tabindex="0"
|
|
25
|
+
<%= "hidden" unless i.zero? %>
|
|
26
|
+
>
|
|
27
|
+
<%= raw highlighted(tab[:key], tab[:lexer]) %>
|
|
28
|
+
</div>
|
|
29
|
+
<% end %>
|
|
30
|
+
</div>
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
function initCodeTabs(root) {
|
|
2
|
+
if (root.classList.contains("is-enhanced")) return
|
|
3
|
+
|
|
4
|
+
const tabs = Array.from(root.querySelectorAll('[role="tab"]'))
|
|
5
|
+
const panels = Array.from(root.querySelectorAll('[role="tabpanel"]'))
|
|
6
|
+
if (tabs.length === 0) return
|
|
7
|
+
|
|
8
|
+
root.classList.add("is-enhanced")
|
|
9
|
+
|
|
10
|
+
function activate(index, { focus = true } = {}) {
|
|
11
|
+
tabs.forEach((tab, i) => {
|
|
12
|
+
const selected = i === index
|
|
13
|
+
tab.setAttribute("aria-selected", String(selected))
|
|
14
|
+
tab.setAttribute("tabindex", selected ? "0" : "-1")
|
|
15
|
+
panels[i].hidden = !selected
|
|
16
|
+
})
|
|
17
|
+
if (focus) tabs[index].focus()
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
tabs.forEach((tab, i) => {
|
|
21
|
+
tab.addEventListener("click", () => activate(i))
|
|
22
|
+
tab.addEventListener("keydown", (event) => {
|
|
23
|
+
let next = null
|
|
24
|
+
switch (event.key) {
|
|
25
|
+
case "ArrowRight":
|
|
26
|
+
next = (i + 1) % tabs.length
|
|
27
|
+
break
|
|
28
|
+
case "ArrowLeft":
|
|
29
|
+
next = (i - 1 + tabs.length) % tabs.length
|
|
30
|
+
break
|
|
31
|
+
case "Home":
|
|
32
|
+
next = 0
|
|
33
|
+
break
|
|
34
|
+
case "End":
|
|
35
|
+
next = tabs.length - 1
|
|
36
|
+
break
|
|
37
|
+
default:
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
event.preventDefault()
|
|
41
|
+
activate(next)
|
|
42
|
+
})
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function initAllCodeTabs() {
|
|
47
|
+
document.querySelectorAll(".code-tabs").forEach(initCodeTabs)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (document.readyState === "loading") {
|
|
51
|
+
document.addEventListener("DOMContentLoaded", initAllCodeTabs)
|
|
52
|
+
} else {
|
|
53
|
+
initAllCodeTabs()
|
|
54
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
require "rouge"
|
|
2
|
+
require "securerandom"
|
|
3
|
+
|
|
4
|
+
class Shared::CodeTabs < Bridgetown::Component
|
|
5
|
+
TABS = [
|
|
6
|
+
{ key: :erb, label: "ERB", lexer: "erb" },
|
|
7
|
+
{ key: :slim, label: "Slim", lexer: "slim" },
|
|
8
|
+
{ key: :phlex, label: "Phlex", lexer: "ruby" }
|
|
9
|
+
].freeze
|
|
10
|
+
|
|
11
|
+
attr_reader :id
|
|
12
|
+
|
|
13
|
+
def initialize(erb:, slim: nil, phlex: nil)
|
|
14
|
+
@samples = { erb: erb, slim: slim, phlex: phlex }
|
|
15
|
+
@id = "code-tabs-#{SecureRandom.hex(4)}"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def tabs
|
|
19
|
+
TABS.select { |tab| @samples[tab[:key]] }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def tab_id(key)
|
|
23
|
+
"#{id}-tab-#{key}"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def panel_id(key)
|
|
27
|
+
"#{id}-panel-#{key}"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def highlighted(key, lexer_name)
|
|
31
|
+
formatter = Rouge::Formatters::HTML.new
|
|
32
|
+
lexer = Rouge::Lexer.find(lexer_name) || Rouge::Lexers::PlainText.new
|
|
33
|
+
inner = formatter.format(lexer.lex(@samples[key].to_s))
|
|
34
|
+
%(<pre class="highlight"><code>#{inner}</code></pre>)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -29,15 +29,19 @@ In Simple Form, both go inside `input_html`:
|
|
|
29
29
|
|
|
30
30
|
A placeholder is **not** a label. It disappears as soon as the user types, browsers don't reliably expose it as the accessible name, and it fails users who rely on the prompt staying visible while they fill the field.
|
|
31
31
|
|
|
32
|
-
##
|
|
33
|
-
{:#simple-form-
|
|
32
|
+
## SimpleFormInputMissingAccessibleName
|
|
33
|
+
{:#simple-form-input-missing-accessible-name}
|
|
34
34
|
|
|
35
|
-
Applies to: Simple Form `form.input` calls
|
|
35
|
+
Applies to: Simple Form `form.input` calls with `label: false` or `label: ""` — regardless of input type. `as: :hidden` is skipped, since hidden inputs don't render a visible control.
|
|
36
36
|
|
|
37
37
|
The rule passes when `input_html` provides `aria-label` or `aria-labelledby`. Both hash and string keys are accepted (`aria: { label: "..." }` and `"aria-label" => "..."`).
|
|
38
38
|
|
|
39
39
|
### Bad
|
|
40
40
|
|
|
41
|
+
```erb
|
|
42
|
+
<%%= form.input :name, label: false %>
|
|
43
|
+
```
|
|
44
|
+
|
|
41
45
|
```erb
|
|
42
46
|
<%%= form.input :sort_by, collection: opts, label: false %>
|
|
43
47
|
```
|
|
@@ -48,17 +52,18 @@ The rule passes when `input_html` provides `aria-label` or `aria-labelledby`. Bo
|
|
|
48
52
|
|
|
49
53
|
### Good
|
|
50
54
|
|
|
51
|
-
Render a visible label by removing `label: false
|
|
55
|
+
Render a visible label by removing `label: false` and letting Simple Form generate one from the attribute name, or by passing an explicit string:
|
|
52
56
|
|
|
53
57
|
```erb
|
|
58
|
+
<%%= form.input :name %>
|
|
54
59
|
<%%= form.input :sort_by, collection: opts, label: "Sort by" %>
|
|
55
60
|
```
|
|
56
61
|
|
|
57
62
|
Or, when there's no visible label, supply `aria-label`:
|
|
58
63
|
|
|
59
64
|
```erb
|
|
60
|
-
<%%= form.input :
|
|
61
|
-
input_html: { aria: { label: "
|
|
65
|
+
<%%= form.input :name, label: false,
|
|
66
|
+
input_html: { aria: { label: "Name" } } %>
|
|
62
67
|
```
|
|
63
68
|
|
|
64
69
|
Or `aria-labelledby` pointing to existing visible text:
|
|
@@ -69,19 +74,25 @@ Or `aria-labelledby` pointing to existing visible text:
|
|
|
69
74
|
input_html: { aria: { labelledby: "sort-label" } } %>
|
|
70
75
|
```
|
|
71
76
|
|
|
77
|
+
`as: :hidden` doesn't render a visible control, so it's exempt:
|
|
78
|
+
|
|
79
|
+
```erb
|
|
80
|
+
<%%= form.input :secret, as: :hidden, label: false %>
|
|
81
|
+
```
|
|
82
|
+
|
|
72
83
|
### Slim equivalent
|
|
73
84
|
|
|
74
85
|
```slim
|
|
75
|
-
= form.input :
|
|
86
|
+
= form.input :name, label: false, input_html: { aria: { label: "Name" } }
|
|
76
87
|
```
|
|
77
88
|
|
|
78
89
|
## What this rule doesn't catch
|
|
79
90
|
|
|
80
91
|
This rule has a deliberately narrow scope. Things it doesn't flag:
|
|
81
92
|
|
|
82
|
-
- **Plain
|
|
93
|
+
- **Plain HTML form controls.** The rule targets Simple Form's `form.input` helper. A bare `<input>`, `<select>`, or `<textarea>` with no associated `<label>` is not reported.
|
|
83
94
|
- **Other Simple Form helpers.** `form.select`, `form.association`, and similar helpers aren't checked — only `form.input`.
|
|
84
|
-
- **
|
|
95
|
+
- **Inputs without `label: false`.** Simple Form auto-generates a label from the attribute name, so `form.input :name` is treated as having a visible label. The rule only kicks in once the label is explicitly suppressed.
|
|
85
96
|
- **Phlex views.** The rule is a no-op in Phlex by design. The Phlex pipeline only walks receiverless calls, so `form.input(...)` isn't surfaced as a candidate node. Simple Form code embedded inside a Phlex component won't be checked.
|
|
86
97
|
- **Bad accessible name content.** `aria: { label: "" }` will pass the rule check above (an empty aria-label is its own problem, but not this rule's). `aria: { label: "select" }` will pass. The linter checks that an aria attribute is present, not that the value is useful.
|
|
87
98
|
- **Dynamic accessible names.** `aria: { label: variable }` satisfies the rule even if `variable` is `nil` at runtime.
|
|
@@ -67,15 +67,31 @@ Applies to: HTML `<img>` elements.
|
|
|
67
67
|
|
|
68
68
|
### Bad
|
|
69
69
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
70
|
+
<%= render Shared::CodeTabs.new(
|
|
71
|
+
erb: <<~ERB,
|
|
72
|
+
<img src="hero.jpg">
|
|
73
|
+
ERB
|
|
74
|
+
slim: <<~SLIM,
|
|
75
|
+
img src="hero.jpg"
|
|
76
|
+
SLIM
|
|
77
|
+
phlex: <<~PHLEX,
|
|
78
|
+
img(src: "hero.jpg")
|
|
79
|
+
PHLEX
|
|
80
|
+
) %>
|
|
73
81
|
|
|
74
82
|
### Good
|
|
75
83
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
84
|
+
<%= render Shared::CodeTabs.new(
|
|
85
|
+
erb: <<~ERB,
|
|
86
|
+
<img src="hero.jpg" alt="Team celebrating after a product launch">
|
|
87
|
+
ERB
|
|
88
|
+
slim: <<~SLIM,
|
|
89
|
+
img src="hero.jpg" alt="Team celebrating after a product launch"
|
|
90
|
+
SLIM
|
|
91
|
+
phlex: <<~PHLEX,
|
|
92
|
+
img(src: "hero.jpg", alt: "Team celebrating after a product launch")
|
|
93
|
+
PHLEX
|
|
94
|
+
) %>
|
|
79
95
|
|
|
80
96
|
For a decorative image, use an empty `alt` so assistive tech skips it:
|
|
81
97
|
|
|
@@ -9,6 +9,19 @@ module A11y
|
|
|
9
9
|
class PhlexRunner
|
|
10
10
|
PHLEX_PATTERN = /\bdef\s+view_template\b/
|
|
11
11
|
|
|
12
|
+
# Phlex auto-emits the return value of these calls into the document:
|
|
13
|
+
# `plain` / `text` are built-in; the rest are registered as value
|
|
14
|
+
# helpers by phlex-rails via `register_value_helper`.
|
|
15
|
+
TEXT_CALLS = %w[
|
|
16
|
+
plain text
|
|
17
|
+
t translate l localize
|
|
18
|
+
pluralize truncate
|
|
19
|
+
number_to_currency number_to_human number_to_human_size
|
|
20
|
+
number_to_percentage number_to_phone
|
|
21
|
+
number_with_delimiter number_with_precision
|
|
22
|
+
highlight excerpt
|
|
23
|
+
].to_set.freeze
|
|
24
|
+
|
|
12
25
|
def initialize(rules = nil, configuration: Configuration.new)
|
|
13
26
|
@rules = rules || configuration.enabled_rules
|
|
14
27
|
@configuration = configuration
|
|
@@ -161,7 +174,7 @@ module A11y
|
|
|
161
174
|
end
|
|
162
175
|
|
|
163
176
|
def text_call?(node)
|
|
164
|
-
receiverless_call?(node) && node.name.to_s
|
|
177
|
+
receiverless_call?(node) && TEXT_CALLS.include?(node.name.to_s)
|
|
165
178
|
end
|
|
166
179
|
|
|
167
180
|
def receiverless_call?(node)
|
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
module A11y
|
|
4
4
|
module Lint
|
|
5
5
|
module Rules
|
|
6
|
-
# Checks that Simple Form `form.input` calls
|
|
7
|
-
# (via `collection:` or `as: :select`) with `label: false` /
|
|
6
|
+
# Checks that Simple Form `form.input` calls with `label: false` /
|
|
8
7
|
# `label: ""` provide an accessible name through `input_html:`
|
|
9
|
-
# `aria-label` or `aria-labelledby` (WCAG 4.1.2).
|
|
10
|
-
|
|
8
|
+
# `aria-label` or `aria-labelledby` (WCAG 4.1.2). Skips
|
|
9
|
+
# `as: :hidden`, which doesn't render a visible control.
|
|
10
|
+
class SimpleFormInputMissingAccessibleName < NodeRule
|
|
11
11
|
def check
|
|
12
12
|
return if no_offense?
|
|
13
13
|
|
|
@@ -18,14 +18,13 @@ module A11y
|
|
|
18
18
|
|
|
19
19
|
def no_offense?
|
|
20
20
|
!helper_call ||
|
|
21
|
-
!select_like? ||
|
|
22
21
|
!helper_call.label_hidden? ||
|
|
22
|
+
hidden_input? ||
|
|
23
23
|
aria_label_in_input_html?
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
-
def
|
|
27
|
-
helper_call.
|
|
28
|
-
helper_call.keyword_symbol?(:as, :select)
|
|
26
|
+
def hidden_input?
|
|
27
|
+
helper_call.keyword_symbol?(:as, :hidden)
|
|
29
28
|
end
|
|
30
29
|
|
|
31
30
|
def aria_label_in_input_html?
|
|
@@ -53,7 +52,7 @@ module A11y
|
|
|
53
52
|
|
|
54
53
|
def offense_message
|
|
55
54
|
<<~MSG.strip
|
|
56
|
-
form.input
|
|
55
|
+
form.input missing an accessible name \
|
|
57
56
|
requires aria-label or aria-labelledby in input_html (WCAG 4.1.2)
|
|
58
57
|
MSG
|
|
59
58
|
end
|
data/lib/a11y/lint/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: a11y-lint
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.14.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Abdullah Hashim
|
|
@@ -45,6 +45,7 @@ executables:
|
|
|
45
45
|
extensions: []
|
|
46
46
|
extra_rdoc_files: []
|
|
47
47
|
files:
|
|
48
|
+
- ".claude/commands/task.md"
|
|
48
49
|
- ".claude/rules/testing.md"
|
|
49
50
|
- ".rubocop.yml"
|
|
50
51
|
- CHANGELOG.md
|
|
@@ -55,6 +56,7 @@ files:
|
|
|
55
56
|
- Rakefile
|
|
56
57
|
- docs/.gitignore
|
|
57
58
|
- docs/.ruby-version
|
|
59
|
+
- docs/CONTRIBUTING.md
|
|
58
60
|
- docs/Gemfile
|
|
59
61
|
- docs/Gemfile.lock
|
|
60
62
|
- docs/README.md
|
|
@@ -80,6 +82,10 @@ files:
|
|
|
80
82
|
- docs/src/404.html
|
|
81
83
|
- docs/src/500.html
|
|
82
84
|
- docs/src/CNAME
|
|
85
|
+
- docs/src/_components/shared/code_tabs.css
|
|
86
|
+
- docs/src/_components/shared/code_tabs.erb
|
|
87
|
+
- docs/src/_components/shared/code_tabs.js
|
|
88
|
+
- docs/src/_components/shared/code_tabs.rb
|
|
83
89
|
- docs/src/_components/shared/navbar.erb
|
|
84
90
|
- docs/src/_components/shared/navbar.rb
|
|
85
91
|
- docs/src/_data/site_metadata.yml
|
|
@@ -124,7 +130,7 @@ files:
|
|
|
124
130
|
- lib/a11y/lint/rules/robust/button_missing_accessible_name.rb
|
|
125
131
|
- lib/a11y/lint/rules/robust/button_tag_missing_accessible_name.rb
|
|
126
132
|
- lib/a11y/lint/rules/robust/link_to_missing_accessible_name.rb
|
|
127
|
-
- lib/a11y/lint/rules/robust/
|
|
133
|
+
- lib/a11y/lint/rules/robust/simple_form_input_missing_accessible_name.rb
|
|
128
134
|
- lib/a11y/lint/slim_node.rb
|
|
129
135
|
- lib/a11y/lint/slim_runner.rb
|
|
130
136
|
- lib/a11y/lint/version.rb
|