feat: add template-mouse-events-have-key-events#18
Conversation
🏎️ Benchmark Comparison
Full mitata output |
There was a problem hiding this comment.
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-eventsrule implementation with configurable hover-in/out handler lists. - Adds comprehensive RuleTester coverage for both
.gjs/.gtstemplate 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.
There was a problem hiding this comment.
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.
78c675e to
dafb559
Compare
There was a problem hiding this comment.
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.
…out/mouseleave hover-out cases)
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
5c7ffa0 to
41311a6
Compare
41311a6 to
006b5b9
Compare
There was a problem hiding this comment.
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'); | |||
There was a problem hiding this comment.
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.
| const { dom } = require('aria-query'); |
| if (!dom.has(node.tag.toLowerCase())) { | ||
| return; | ||
| } |
There was a problem hiding this comment.
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.
| // Component — not a DOM element. | ||
| '<template><CustomCard {{on "mouseover" this.onHover}} /></template>', | ||
|
|
||
| // Custom element — not in aria-query's dom map. |
There was a problem hiding this comment.
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.
| // Custom element — not in aria-query's dom map. | |
| // Custom element — skipped because `isNativeElement()` returns false. |
| // 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). |
There was a problem hiding this comment.
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.
| // 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). |
| // 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>', | ||
|
|
There was a problem hiding this comment.
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.
… comments; add mouseout+focusout pairing case
Note
This is part of a series where Claude has audited
eslint-plugin-emberagainst jsx-a11y, vuejs-accessibility, angular-eslint, lit-a11y and html-validate,ember-template-lint, and the HTML and WCAG specs.Summary
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+:focusthan paired JS handlers — see Inclusive Components: Tooltips.)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 viahoverInHandlers/hoverOutHandlersconfig 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
Allows
Prior art
Verified each peer in source:
mouse-events-have-key-events['onMouseOver'](configurable)['onMouseOut'](configurable)onFocus/onBluronlymouse-events-have-key-eventsmouseover, mouseenter, hover(hardcoded)mouseout, mouseleave(hardcoded)focus/focusinandblur/focusoutmouse-events-have-key-events@mouseoveronly@mouseoutonly@focus/@bluronlymouse-events-have-key-eventsmouseoveronlymouseoutonlyfocus/bluronlyOur defaults now match jsx-a11y / @angular-eslint/template / lit-a11y (
mouseover/mouseoutonly).mouseenter/mouseleaveare 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 onlyfocus/blur). vuejs-accessibility hardcodes the wider mouseenter+mouseleave set (no config); we diverge.