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:
87
index.html
87
index.html
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user