mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-24 13:51:09 -05:00
- Introduced a comprehensive Secret Management Guide detailing best practices, secret classification, development setup, production management, rotation procedures, and emergency protocols. - Implemented a client-side performance monitoring script to track various metrics including page load performance, paint metrics, layout shifts, and memory usage. - Enhanced search accessibility with keyboard navigation support for search results, ensuring compliance with WCAG standards and improving user experience.
351 lines
8.1 KiB
Markdown
351 lines
8.1 KiB
Markdown
# Accessible Component Patterns
|
|
|
|
This document provides code examples for creating accessible components in ThrillWiki. Follow these patterns to ensure WCAG 2.1 AA compliance.
|
|
|
|
## Button
|
|
|
|
### Standard Button
|
|
```django
|
|
{% include 'components/ui/button.html' with
|
|
text='Save Changes'
|
|
variant='primary'
|
|
type='submit'
|
|
%}
|
|
```
|
|
|
|
### Icon Button (REQUIRED: aria_label)
|
|
```django
|
|
{% include 'components/ui/button.html' with
|
|
icon=close_svg
|
|
size='icon'
|
|
aria_label='Close dialog'
|
|
variant='ghost'
|
|
%}
|
|
```
|
|
|
|
**Important**: Icon-only buttons MUST include `aria_label` for screen reader accessibility.
|
|
|
|
### Disabled Button
|
|
```django
|
|
{% include 'components/ui/button.html' with
|
|
text='Submit'
|
|
disabled=True
|
|
%}
|
|
```
|
|
|
|
### Button with HTMX
|
|
```django
|
|
{% include 'components/ui/button.html' with
|
|
text='Load More'
|
|
hx_get='/api/items?page=2'
|
|
hx_target='#item-list'
|
|
hx_swap='beforeend'
|
|
%}
|
|
```
|
|
|
|
## Form Field
|
|
|
|
### Standard Field
|
|
```django
|
|
{% include 'forms/partials/form_field.html' with
|
|
field=form.email
|
|
%}
|
|
```
|
|
|
|
The form_field.html component automatically handles:
|
|
- Label association via `for` attribute
|
|
- Error message display with proper ARIA
|
|
- Required field indication
|
|
- Help text with `aria-describedby`
|
|
|
|
### Field with Help Text
|
|
```django
|
|
{% include 'forms/partials/form_field.html' with
|
|
field=form.password
|
|
help_text='Must be at least 8 characters'
|
|
%}
|
|
```
|
|
|
|
### Field with HTMX Validation
|
|
```django
|
|
{% include 'forms/partials/form_field.html' with
|
|
field=form.username
|
|
hx_validate=True
|
|
hx_validate_url='/api/validate-username/'
|
|
%}
|
|
```
|
|
|
|
### Complete Form Example
|
|
```django
|
|
<form method="post" role="form" aria-label="User registration form">
|
|
{% csrf_token %}
|
|
|
|
<fieldset>
|
|
<legend class="sr-only">Account Information</legend>
|
|
{% include 'forms/partials/form_field.html' with field=form.username %}
|
|
{% include 'forms/partials/form_field.html' with field=form.email %}
|
|
</fieldset>
|
|
|
|
<fieldset>
|
|
<legend class="sr-only">Security</legend>
|
|
{% include 'forms/partials/form_field.html' with field=form.password1 %}
|
|
{% include 'forms/partials/form_field.html' with field=form.password2 %}
|
|
</fieldset>
|
|
|
|
{% include 'components/ui/button.html' with
|
|
text='Create Account'
|
|
type='submit'
|
|
variant='primary'
|
|
%}
|
|
</form>
|
|
```
|
|
|
|
## Modal
|
|
|
|
### Basic Modal
|
|
```django
|
|
{% extends 'components/modals/modal_base.html' %}
|
|
|
|
{% block modal_body %}
|
|
<p>Are you sure you want to delete this item?</p>
|
|
{% endblock %}
|
|
```
|
|
|
|
### Modal with Actions
|
|
```django
|
|
{% extends 'components/modals/modal_base.html' %}
|
|
|
|
{% block modal_body %}
|
|
<p>This action cannot be undone.</p>
|
|
{% endblock %}
|
|
|
|
{% block modal_footer %}
|
|
<button @click="{{ show_var }} = false" type="button">
|
|
Cancel
|
|
</button>
|
|
{% include 'components/ui/button.html' with
|
|
text='Delete'
|
|
variant='destructive'
|
|
x_on_click='confirmDelete()'
|
|
%}
|
|
{% endblock %}
|
|
```
|
|
|
|
The modal_inner.html component automatically provides:
|
|
- `role="dialog"` and `aria-modal="true"`
|
|
- `aria-labelledby` pointing to title
|
|
- `aria-describedby` pointing to body (and subtitle if present)
|
|
- Focus trap with Tab/Shift+Tab cycling
|
|
- Home/End key support for first/last focusable element
|
|
- Escape key to close (configurable)
|
|
- Auto-focus on first focusable element
|
|
|
|
## Navigation Menu
|
|
|
|
### Dropdown Menu
|
|
```django
|
|
<div x-data="{ open: false }" @click.outside="open = false" @keydown.escape="open = false">
|
|
<button
|
|
@click="open = !open"
|
|
aria-haspopup="true"
|
|
:aria-expanded="open.toString()"
|
|
aria-label="User menu">
|
|
<span>Menu</span>
|
|
<i class="fas fa-chevron-down" aria-hidden="true"></i>
|
|
</button>
|
|
|
|
<div
|
|
x-show="open"
|
|
x-transition
|
|
role="menu"
|
|
aria-label="User account options"
|
|
class="dropdown-menu">
|
|
|
|
<a role="menuitem" href="/profile" class="menu-item">Profile</a>
|
|
<a role="menuitem" href="/settings" class="menu-item">Settings</a>
|
|
<div role="separator" class="border-t"></div>
|
|
<button role="menuitem" type="submit" class="menu-item">Logout</button>
|
|
</div>
|
|
</div>
|
|
```
|
|
|
|
## Search
|
|
|
|
### Accessible Search with Results
|
|
```django
|
|
<div role="search">
|
|
<label for="search-input" class="sr-only">Search parks and rides</label>
|
|
<input
|
|
id="search-input"
|
|
type="search"
|
|
placeholder="Search..."
|
|
hx-get="/search"
|
|
hx-target="#search-results"
|
|
hx-trigger="input changed delay:300ms"
|
|
autocomplete="off"
|
|
aria-describedby="search-status"
|
|
aria-controls="search-results"
|
|
/>
|
|
|
|
<div
|
|
id="search-results"
|
|
role="listbox"
|
|
aria-label="Search results"
|
|
aria-live="polite">
|
|
<!-- Results populated by HTMX -->
|
|
</div>
|
|
|
|
<div id="search-status" class="sr-only" aria-live="polite" aria-atomic="true">
|
|
<!-- Status announcements -->
|
|
</div>
|
|
</div>
|
|
```
|
|
|
|
### Search Result Item
|
|
```django
|
|
<div role="option" aria-selected="false" class="search-result">
|
|
<a href="{{ result.url }}">
|
|
<span>{{ result.name }}</span>
|
|
<span class="text-muted-foreground">{{ result.type }}</span>
|
|
</a>
|
|
</div>
|
|
```
|
|
|
|
## Live Regions
|
|
|
|
### Status Announcements
|
|
```django
|
|
<div role="status" aria-live="polite" aria-atomic="true" class="sr-only">
|
|
{{ status_message }}
|
|
</div>
|
|
```
|
|
|
|
Use `aria-live="polite"` for non-urgent updates that can wait for user to finish current action.
|
|
|
|
### Alert Messages
|
|
```django
|
|
<div role="alert" aria-live="assertive">
|
|
<i class="fas fa-exclamation-circle" aria-hidden="true"></i>
|
|
{{ error_message }}
|
|
</div>
|
|
```
|
|
|
|
Use `aria-live="assertive"` for critical errors that require immediate attention.
|
|
|
|
### Loading State
|
|
```django
|
|
<div role="status" aria-live="polite" aria-busy="true">
|
|
<span class="sr-only">Loading results...</span>
|
|
<div class="htmx-indicator" aria-hidden="true">
|
|
<i class="fas fa-spinner fa-spin"></i>
|
|
</div>
|
|
</div>
|
|
```
|
|
|
|
## Images
|
|
|
|
### Meaningful Image
|
|
```django
|
|
<img
|
|
src="{{ park.image.url }}"
|
|
alt="{{ park.name }} - {{ park.location }}"
|
|
/>
|
|
```
|
|
|
|
### Decorative Image
|
|
```django
|
|
<img src="/decorative-pattern.svg" alt="" role="presentation" />
|
|
```
|
|
|
|
### Avatar with Name
|
|
```django
|
|
{% if user.profile.avatar %}
|
|
<img
|
|
src="{{ user.profile.avatar.url }}"
|
|
alt="{{ user.get_full_name|default:user.username }}'s profile picture"
|
|
class="avatar"
|
|
/>
|
|
{% else %}
|
|
<div class="avatar-placeholder" aria-hidden="true">
|
|
{{ user.username.0|upper }}
|
|
</div>
|
|
{% endif %}
|
|
```
|
|
|
|
## Breadcrumbs
|
|
|
|
```django
|
|
{% include 'components/navigation/breadcrumbs.html' %}
|
|
```
|
|
|
|
The breadcrumbs component automatically provides:
|
|
- `<nav>` element with `aria-label="Breadcrumb"`
|
|
- Ordered list for semantic structure
|
|
- `aria-current="page"` on current page
|
|
- Hidden separators with `aria-hidden="true"`
|
|
- Schema.org JSON-LD structured data
|
|
|
|
## Skip Link
|
|
|
|
Add to the top of your base template:
|
|
```django
|
|
<a href="#main-content" class="skip-link sr-only-focusable">
|
|
Skip to main content
|
|
</a>
|
|
|
|
<!-- ... header and navigation ... -->
|
|
|
|
<main id="main-content" tabindex="-1">
|
|
{% block content %}{% endblock %}
|
|
</main>
|
|
```
|
|
|
|
## Theme Toggle
|
|
|
|
```django
|
|
<button
|
|
@click="toggleTheme()"
|
|
aria-label="Toggle theme"
|
|
:aria-pressed="isDarkMode.toString()"
|
|
class="theme-toggle">
|
|
<i class="fas fa-sun" aria-hidden="true"></i>
|
|
<i class="fas fa-moon" aria-hidden="true"></i>
|
|
</button>
|
|
```
|
|
|
|
## Focus Management Utilities
|
|
|
|
### Focus Trap (Alpine.js)
|
|
```javascript
|
|
// Already implemented in modal_inner.html
|
|
@keydown.tab.prevent="trapFocus($event)"
|
|
```
|
|
|
|
### Return Focus After Action
|
|
```javascript
|
|
// Store the trigger element
|
|
const trigger = document.activeElement;
|
|
|
|
// After modal closes
|
|
trigger.focus();
|
|
```
|
|
|
|
## Testing Your Components
|
|
|
|
### Quick Accessibility Audit
|
|
1. Can you Tab through all interactive elements?
|
|
2. Is focus indicator visible?
|
|
3. Does Enter/Space activate buttons?
|
|
4. Does Escape close modals/dropdowns?
|
|
5. Does screen reader announce all content?
|
|
6. Is color not the only indicator of state?
|
|
|
|
### Automated Testing
|
|
```bash
|
|
# Run accessibility tests
|
|
python manage.py test backend.tests.accessibility
|
|
|
|
# Use axe browser extension for quick audits
|
|
# Install from: https://www.deque.com/axe/
|
|
```
|