audit pass 9: XSS fix, focus management, ARIA labels, and :focus-visible gaps
JS: - Lightbox: remove keydown listener on close (memory leak fix) - Lightbox: restore focus to trigger on close - Right drawer: fix aria-hidden="false" anti-pattern to removeAttribute - Markdown renderer: block javascript:/data: protocol URIs in link and image replacements to prevent XSS - Sidebar submenus: add aria-expanded tracking on toggle; hide decorative chevron from screen readers; initialize aria-expanded on mount CSS: - Add :focus-visible to .lt-sidebar-toggle (interactive button) - Add :focus-visible to .lt-dropzone (focusable container) - Fix .lt-stat-card:focus-visible outline-offset to -2px (clip-path clips it) - Add light theme override for .lt-nav-drawer-link:focus-visible - Adjust .lt-split-divider:focus-visible outline-offset to 3px HTML: - Range input: update aria-valuenow dynamically on input event - Combobox label: add for="demo-combobox-input" association - Typeahead label: add for="demo-typeahead-input" association - Dropzone file input: add aria-label - Notification items: add descriptive aria-label to all 4 items; add aria-hidden="true" to decorative dot spans - Mark all read button: add type="button" to prevent accidental form submit Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1616,7 +1616,7 @@
|
||||
const ov = document.getElementById(ovId);
|
||||
if (_mnOpen) _mnSetOpen(false);
|
||||
drawer.classList.add('is-open');
|
||||
drawer.setAttribute('aria-hidden', 'false');
|
||||
drawer.removeAttribute('aria-hidden');
|
||||
if (ov) ov.classList.add('is-open');
|
||||
_lockScroll();
|
||||
if (triggerEl) _modalTriggers.set(drawer, triggerEl);
|
||||
@@ -2175,12 +2175,19 @@
|
||||
if (!label) return;
|
||||
label.setAttribute('tabindex', '0');
|
||||
label.setAttribute('role', 'button');
|
||||
const _toggle = () => group.classList.toggle('is-open');
|
||||
const chevron = label.querySelector('.chevron, .lt-sidebar-chevron');
|
||||
if (chevron) chevron.setAttribute('aria-hidden', 'true');
|
||||
const _toggle = () => {
|
||||
group.classList.toggle('is-open');
|
||||
label.setAttribute('aria-expanded', group.classList.contains('is-open') ? 'true' : 'false');
|
||||
};
|
||||
label.setAttribute('aria-expanded', group.classList.contains('is-open') ? 'true' : 'false');
|
||||
label.addEventListener('click', _toggle);
|
||||
label.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); _toggle(); } });
|
||||
// Open group if it contains the active link
|
||||
if (group.querySelector('.lt-sidebar-sub-link.active, .lt-sidebar-sub-link[aria-current="page"]')) {
|
||||
group.classList.add('is-open');
|
||||
label.setAttribute('aria-expanded', 'true');
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -2581,6 +2588,7 @@
|
||||
if (!_overlay) return;
|
||||
_overlay.classList.remove('is-open');
|
||||
_unlockScroll();
|
||||
if (_lbKeyBound) { document.removeEventListener('keydown', _lbKeyBound); _lbKeyBound = null; }
|
||||
if (_lbTrigger && document.contains(_lbTrigger)) { _lbTrigger.focus(); }
|
||||
_lbTrigger = null;
|
||||
},
|
||||
@@ -2713,10 +2721,16 @@
|
||||
.replace(/\*(.+?)\*/g, '<em>$1</em>')
|
||||
.replace(/__(.+?)__/g, '<strong>$1</strong>')
|
||||
.replace(/_(.+?)_/g, '<em>$1</em>')
|
||||
// Links
|
||||
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>')
|
||||
// Images
|
||||
.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<img src="$2" alt="$1" style="max-width:100%">')
|
||||
// Links — block javascript: and data: URIs
|
||||
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, text, url) => {
|
||||
const safeUrl = /^(https?:\/\/|\/|#|\.\.?\/)/i.test(url) ? url : '#';
|
||||
return `<a href="${safeUrl}" target="_blank" rel="noopener noreferrer">${text}</a>`;
|
||||
})
|
||||
// Images — block javascript: and data: URIs
|
||||
.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (_, alt, src) => {
|
||||
const safeSrc = /^(https?:\/\/|\/|\.\.?\/)/i.test(src) ? src : '';
|
||||
return `<img src="${safeSrc}" alt="${alt}" style="max-width:100%">`;
|
||||
})
|
||||
// Blockquote
|
||||
.replace(/^>\s(.+)$/gm, '<blockquote>$1</blockquote>')
|
||||
// Horizontal rule
|
||||
|
||||
Reference in New Issue
Block a user