---
description: Combobox component accessibility compliance pattern
globs: *.vue, *.jsx, *.tsx, *.html, *.php, *.js, *.ts, *.liquid
alwaysApply: false
---
# Combobox Component Accessibility Standards

Ensures combobox components follow WCAG compliance and WAI-ARIA Combobox Pattern specifications.

<rule>
name: combobox_accessibility_standards
description: Enforce combobox component accessibility standards and WAI-ARIA Combobox Pattern compliance
filters:
  - type: file_extension
    pattern: "\\.(vue|jsx|tsx|html|liquid|php|js|ts)$"

actions:
  - type: enforce
    conditions:
      # Combobox role requirement
      - pattern: "(?i)<(div|section)[^>]*(?:combobox|autocomplete)[^>]*>"
        pattern_negate: "role=\"combobox\""
        message: "Combobox containers must have role='combobox' attribute."

      # aria-expanded requirement
      - pattern: "(?i)<[^>]*role=\"combobox\"[^>]*>"
        pattern_negate: "aria-expanded=\"(true|false)\""
        message: "Combobox elements must have aria-expanded attribute set to 'true' or 'false'."

      # aria-haspopup requirement
      - pattern: "(?i)<[^>]*role=\"combobox\"[^>]*>"
        pattern_negate: "aria-haspopup=\"listbox\""
        message: "Combobox elements must have aria-haspopup='listbox' attribute."

      # aria-controls requirement
      - pattern: "(?i)<[^>]*role=\"combobox\"[^>]*>"
        pattern_negate: "aria-controls=\"[^\"]+\""
        message: "Combobox elements must have aria-controls attribute referencing the ID of the associated listbox."

      # aria-autocomplete requirement
      - pattern: "(?i)<[^>]*role=\"combobox\"[^>]*>"
        pattern_negate: "aria-autocomplete=\"(list|both|inline|none)\""
        message: "Combobox elements must have aria-autocomplete attribute set to 'list', 'both', 'inline', or 'none'."

      # aria-activedescendant requirement when expanded
      - pattern: "(?i)<[^>]*role=\"combobox\"[^>]*aria-expanded=\"true\"[^>]*>"
        pattern_negate: "aria-activedescendant=\"[^\"]+\""
        message: "Expanded combobox elements must have aria-activedescendant attribute referencing the ID of the active option."

      # Listbox role requirement
      - pattern: "(?i)<(div|ul)[^>]*(?:listbox|dropdown|popup)[^>]*>"
        pattern_negate: "role=\"listbox\""
        message: "Listbox containers must have role='listbox' attribute."

      # Option role requirement
      - pattern: "(?i)<(div|li)[^>]*(?:option|item)[^>]*>"
        pattern_negate: "role=\"option\""
        message: "Listbox options must have role='option' attribute."

      # Option ID requirement for aria-activedescendant
      - pattern: "(?i)<[^>]*role=\"option\"[^>]*>"
        pattern_negate: "id=\"[^\"]+\""
        message: "Listbox options must have unique id attributes for aria-activedescendant to reference them."

      # aria-selected requirement for options
      - pattern: "(?i)<[^>]*role=\"option\"[^>]*>"
        pattern_negate: "aria-selected=\"(true|false)\""
        message: "Listbox options must have aria-selected attribute set to 'true' or 'false'."

      # Missing keyboard event handlers
      - pattern: "(?i)<[^>]*role=\"combobox\"[^>]*>"
        pattern_negate: "(onKeyDown|onkeydown|@keydown|v-on:keydown)"
        message: "Combobox elements should handle keyboard events (Arrow keys, Enter, Escape, etc.)."

      # Missing status region
      - pattern: "(?i)<[^>]*role=\"combobox\"[^>]*>"
        pattern_negate: "aria-controls=\"[^\"]+\".*?<[^>]*role=\"status\""
        message: "Combobox should have a status region to announce available options."

  - type: suggest
    message: |
      **Combobox Component Accessibility Best Practices:**

      **Required ARIA Attributes:**
      - **role='combobox':** Set on the input container element
      - **aria-expanded:** 'true' if listbox is visible, 'false' if hidden
      - **aria-haspopup='listbox':** Indicates the combobox has a listbox popup
      - **aria-controls:** Reference to the ID of the associated listbox
      - **aria-autocomplete:** 'list', 'both', 'inline', or 'none' based on behavior
      - **aria-activedescendant:** Reference to the ID of the currently active option (remove when listbox is hidden)
      - **role='listbox':** Set on the popup container element (preferably on a `ul` element)
      - **role='option':** Set on each selectable item in the listbox (preferably on an `li` element)
      - **id:** Unique ID on each option element for `aria-activedescendant` to reference
      - **aria-selected:** 'true' or 'false' on each option
      - **role='status':** Set on a visually hidden element to announce available options

      **Keyboard Interaction Requirements:**
      - **Down Arrow:** Open listbox and move focus to first option
      - **Up Arrow:** Open listbox and move focus to last option
      - **Enter/Space:** Select focused option and close listbox
      - **Escape:** Close listbox without selection
      - **Tab:** Move focus to next focusable element
      - **Shift+Tab:** Move focus to previous focusable element
      - **Home/End:** Move focus to first/last option
      - **Character Keys:** Filter options based on input

      **Focus Management:**
      - Focus should remain on the input while navigating options
      - Use aria-activedescendant to indicate the currently focused option
      - Return focus to input after selection or closing
      - Ensure focus is trapped within the combobox while open

      **Status Region Requirements:**
      - Must announce number of available options when listbox opens
      - Must announce when no options are available
      - Must use proper pluralization ("1 item available" vs "2 items available")
      - Must be visually hidden but available to screen readers
      - Should update dynamically as options are filtered

      **Semantic HTML Structure:**
      - Use `ul` element for the listbox container
      - Use `li` elements for individual options
      - This provides better semantic structure and is more appropriate for lists

      **Implementation Example:**
      ```html
      <div class="combobox-container">
        <label for="combobox-input">Select an option:</label>
        <input type="text"
               id="combobox-input"
               role="combobox"
               aria-expanded="false"
               aria-haspopup="listbox"
               aria-controls="listbox-popup"
               aria-autocomplete="list">
        <ul id="listbox-popup"
            role="listbox"
            hidden>
          <li role="option"
              id="option-1"
              aria-selected="false">
            Option 1
          </li>
          <li role="option"
              id="option-2"
              aria-selected="false">
            Option 2
          </li>
        </ul>
        <div id="listbox-status"
             role="status"
             class="visually-hidden">
          <!-- Status messages will be dynamically updated -->
        </div>
      </div>

      <style>
        .visually-hidden {
          position: absolute;
          width: 1px;
          height: 1px;
          padding: 0;
          margin: -1px;
          overflow: hidden;
          clip: rect(0, 0, 0, 0);
          white-space: nowrap;
          border: 0;
        }

        #listbox-popup {
          list-style: none;
          padding: 0;
          margin: 0;
        }
      </style>

      <script>
        const input = document.getElementById('combobox-input');
        const listbox = document.getElementById('listbox-popup');
        const statusElement = document.getElementById('listbox-status');

        // Status message handling
        function updateStatusMessage(count) {
          if (count === 0) {
            statusElement.textContent = 'No items available';
          } else {
            statusElement.textContent = `${count} ${count === 1 ? 'item' : 'items'} available`;
          }
        }

        // Show listbox
        function showListbox() {
          listbox.hidden = false;
          input.setAttribute('aria-expanded', 'true');
          const options = listbox.querySelectorAll('[role="option"]');
          updateStatusMessage(options.length);
        }

        // Hide listbox
        function hideListbox() {
          listbox.hidden = true;
          input.setAttribute('aria-expanded', 'false');
          input.removeAttribute('aria-activedescendant'); // Important: remove when hiding
          statusElement.textContent = '';
        }

        // Set active option
        function setActiveOption(optionId) {
          input.setAttribute('aria-activedescendant', optionId);
        }

        // Example usage:
        // When opening listbox with options:
        showListbox();
        // When setting active option:
        setActiveOption('option-1');
        // When closing listbox:
        hideListbox();
      </script>
      ```

      **JavaScript Considerations:**
      - Implement proper event listeners for all keyboard interactions
      - Update ARIA attributes dynamically based on state
      - **Remove `aria-activedescendant` when hiding the listbox** to avoid referencing hidden elements
      - Handle focus management and trapping
      - Implement proper filtering and selection logic
      - Update status messages for all state changes
      - Ensure proper pluralization in status messages
      - Handle edge cases (no matches, empty input, etc.)

      **Accessibility Notes:**
      - Status region helps screen readers understand available options
      - Proper pluralization improves user experience
      - Clear status messages help users understand the current state
      - Visual feedback should match announced status
      - Test with screen readers to ensure proper announcement

metadata:
  priority: high
  version: 1.0
</rule>
