Skip to content

feat: add template-mouse-events-have-key-events#18

Open
johanrd wants to merge 2 commits intomasterfrom
feat/template-mouse-events-have-key-events
Open

feat: add template-mouse-events-have-key-events#18
johanrd wants to merge 2 commits intomasterfrom
feat/template-mouse-events-have-key-events

Conversation

@johanrd
Copy link
Copy Markdown
Owner

@johanrd johanrd commented Apr 21, 2026

Note

This is part of a series where Claude has audited eslint-plugin-ember against jsx-a11y, vuejs-accessibility, angular-eslint, lit-a11y and html-validate, ember-template-lint, and the HTML and WCAG specs.

Summary

  • Premise 1: Mouse hover events (mouseover/mouseenter/mouseout/mouseleave) only fire for pointer input; keyboard-only users can't trigger them. Pointer-only UI transitions (hover-revealed tooltips, highlight effects, etc.) violate WCAG 2.1 SC 2.1.1 Keyboard (Level A) in practice. Strictly, SC 2.1.1 accepts any keyboard path; the specific "pair hover-in with focus, hover-out with blur" heuristic this rule checks is convention — sourced from WAI-ARIA APG keyboard-interaction guidance (authoring guidance, non-normative) and from all four peer a11y plugins adopting the pattern as the strongest static-analysis proxy. (Many simple hover effects are cleaner handled via CSS :hover + :focus than paired JS handlers — see Inclusive Components: Tooltips.)
  • Premise 2: This concept is covered by all four peer a11y plugins (verified in source — see Prior art) but not by our plugin today.
  • Conclusion: Add template-mouse-events-have-key-events. By default flags {{on "mouseover" …}} without a paired focus listener, and {{on "mouseout" …}} without a paired blur listener — matching jsx-a11y / @angular-eslint/template / lit-a11y defaults. {{on "mouseenter" …}} and {{on "mouseleave" …}} are opt-in via hoverInHandlers / hoverOutHandlers config options. (Rationale for opting them out of default: they don't bubble, are often chosen specifically for per-element hover effects expressible via CSS :hover + :focus, and flagging them by default produces noisy false positives on common authoring patterns.)

Flags

<div {{on "mouseover" this.showTooltip}}></div>
<div {{on "mouseleave" this.hideTooltip}}></div>

Allows

<div {{on "mouseover" this.showTooltip}} {{on "focus" this.showTooltip}}></div>
<div {{on "mouseleave" this.hideTooltip}} {{on "focusout" this.hideTooltip}}></div>

Prior art

Verified each peer in source:

Plugin Rule hover-in set hover-out set accepted pairs
jsx-a11y mouse-events-have-key-events ['onMouseOver'] (configurable) ['onMouseOut'] (configurable) onFocus/onBlur only
vuejs-accessibility mouse-events-have-key-events mouseover, mouseenter, hover (hardcoded) mouseout, mouseleave (hardcoded) focus/focusin and blur/focusout
lit-a11y mouse-events-have-key-events @mouseover only @mouseout only @focus/@blur only
@angular-eslint/template mouse-events-have-key-events mouseover only mouseout only focus/blur only

Our defaults now match jsx-a11y / @angular-eslint/template / lit-a11y (mouseover / mouseout only). mouseenter / mouseleave are opt-in via config. Accepted focus/blur pairings accept both bubbling (focus/blur) and non-bubbling (focusin/focusout) variants — a slight superset of lit-a11y + @angular-eslint/template on the focus side (they accept only focus/blur). vuejs-accessibility hardcodes the wider mouseenter+mouseleave set (no config); we diverge.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 21, 2026

🏎️ Benchmark Comparison

Benchmark Control (p50) Experiment (p50) Δ
js small 13.93 ms 13.87 ms -0.4%
🟢 js medium 6.99 ms 6.78 ms -3.0%
🟢 js large 2.80 ms 2.74 ms -2.3%
gjs small 1.25 ms 1.24 ms -0.8%
gjs medium 625.18 µs 621.37 µs -0.6%
gjs large 246.29 µs 244.56 µs -0.7%
gts small 1.25 ms 1.24 ms -0.8%
gts medium 625.27 µs 620.44 µs -0.8%
gts large 244.89 µs 248.26 µs +1.4%

🟢 faster · 🔴 slower · 🟠 slightly slower · ⚪ within 2%

Full mitata output
clk: ~3.09 GHz
cpu: AMD EPYC 7763 64-Core Processor
runtime: node 24.15.0 (x64-linux)

benchmark                   avg (min … max) p75 / p99    (min … top 1%)
------------------------------------------- -------------------------------
js small (control)            16.63 ms/iter  17.50 ms █                    
                      (12.12 ms … 31.01 ms)  30.41 ms █▂▅ ▂                
                    (  5.65 mb …  10.37 mb)   7.27 mb ███▆█▃▆▃▁▁▁▃▃▃▁▆▁▃▁▁▃

js small (experiment)         14.47 ms/iter  15.19 ms    ██                
                      (12.55 ms … 20.91 ms)  18.45 ms ▅▅████               
                    (  6.75 mb …   7.71 mb)   6.84 mb ██████▅▅██▅██▅▅▅▅▁▁▁▅

                             ┌                                            ┐
                             ╷┌─────────┬─┐                               ╷
          js small (control) ├┤         │ ├───────────────────────────────┤
                             ╵└─────────┴─┘                               ╵
                              ╷ ┌──┬─┐       ╷
       js small (experiment)  ├─┤  │ ├───────┤
                              ╵ └──┴─┘       ╵
                             └                                            ┘
                             12.12 ms           21.26 ms           30.41 ms

summary
  js small (experiment)
   1.15x faster than js small (control)

------------------------------------------- -------------------------------
js medium (control)            7.72 ms/iter   7.99 ms  █                   
                       (6.60 ms … 14.57 ms)  14.46 ms ██                   
                    (  2.71 mb …   4.37 mb)   3.53 mb ██▅▄▅▅▂▃▂▂▁▂▁▁▁▁▂▁▁▁▂

js medium (experiment)         7.40 ms/iter   7.46 ms  █                   
                       (6.42 ms … 14.15 ms)  13.09 ms ▂█                   
                    (  2.83 mb …   4.14 mb)   3.52 mb ██▅▆▃▃▂▃▂▂▂▁▂▂▁▁▂▁▁▁▂

                             ┌                                            ┐
                              ╷┌────┬─┐                                   ╷
         js medium (control)  ├┤    │ ├───────────────────────────────────┤
                              ╵└────┴─┘                                   ╵
                             ╷┌───┬┐                              ╷
      js medium (experiment) ├┤   │├──────────────────────────────┤
                             ╵└───┴┘                              ╵
                             └                                            ┘
                             6.42 ms           10.44 ms            14.46 ms

summary
  js medium (experiment)
   1.04x faster than js medium (control)

------------------------------------------- -------------------------------
js large (control)             3.27 ms/iter   3.03 ms  █                   
                       (2.43 ms … 12.24 ms)   8.53 ms  █                   
                    (323.24 kb …   2.63 mb)   1.44 mb ██▅▃▃▂▁▂▁▂▁▁▁▁▁▁▁▁▁▁▁

js large (experiment)          2.99 ms/iter   2.83 ms  █                   
                        (2.53 ms … 7.62 ms)   6.15 ms ▆█                   
                    (311.23 kb …   2.55 mb)   1.43 mb ██▅▁▃▂▂▂▂▁▁▁▁▁▁▂▁▁▁▁▁

                             ┌                                            ┐
                             ╷ ┌───┬                                      ╷
          js large (control) ├─┤   │──────────────────────────────────────┤
                             ╵ └───┴                                      ╵
                              ┌──┬                      ╷
       js large (experiment)  │  │──────────────────────┤
                              └──┴                      ╵
                             └                                            ┘
                             2.43 ms            5.48 ms             8.53 ms

summary
  js large (experiment)
   1.09x faster than js large (control)

------------------------------------------- -------------------------------
gjs small (control)            1.40 ms/iter   1.34 ms █                    
                        (1.21 ms … 7.01 ms)   5.86 ms █                    
                    (290.48 kb …   1.69 mb)   1.06 mb ██▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

gjs small (experiment)         1.36 ms/iter   1.27 ms █                    
                        (1.21 ms … 6.15 ms)   4.67 ms █                    
                    (402.20 kb …   1.60 mb)   1.06 mb █▅▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

                             ┌                                            ┐
                             ┌─┬                                          ╷
         gjs small (control) │ │──────────────────────────────────────────┤
                             └─┴                                          ╵
                             ┌─┬                              ╷
      gjs small (experiment) │ │──────────────────────────────┤
                             └─┴                              ╵
                             └                                            ┘
                             1.21 ms            3.53 ms             5.86 ms

summary
  gjs small (experiment)
   1.03x faster than gjs small (control)

------------------------------------------- -------------------------------
gjs medium (control)         674.11 µs/iter 641.13 µs ▄█                   
                      (595.84 µs … 5.41 ms)   1.69 ms ██                   
                    (  2.77 kb …   1.23 mb) 541.38 kb ██▃▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

gjs medium (experiment)      674.95 µs/iter 638.67 µs █                    
                      (591.52 µs … 5.66 ms)   2.85 ms █                    
                    ( 75.13 kb …   1.08 mb) 540.84 kb █▄▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

                             ┌                                            ┐
                             ┌─┬                   ╷
        gjs medium (control) │ │───────────────────┤
                             └─┴                   ╵
                             ┌─┬                                          ╷
     gjs medium (experiment) │ │──────────────────────────────────────────┤
                             └─┴                                          ╵
                             └                                            ┘
                             591.52 µs           1.72 ms            2.85 ms

summary
  gjs medium (control)
   1x faster than gjs medium (experiment)

------------------------------------------- -------------------------------
gjs large (control)          269.94 µs/iter 262.35 µs  █                   
                      (235.10 µs … 5.34 ms) 347.21 µs  █▇                  
                    (170.98 kb … 667.59 kb) 217.21 kb ▇███▃█▅▅▂▁▂▁▁▁▁▁▁▁▁▁▁

gjs large (experiment)       278.93 µs/iter 263.15 µs █▅                   
                      (234.89 µs … 5.80 ms) 509.25 µs ██▃                  
                    (143.75 kb … 739.04 kb) 216.84 kb ███▅▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

                             ┌                                            ┐
                             ╷┌────┬           ╷
         gjs large (control) ├┤    │───────────┤
                             ╵└────┴           ╵
                             ╷┌─────┬                                     ╷
      gjs large (experiment) ├┤     │─────────────────────────────────────┤
                             ╵└─────┴                                     ╵
                             └                                            ┘
                             234.89 µs         372.07 µs          509.25 µs

summary
  gjs large (control)
   1.03x faster than gjs large (experiment)

------------------------------------------- -------------------------------
gts small (control)            1.35 ms/iter   1.27 ms █                    
                        (1.21 ms … 6.71 ms)   5.74 ms █                    
                    (481.06 kb …   1.66 mb)   1.06 mb █▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

gts small (experiment)         1.32 ms/iter   1.26 ms █                    
                        (1.20 ms … 6.19 ms)   4.98 ms █                    
                    (355.77 kb …   1.78 mb)   1.05 mb █▃▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

                             ┌                                            ┐
                             ┌┬                                           ╷
         gts small (control) ││───────────────────────────────────────────┤
                             └┴                                           ╵
                             ┌┬                                   ╷
      gts small (experiment) ││───────────────────────────────────┤
                             └┴                                   ╵
                             └                                            ┘
                             1.20 ms            3.47 ms             5.74 ms

summary
  gts small (experiment)
   1.02x faster than gts small (control)

------------------------------------------- -------------------------------
gts medium (control)         681.62 µs/iter 643.98 µs  █                   
                      (593.75 µs … 5.77 ms)   1.33 ms ▃█                   
                    (488.51 kb … 985.85 kb) 541.81 kb ██▅▃▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

gts medium (experiment)      666.18 µs/iter 636.44 µs  █                   
                      (590.93 µs … 5.86 ms)   1.31 ms  █                   
                    (106.58 kb …   1.14 mb) 540.35 kb ██▄▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

                             ┌                                            ┐
                             ╷┌────┬                                      ╷
        gts medium (control) ├┤    │──────────────────────────────────────┤
                             ╵└────┴                                      ╵
                             ╷┌───┬                                      ╷
     gts medium (experiment) ├┤   │──────────────────────────────────────┤
                             ╵└───┴                                      ╵
                             └                                            ┘
                             590.93 µs          958.46 µs           1.33 ms

summary
  gts medium (experiment)
   1.02x faster than gts medium (control)

------------------------------------------- -------------------------------
gts large (control)          269.00 µs/iter 262.45 µs  █                   
                      (234.82 µs … 4.89 ms) 357.90 µs  █▅                  
                    (216.09 kb … 808.88 kb) 216.89 kb ▇██▄▄█▅▂▂▂▁▁▁▁▁▁▁▁▁▁▁

gts large (experiment)       270.64 µs/iter 264.34 µs  █▃                  
                      (237.62 µs … 5.01 ms) 355.44 µs  ██                  
                    (170.17 kb … 660.73 kb) 216.62 kb ███▄▄█▆▄▁▂▁▁▁▁▁▁▁▁▁▁▁

                             ┌                                            ┐
                             ╷ ┌─────────┬                                ╷
         gts large (control) ├─┤         │────────────────────────────────┤
                             ╵ └─────────┴                                ╵
                              ╷ ┌─────────┬                              ╷
      gts large (experiment)  ├─┤         │──────────────────────────────┤
                              ╵ └─────────┴                              ╵
                             └                                            ┘
                             234.82 µs         296.36 µs          357.90 µs

summary
  gts large (control)
   1.01x faster than gts large (experiment)

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new accessibility-focused template rule to enforce keyboard equivalents for hover-driven UI behavior by requiring {{on "mouseover"}}/{{on "mouseout"}} to be paired with appropriate focus/blur handlers (with opt-in support for mouseenter/mouseleave via configuration).

Changes:

  • Introduces ember/template-mouse-events-have-key-events rule implementation with configurable hover-in/out handler lists.
  • Adds comprehensive RuleTester coverage for both .gjs/.gts template blocks and raw .hbs.
  • Documents the rule and surfaces it in the README rules list (plus an additional peer-parity audit fixture).

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
lib/rules/template-mouse-events-have-key-events.js Implements the new rule and options schema/messages.
tests/lib/rules/template-mouse-events-have-key-events.js Adds primary unit tests covering defaults and configuration behavior.
tests/audit/mouse-events-have-key-events/peer-parity.js Adds parity/audit test cases translated from peer plugins.
docs/rules/template-mouse-events-have-key-events.md Adds rule documentation, rationale, examples, and options.
README.md Adds the new rule entry to the auto-generated rules table.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread docs/rules/template-mouse-events-have-key-events.md
Comment thread tests/audit/mouse-events-have-key-events/peer-parity.js Outdated
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread docs/rules/template-mouse-events-have-key-events.md
Comment thread lib/rules/template-mouse-events-have-key-events.js Outdated
@johanrd johanrd force-pushed the feat/template-mouse-events-have-key-events branch from 78c675e to dafb559 Compare April 22, 2026 17:15
johanrd added a commit that referenced this pull request Apr 23, 2026
@johanrd johanrd requested a review from Copilot April 24, 2026 08:36
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread tests/lib/rules/template-mouse-events-have-key-events.js
johanrd added a commit that referenced this pull request Apr 24, 2026
@johanrd johanrd requested a review from Copilot April 24, 2026 13:40
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread tests/lib/rules/template-mouse-events-have-key-events.js
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@johanrd johanrd requested a review from Copilot April 24, 2026 18:53
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

johanrd added a commit that referenced this pull request Apr 27, 2026
@johanrd johanrd force-pushed the feat/template-mouse-events-have-key-events branch from 5c7ffa0 to 41311a6 Compare April 27, 2026 18:42
@johanrd johanrd force-pushed the feat/template-mouse-events-have-key-events branch from 41311a6 to 006b5b9 Compare April 27, 2026 19:26
@johanrd johanrd requested a review from Copilot April 27, 2026 19:33
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 5 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@@ -0,0 +1,121 @@
'use strict';

const { dom } = require('aria-query');
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The aria-query dom import isn't needed for this rule’s element filtering. isNativeElement() already excludes components, scope-shadowed tags, and custom elements; additionally, aria-query’s dom map is an ARIA mapping (and may not include all native SVG/MathML tags), so using it here can create false negatives. Consider removing the aria-query dependency from this rule and relying solely on isNativeElement() for the element gate.

Suggested change
const { dom } = require('aria-query');

Copilot uses AI. Check for mistakes.
Comment on lines +91 to +93
if (!dom.has(node.tag.toLowerCase())) {
return;
}
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This dom.has(node.tag.toLowerCase()) check is redundant with isNativeElement() (custom elements and non-native tags are already excluded). Keeping both gates also restricts the rule to the intersection of the two tag sets, which may unintentionally skip some native elements. Consider removing this check.

Copilot uses AI. Check for mistakes.
// Component — not a DOM element.
'<template><CustomCard {{on "mouseover" this.onHover}} /></template>',

// Custom element — not in aria-query's dom map.
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment attributes skipping <my-card> to aria-query’s dom map, but the rule actually skips it earlier via isNativeElement() (which returns false for custom elements). Updating the comment would avoid misleading future maintainers about why the case is valid.

Suggested change
// Custom element — not in aria-query's dom map.
// Custom element — skipped because `isNativeElement()` returns false.

Copilot uses AI. Check for mistakes.
Comment on lines +63 to +66
// Custom (dasherized) element — also not in aria-query's dom map,
// so our rule skips. lit-a11y has `allowCustomElements`/`allowList`
// options; we don't support that, but the default behavior aligns
// (no flag on unknown tags).
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment says the rule skips custom (dasherized) elements because they aren’t in aria-query’s dom map, but the implementation skips them via isNativeElement() before consulting dom. Consider rewording to reflect the actual guard condition so the audit stays accurate.

Suggested change
// Custom (dasherized) element — also not in aria-query's dom map,
// so our rule skips. lit-a11y has `allowCustomElements`/`allowList`
// options; we don't support that, but the default behavior aligns
// (no flag on unknown tags).
// Custom (dasherized) element — our rule skips because it fails the
// native-element guard (`isNativeElement()`), so it is not checked.
// lit-a11y has `allowCustomElements`/`allowList` options; we don't
// support that, but the default behavior aligns (no flag on unknown tags).

Copilot uses AI. Check for mistakes.
Comment on lines +22 to +25
// Hover-out paired with blur.
'<template><div {{on "mouseout" this.onLeave}} {{on "blur" this.onLeave}}></div></template>',
'<template><div {{on "mouseleave" this.onLeave}} {{on "focusout" this.onLeave}}></div></template>',

Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rule explicitly treats focusout as a valid hover-out pairing (FOCUS_OUT_EVENTS includes it), but the unit tests don’t currently include a passing case for {{on "mouseout" …}} paired with {{on "focusout" …}} (the only focusout example uses mouseleave, which is not checked by default). Adding a valid test for mouseout+focusout would cover this documented behavior.

Copilot uses AI. Check for mistakes.
… comments; add mouseout+focusout pairing case
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants