# Combined E2E & ARIA Patterns - Real-World Examples

This reference demonstrates how to use E2E testing attributes and ARIA accessibility attributes together in production-ready components.

## Table of Contents

- [Overview](#overview)
- [Complete Form Example](#complete-form-example)
- [Product Listing](#product-listing)
- [Shopping Cart](#shopping-cart)
- [Modal Dialogs](#modal-dialogs)
- [Navigation Patterns](#navigation-patterns)
- [Data Tables](#data-tables)
- [Search and Filters](#search-and-filters)
- [Notifications and Alerts](#notifications-and-alerts)
- [Multi-Step Forms](#multi-step-forms)
- [Best Practices Summary](#best-practices-summary)

## Overview

Combining E2E and ARIA attributes creates components that are:
- **Testable**: Reliable selectors for automated testing
- **Accessible**: Usable with assistive technologies
- **Maintainable**: Attributes survive refactoring
- **Compliant**: Meet WCAG and legal requirements

**Pattern**: Every interactive element should have both types of attributes.

## Complete Form Example

### Registration Form

```html
<form
  (ngSubmit)="onSubmit()"
  data-what="registration-form"
  data-which="user-signup"
  role="form"
  aria-labelledby="form-title">

  <h2 id="form-title">Create Account</h2>

  <!-- Username Field -->
  <div class="form-field">
    <label id="username-label" for="username-input">
      Username <span aria-label="required">*</span>
    </label>
    <input
      id="username-input"
      type="text"
      [(ngModel)]="username"
      name="username"
      data-what="username-input"
      data-which="registration-form"
      aria-labelledby="username-label"
      aria-required="true"
      [attr.aria-invalid]="usernameError ? 'true' : null"
      aria-describedby="username-hint username-error"
      (blur)="validateUsername()" />

    <span id="username-hint" class="hint">
      3-20 characters, letters and numbers only
    </span>

    @if (usernameError) {
      <span
        id="username-error"
        class="error"
        data-what="error-message"
        data-which="username-field"
        role="alert"
        aria-live="polite">
        {{ usernameError }}
      </span>
    }
  </div>

  <!-- Email Field -->
  <div class="form-field">
    <label id="email-label" for="email-input">
      Email Address <span aria-label="required">*</span>
    </label>
    <input
      id="email-input"
      type="email"
      [(ngModel)]="email"
      name="email"
      data-what="email-input"
      data-which="registration-form"
      aria-labelledby="email-label"
      aria-required="true"
      [attr.aria-invalid]="emailError ? 'true' : null"
      aria-describedby="email-hint email-error"
      (blur)="validateEmail()" />

    <span id="email-hint" class="hint">
      We'll never share your email
    </span>

    @if (emailError) {
      <span
        id="email-error"
        class="error"
        data-what="error-message"
        data-which="email-field"
        role="alert"
        aria-live="polite">
        {{ emailError }}
      </span>
    }
  </div>

  <!-- Password Field -->
  <div class="form-field">
    <label id="password-label" for="password-input">
      Password <span aria-label="required">*</span>
    </label>
    <input
      id="password-input"
      type="password"
      [(ngModel)]="password"
      name="password"
      data-what="password-input"
      data-which="registration-form"
      aria-labelledby="password-label"
      aria-required="true"
      [attr.aria-invalid]="passwordError ? 'true' : null"
      aria-describedby="password-hint password-error"
      (input)="checkPasswordStrength()" />

    <span id="password-hint" class="hint">
      At least 8 characters with uppercase, lowercase, and numbers
    </span>

    <!-- Password Strength Indicator -->
    <div
      class="password-strength"
      data-what="password-strength"
      data-which="registration-form"
      [attr.data-strength]="passwordStrength"
      role="status"
      aria-live="polite"
      aria-label="Password strength indicator">
      Strength: {{ passwordStrength }}
    </div>

    @if (passwordError) {
      <span
        id="password-error"
        class="error"
        data-what="error-message"
        data-which="password-field"
        role="alert"
        aria-live="polite">
        {{ passwordError }}
      </span>
    }
  </div>

  <!-- Terms Checkbox -->
  <div class="form-field">
    <label>
      <input
        id="terms-checkbox"
        type="checkbox"
        [(ngModel)]="agreedToTerms"
        name="terms"
        data-what="terms-checkbox"
        data-which="registration-form"
        aria-required="true"
        aria-describedby="terms-label" />
      <span id="terms-label">
        I agree to the <a href="/terms" target="_blank" aria-label="Terms of service (opens in new tab)">Terms of Service</a>
      </span>
    </label>
  </div>

  <!-- Form Actions -->
  <div class="form-actions">
    <button
      type="submit"
      [disabled]="!isFormValid"
      data-what="submit-button"
      data-which="registration-form"
      [attr.aria-disabled]="!isFormValid"
      aria-label="Submit registration form">
      Create Account
    </button>

    <button
      type="button"
      (click)="onCancel()"
      data-what="cancel-button"
      data-which="registration-form"
      aria-label="Cancel registration">
      Cancel
    </button>
  </div>

  <!-- Loading State -->
  @if (isSubmitting) {
    <div
      class="loading"
      data-what="loading-indicator"
      data-which="registration-form"
      role="status"
      aria-live="polite"
      aria-label="Submitting registration">
      <span aria-hidden="true">⏳</span>
      Submitting...
    </div>
  }
</form>
```

## Product Listing

### Product Grid with Filters

```html
<div class="product-catalog">
  <!-- Search and Filter Section -->
  <div
    class="filters"
    data-what="filter-section"
    data-which="product-catalog"
    role="search"
    aria-label="Product search and filters">

    <!-- Search Input -->
    <input
      type="search"
      [(ngModel)]="searchQuery"
      (input)="onSearch()"
      placeholder="Search products..."
      data-what="search-input"
      data-which="product-catalog"
      aria-label="Search products"
      aria-describedby="search-hint" />
    <span id="search-hint" class="sr-only">
      Search by product name or SKU
    </span>

    <!-- Category Filter -->
    <select
      [(ngModel)]="selectedCategory"
      (change)="applyFilters()"
      data-what="category-select"
      data-which="product-filter"
      aria-label="Filter by category">
      <option value="">All Categories</option>
      @for (category of categories; track category.id) {
        <option [value]="category.id">{{ category.name }}</option>
      }
    </select>

    <!-- Price Range Filter -->
    <div class="price-range">
      <label id="price-range-label">Price Range</label>
      <input
        type="range"
        [(ngModel)]="maxPrice"
        (change)="applyFilters()"
        min="0"
        max="1000"
        data-what="price-slider"
        data-which="product-filter"
        aria-labelledby="price-range-label"
        aria-valuemin="0"
        aria-valuemax="1000"
        [attr.aria-valuenow]="maxPrice"
        [attr.aria-valuetext]="'Maximum price: $' + maxPrice" />
      <output>${{ maxPrice }}</output>
    </div>

    <!-- Results Count -->
    <div
      class="results-count"
      data-what="results-count"
      data-which="product-catalog"
      role="status"
      aria-live="polite"
      aria-atomic="true">
      Showing {{ filteredProducts.length }} of {{ totalProducts }} products
    </div>
  </div>

  <!-- Product Grid -->
  <div
    class="product-grid"
    data-what="product-grid"
    data-which="catalog"
    role="list"
    aria-label="Product list">

    @for (product of filteredProducts; track product.id) {
      <article
        class="product-card"
        data-what="product-card"
        [attr.data-which]="product.id"
        [attr.data-status]="product.stock > 0 ? 'in-stock' : 'out-of-stock'"
        role="listitem"
        [attr.aria-label]="product.name + ', $' + product.price">

        <!-- Product Image -->
        <a
          [routerLink]="['/products', product.id]"
          data-what="product-link"
          [attr.data-which]="product.id"
          [attr.aria-label]="'View details for ' + product.name">
          <img
            [src]="product.image"
            [alt]="product.name"
            loading="lazy" />
        </a>

        <!-- Product Info -->
        <div class="product-info">
          <h3>
            <a
              [routerLink]="['/products', product.id]"
              data-what="product-link"
              [attr.data-which]="product.id">
              {{ product.name }}
            </a>
          </h3>

          <p class="price" aria-label="Price">
            {{ product.price | currency }}
          </p>

          <!-- Stock Status -->
          <div
            class="stock-status"
            data-what="stock-badge"
            [attr.data-which]="product.id"
            [attr.data-status]="product.stock > 0 ? 'in-stock' : 'out-of-stock'"
            role="status"
            [attr.aria-label]="product.stock > 0 ? 'In stock' : 'Out of stock'">
            @if (product.stock > 0) {
              <span class="in-stock">In Stock</span>
            } @else {
              <span class="out-of-stock">Out of Stock</span>
            }
          </div>

          <!-- Add to Cart Button -->
          <button
            (click)="addToCart(product)"
            [disabled]="product.stock === 0"
            data-what="add-to-cart-button"
            [attr.data-which]="product.id"
            [attr.aria-disabled]="product.stock === 0"
            [attr.aria-label]="'Add ' + product.name + ' to cart'">
            @if (product.stock > 0) {
              <span aria-hidden="true">🛒</span> Add to Cart
            } @else {
              <span>Unavailable</span>
            }
          </button>
        </div>
      </article>
    }
  </div>

  <!-- Empty State -->
  @if (filteredProducts.length === 0) {
    <div
      class="empty-state"
      data-what="empty-state"
      data-which="product-catalog"
      role="status"
      aria-live="polite">
      <p>No products found matching your criteria.</p>
      <button
        (click)="clearFilters()"
        data-what="clear-filters-button"
        data-which="product-catalog"
        aria-label="Clear all filters">
        Clear Filters
      </button>
    </div>
  }
</div>
```

## Shopping Cart

```html
<div
  class="shopping-cart"
  data-what="shopping-cart"
  data-which="checkout"
  role="region"
  aria-labelledby="cart-title">

  <h2 id="cart-title">Shopping Cart</h2>

  <!-- Cart Items List -->
  <ul
    class="cart-items"
    data-what="cart-items-list"
    data-which="shopping-cart"
    role="list"
    aria-label="Items in your cart">

    @for (item of cartItems; track item.id) {
      <li
        class="cart-item"
        data-what="cart-item"
        [attr.data-which]="item.id"
        role="listitem">

        <!-- Item Image -->
        <img
          [src]="item.image"
          [alt]="item.name"
          class="item-image" />

        <!-- Item Details -->
        <div class="item-details">
          <h3>{{ item.name }}</h3>
          <p class="item-price" aria-label="Price per item">
            {{ item.price | currency }}
          </p>

          <!-- Quantity Controls -->
          <div
            class="quantity-controls"
            role="group"
            aria-labelledby="quantity-label-{{ item.id }}">

            <span id="quantity-label-{{ item.id }}" class="sr-only">
              Quantity for {{ item.name }}
            </span>

            <button
              (click)="decreaseQuantity(item)"
              [disabled]="item.quantity <= 1"
              data-what="decrease-button"
              [attr.data-which]="item.id"
              [attr.aria-disabled]="item.quantity <= 1"
              [attr.aria-label]="'Decrease quantity of ' + item.name">
              <span aria-hidden="true">−</span>
            </button>

            <input
              type="number"
              [(ngModel)]="item.quantity"
              (change)="updateQuantity(item)"
              min="1"
              [max]="item.maxQuantity"
              data-what="quantity-input"
              [attr.data-which]="item.id"
              [attr.aria-label]="'Quantity of ' + item.name"
              aria-valuemin="1"
              [attr.aria-valuemax]="item.maxQuantity"
              [attr.aria-valuenow]="item.quantity" />

            <button
              (click)="increaseQuantity(item)"
              [disabled]="item.quantity >= item.maxQuantity"
              data-what="increase-button"
              [attr.data-which]="item.id"
              [attr.aria-disabled]="item.quantity >= item.maxQuantity"
              [attr.aria-label]="'Increase quantity of ' + item.name">
              <span aria-hidden="true">+</span>
            </button>
          </div>

          <!-- Remove Button -->
          <button
            (click)="removeItem(item)"
            class="remove-button"
            data-what="remove-button"
            [attr.data-which]="item.id"
            [attr.aria-label]="'Remove ' + item.name + ' from cart'">
            <span aria-hidden="true">🗑️</span> Remove
          </button>

          <!-- Item Subtotal -->
          <p
            class="item-subtotal"
            data-what="item-subtotal"
            [attr.data-which]="item.id"
            [attr.aria-label]="'Subtotal for ' + item.name">
            Subtotal: {{ item.price * item.quantity | currency }}
          </p>
        </div>
      </li>
    }
  </ul>

  <!-- Cart Summary -->
  <div
    class="cart-summary"
    data-what="cart-summary"
    data-which="shopping-cart"
    role="region"
    aria-labelledby="summary-title">

    <h3 id="summary-title">Order Summary</h3>

    <dl>
      <dt>Subtotal:</dt>
      <dd data-what="subtotal" data-which="cart-summary">
        {{ subtotal | currency }}
      </dd>

      <dt>Tax:</dt>
      <dd data-what="tax" data-which="cart-summary">
        {{ tax | currency }}
      </dd>

      <dt>Shipping:</dt>
      <dd data-what="shipping" data-which="cart-summary">
        {{ shipping | currency }}
      </dd>

      <dt><strong>Total:</strong></dt>
      <dd data-what="total" data-which="cart-summary">
        <strong>{{ total | currency }}</strong>
      </dd>
    </dl>

    <!-- Checkout Button -->
    <button
      (click)="proceedToCheckout()"
      [disabled]="cartItems.length === 0"
      class="checkout-button"
      data-what="checkout-button"
      data-which="shopping-cart"
      [attr.aria-disabled]="cartItems.length === 0"
      aria-label="Proceed to checkout">
      Proceed to Checkout
    </button>
  </div>

  <!-- Empty Cart Message -->
  @if (cartItems.length === 0) {
    <div
      class="empty-cart"
      data-what="empty-state"
      data-which="shopping-cart"
      role="status"
      aria-live="polite">
      <p>Your cart is empty</p>
      <a
        routerLink="/products"
        data-what="continue-shopping-link"
        data-which="shopping-cart"
        aria-label="Continue shopping">
        Continue Shopping
      </a>
    </div>
  }
</div>
```

## Modal Dialogs

### Confirmation Dialog

```html
@if (showDeleteDialog) {
  <!-- Overlay -->
  <div
    class="modal-overlay"
    (click)="cancelDelete()"
    data-what="modal-overlay"
    data-which="delete-confirmation">
  </div>

  <!-- Dialog -->
  <div
    class="modal"
    data-what="confirmation-dialog"
    data-which="delete-item"
    role="alertdialog"
    aria-modal="true"
    aria-labelledby="dialog-title"
    aria-describedby="dialog-description"
    #dialogElement
    tabindex="-1">

    <h2 id="dialog-title">Confirm Deletion</h2>

    <p id="dialog-description">
      Are you sure you want to delete "{{ itemToDelete?.name }}"?
      This action cannot be undone.
    </p>

    <!-- Dialog Actions -->
    <div class="dialog-actions">
      <button
        (click)="confirmDelete()"
        class="danger-button"
        data-what="confirm-button"
        data-which="delete-dialog"
        aria-label="Confirm deletion">
        Delete
      </button>

      <button
        (click)="cancelDelete()"
        data-what="cancel-button"
        data-which="delete-dialog"
        aria-label="Cancel deletion"
        autofocus>
        Cancel
      </button>
    </div>

    <!-- Close Button -->
    <button
      (click)="cancelDelete()"
      class="close-button"
      data-what="close-button"
      data-which="dialog"
      aria-label="Close dialog">
      <span aria-hidden="true">×</span>
    </button>
  </div>
}
```

## Navigation Patterns

### Main Navigation with Dropdown

```html
<nav
  class="main-nav"
  data-what="main-navigation"
  data-which="header"
  role="navigation"
  aria-label="Main navigation">

  <ul role="menubar">
    @for (item of menuItems; track item.id) {
      <li role="none">
        @if (item.children) {
          <!-- Dropdown Menu Item -->
          <button
            (click)="toggleSubmenu(item.id)"
            (keydown.arrowDown)="openSubmenu(item.id)"
            [attr.aria-expanded]="item.expanded"
            aria-haspopup="menu"
            [attr.aria-controls]="'submenu-' + item.id"
            data-what="nav-menu-button"
            [attr.data-which]="item.id"
            role="menuitem">
            {{ item.label }}
            <span aria-hidden="true">▼</span>
          </button>

          <!-- Submenu -->
          <ul
            [id]="'submenu-' + item.id"
            [attr.aria-hidden]="!item.expanded"
            role="menu"
            [attr.aria-label]="item.label + ' submenu'"
            data-what="submenu"
            [attr.data-which]="item.id">
            @for (child of item.children; track child.id) {
              <li role="none">
                <a
                  [routerLink]="child.url"
                  role="menuitem"
                  data-what="nav-link"
                  [attr.data-which]="child.id"
                  [attr.aria-current]="isCurrentPage(child.url) ? 'page' : null">
                  {{ child.label }}
                </a>
              </li>
            }
          </ul>
        } @else {
          <!-- Simple Link -->
          <a
            [routerLink]="item.url"
            role="menuitem"
            data-what="nav-link"
            [attr.data-which]="item.id"
            [attr.aria-current]="isCurrentPage(item.url) ? 'page' : null">
            {{ item.label }}
          </a>
        }
      </li>
    }
  </ul>
</nav>
```

## Data Tables

### Sortable Data Table

```html
<table
  class="data-table"
  data-what="orders-table"
  data-which="customer-dashboard"
  role="table"
  aria-labelledby="table-title"
  aria-describedby="table-description">

  <caption id="table-title">Recent Orders</caption>
  <p id="table-description" class="sr-only">
    A table showing your recent orders with sortable columns
  </p>

  <thead>
    <tr role="row">
      <!-- Sortable Column Headers -->
      <th scope="col" role="columnheader">
        <button
          (click)="sortBy('orderId')"
          data-what="sort-button"
          data-which="order-id-column"
          [attr.aria-sort]="getSortDirection('orderId')"
          aria-label="Sort by order ID">
          Order ID
          <span aria-hidden="true">{{ getSortIcon('orderId') }}</span>
        </button>
      </th>

      <th scope="col" role="columnheader">
        <button
          (click)="sortBy('date')"
          data-what="sort-button"
          data-which="date-column"
          [attr.aria-sort]="getSortDirection('date')"
          aria-label="Sort by date">
          Date
          <span aria-hidden="true">{{ getSortIcon('date') }}</span>
        </button>
      </th>

      <th scope="col" role="columnheader">
        <button
          (click)="sortBy('total')"
          data-what="sort-button"
          data-which="total-column"
          [attr.aria-sort]="getSortDirection('total')"
          aria-label="Sort by total">
          Total
          <span aria-hidden="true">{{ getSortIcon('total') }}</span>
        </button>
      </th>

      <th scope="col" role="columnheader">Status</th>
      <th scope="col" role="columnheader">Actions</th>
    </tr>
  </thead>

  <tbody>
    @for (order of sortedOrders; track order.id) {
      <tr
        role="row"
        data-what="table-row"
        [attr.data-which]="order.id"
        [attr.data-status]="order.status">

        <td role="cell">{{ order.id }}</td>
        <td role="cell">{{ order.date | date }}</td>
        <td role="cell">{{ order.total | currency }}</td>
        <td role="cell">
          <span
            class="status-badge"
            data-what="status-badge"
            [attr.data-which]="order.id"
            [attr.data-status]="order.status">
            {{ order.status }}
          </span>
        </td>
        <td role="cell">
          <button
            (click)="viewOrder(order)"
            data-what="view-button"
            [attr.data-which]="order.id"
            [attr.aria-label]="'View order ' + order.id">
            View
          </button>
          <button
            (click)="downloadInvoice(order)"
            data-what="download-button"
            [attr.data-which]="order.id"
            [attr.aria-label]="'Download invoice for order ' + order.id">
            Invoice
          </button>
        </td>
      </tr>
    }
  </tbody>
</table>
```

## Search and Filters

### Advanced Search with Live Results

```html
<div
  class="search-container"
  data-what="search-container"
  data-which="site-search"
  role="search"
  aria-label="Site search">

  <!-- Search Input with Autocomplete -->
  <div class="search-input-wrapper">
    <input
      type="search"
      [(ngModel)]="searchQuery"
      (input)="onSearchInput()"
      (focus)="showSuggestions = true"
      (blur)="hideSuggestions()"
      placeholder="Search..."
      data-what="search-input"
      data-which="site-search"
      aria-label="Search site"
      aria-autocomplete="list"
      [attr.aria-expanded]="showSuggestions && suggestions.length > 0"
      [attr.aria-controls]="'search-suggestions'"
      [attr.aria-activedescendant]="activeSuggestionId"
      role="combobox" />

    <!-- Search Button -->
    <button
      (click)="performSearch()"
      data-what="search-button"
      data-which="site-search"
      aria-label="Perform search">
      <span aria-hidden="true">🔍</span>
    </button>
  </div>

  <!-- Search Suggestions -->
  @if (showSuggestions && suggestions.length > 0) {
    <ul
      id="search-suggestions"
      class="suggestions"
      data-what="suggestions-list"
      data-which="site-search"
      role="listbox"
      aria-label="Search suggestions">

      @for (suggestion of suggestions; track suggestion.id; let idx = $index) {
        <li
          [id]="'suggestion-' + idx"
          role="option"
          [attr.aria-selected]="idx === activeSuggestionIndex"
          (click)="selectSuggestion(suggestion)"
          data-what="suggestion-item"
          [attr.data-which]="suggestion.id"
          [attr.data-index]="idx">
          {{ suggestion.text }}
        </li>
      }
    </ul>
  }

  <!-- Search Results Count -->
  @if (searchPerformed) {
    <div
      class="results-summary"
      data-what="results-summary"
      data-which="site-search"
      role="status"
      aria-live="polite"
      aria-atomic="true">
      Found {{ searchResults.length }} results for "{{ searchQuery }}"
    </div>
  }
</div>
```

## Notifications and Alerts

### Toast Notifications

```html
<!-- Success Toast -->
@if (showSuccessToast) {
  <div
    class="toast success"
    data-what="toast-notification"
    data-which="success"
    [attr.data-type]="'success'"
    role="status"
    aria-live="polite"
    aria-atomic="true">

    <span aria-hidden="true">✓</span>
    <span>{{ successMessage }}</span>

    <button
      (click)="dismissToast('success')"
      data-what="dismiss-button"
      data-which="success-toast"
      aria-label="Dismiss success notification">
      <span aria-hidden="true">×</span>
    </button>
  </div>
}

<!-- Error Alert -->
@if (showErrorAlert) {
  <div
    class="alert error"
    data-what="alert-notification"
    data-which="error"
    [attr.data-type]="'error'"
    role="alert"
    aria-live="assertive"
    aria-atomic="true">

    <span aria-hidden="true">⚠</span>
    <span>{{ errorMessage }}</span>

    <button
      (click)="dismissAlert('error')"
      data-what="dismiss-button"
      data-which="error-alert"
      aria-label="Dismiss error alert">
      <span aria-hidden="true">×</span>
    </button>
  </div>
}
```

## Multi-Step Forms

### Wizard with Progress Indicator

```html
<div
  class="form-wizard"
  data-what="multi-step-form"
  data-which="checkout-wizard"
  role="region"
  aria-labelledby="wizard-title">

  <h2 id="wizard-title">Checkout Process</h2>

  <!-- Progress Indicator -->
  <ol
    class="progress-steps"
    data-what="progress-indicator"
    data-which="checkout-wizard"
    role="list"
    aria-label="Checkout progress">

    @for (step of steps; track step.id; let idx = $index) {
      <li
        role="listitem"
        data-what="progress-step"
        [attr.data-which]="step.id"
        [attr.data-status]="getStepStatus(idx)"
        [attr.aria-current]="currentStep === idx ? 'step' : null">

        <span
          class="step-number"
          [attr.aria-label]="'Step ' + (idx + 1)">
          {{ idx + 1 }}
        </span>
        <span class="step-label">{{ step.label }}</span>
      </li>
    }
  </ol>

  <!-- Step Content -->
  <div
    class="step-content"
    data-what="step-content"
    [attr.data-which]="'step-' + currentStep"
    role="tabpanel"
    [attr.aria-labelledby]="'step-label-' + currentStep">

    <!-- Content varies by step -->
    {{ currentStepContent }}
  </div>

  <!-- Navigation Buttons -->
  <div class="wizard-navigation">
    <button
      (click)="previousStep()"
      [disabled]="currentStep === 0"
      data-what="previous-button"
      data-which="checkout-wizard"
      [attr.aria-disabled]="currentStep === 0"
      aria-label="Go to previous step">
      ← Previous
    </button>

    @if (currentStep < steps.length - 1) {
      <button
        (click)="nextStep()"
        [disabled]="!isCurrentStepValid"
        data-what="next-button"
        data-which="checkout-wizard"
        [attr.aria-disabled]="!isCurrentStepValid"
        aria-label="Go to next step">
        Next →
      </button>
    } @else {
      <button
        (click)="submitForm()"
        [disabled]="!isFormComplete"
        data-what="submit-button"
        data-which="checkout-wizard"
        [attr.aria-disabled]="!isFormComplete"
        aria-label="Complete checkout">
        Complete Order
      </button>
    }
  </div>
</div>
```

## Best Practices Summary

### Every Interactive Element Needs Both

```html
<!-- ✅ GOOD: Both E2E and ARIA attributes -->
<button
  data-what="submit-button"
  data-which="order-form"
  aria-label="Submit order">
  Submit
</button>

<!-- ❌ BAD: Missing ARIA -->
<button
  data-what="submit-button"
  data-which="order-form">
  Submit
</button>

<!-- ❌ BAD: Missing E2E attributes -->
<button aria-label="Submit order">
  Submit
</button>
```

### Attributes Work Together

- **E2E `data-what`**: Describes element type for testing
- **ARIA role**: Describes element purpose for accessibility
- **E2E `data-which`**: Identifies specific instance for testing
- **ARIA label**: Provides accessible name for screen readers
- **E2E `data-*`**: Additional test context
- **ARIA states**: Dynamic state information

### Maintain Both During Updates

When you update HTML:
1. Add/update E2E attributes for testing
2. Add/update ARIA attributes for accessibility
3. Keep both in sync with element's purpose
4. Update tests to use new selectors
5. Test with screen readers

## Related Documentation

- **[E2E Testing Attributes](e2e-attributes.md)** - Complete E2E attribute reference
- **[ARIA Accessibility Attributes](aria-attributes.md)** - Complete ARIA reference
- **[Main Skill](../SKILL.md)** - HTML template skill overview
- **Testing Guidelines**: `docs/guidelines/testing.md` - Project testing standards

## Summary

Combining E2E and ARIA attributes creates:
- ✅ **Testable** components with reliable selectors
- ✅ **Accessible** interfaces for all users
- ✅ **Maintainable** code that survives refactoring
- ✅ **Compliant** applications meeting legal requirements
- ✅ **Professional** quality expected in production

**Always use both attribute types together on every interactive element.**
