mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-24 11:31:08 -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.
8.1 KiB
8.1 KiB
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
{% include 'components/ui/button.html' with
text='Save Changes'
variant='primary'
type='submit'
%}
Icon Button (REQUIRED: aria_label)
{% 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
{% include 'components/ui/button.html' with
text='Submit'
disabled=True
%}
Button with HTMX
{% 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
{% include 'forms/partials/form_field.html' with
field=form.email
%}
The form_field.html component automatically handles:
- Label association via
forattribute - Error message display with proper ARIA
- Required field indication
- Help text with
aria-describedby
Field with Help Text
{% include 'forms/partials/form_field.html' with
field=form.password
help_text='Must be at least 8 characters'
%}
Field with HTMX Validation
{% include 'forms/partials/form_field.html' with
field=form.username
hx_validate=True
hx_validate_url='/api/validate-username/'
%}
Complete Form Example
<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
{% extends 'components/modals/modal_base.html' %}
{% block modal_body %}
<p>Are you sure you want to delete this item?</p>
{% endblock %}
Modal with Actions
{% 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"andaria-modal="true"aria-labelledbypointing to titlearia-describedbypointing 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
<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
<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
<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
<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
<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
<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
<img
src="{{ park.image.url }}"
alt="{{ park.name }} - {{ park.location }}"
/>
Decorative Image
<img src="/decorative-pattern.svg" alt="" role="presentation" />
Avatar with Name
{% 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
{% include 'components/navigation/breadcrumbs.html' %}
The breadcrumbs component automatically provides:
<nav>element witharia-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:
<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
<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)
// Already implemented in modal_inner.html
@keydown.tab.prevent="trapFocus($event)"
Return Focus After Action
// Store the trigger element
const trigger = document.activeElement;
// After modal closes
trigger.focus();
Testing Your Components
Quick Accessibility Audit
- Can you Tab through all interactive elements?
- Is focus indicator visible?
- Does Enter/Space activate buttons?
- Does Escape close modals/dropdowns?
- Does screen reader announce all content?
- Is color not the only indicator of state?
Automated Testing
# Run accessibility tests
python manage.py test backend.tests.accessibility
# Use axe browser extension for quick audits
# Install from: https://www.deque.com/axe/