# Accessibility Skill

> **Purpose:** WCAG compliance, ARIA attributes, and accessible component patterns
> **When to use:** Any task involving UI components or user interactions
> **Related skills:** wordpress-plugin-foundation-skill.md (always applies)

---

## Quick Reference

```html
<!-- Button with ARIA label -->
<button aria-label="Delete post" class="presszone-forum-delete-btn">
    <svg aria-hidden="true">...</svg>
</button>

<!-- Dropdown with ARIA -->
<button 
    aria-expanded="false" 
    aria-controls="dropdown-menu"
    aria-haspopup="true">
    Menu
</button>
<div id="dropdown-menu" role="menu" hidden>
    <a href="#" role="menuitem">Item 1</a>
</div>

<!-- Modal with focus trap -->
<div role="dialog" aria-modal="true" aria-labelledby="modal-title">
    <h2 id="modal-title">Modal Title</h2>
    <div role="document">Content</div>
</div>

<!-- Form with labels -->
<label for="post-title">Title</label>
<input id="post-title" type="text" required aria-required="true">

<!-- Skip link -->
<a href="#main-content" class="skip-link">Skip to content</a>
```

---

## ARIA Attributes

### Common ARIA Attributes

```html
<!-- Labels and descriptions -->
aria-label="Close dialog"
aria-labelledby="heading-id"
aria-describedby="description-id"

<!-- States -->
aria-expanded="true"
aria-selected="true"
aria-checked="true"
aria-disabled="true"
aria-hidden="true"
aria-pressed="true"

<!-- Properties -->
aria-haspopup="true"
aria-controls="element-id"
aria-owns="element-id"
aria-live="polite"
aria-atomic="true"

<!-- Roles -->
role="button"
role="dialog"
role="menu"
role="menuitem"
role="tab"
role="tabpanel"
```

---

## Semantic HTML

### Use Semantic Elements

```html
<!-- CORRECT - Semantic HTML -->
<nav aria-label="Main navigation">
    <ul>
        <li><a href="/">Home</a></li>
    </ul>
</nav>

<main id="main-content">
    <article>
        <header>
            <h1>Post Title</h1>
        </header>
        <section>
            <p>Content</p>
        </section>
    </article>
</main>

<aside aria-label="Sidebar">
    <section>
        <h2>Related Posts</h2>
    </section>
</aside>

<!-- WRONG - Non-semantic divs -->
<div class="nav">
    <div class="nav-item">Home</div>
</div>
```

---

## Keyboard Navigation

### Focus Management

```javascript
// Make element focusable
element.setAttribute('tabindex', '0');

// Remove from tab order
element.setAttribute('tabindex', '-1');

// Focus element
element.focus();

// Focus trap in modal
function trapFocus(container) {
    const focusableElements = container.querySelectorAll(
        'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
    );
    
    const firstElement = focusableElements[0];
    const lastElement = focusableElements[focusableElements.length - 1];
    
    container.addEventListener('keydown', (e) => {
        if (e.key !== 'Tab') return;
        
        if (e.shiftKey) {
            if (document.activeElement === firstElement) {
                lastElement.focus();
                e.preventDefault();
            }
        } else {
            if (document.activeElement === lastElement) {
                firstElement.focus();
                e.preventDefault();
            }
        }
    });
    
    firstElement.focus();
}
```

### Keyboard Event Handlers

```javascript
// Handle Enter and Space for custom buttons
element.addEventListener('keydown', (e) => {
    if (e.key === 'Enter' || e.key === ' ') {
        e.preventDefault();
        handleClick();
    }
});

// Handle Escape to close
element.addEventListener('keydown', (e) => {
    if (e.key === 'Escape') {
        closeModal();
    }
});

// Arrow key navigation
element.addEventListener('keydown', (e) => {
    switch(e.key) {
        case 'ArrowUp':
            e.preventDefault();
            focusPrevious();
            break;
        case 'ArrowDown':
            e.preventDefault();
            focusNext();
            break;
    }
});
```

---

## Focus Styles

### Visible Focus Indicators

```scss
// CORRECT - Visible focus styles
.presszone-forum-button:focus-visible {
    outline: 2px solid $presszone-forum-primary;
    outline-offset: 2px;
}

// FORBIDDEN - Removing outline without alternative
.presszone-forum-button:focus {
    outline: none;  // NEVER without alternative
}

// CORRECT - Custom focus style
.presszone-forum-button:focus-visible {
    outline: none;
    box-shadow: 0 0 0 3px rgba(31, 113, 221, 0.5);
}
```

---

## Buttons and Links

### Proper Button Usage

```html
<!-- CORRECT - Button for actions -->
<button type="button" onclick="deletePost()">Delete</button>

<!-- CORRECT - Link for navigation -->
<a href="/posts/123">View Post</a>

<!-- WRONG - Link styled as button for action -->
<a href="#" onclick="deletePost()">Delete</a>

<!-- WRONG - Div as button -->
<div onclick="deletePost()">Delete</div>
```

### Icon-Only Buttons

```html
<!-- CORRECT - Icon button with aria-label -->
<button aria-label="Delete post" class="presszone-forum-icon-btn">
    <svg aria-hidden="true" focusable="false">
        <use href="#icon-delete"></use>
    </svg>
</button>

<!-- WRONG - Icon button without label -->
<button class="presszone-forum-icon-btn">
    <svg>...</svg>
</button>
```

---

## Forms

### Form Labels

```html
<!-- CORRECT - Explicit label -->
<label for="post-title">Post Title</label>
<input id="post-title" type="text" required>

<!-- CORRECT - Implicit label -->
<label>
    Post Title
    <input type="text" required>
</label>

<!-- WRONG - No label -->
<input type="text" placeholder="Post Title">
```

### Required Fields

```html
<!-- CORRECT - Required with ARIA -->
<label for="email">
    Email <span aria-label="required">*</span>
</label>
<input 
    id="email" 
    type="email" 
    required 
    aria-required="true"
    aria-describedby="email-error">
<div id="email-error" role="alert" aria-live="polite"></div>
```

### Error Messages

```html
<!-- CORRECT - Associated error message -->
<label for="username">Username</label>
<input 
    id="username" 
    type="text" 
    aria-invalid="true"
    aria-describedby="username-error">
<div id="username-error" role="alert">
    Username must be at least 3 characters
</div>
```

---

## Modals and Dialogs

### Accessible Modal

```html
<div 
    role="dialog" 
    aria-modal="true" 
    aria-labelledby="modal-title"
    class="presszone-forum-modal">
    
    <div role="document">
        <h2 id="modal-title">Confirm Delete</h2>
        <p>Are you sure you want to delete this post?</p>
        
        <button type="button">Cancel</button>
        <button type="button">Delete</button>
    </div>
</div>
```

```javascript
class AccessibleModal {
    constructor(element) {
        this.element = element;
        this.previousFocus = null;
    }
    
    open() {
        // Save current focus
        this.previousFocus = document.activeElement;
        
        // Show modal
        this.element.removeAttribute('hidden');
        
        // Trap focus
        this.trapFocus();
        
        // Focus first focusable element
        const firstFocusable = this.element.querySelector(
            'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
        );
        firstFocusable?.focus();
        
        // Prevent body scroll
        document.body.style.overflow = 'hidden';
    }
    
    close() {
        // Hide modal
        this.element.setAttribute('hidden', '');
        
        // Restore focus
        this.previousFocus?.focus();
        
        // Restore body scroll
        document.body.style.overflow = '';
    }
    
    trapFocus() {
        // Implementation from keyboard navigation section
    }
}
```

---

## Dropdowns and Menus

### Accessible Dropdown

```html
<div class="presszone-forum-dropdown">
    <button 
        aria-expanded="false" 
        aria-controls="dropdown-menu"
        aria-haspopup="true"
        id="dropdown-trigger">
        Options
    </button>
    
    <ul 
        id="dropdown-menu" 
        role="menu" 
        aria-labelledby="dropdown-trigger"
        hidden>
        <li role="none">
            <a href="#" role="menuitem">Edit</a>
        </li>
        <li role="none">
            <a href="#" role="menuitem">Delete</a>
        </li>
    </ul>
</div>
```

```javascript
class AccessibleDropdown {
    constructor(trigger, menu) {
        this.trigger = trigger;
        this.menu = menu;
        this.isOpen = false;
        
        this.bindEvents();
    }
    
    bindEvents() {
        this.trigger.addEventListener('click', () => this.toggle());
        
        this.trigger.addEventListener('keydown', (e) => {
            if (e.key === 'ArrowDown') {
                e.preventDefault();
                this.open();
                this.focusFirstItem();
            }
        });
        
        // Close on Escape
        this.menu.addEventListener('keydown', (e) => {
            if (e.key === 'Escape') {
                this.close();
                this.trigger.focus();
            }
        });
        
        // Arrow key navigation
        this.menu.addEventListener('keydown', (e) => {
            if (e.key === 'ArrowDown') {
                e.preventDefault();
                this.focusNextItem();
            } else if (e.key === 'ArrowUp') {
                e.preventDefault();
                this.focusPreviousItem();
            }
        });
    }
    
    toggle() {
        this.isOpen ? this.close() : this.open();
    }
    
    open() {
        this.isOpen = true;
        this.menu.removeAttribute('hidden');
        this.trigger.setAttribute('aria-expanded', 'true');
    }
    
    close() {
        this.isOpen = false;
        this.menu.setAttribute('hidden', '');
        this.trigger.setAttribute('aria-expanded', 'false');
    }
    
    focusFirstItem() {
        const firstItem = this.menu.querySelector('[role="menuitem"]');
        firstItem?.focus();
    }
    
    focusNextItem() {
        const items = Array.from(this.menu.querySelectorAll('[role="menuitem"]'));
        const currentIndex = items.indexOf(document.activeElement);
        const nextIndex = (currentIndex + 1) % items.length;
        items[nextIndex].focus();
    }
    
    focusPreviousItem() {
        const items = Array.from(this.menu.querySelectorAll('[role="menuitem"]'));
        const currentIndex = items.indexOf(document.activeElement);
        const prevIndex = currentIndex <= 0 ? items.length - 1 : currentIndex - 1;
        items[prevIndex].focus();
    }
}
```

---

## Tabs

### Accessible Tab Panel

```html
<div class="presszone-forum-tabs">
    <div role="tablist" aria-label="Forum sections">
        <button 
            role="tab" 
            aria-selected="true" 
            aria-controls="panel-1"
            id="tab-1">
            Overview
        </button>
        <button 
            role="tab" 
            aria-selected="false" 
            aria-controls="panel-2"
            id="tab-2"
            tabindex="-1">
            Posts
        </button>
    </div>
    
    <div 
        role="tabpanel" 
        id="panel-1" 
        aria-labelledby="tab-1">
        Overview content
    </div>
    
    <div 
        role="tabpanel" 
        id="panel-2" 
        aria-labelledby="tab-2"
        hidden>
        Posts content
    </div>
</div>
```

---

## Live Regions

### Announcing Dynamic Content

```html
<!-- Polite announcement (waits for pause) -->
<div aria-live="polite" aria-atomic="true" class="sr-only">
    <!-- Content updated via JavaScript -->
</div>

<!-- Assertive announcement (interrupts) -->
<div aria-live="assertive" aria-atomic="true" class="sr-only">
    <!-- Urgent updates -->
</div>

<!-- Status messages -->
<div role="status" aria-live="polite" class="sr-only">
    Post saved successfully
</div>

<!-- Alerts -->
<div role="alert" aria-live="assertive">
    Error: Failed to save post
</div>
```

```javascript
// Announce message to screen readers
function announce(message, priority = 'polite') {
    const announcer = document.createElement('div');
    announcer.setAttribute('aria-live', priority);
    announcer.setAttribute('aria-atomic', 'true');
    announcer.className = 'sr-only';
    announcer.textContent = message;
    
    document.body.appendChild(announcer);
    
    // Remove after announcement
    setTimeout(() => announcer.remove(), 1000);
}

// Usage
announce('Post created successfully');
announce('Error occurred', 'assertive');
```

---

## Screen Reader Only Content

### Visually Hidden but Accessible

```scss
// Screen reader only class
.sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border-width: 0;
}

// Focusable when navigating with keyboard
.sr-only-focusable:focus {
    position: static;
    width: auto;
    height: auto;
    padding: inherit;
    margin: inherit;
    overflow: visible;
    clip: auto;
    white-space: normal;
}
```

```html
<!-- Skip link -->
<a href="#main-content" class="sr-only sr-only-focusable">
    Skip to main content
</a>

<!-- Additional context for screen readers -->
<button>
    Delete
    <span class="sr-only">post by John Doe</span>
</button>
```

---

## Images and Icons

### Alt Text

```html
<!-- CORRECT - Descriptive alt text -->
<img src="avatar.jpg" alt="John Doe's profile picture">

<!-- CORRECT - Decorative image -->
<img src="decoration.png" alt="" role="presentation">

<!-- CORRECT - Icon with text -->
<button>
    <svg aria-hidden="true" focusable="false">...</svg>
    Delete
</button>

<!-- CORRECT - Icon without text -->
<button aria-label="Delete post">
    <svg aria-hidden="true" focusable="false">...</svg>
</button>
```

---

## Color Contrast

### WCAG AA Requirements

```scss
// Minimum contrast ratios:
// - Normal text: 4.5:1
// - Large text (18pt+): 3:1
// - UI components: 3:1

// CORRECT - Sufficient contrast
.presszone-forum-text {
    color: #1a1a1a;           // Dark text
    background: #ffffff;       // White background
    // Contrast ratio: 16.1:1 ✓
}

// WRONG - Insufficient contrast
.presszone-forum-text-muted {
    color: #cccccc;           // Light gray text
    background: #ffffff;       // White background
    // Contrast ratio: 1.6:1 ✗
}

// CORRECT - Sufficient contrast for muted text
.presszone-forum-text-muted {
    color: #767676;           // Medium gray
    background: #ffffff;       // White background
    // Contrast ratio: 4.5:1 ✓
}
```

---

## Reduced Motion

### Respect User Preferences

```scss
// REQUIRED - Disable animations for users who prefer reduced motion
@media (prefers-reduced-motion: reduce) {
    .presszone-forum-wrapper * {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
        scroll-behavior: auto !important;
    }
}
```

---

## Loading States

### Accessible Loading Indicators

```html
<!-- Loading spinner with announcement -->
<div 
    role="status" 
    aria-live="polite" 
    aria-label="Loading content">
    <svg aria-hidden="true" class="spinner">...</svg>
    <span class="sr-only">Loading...</span>
</div>

<!-- Button loading state -->
<button aria-busy="true" disabled>
    <span class="spinner" aria-hidden="true"></span>
    <span class="sr-only">Loading...</span>
    <span aria-hidden="true">Save</span>
</button>
```

---

## Common Mistakes

| Mistake | Fix |
|---------|-----|
| Icon-only button without label | Add `aria-label` |
| Removing focus outline | Provide visible alternative with `:focus-visible` |
| Using `<div>` as button | Use `<button>` element |
| Missing form labels | Add `<label>` with `for` attribute |
| Not managing focus in modals | Trap focus and restore on close |
| Missing `aria-expanded` on dropdowns | Add and toggle on open/close |
| Decorative images with alt text | Use `alt=""` and `role="presentation"` |
| Low color contrast | Ensure 4.5:1 ratio for text |
| Not disabling animations | Add `prefers-reduced-motion` media query |
| Missing keyboard navigation | Handle Enter, Space, Arrow keys |

---

## Testing Checklist

- [ ] All interactive elements keyboard accessible
- [ ] Focus visible on all interactive elements
- [ ] All images have appropriate alt text
- [ ] All form inputs have labels
- [ ] Color contrast meets WCAG AA (4.5:1)
- [ ] Animations respect `prefers-reduced-motion`
- [ ] Modals trap focus and restore on close
- [ ] Dropdowns have `aria-expanded` attribute
- [ ] Icon-only buttons have `aria-label`
- [ ] Error messages associated with inputs
- [ ] Live regions announce dynamic content
- [ ] Skip links present for keyboard users
- [ ] Semantic HTML used throughout
- [ ] Screen reader tested (NVDA, JAWS, VoiceOver)

---

## Integration with Other Skills

- **wordpress-plugin-foundation-skill.md** - Security and compliance (always applies)
- **javascript-skill.md** - JavaScript for interactive components
- **css-scss-skill.md** - Focus styles and visual accessibility
- **frontend-architecture-skill.md** - Accessible component patterns
