Improve accessibility throughout the application

- Add skip-to-content link for keyboard users
- Add ARIA roles and labels to modals (dialog, aria-modal, aria-labelledby)
- Make inventory items keyboard accessible (tabindex, role=button, onkeydown)
- Make container legend items keyboard accessible with aria-pressed state
- Make stats clickable area keyboard accessible
- Add aria-labels to FAB and lock buttons
- Add aria-hidden to decorative SVGs and color dots
- Add live regions for toast notifications and loading states
- Associate form labels with inputs via for= attribute
- Add visually-hidden CSS class for screen reader text

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Eric Wagoner
2025-12-24 10:30:14 -05:00
parent f39a9b0c5a
commit fa9e24763f

View File

@@ -591,6 +591,19 @@
font-size: 0.9rem;
}
/* Accessibility: visually hidden but available to screen readers */
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
.toast {
position: fixed;
bottom: 100px;
@@ -622,8 +635,9 @@
</style>
</head>
<body>
<div class="loading-overlay" id="loadingOverlay">
<div class="loading-spinner"></div>
<a href="#inventoryList" class="visually-hidden" id="skipLink" style="position:absolute;left:-9999px;top:auto;width:1px;height:1px;overflow:hidden;" onfocus="this.style.cssText='position:fixed;top:10px;left:10px;z-index:9999;background:var(--forest);color:white;padding:10px 20px;border-radius:8px;text-decoration:none;font-weight:500;';" onblur="this.style.cssText='position:absolute;left:-9999px;top:auto;width:1px;height:1px;overflow:hidden;';">Skip to inventory</a>
<div class="loading-overlay" id="loadingOverlay" role="status" aria-live="polite" aria-label="Loading">
<div class="loading-spinner" aria-hidden="true"></div>
</div>
<div class="error-banner" id="errorBanner" style="display: none;"></div>
@@ -642,7 +656,7 @@
<div class="stat-number" id="inStockCount">0</div>
<div class="stat-label">In Stock</div>
</div>
<div class="stat stat-clickable" id="outStat" onclick="copyShoppingList()" title="Tap to copy shopping list">
<div class="stat stat-clickable" id="outStat" onclick="copyShoppingList()" onkeydown="if(event.key==='Enter'||event.key===' '){event.preventDefault();copyShoppingList();}" tabindex="0" role="button" aria-label="Copy out of stock items to clipboard" title="Tap to copy shopping list">
<div class="stat-number" id="outCount">0</div>
<div class="stat-label">Out of Stock 📋</div>
</div>
@@ -650,10 +664,11 @@
<div class="search-section">
<div class="search-wrapper">
<svg class="search-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<svg class="search-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
<input type="text" class="search-input" id="searchInput" placeholder="Search ingredients...">
<label for="searchInput" class="visually-hidden">Search ingredients</label>
<input type="text" class="search-input" id="searchInput" placeholder="Search ingredients..." aria-label="Search ingredients">
</div>
</div>
@@ -677,30 +692,30 @@
<p style="font-size: 0.9rem; color: var(--soft-gray);">Print this QR code and place it on your pantry shelf for quick access.</p>
</div>
<button class="lock-indicator locked" id="lockIndicator" onclick="toggleLock()">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<button class="lock-indicator locked" id="lockIndicator" onclick="toggleLock()" aria-label="Toggle edit mode">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
</svg>
<span id="lockText">View Only</span>
</button>
<button class="fab locked" id="fab" onclick="openAddModal()">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<button class="fab locked" id="fab" onclick="openAddModal()" aria-label="Add new item">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<line x1="12" y1="5" x2="12" y2="19"></line>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
</button>
<div class="toast" id="toast"></div>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<!-- PIN Modal -->
<div class="modal-overlay" id="pinModal">
<div class="modal-overlay" id="pinModal" role="dialog" aria-modal="true" aria-labelledby="pinModalTitle">
<div class="modal">
<div class="modal-header">
<h3 class="modal-title">Enter PIN to Edit</h3>
<button class="modal-close" onclick="closeModal('pinModal')">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<h3 class="modal-title" id="pinModalTitle">Enter PIN to Edit</h3>
<button class="modal-close" onclick="closeModal('pinModal')" aria-label="Close dialog">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
@@ -708,8 +723,8 @@
</div>
<div class="modal-body">
<div class="form-group">
<label class="form-label">4-Digit PIN</label>
<input type="password" class="form-input pin-input" id="pinInput" maxlength="4" placeholder="••••" inputmode="numeric">
<label class="form-label" for="pinInput">4-Digit PIN</label>
<input type="password" class="form-input pin-input" id="pinInput" maxlength="4" placeholder="••••" inputmode="numeric" aria-describedby="pinHelpText">
</div>
<button class="btn btn-primary" onclick="verifyPin()">Unlock</button>
<p id="pinHelpText" style="text-align: center; margin-top: 16px; font-size: 0.85rem; color: var(--soft-gray);"></p>
@@ -718,12 +733,12 @@
</div>
<!-- Set PIN Modal -->
<div class="modal-overlay" id="setPinModal">
<div class="modal-overlay" id="setPinModal" role="dialog" aria-modal="true" aria-labelledby="setPinModalTitle">
<div class="modal">
<div class="modal-header">
<h3 class="modal-title">Set Edit PIN</h3>
<button class="modal-close" onclick="closeModal('setPinModal')">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<h3 class="modal-title" id="setPinModalTitle">Set Edit PIN</h3>
<button class="modal-close" onclick="closeModal('setPinModal')" aria-label="Close dialog">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
@@ -734,11 +749,11 @@
Set a PIN that your household will use to edit the inventory. Anyone can view without the PIN.
</p>
<div class="form-group">
<label class="form-label">New 4-Digit PIN</label>
<label class="form-label" for="newPinInput">New 4-Digit PIN</label>
<input type="password" class="form-input pin-input" id="newPinInput" maxlength="4" placeholder="••••" inputmode="numeric">
</div>
<div class="form-group">
<label class="form-label">Confirm PIN</label>
<label class="form-label" for="confirmPinInput">Confirm PIN</label>
<input type="password" class="form-input pin-input" id="confirmPinInput" maxlength="4" placeholder="••••" inputmode="numeric">
</div>
<button class="btn btn-primary" onclick="setPin()">Save PIN</button>
@@ -747,12 +762,12 @@
</div>
<!-- Add/Edit Item Modal -->
<div class="modal-overlay" id="itemModal">
<div class="modal-overlay" id="itemModal" role="dialog" aria-modal="true" aria-labelledby="itemModalTitle">
<div class="modal">
<div class="modal-header">
<h3 class="modal-title" id="itemModalTitle">Add Item</h3>
<button class="modal-close" onclick="closeModal('itemModal')">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<button class="modal-close" onclick="closeModal('itemModal')" aria-label="Close dialog">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
@@ -760,11 +775,11 @@
</div>
<div class="modal-body">
<div class="form-group">
<label class="form-label">Item Name</label>
<label class="form-label" for="itemName">Item Name</label>
<input type="text" class="form-input" id="itemName" placeholder="e.g., All Purpose Flour">
</div>
<div class="form-group">
<label class="form-label">Container Type</label>
<label class="form-label" for="itemContainer">Container Type</label>
<select class="form-select" id="itemContainer">
<!-- Options loaded from containers.json -->
</select>
@@ -924,14 +939,26 @@
const count = counts[key] || 0;
const item = document.createElement('div');
item.className = 'legend-item' + (containerFilter === key ? ' active' : '');
item.onclick = () => {
item.tabIndex = 0;
item.setAttribute('role', 'button');
item.setAttribute('aria-pressed', containerFilter === key ? 'true' : 'false');
item.setAttribute('aria-label', `Filter by ${value.category}: ${value.name}, ${count} items`);
const toggleFilter = () => {
containerFilter = containerFilter === key ? null : key;
renderContainerLegend();
renderInventory();
};
item.onclick = toggleFilter;
item.onkeydown = (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
toggleFilter();
}
};
const dot = document.createElement('div');
dot.className = 'container-dot';
dot.style.background = value.color;
dot.setAttribute('aria-hidden', 'true');
const span = document.createElement('span');
span.textContent = `${value.category}: ${value.name} (${count})`;
item.appendChild(dot);
@@ -981,11 +1008,11 @@
}
list.innerHTML = filtered.map(item => `
<div class="inventory-item ${item.outOfStock ? 'out-of-stock' : ''}" onclick="openEditModal(${item.id})">
<div class="inventory-item ${item.outOfStock ? 'out-of-stock' : ''}" onclick="openEditModal(${item.id})" onkeydown="if(event.key==='Enter'||event.key===' '){event.preventDefault();openEditModal(${item.id});}" tabindex="0" role="button" aria-label="${escapeHtml(item.name)}, ${item.outOfStock ? 'out of stock' : 'in stock'}">
<div class="item-info">
<div class="item-name">${escapeHtml(item.name)}</div>
<div class="item-container">
<div class="container-dot" style="background: ${containerTypes[item.container]?.color || '#ccc'}"></div>
<div class="container-dot" style="background: ${containerTypes[item.container]?.color || '#ccc'}" aria-hidden="true"></div>
${containerTypes[item.container] ? `${containerTypes[item.container].category}: ${containerTypes[item.container].name}` : 'Unknown'}
</div>
</div>