AccessiWeather is designed with accessibility as a core priority, ensuring full functionality for screen reader users and keyboard navigation. This document outlines the accessibility standards and patterns used throughout the application.
- Screen Reader Compatibility: All interactive UI elements must have proper ARIA attributes
- Keyboard Navigation: All functionality must be accessible via keyboard (Tab/Shift+Tab navigation)
- Clear Labels: Every control must have both a short label and detailed description
- Logical Flow: UI elements must be ordered in a way that makes sense when navigating linearly
All Toga widgets with user interaction must have:
-
aria_label: Short, concise description (< 50 characters)- Used by screen readers as the primary identifier
- Should be clear and unambiguous
- Example:
"Toggle weather alerts"
-
aria_description: Detailed explanation (> 20 characters)- Provides context about the widget's purpose and behavior
- Explains what the control does and what values are valid
- Example:
"Master control to enable or disable all weather alert functionality."
enable_alerts_switch = toga.Switch(
"Enable weather alerts", # Visible on-screen text
value=True,
id="enable_alerts_switch"
)
enable_alerts_switch.aria_label = "Toggle weather alerts"
enable_alerts_switch.aria_description = (
"Master control to enable or disable all weather alert functionality."
)Guidelines:
aria_label: Start with "Toggle" to indicate it's a switcharia_description: Explain what the switch controls and the impact of changing it
alert_global_cooldown_input = toga.NumberInput(
value=5,
min=0,
max=60,
id="alert_global_cooldown_input"
)
alert_global_cooldown_input.aria_label = "Global notification cooldown"
alert_global_cooldown_input.aria_description = (
"Set the minimum number of minutes to wait between any alert notifications, "
"from 0 to 60 minutes. This prevents notification spam across all alerts."
)Guidelines:
aria_label: Short description of the value being setaria_description: Include valid range (min to max) and explain the purpose
update_channel_selection = toga.Selection(
items=["Stable", "Beta", "Development"],
id="update_channel_selection"
)
update_channel_selection.aria_label = "Update channel selection"
update_channel_selection.aria_description = (
"Choose which release channel to follow for application updates."
)Guidelines:
aria_label: End with "selection" to indicate it's a dropdownaria_description: Explain what the selection controls
Widgets are added to containers in the order they should be navigated:
- Master Controls: Primary enable/disable switches
- Configuration Options: Detailed settings in logical groups
- Action Buttons: OK, Cancel, Apply buttons
# Set initial focus on the most important widget
dialog.primary_widget.focus()Guidelines:
- Set initial focus on the most relevant widget for the user's task
.focus()may fail silently on some platforms - use try/except if critical
The alert notifications settings tab follows this structure:
-
Master Controls (top)
- Enable weather alerts switch
- Enable alert notifications switch
-
Severity Level Filters (middle)
- Extreme severity switch
- Severe severity switch
- Moderate severity switch
- Minor severity switch
- Unknown severity switch
-
Rate Limiting Settings (bottom)
- Global cooldown input (0-60 minutes)
- Per-alert cooldown input (0-1440 minutes)
- Max notifications per hour input (1-100)
# Master control switch with full accessibility
dialog.enable_alerts_switch = toga.Switch(
"Enable weather alerts",
value=dialog.current_settings.enable_alerts,
style=Pack(margin_bottom=10),
id="enable_alerts_switch",
)
dialog.enable_alerts_switch.aria_label = "Toggle weather alerts"
dialog.enable_alerts_switch.aria_description = (
"Master control to enable or disable all weather alert functionality."
)
notifications_box.add(dialog.enable_alerts_switch)
# Rate limiting number input with full accessibility
dialog.alert_global_cooldown_input = toga.NumberInput(
value=getattr(dialog.current_settings, "alert_global_cooldown_minutes", 5),
min=0,
max=60,
style=Pack(margin_bottom=12),
id="alert_global_cooldown_input",
)
dialog.alert_global_cooldown_input.aria_label = "Global notification cooldown"
dialog.alert_global_cooldown_input.aria_description = (
"Set the minimum number of minutes to wait between any alert notifications, "
"from 0 to 60 minutes. This prevents notification spam across all alerts."
)
notifications_box.add(dialog.alert_global_cooldown_input)All accessibility features must be tested:
import os
import toga
from unittest.mock import MagicMock
@pytest.fixture
def mock_app():
"""Create a mock Toga app instance with dummy backend."""
os.environ["TOGA_BACKEND"] = "toga_dummy"
app = toga.App("Test AccessiWeather", "org.beeware.test")
app.config = MagicMock()
app.on_exit = lambda: True
yield app
def test_switch_has_aria_attributes(mock_app, mock_config_manager):
"""Verify switches have proper aria attributes."""
dialog = SettingsDialog(app=mock_app, config_manager=mock_config_manager)
dialog.show_and_prepare() # Initialize UI widgets
# Check aria_label exists and is concise
assert dialog.enable_alerts_switch.aria_label == "Toggle weather alerts"
assert len(dialog.enable_alerts_switch.aria_label) < 50
# Check aria_description provides context
assert "master control" in dialog.enable_alerts_switch.aria_description.lower()
assert len(dialog.enable_alerts_switch.aria_description) > 20For each interactive widget, tests must verify:
- Widget exists and is not None
aria_labelis present and concise (< 50 characters)aria_descriptionis present and informative (> 20 characters)- Descriptions contain key context words
- NumberInput widgets have proper
min/maxconstraints
- Windows: NVDA, JAWS
- macOS: VoiceOver
- Linux: Orca
Manual testing checklist:
- Navigate to settings dialog
- Tab through all alert widgets
- Verify each widget announces:
- Widget type (switch, number input, dropdown)
- Current value
- aria_label
- aria_description
- Verify keyboard shortcuts work (Space to toggle, Arrow keys for inputs)
# BAD: No aria attributes
cooldown_input = toga.NumberInput(value=5, min=0, max=60)
notifications_box.add(cooldown_input)# GOOD: Full aria attributes
cooldown_input = toga.NumberInput(value=5, min=0, max=60, id="cooldown_input")
cooldown_input.aria_label = "Global notification cooldown"
cooldown_input.aria_description = (
"Set the minimum number of minutes to wait between any alert notifications, "
"from 0 to 60 minutes. This prevents notification spam across all alerts."
)
notifications_box.add(cooldown_input)# BAD: Too vague
switch.aria_description = "Turns on alerts"# GOOD: Clear and informative
switch.aria_description = (
"Master control to enable or disable all weather alert functionality."
)When adding any new interactive widget:
- Add
idparameter for automated testing - Set
aria_label(short, < 50 chars) - Set
aria_description(detailed, > 20 chars) - Add to appropriate container in logical navigation order
- Write unit tests verifying aria attributes
Before merging UI changes:
- All interactive widgets have
aria_label - All interactive widgets have
aria_description - Labels are concise (< 50 characters)
- Descriptions are informative (> 20 characters)
- Navigation order is logical
- Unit tests verify aria attributes
- Manual screen reader testing completed
For accessibility questions or issues:
- Open a GitHub issue with the
accessibilitylabel - Tag @accessibility-team in pull requests