Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 35 additions & 34 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -256,40 +256,41 @@ To disable a rule for an entire `.gjs`/`.gts` file, use a regular ESLint file-le

### Accessibility

| Name                                            | Description | 💼 | 🔧 | 💡 |
| :--------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------- | :- | :- | :- |
| [template-link-href-attributes](docs/rules/template-link-href-attributes.md) | require href attribute on link elements | 📋 | | |
| [template-no-abstract-roles](docs/rules/template-no-abstract-roles.md) | disallow abstract ARIA roles | 📋 | | |
| [template-no-accesskey-attribute](docs/rules/template-no-accesskey-attribute.md) | disallow accesskey attribute | 📋 | 🔧 | |
| [template-no-aria-hidden-body](docs/rules/template-no-aria-hidden-body.md) | disallow aria-hidden on body element | 📋 | 🔧 | |
| [template-no-aria-unsupported-elements](docs/rules/template-no-aria-unsupported-elements.md) | disallow ARIA roles, states, and properties on elements that do not support them | 📋 | | |
| [template-no-autofocus-attribute](docs/rules/template-no-autofocus-attribute.md) | disallow autofocus attribute | 📋 | 🔧 | |
| [template-no-duplicate-landmark-elements](docs/rules/template-no-duplicate-landmark-elements.md) | disallow duplicate landmark elements without unique labels | 📋 | | |
| [template-no-empty-headings](docs/rules/template-no-empty-headings.md) | disallow empty heading elements | 📋 | | |
| [template-no-heading-inside-button](docs/rules/template-no-heading-inside-button.md) | disallow heading elements inside button elements | 📋 | | |
| [template-no-invalid-aria-attributes](docs/rules/template-no-invalid-aria-attributes.md) | disallow invalid aria-* attributes | 📋 | | |
| [template-no-invalid-interactive](docs/rules/template-no-invalid-interactive.md) | disallow non-interactive elements with interactive handlers | 📋 | | |
| [template-no-invalid-link-text](docs/rules/template-no-invalid-link-text.md) | disallow invalid or uninformative link text content | 📋 | | |
| [template-no-invalid-link-title](docs/rules/template-no-invalid-link-title.md) | disallow invalid title attributes on link elements | 📋 | | |
| [template-no-invalid-role](docs/rules/template-no-invalid-role.md) | disallow invalid ARIA roles | 📋 | | |
| [template-no-nested-interactive](docs/rules/template-no-nested-interactive.md) | disallow nested interactive elements | 📋 | | |
| [template-no-nested-landmark](docs/rules/template-no-nested-landmark.md) | disallow nested landmark elements | 📋 | | |
| [template-no-pointer-down-event-binding](docs/rules/template-no-pointer-down-event-binding.md) | disallow pointer down event bindings | 📋 | | |
| [template-no-positive-tabindex](docs/rules/template-no-positive-tabindex.md) | disallow positive tabindex values | 📋 | | |
| [template-no-redundant-role](docs/rules/template-no-redundant-role.md) | disallow redundant role attributes | 📋 | 🔧 | |
| [template-no-unsupported-role-attributes](docs/rules/template-no-unsupported-role-attributes.md) | disallow ARIA attributes that are not supported by the element role | 📋 | 🔧 | |
| [template-no-whitespace-within-word](docs/rules/template-no-whitespace-within-word.md) | disallow excess whitespace within words (e.g. "W e l c o m e") | 📋 | | |
| [template-require-aria-activedescendant-tabindex](docs/rules/template-require-aria-activedescendant-tabindex.md) | require non-interactive elements with aria-activedescendant to have tabindex | 📋 | 🔧 | |
| [template-require-context-role](docs/rules/template-require-context-role.md) | require ARIA roles to be used in appropriate context | 📋 | | |
| [template-require-iframe-title](docs/rules/template-require-iframe-title.md) | require iframe elements to have a title attribute | 📋 | | |
| [template-require-input-label](docs/rules/template-require-input-label.md) | require label for form input elements | 📋 | | |
| [template-require-lang-attribute](docs/rules/template-require-lang-attribute.md) | require lang attribute on html element | 📋 | | |
| [template-require-mandatory-role-attributes](docs/rules/template-require-mandatory-role-attributes.md) | require mandatory ARIA attributes for ARIA roles | 📋 | | |
| [template-require-media-caption](docs/rules/template-require-media-caption.md) | require captions for audio and video elements | 📋 | | |
| [template-require-presentational-children](docs/rules/template-require-presentational-children.md) | require presentational elements to only contain presentational children | 📋 | | |
| [template-require-valid-alt-text](docs/rules/template-require-valid-alt-text.md) | require valid alt text for images and other elements | 📋 | | |
| [template-require-valid-form-groups](docs/rules/template-require-valid-form-groups.md) | require grouped form controls to have fieldset/legend or WAI-ARIA group labeling | | | |
| [template-table-groups](docs/rules/template-table-groups.md) | require table elements to use table grouping elements | 📋 | | |
| Name                                            | Description | 💼 | 🔧 | 💡 |
| :--------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------- | :- | :- | :- |
| [template-link-href-attributes](docs/rules/template-link-href-attributes.md) | require href attribute on link elements | 📋 | | |
| [template-mouse-events-have-key-events](docs/rules/template-mouse-events-have-key-events.md) | require mouseover/mouseout to be accompanied by focus/blur (or focusin/focusout) for keyboard-only users | | | |
| [template-no-abstract-roles](docs/rules/template-no-abstract-roles.md) | disallow abstract ARIA roles | 📋 | | |
| [template-no-accesskey-attribute](docs/rules/template-no-accesskey-attribute.md) | disallow accesskey attribute | 📋 | 🔧 | |
| [template-no-aria-hidden-body](docs/rules/template-no-aria-hidden-body.md) | disallow aria-hidden on body element | 📋 | 🔧 | |
| [template-no-aria-unsupported-elements](docs/rules/template-no-aria-unsupported-elements.md) | disallow ARIA roles, states, and properties on elements that do not support them | 📋 | | |
| [template-no-autofocus-attribute](docs/rules/template-no-autofocus-attribute.md) | disallow autofocus attribute | 📋 | 🔧 | |
| [template-no-duplicate-landmark-elements](docs/rules/template-no-duplicate-landmark-elements.md) | disallow duplicate landmark elements without unique labels | 📋 | | |
| [template-no-empty-headings](docs/rules/template-no-empty-headings.md) | disallow empty heading elements | 📋 | | |
| [template-no-heading-inside-button](docs/rules/template-no-heading-inside-button.md) | disallow heading elements inside button elements | 📋 | | |
| [template-no-invalid-aria-attributes](docs/rules/template-no-invalid-aria-attributes.md) | disallow invalid aria-* attributes | 📋 | | |
| [template-no-invalid-interactive](docs/rules/template-no-invalid-interactive.md) | disallow non-interactive elements with interactive handlers | 📋 | | |
| [template-no-invalid-link-text](docs/rules/template-no-invalid-link-text.md) | disallow invalid or uninformative link text content | 📋 | | |
| [template-no-invalid-link-title](docs/rules/template-no-invalid-link-title.md) | disallow invalid title attributes on link elements | 📋 | | |
| [template-no-invalid-role](docs/rules/template-no-invalid-role.md) | disallow invalid ARIA roles | 📋 | | |
| [template-no-nested-interactive](docs/rules/template-no-nested-interactive.md) | disallow nested interactive elements | 📋 | | |
| [template-no-nested-landmark](docs/rules/template-no-nested-landmark.md) | disallow nested landmark elements | 📋 | | |
| [template-no-pointer-down-event-binding](docs/rules/template-no-pointer-down-event-binding.md) | disallow pointer down event bindings | 📋 | | |
| [template-no-positive-tabindex](docs/rules/template-no-positive-tabindex.md) | disallow positive tabindex values | 📋 | | |
| [template-no-redundant-role](docs/rules/template-no-redundant-role.md) | disallow redundant role attributes | 📋 | 🔧 | |
| [template-no-unsupported-role-attributes](docs/rules/template-no-unsupported-role-attributes.md) | disallow ARIA attributes that are not supported by the element role | 📋 | 🔧 | |
| [template-no-whitespace-within-word](docs/rules/template-no-whitespace-within-word.md) | disallow excess whitespace within words (e.g. "W e l c o m e") | 📋 | | |
| [template-require-aria-activedescendant-tabindex](docs/rules/template-require-aria-activedescendant-tabindex.md) | require non-interactive elements with aria-activedescendant to have tabindex | 📋 | 🔧 | |
| [template-require-context-role](docs/rules/template-require-context-role.md) | require ARIA roles to be used in appropriate context | 📋 | | |
| [template-require-iframe-title](docs/rules/template-require-iframe-title.md) | require iframe elements to have a title attribute | 📋 | | |
| [template-require-input-label](docs/rules/template-require-input-label.md) | require label for form input elements | 📋 | | |
| [template-require-lang-attribute](docs/rules/template-require-lang-attribute.md) | require lang attribute on html element | 📋 | | |
| [template-require-mandatory-role-attributes](docs/rules/template-require-mandatory-role-attributes.md) | require mandatory ARIA attributes for ARIA roles | 📋 | | |
| [template-require-media-caption](docs/rules/template-require-media-caption.md) | require captions for audio and video elements | 📋 | | |
| [template-require-presentational-children](docs/rules/template-require-presentational-children.md) | require presentational elements to only contain presentational children | 📋 | | |
| [template-require-valid-alt-text](docs/rules/template-require-valid-alt-text.md) | require valid alt text for images and other elements | 📋 | | |
| [template-require-valid-form-groups](docs/rules/template-require-valid-form-groups.md) | require grouped form controls to have fieldset/legend or WAI-ARIA group labeling | | | |
| [template-table-groups](docs/rules/template-table-groups.md) | require table elements to use table grouping elements | 📋 | | |

### Best Practices

Expand Down
48 changes: 48 additions & 0 deletions docs/rules/template-mouse-events-have-key-events.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# ember/template-mouse-events-have-key-events

<!-- end auto-generated rule header -->

Enforce that `{{on "mouseover" …}}` is accompanied by `{{on "focus" …}}` / `{{on "focusin" …}}`, and `{{on "mouseout" …}}` by `{{on "blur" …}}` / `{{on "focusout" …}}`. `{{on "mouseenter" …}}` / `{{on "mouseleave" …}}` are NOT checked by default — opt in via `hoverInHandlers` / `hoverOutHandlers` options (see below).

Keyboard-only users can't trigger mouse events. Pairing hover-in events with focus events (and hover-out events with blur events) ensures the same UI state transitions happen for keyboard navigation.

## On "normative basis"

[WCAG 2.1 SC 2.1.1 Keyboard (Level A)](https://www.w3.org/WAI/WCAG21/Understanding/keyboard) requires all functionality to be operable via the keyboard. Pointer-only UI transitions (hover effects that show/hide content, highlight rows, etc.) don't satisfy this when no keyboard equivalent exists. However, this rule's specific "hover event + focus event pairing" heuristic isn't literally mandated by the SC — [Understanding 2.1.1](https://www.w3.org/WAI/WCAG21/Understanding/keyboard) allows any keyboard path. The event-pairing convention comes from [WAI-ARIA APG keyboard-interaction guidance](https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/) (authoring guidance, not normative), and from all four peer a11y plugins adopting it as the strongest static-analysis proxy. This rule follows the convention.

For many simple hover effects the cleaner fix is a CSS `:hover` + `:focus` combined selector rather than paired JS handlers — [Inclusive Components: Tooltips](https://inclusive-components.design/tooltips-toggletips/) is the canonical reference.

## Examples

This rule **forbids** the following:

```gjs
<template>
<div {{on "mouseover" this.showTooltip}}></div>
<div {{on "mouseout" this.hideTooltip}}></div>
</template>
```
Comment thread
johanrd marked this conversation as resolved.

This rule **allows** the following:

```gjs
<template>
<div {{on "mouseover" this.showTooltip}} {{on "focus" this.showTooltip}}></div>
<div {{on "mouseout" this.hideTooltip}} {{on "focusout" this.hideTooltip}}></div>
</template>
```
Comment thread
johanrd marked this conversation as resolved.

## Options

- `hoverInHandlers` (default `["mouseover"]`) — which events require a focus pair. Matches jsx-a11y's default. Add `"mouseenter"` to also check the non-bubbling per-element variant.
- `hoverOutHandlers` (default `["mouseout"]`) — which events require a blur pair. Matches jsx-a11y's default. Add `"mouseleave"` to also check the non-bubbling per-element variant.

### Why are `mouseenter` / `mouseleave` opt-in?

`mouseenter`/`mouseleave` don't bubble — they fire once on entry/exit of the bound element, never on transitions between children. Authors frequently choose them specifically because they want a per-element effect (highlight one row, show one tooltip) that doesn't fire for every child element transition. Those effects are often cleaner to express with CSS `:hover` + `:focus` combined selectors than paired JS handlers. Flagging `mouseenter`/`mouseleave` by default therefore produces noisy false positives on a common authoring pattern. We default to jsx-a11y's narrower handler set; opt in when your project wants the wider check.

## References

- [WAI-ARIA APG — Keyboard Interaction](https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/)
- [MDN — Keyboard-navigable JavaScript widgets](https://developer.mozilla.org/en-US/docs/Web/Accessibility/Keyboard-navigable_JavaScript_widgets)
- [`mouse-events-have-key-events` — eslint-plugin-jsx-a11y](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/docs/rules/mouse-events-have-key-events.md)
Loading
Loading