mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-25 01:11:09 -05:00
Add secret management guide, client-side performance monitoring, and search accessibility enhancements
- 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.
This commit is contained in:
220
static/js/search-accessibility.js
Normal file
220
static/js/search-accessibility.js
Normal file
@@ -0,0 +1,220 @@
|
||||
/**
|
||||
* Search Results Keyboard Navigation
|
||||
* Handles Arrow Up/Down, Enter, and Escape keys for accessible search
|
||||
*
|
||||
* This module enhances search inputs with keyboard navigation for WCAG compliance:
|
||||
* - Arrow Down: Navigate to next search result
|
||||
* - Arrow Up: Navigate to previous search result (or back to input)
|
||||
* - Enter: Select current result (navigate to link)
|
||||
* - Escape: Close search results and blur input
|
||||
*
|
||||
* Usage:
|
||||
* The script automatically initializes on DOMContentLoaded for all search inputs
|
||||
* with hx-target attribute pointing to a results container.
|
||||
*
|
||||
* HTMX Integration:
|
||||
* Results should include role="option" on each selectable item.
|
||||
* The script listens for htmx:afterSwap to reinitialize when results change.
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Initialize keyboard navigation for a search input
|
||||
* @param {HTMLInputElement} input - The search input element
|
||||
*/
|
||||
function initSearchAccessibility(input) {
|
||||
const targetSelector = input.getAttribute('hx-target');
|
||||
if (!targetSelector) return;
|
||||
|
||||
const resultsContainer = document.querySelector(targetSelector);
|
||||
if (!resultsContainer) return;
|
||||
|
||||
// Track if we've already initialized this input
|
||||
if (input.dataset.searchAccessibilityInit) return;
|
||||
input.dataset.searchAccessibilityInit = 'true';
|
||||
|
||||
let currentIndex = -1;
|
||||
|
||||
/**
|
||||
* Update visual and ARIA selection state for results
|
||||
* @param {NodeList} results - List of result option elements
|
||||
* @param {number} index - Index of currently selected item (-1 for none)
|
||||
*/
|
||||
function updateSelection(results, index) {
|
||||
results.forEach((result, i) => {
|
||||
if (i === index) {
|
||||
result.setAttribute('aria-selected', 'true');
|
||||
result.classList.add('bg-accent');
|
||||
result.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
||||
} else {
|
||||
result.setAttribute('aria-selected', 'false');
|
||||
result.classList.remove('bg-accent');
|
||||
}
|
||||
});
|
||||
|
||||
// Update aria-activedescendant on the input
|
||||
if (index >= 0 && results[index]) {
|
||||
const resultId = results[index].id || `search-result-${index}`;
|
||||
if (!results[index].id) {
|
||||
results[index].id = resultId;
|
||||
}
|
||||
input.setAttribute('aria-activedescendant', resultId);
|
||||
} else {
|
||||
input.removeAttribute('aria-activedescendant');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Announce result count to screen readers
|
||||
* @param {number} count - Number of results found
|
||||
*/
|
||||
function announceResults(count) {
|
||||
const statusId = input.getAttribute('aria-describedby');
|
||||
if (statusId) {
|
||||
const statusEl = document.getElementById(statusId);
|
||||
if (statusEl) {
|
||||
if (count === 0) {
|
||||
statusEl.textContent = 'No results found';
|
||||
} else if (count === 1) {
|
||||
statusEl.textContent = '1 result found. Use arrow keys to navigate.';
|
||||
} else {
|
||||
statusEl.textContent = `${count} results found. Use arrow keys to navigate.`;
|
||||
}
|
||||
// Clear after a delay to allow re-announcement on new searches
|
||||
setTimeout(() => {
|
||||
statusEl.textContent = '';
|
||||
}, 1500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle keydown events for navigation
|
||||
* @param {KeyboardEvent} e - The keyboard event
|
||||
*/
|
||||
function handleKeydown(e) {
|
||||
const results = resultsContainer.querySelectorAll('[role="option"]');
|
||||
|
||||
// If no results and not escape, let default behavior happen
|
||||
if (results.length === 0 && e.key !== 'Escape') {
|
||||
return;
|
||||
}
|
||||
|
||||
switch(e.key) {
|
||||
case 'ArrowDown':
|
||||
e.preventDefault();
|
||||
if (results.length > 0) {
|
||||
currentIndex = Math.min(currentIndex + 1, results.length - 1);
|
||||
updateSelection(results, currentIndex);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'ArrowUp':
|
||||
e.preventDefault();
|
||||
if (currentIndex > 0) {
|
||||
currentIndex = currentIndex - 1;
|
||||
updateSelection(results, currentIndex);
|
||||
} else if (currentIndex === 0) {
|
||||
// Move back to input
|
||||
currentIndex = -1;
|
||||
updateSelection(results, currentIndex);
|
||||
input.focus();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'Enter':
|
||||
if (currentIndex >= 0 && results[currentIndex]) {
|
||||
e.preventDefault();
|
||||
const link = results[currentIndex].querySelector('a');
|
||||
if (link) {
|
||||
link.click();
|
||||
} else if (results[currentIndex].tagName === 'A') {
|
||||
results[currentIndex].click();
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'Escape':
|
||||
e.preventDefault();
|
||||
// Clear results and reset
|
||||
resultsContainer.innerHTML = '';
|
||||
currentIndex = -1;
|
||||
input.blur();
|
||||
break;
|
||||
|
||||
case 'Home':
|
||||
if (results.length > 0) {
|
||||
e.preventDefault();
|
||||
currentIndex = 0;
|
||||
updateSelection(results, currentIndex);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'End':
|
||||
if (results.length > 0) {
|
||||
e.preventDefault();
|
||||
currentIndex = results.length - 1;
|
||||
updateSelection(results, currentIndex);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Add keydown listener
|
||||
input.addEventListener('keydown', handleKeydown);
|
||||
|
||||
// Reset index when input changes
|
||||
input.addEventListener('input', function() {
|
||||
currentIndex = -1;
|
||||
});
|
||||
|
||||
// Reset on focus
|
||||
input.addEventListener('focus', function() {
|
||||
currentIndex = -1;
|
||||
});
|
||||
|
||||
// Listen for HTMX swap events to announce results
|
||||
resultsContainer.addEventListener('htmx:afterSwap', function() {
|
||||
currentIndex = -1;
|
||||
const results = resultsContainer.querySelectorAll('[role="option"]');
|
||||
announceResults(results.length);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize all search inputs on the page
|
||||
*/
|
||||
function initAllSearchInputs() {
|
||||
const searchInputs = document.querySelectorAll('input[type="search"][hx-target], input[type="text"][hx-target][name="q"]');
|
||||
searchInputs.forEach(initSearchAccessibility);
|
||||
}
|
||||
|
||||
// Initialize on DOMContentLoaded
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initAllSearchInputs);
|
||||
} else {
|
||||
initAllSearchInputs();
|
||||
}
|
||||
|
||||
// Re-initialize after HTMX swaps that might add new search inputs
|
||||
document.body.addEventListener('htmx:afterSettle', function(evt) {
|
||||
// Only reinitialize if new search inputs were added
|
||||
if (evt.target.querySelector && evt.target.querySelector('input[type="search"], input[name="q"]')) {
|
||||
initAllSearchInputs();
|
||||
}
|
||||
});
|
||||
|
||||
// Also listen for status announcements from HTMX responses
|
||||
document.body.addEventListener('announceStatus', function(evt) {
|
||||
const announcer = document.getElementById('status-announcer');
|
||||
if (announcer && evt.detail && evt.detail.message) {
|
||||
announcer.textContent = evt.detail.message;
|
||||
setTimeout(() => {
|
||||
announcer.textContent = '';
|
||||
}, 1500);
|
||||
}
|
||||
});
|
||||
|
||||
})();
|
||||
Reference in New Issue
Block a user