a11y-lint 0.14.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 +6 -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/images-need-alt-text.md +22 -6
- data/lib/a11y/lint/phlex_runner.rb +14 -1
- data/lib/a11y/lint/version.rb +1 -1
- metadata +7 -1
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,12 @@ 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
|
+
|
|
10
16
|
## [0.14.0] - 2026-04-28
|
|
11
17
|
|
|
12
18
|
### Changed
|
|
@@ -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
|
|
@@ -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)
|
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.14.
|
|
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
|