feat: 4 new LiveView rules (#102, #103, #105, #106)#109
Merged
Conversation
Builds on the LiveViewRules framework shipped in 0.13.0 by adding four new rules that catch Phoenix-specific a11y anti-patterns axe-core can't see. Each rule is a single module in lib/excessibility/live_view_rules/rules/, auto-discovered at compile time. - :toggle_missing_aria_state (#102) Parses the serialized JSON in phx-click to detect JS.toggle/show/hide operations; flags triggers that have no aria-expanded. Surfaces the target id from the JS command in the finding message. - :click_away_without_escape (#103) Flags phx-click-away without a matching phx-window-keydown or phx-keydown paired with phx-key="Escape". role="dialog" / role="alertdialog" are accepted as evidence of keyboard dismissal. - :debounce_without_live_region (#105) Conservative heuristic: flags <input phx-debounce> only when the whole snapshot has zero aria-live / role=status|alert|log regions. Avoids false positives when an app announces updates somewhere else in the page. - :hidden_form_control_without_aria (#106) Flags <input type="checkbox|radio" class="hidden|sr-only"> when the wrapping <label> doesn't expose state via aria-checked, aria-pressed, role="checkbox" or role="radio". Also registers discovered rule files as @external_resource so edits to existing rules trigger recompilation of the scanner module. (Adding a brand-new rule file still requires touching the scanner or running mix clean — documented in CHANGELOG.) The #104 cross-snapshot diffing rule is deliberately left for a follow-up PR because it requires a different rule protocol (two snapshots instead of one HTML) and new snapshot-pairing logic in mix excessibility. Version stays at 0.13.0 (not yet published to Hex). Closes #102 Closes #103 Closes #105 Closes #106 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Builds on the
LiveViewRulesframework shipped in #108 by addingfour new rules that catch Phoenix-specific a11y anti-patterns
axe-core can't see. Each rule is an isolated module under
lib/excessibility/live_view_rules/rules/and is auto-discoveredat compile time.
Version stays at 0.13.0 — the previous PR's version is not yet
published to Hex, so these additions fold into the same release.
Rules added
:toggle_missing_aria_state(closes #102)Parses the serialized JSON in
phx-clickto detectJS.toggle/show/hideoperations, and flags triggers that have noaria-expanded. The target id from the JS command surfaces in thefinding message.
:click_away_without_escape(closes #103)Flags
phx-click-awaywithout a matchingphx-window-keydown/phx-keydown+phx-key=\"Escape\"on the same element.role=\"dialog\"androle=\"alertdialog\"are accepted asevidence of keyboard dismissal.
:debounce_without_live_region(closes #105)Conservative heuristic: flags
<input phx-debounce>only when thewhole snapshot has zero
aria-live/role=\"status\"/role=\"alert\"/role=\"log\"regions. This avoids falsepositives when the app announces updates in a live region that
isn't directly adjacent to the input.
:hidden_form_control_without_aria(closes #106)Flags
<input type=\"checkbox|radio\" class=\"hidden|sr-only\">when the wrapping
<label>(resolved viafor=or structuralcontainment) doesn't expose checked state via
aria-checked,aria-pressed,role=\"checkbox\", orrole=\"radio\".Framework fix
Drive-by:
Excessibility.LiveViewRulesnow registers everydiscovered rule file as
@external_resource. Without this, editingan existing rule wouldn't trigger recompilation of the scanner
module and the edit would silently not take effect (I hit this
while writing the new rules). Adding a brand-new rule file still
requires
mix cleanor touching the scanner module — that'sdocumented in CHANGELOG and is a known Elixir limitation of
compile-time file globs.
Deferred
#104 cross-snapshot diffing is intentionally not in this PR.
It needs:
check(prev_tree, curr_tree, opts)instead of
check(tree, opts)mix excessibility(before/after pairsfrom the same test interaction)
represents an interaction"
It's worth its own focused PR where we can design the cross-
snapshot protocol cleanly rather than wedging it into the single-
snapshot framework.
Test plan
mix test test/live_view_rules/— 48 rule tests passingmix test— 637 tests passingmix format --check-formattedMIX_ENV=test mix credo --strict— zero issuesmix compile --warnings-as-errors(e.g. topps_pro's `pill_dropdown`) to confirm rule hit rate
matches expectations
🤖 Generated with Claude Code