personal memory agent
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

a11y: add form semantics, labels, and keyboard submit to support workspace

Wrap feedback and reply areas in <form> elements with visible labels, inline validation errors on empty submit, and Ctrl/Cmd+Enter keyboard submission. Adds novalidate with custom error handling to keep the existing showStatus() pattern for API errors.

+84 -27
+84 -27
apps/support/workspace.html
··· 161 161 } 162 162 .support-status-msg.success { color: #2e7d32; } 163 163 .support-status-msg.error { color: #c62828; } 164 + .support-field-error { 165 + font-size: 0.8rem; 166 + color: #c62828; 167 + margin-top: 0.25rem; 168 + display: none; 169 + } 170 + .support-keyboard-hint { 171 + font-size: 0.8rem; 172 + color: var(--muted, #888); 173 + margin-left: 0.5rem; 174 + } 164 175 165 176 /* Attachments */ 166 177 .support-attachments { ··· 251 262 <div id="section-feedback" class="support-section" role="tabpanel" aria-labelledby="tab-feedback"> 252 263 <p style="margin-bottom:1rem;">Share your impressions, ideas, or anything on your mind. Your feedback shapes the product.</p> 253 264 <div class="support-feedback-form"> 254 - <textarea id="feedback-text" placeholder="What's on your mind?"></textarea> 255 - <div class="support-feedback-options"> 256 - <label><input type="checkbox" id="feedback-anonymous"> Submit anonymously</label> 257 - </div> 258 - <button class="support-btn" id="feedback-submit">Send Feedback</button> 259 - <div id="feedback-status" class="support-status-msg"></div> 265 + <form id="feedback-form" novalidate> 266 + <label for="feedback-text">your feedback</label> 267 + <textarea id="feedback-text" placeholder="What's on your mind?" required></textarea> 268 + <div class="support-field-error" id="feedback-error">please write something before sending</div> 269 + <div class="support-feedback-options"> 270 + <label><input type="checkbox" id="feedback-anonymous"> Submit anonymously</label> 271 + </div> 272 + <button class="support-btn" id="feedback-submit" type="submit">Send Feedback</button> 273 + <span class="support-keyboard-hint">ctrl+enter to send</span> 274 + <div id="feedback-status" class="support-status-msg"></div> 275 + </form> 260 276 </div> 261 277 </div> 262 278 ··· 424 440 425 441 if (t.status !== 'resolved') { 426 442 html += `<div class="support-reply-form"> 427 - <textarea id="reply-text" placeholder="Write a reply..."></textarea> 428 - <div class="support-drop-zone" id="attach-zone" role="button" tabindex="0" aria-label="Attach files — drop files here or click to browse"> 429 - <input type="file" id="attach-input" multiple accept=".png,.jpg,.jpeg,.gif,.webp,.svg,.pdf,.txt,.csv,.html,.md,.xml,.json"> 430 - Drop files here or click to attach (max 10 MB each, up to 5 files) 431 - </div> 432 - <div class="support-file-list" id="attach-file-list"></div> 433 - <div style="display:flex;gap:0.5rem;margin-top:0.5rem;"> 434 - <button class="support-btn" id="reply-submit">Send Reply</button> 435 - <button class="support-btn support-btn-secondary" id="attach-only-submit" style="display:none;">Upload Files Only</button> 436 - </div> 437 - <div id="reply-status" class="support-status-msg"></div> 443 + <form id="reply-form" novalidate> 444 + <label for="reply-text">your reply</label> 445 + <textarea id="reply-text" placeholder="Write a reply..." required></textarea> 446 + <div class="support-field-error" id="reply-error">please write something before sending</div> 447 + <div class="support-drop-zone" id="attach-zone" role="button" tabindex="0" aria-label="Attach files — drop files here or click to browse"> 448 + <input type="file" id="attach-input" multiple accept=".png,.jpg,.jpeg,.gif,.webp,.svg,.pdf,.txt,.csv,.html,.md,.xml,.json"> 449 + Drop files here or click to attach (max 10 MB each, up to 5 files) 450 + </div> 451 + <div class="support-file-list" id="attach-file-list"></div> 452 + <div style="display:flex;gap:0.5rem;margin-top:0.5rem;align-items:center;"> 453 + <button class="support-btn" id="reply-submit" type="submit">Send Reply</button> 454 + <button class="support-btn support-btn-secondary" id="attach-only-submit" type="button" style="display:none;">Upload Files Only</button> 455 + <span class="support-keyboard-hint">ctrl+enter to send</span> 456 + </div> 457 + <div id="reply-status" class="support-status-msg"></div> 458 + </form> 438 459 </div>`; 439 460 } 440 461 ··· 535 556 return uploaded; 536 557 } 537 558 559 + const replyForm = document.getElementById('reply-form'); 560 + const replyError = document.getElementById('reply-error'); 561 + const replyText = document.getElementById('reply-text'); 538 562 const replyBtn = document.getElementById('reply-submit'); 539 - if (replyBtn) { 540 - replyBtn.addEventListener('click', async () => { 541 - const text = document.getElementById('reply-text').value.trim(); 542 - if (!text && !pendingFiles.length) return; 563 + if (replyForm) { 564 + replyForm.addEventListener('submit', async e => { 565 + e.preventDefault(); 566 + const text = replyText.value.trim(); 567 + if (!text && !pendingFiles.length) { 568 + replyError.style.display = 'block'; 569 + replyText.focus(); 570 + return; 571 + } 572 + replyError.style.display = 'none'; 543 573 replyBtn.disabled = true; 544 574 if (attachOnlyBtn) attachOnlyBtn.disabled = true; 545 575 const status = document.getElementById('reply-status'); ··· 570 600 replyBtn.disabled = false; 571 601 if (attachOnlyBtn) attachOnlyBtn.disabled = false; 572 602 }); 603 + replyText.addEventListener('input', () => { 604 + replyError.style.display = 'none'; 605 + }); 606 + replyText.addEventListener('keydown', e => { 607 + if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { 608 + e.preventDefault(); 609 + replyForm.requestSubmit(); 610 + } 611 + }); 573 612 } 574 613 575 614 // Upload files only (no reply text) ··· 600 639 } 601 640 602 641 // Feedback submission 603 - document.getElementById('feedback-submit').addEventListener('click', async function() { 604 - const text = document.getElementById('feedback-text').value.trim(); 605 - if (!text) return; 642 + const feedbackForm = document.getElementById('feedback-form'); 643 + const feedbackError = document.getElementById('feedback-error'); 644 + const feedbackTextEl = document.getElementById('feedback-text'); 645 + feedbackForm.addEventListener('submit', async e => { 646 + e.preventDefault(); 647 + const text = feedbackTextEl.value.trim(); 648 + if (!text) { 649 + feedbackError.style.display = 'block'; 650 + feedbackTextEl.focus(); 651 + return; 652 + } 653 + feedbackError.style.display = 'none'; 606 654 const anon = document.getElementById('feedback-anonymous').checked; 607 655 const status = document.getElementById('feedback-status'); 608 - this.disabled = true; 656 + document.getElementById('feedback-submit').disabled = true; 609 657 610 658 try { 611 659 const resp = await fetch('/app/support/api/feedback', { ··· 616 664 if (resp.ok) { 617 665 status.textContent = 'Thanks for your feedback!'; 618 666 status.className = 'support-status-msg success'; 619 - document.getElementById('feedback-text').value = ''; 667 + feedbackTextEl.value = ''; 620 668 } else { 621 669 throw new Error('Failed'); 622 670 } ··· 624 672 status.textContent = 'Failed to submit feedback.'; 625 673 status.className = 'support-status-msg error'; 626 674 } 627 - this.disabled = false; 675 + document.getElementById('feedback-submit').disabled = false; 676 + }); 677 + feedbackTextEl.addEventListener('input', () => { 678 + feedbackError.style.display = 'none'; 679 + }); 680 + feedbackTextEl.addEventListener('keydown', e => { 681 + if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { 682 + e.preventDefault(); 683 + feedbackForm.requestSubmit(); 684 + } 628 685 }); 629 686 630 687 // Load announcements