Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .jules/palette.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 2024-05-23 - Accessible Toast Notifications
**Learning:** Toast notifications often disappear too quickly for some users. Implementing a 5000ms minimum duration AND a manual close button ensures compliance with accessibility standards (WCAG 2.2.1 Timing Adjustable) and improves usability for everyone.
**Action:** When implementing temporary feedback messages, always include a visual close button and ensure the timeout is sufficient (>= 5000ms), or allow user preference to extend it.

## 2024-04-10 - Keyboard Shortcuts and DOM Node Preservation
**Learning:** When adding visual `<kbd>` hints to buttons for keyboard shortcuts, using `textContent` for state saving (like loading text) strips away the nested HTML tags.
**Action:** Preserve structural integrity by saving `childNodes` (e.g., `Array.from(element.childNodes)`) and restoring them via `replaceChildren(...)` instead of `textContent`. Always implement cross-platform (Ctrl/Meta) listeners, use `event.preventDefault()`, and programmatically focus the triggered element for keyboard shortcuts.
32 changes: 20 additions & 12 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,8 @@ <h1>Welcome to 5ive</h1>
<p>Sample accessible button component:</p>

<!-- βœ… GOOD UX: Semantic button with proper labeling and focus handling -->
<button type="button" class="btn" id="action-btn">
Click Me (Accessible)
<button type="button" class="btn" id="action-btn" aria-keyshortcuts="Control+Enter Meta+Enter">
Click Me (Accessible) <kbd>Ctrl</kbd>+<kbd>Enter</kbd>
</button>

<div id="feedback" class="feedback" aria-live="polite"></div>
Expand All @@ -122,6 +122,20 @@ <h1>Welcome to 5ive</h1>
const feedback = document.getElementById('feedback');
let feedbackTimeout;

// Pre-create elements for optimization
const cachedSpinner = document.createElement('span');
cachedSpinner.className = 'spinner';
cachedSpinner.setAttribute('aria-hidden', 'true');
const loadingText = document.createTextNode('Loading...');

document.addEventListener('keydown', (e) => {
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
e.preventDefault();
actionBtn.focus();
actionBtn.click();
}
});

actionBtn.addEventListener('click', () => {
// Clear any existing timeout
if (feedbackTimeout) clearTimeout(feedbackTimeout);
Expand All @@ -131,24 +145,18 @@ <h1>Welcome to 5ive</h1>
feedback.textContent = '';

// Set loading state
// βœ… Sentinel: Avoid innerHTML to prevent XSS
const originalText = actionBtn.textContent;
// βœ… Sentinel: Avoid innerHTML to prevent XSS, preserve nodes
const originalNodes = Array.from(actionBtn.childNodes);
actionBtn.disabled = true;
actionBtn.setAttribute('aria-busy', 'true');
actionBtn.textContent = '';

const spinner = document.createElement('span');
spinner.className = 'spinner';
spinner.setAttribute('aria-hidden', 'true');
actionBtn.appendChild(spinner);
actionBtn.appendChild(document.createTextNode('Loading...'));
actionBtn.replaceChildren(cachedSpinner, loadingText);

// Simulate async operation
setTimeout(() => {
// Reset state
actionBtn.disabled = false;
actionBtn.removeAttribute('aria-busy');
actionBtn.textContent = originalText;
actionBtn.replaceChildren(...originalNodes);

// Show success with icon and transition
// βœ… Sentinel: Using textContent and DOM methods instead of innerHTML
Expand Down