Files
marco/.agents/skills/ember-best-practices/rules/a11y-keyboard-navigation.md

4.3 KiB

title, impact, impactDescription, tags
title impact impactDescription tags
Keyboard Navigation Support HIGH Critical for keyboard-only users accessibility, a11y, keyboard, focus-management

Keyboard Navigation Support

Ensure all interactive elements are keyboard accessible and focus management is handled properly, especially in modals and dynamic content.

Incorrect (no keyboard support):

// app/components/dropdown.gjs
<template>
  <div class="dropdown" {{on "click" this.toggleMenu}}>
    Menu
    {{#if this.isOpen}}
      <div class="dropdown-menu">
        <div {{on "click" this.selectOption}}>Option 1</div>
        <div {{on "click" this.selectOption}}>Option 2</div>
      </div>
    {{/if}}
  </div>
</template>

Correct (full keyboard support with custom modifier):

// app/modifiers/focus-first.js
import { modifier } from 'ember-modifier';

export default modifier((element, [selector = 'button']) => {
  // Focus first matching element when modifier runs
  element.querySelector(selector)?.focus();
});
// app/components/dropdown.gjs
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { fn } from '@ember/helper';
import focusFirst from '../modifiers/focus-first';

class Dropdown extends Component {
  @tracked isOpen = false;

  @action
  toggleMenu() {
    this.isOpen = !this.isOpen;
  }

  @action
  handleButtonKeyDown(event) {
    if (event.key === 'ArrowDown') {
      event.preventDefault();
      this.isOpen = true;
    }
  }

  @action
  handleMenuKeyDown(event) {
    if (event.key === 'Escape') {
      this.isOpen = false;
      // Return focus to button
      event.target.closest('.dropdown').querySelector('button').focus();
    }
    // Handle arrow key navigation between menu items
    if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
      event.preventDefault();
      this.moveFocus(event.key === 'ArrowDown' ? 1 : -1);
    }
  }

  moveFocus(direction) {
    const items = Array.from(document.querySelectorAll('[role="menuitem"] button'));
    const currentIndex = items.indexOf(document.activeElement);
    const nextIndex = (currentIndex + direction + items.length) % items.length;
    items[nextIndex]?.focus();
  }

  @action
  selectOption(value) {
    this.args.onSelect?.(value);
    this.isOpen = false;
  }

  <template>
    <div class="dropdown">
      <button
        type="button"
        {{on "click" this.toggleMenu}}
        {{on "keydown" this.handleButtonKeyDown}}
        aria-haspopup="true"
        aria-expanded="{{this.isOpen}}"
      >
        Menu
      </button>

      {{#if this.isOpen}}
        <ul
          class="dropdown-menu"
          role="menu"
          {{focusFirst '[role="menuitem"] button'}}
          {{on "keydown" this.handleMenuKeyDown}}
        >
          <li role="menuitem">
            <button type="button" {{on "click" (fn this.selectOption "1")}}>
              Option 1
            </button>
          </li>
          <li role="menuitem">
            <button type="button" {{on "click" (fn this.selectOption "2")}}>
              Option 2
            </button>
          </li>
        </ul>
      {{/if}}
    </div>
  </template>
}

For focus trapping in modals, use ember-focus-trap:

ember install ember-focus-trap
// app/components/modal.gjs
import FocusTrap from 'ember-focus-trap/components/focus-trap';

<template>
  {{#if this.showModal}}
    <FocusTrap @isActive={{true}} @initialFocus="#modal-title">
      <div class="modal" role="dialog" aria-modal="true" aria-labelledby="modal-title">
        <h2 id="modal-title">{{@title}}</h2>
        {{yield}}
        <button type="button" {{on "click" this.closeModal}}>Close</button>
      </div>
    </FocusTrap>
  {{/if}}
</template>

Alternative: Use libraries for keyboard support:

For complex keyboard interactions, consider using libraries that abstract keyboard support patterns:

npm install @fluentui/keyboard-keys

Or use tabster for comprehensive keyboard navigation management including focus trapping, arrow key navigation, and modalizers.

Proper keyboard navigation ensures all users can interact with your application effectively.

Reference: Ember Accessibility - Keyboard