audit pass 10-11: type=button, XSS escaping, focus/ARIA fixes
HTML: - Add type="button" to all buttons outside forms (22 instances) - Add aria-label="Add comment" to unlabelled textarea#td-comment JS: - Escape alt text and link text in markdown renderer with escHtml() to prevent XSS in image alt/link content - Fix nested modal focus: only restore trigger focus when no other modal is still open; add document.contains guard CSS: - Add .lt-nav-link:focus-visible focus ring (was missing entirely) - Fix .lt-typeahead-option (dead selector) → .lt-typeahead-item with :hover, .is-focused, and :focus-visible for light theme Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -487,6 +487,7 @@ hr {
|
||||
background: var(--accent-cyan-dim);
|
||||
}
|
||||
.lt-nav-link:hover::after { left: 0; right: 0; box-shadow: var(--glow-cyan); }
|
||||
.lt-nav-link:focus-visible { outline: 2px solid var(--accent-cyan); outline-offset: -2px; color: var(--accent-cyan); }
|
||||
|
||||
.lt-nav-link.active {
|
||||
color: var(--accent-orange);
|
||||
@@ -3884,7 +3885,9 @@ html[data-theme="light"] .lt-empty-state-title { color: var(--text-secondary); }
|
||||
html[data-theme="light"] .lt-combobox-dropdown,
|
||||
html[data-theme="light"] .lt-typeahead-dropdown { background: var(--bg-card); border-color: var(--border-color); box-shadow: 0 4px 16px rgba(0,0,0,0.1); }
|
||||
html[data-theme="light"] .lt-combobox-option:hover,
|
||||
html[data-theme="light"] .lt-typeahead-option:hover { background: var(--accent-cyan-dim); }
|
||||
html[data-theme="light"] .lt-typeahead-item:hover,
|
||||
html[data-theme="light"] .lt-typeahead-item.is-focused,
|
||||
html[data-theme="light"] .lt-typeahead-item:focus-visible { background: var(--accent-cyan-dim); color: var(--accent-cyan); }
|
||||
html[data-theme="light"] .lt-combobox-tag { background: var(--accent-cyan-dim); color: var(--accent-cyan); border-color: var(--accent-cyan-border); }
|
||||
|
||||
/* — Sortable ghost — */
|
||||
|
||||
@@ -159,7 +159,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="lt-notif-panel-footer">
|
||||
<button class="lt-btn lt-btn-sm lt-btn-ghost" style="width:100%;font-size:0.72rem">View all notifications</button>
|
||||
<button type="button" class="lt-btn lt-btn-sm lt-btn-ghost" style="width:100%;font-size:0.72rem">View all notifications</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -225,7 +225,7 @@
|
||||
<!-- Add a comment -->
|
||||
<div class="lt-divider-label" style="margin:1rem 0 0.75rem">Add Comment</div>
|
||||
<div class="lt-form-group" style="margin-bottom:0.5rem">
|
||||
<textarea id="td-comment" class="lt-input lt-textarea" rows="2" placeholder="Leave a comment…" style="resize:vertical"></textarea>
|
||||
<textarea id="td-comment" class="lt-input lt-textarea" rows="2" placeholder="Leave a comment…" style="resize:vertical" aria-label="Add comment"></textarea>
|
||||
</div>
|
||||
<button type="button" class="lt-btn lt-btn-sm" onclick="
|
||||
const c=document.getElementById('td-comment');
|
||||
@@ -267,7 +267,7 @@
|
||||
<h1 class="lt-page-title">Dashboard</h1>
|
||||
<div class="lt-btn-group">
|
||||
<a href="/create" class="lt-btn lt-btn-primary">New Ticket</a>
|
||||
<button class="lt-btn lt-btn-sm" data-modal-open="export-modal">Export</button>
|
||||
<button type="button" class="lt-btn lt-btn-sm" data-modal-open="export-modal">Export</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -370,8 +370,8 @@
|
||||
</div>
|
||||
|
||||
<div class="lt-btn-group">
|
||||
<button class="lt-btn lt-btn-sm">Apply</button>
|
||||
<button class="lt-btn lt-btn-sm lt-btn-ghost">Reset</button>
|
||||
<button type="button" class="lt-btn lt-btn-sm">Apply</button>
|
||||
<button type="button" class="lt-btn lt-btn-sm lt-btn-ghost">Reset</button>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
@@ -388,7 +388,7 @@
|
||||
</div>
|
||||
<!-- Advanced filter dropdown -->
|
||||
<div class="lt-dropdown-wrap" id="adv-filter-wrap">
|
||||
<button class="lt-btn lt-btn-sm lt-dropdown-trigger" id="adv-filter-btn" aria-expanded="false" aria-haspopup="true">Advanced ▾</button>
|
||||
<button type="button" class="lt-btn lt-btn-sm lt-dropdown-trigger" id="adv-filter-btn" aria-expanded="false" aria-haspopup="true">Advanced ▾</button>
|
||||
<div class="lt-dropdown-panel" id="adv-filter-panel" aria-hidden="true">
|
||||
<div style="padding:0.75rem;display:grid;gap:0.5rem;width:clamp(200px,60vw,260px)">
|
||||
<div class="lt-form-group" style="margin:0">
|
||||
@@ -433,7 +433,7 @@
|
||||
<span class="lt-text-muted lt-text-xs lt-hide-xs" id="ticket-result-count">42 results</span>
|
||||
<!-- Bulk actions dropdown -->
|
||||
<div class="lt-dropdown-wrap" id="bulk-action-wrap">
|
||||
<button class="lt-btn lt-btn-sm lt-btn-ghost lt-dropdown-trigger" id="bulk-action-btn" aria-expanded="false" aria-haspopup="true">Bulk Actions ▾</button>
|
||||
<button type="button" class="lt-btn lt-btn-sm lt-btn-ghost lt-dropdown-trigger" id="bulk-action-btn" aria-expanded="false" aria-haspopup="true">Bulk Actions ▾</button>
|
||||
<div class="lt-dropdown-panel lt-dropdown-panel--right" id="bulk-action-panel" aria-hidden="true">
|
||||
<button type="button" class="lt-dropdown-item" onclick="lt.toast.success('Closed selected tickets');this.closest('.lt-dropdown-panel').setAttribute('aria-hidden','true')">✓ Close Selected</button>
|
||||
<button type="button" class="lt-dropdown-item" onclick="lt.toast.info('Reassign dialog…');this.closest('.lt-dropdown-panel').setAttribute('aria-hidden','true')">↩ Reassign…</button>
|
||||
@@ -486,7 +486,7 @@
|
||||
<td data-label="Actions">
|
||||
<div class="lt-btn-group">
|
||||
<a href="/ticket/123456789" class="lt-btn lt-btn-sm">View</a>
|
||||
<button class="lt-btn lt-btn-sm lt-btn-danger">Close</button>
|
||||
<button type="button" class="lt-btn lt-btn-sm lt-btn-danger">Close</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -692,12 +692,12 @@
|
||||
<div class="lt-subsection-header">Buttons</div>
|
||||
<div class="lt-section-body">
|
||||
<div class="lt-btn-group">
|
||||
<button class="lt-btn">Default</button>
|
||||
<button class="lt-btn lt-btn-primary">Primary</button>
|
||||
<button class="lt-btn lt-btn-danger">Danger</button>
|
||||
<button class="lt-btn lt-btn-sm">Small</button>
|
||||
<button class="lt-btn lt-btn-ghost">Ghost</button>
|
||||
<button class="lt-btn" disabled>Disabled</button>
|
||||
<button type="button" class="lt-btn">Default</button>
|
||||
<button type="button" class="lt-btn lt-btn-primary">Primary</button>
|
||||
<button type="button" class="lt-btn lt-btn-danger">Danger</button>
|
||||
<button type="button" class="lt-btn lt-btn-sm">Small</button>
|
||||
<button type="button" class="lt-btn lt-btn-ghost">Ghost</button>
|
||||
<button type="button" class="lt-btn" disabled>Disabled</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -780,8 +780,8 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="lt-btn-group">
|
||||
<button class="lt-btn lt-btn-primary">Submit</button>
|
||||
<button class="lt-btn lt-btn-ghost">Cancel</button>
|
||||
<button type="button" class="lt-btn lt-btn-primary">Submit</button>
|
||||
<button type="button" class="lt-btn lt-btn-ghost">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
@@ -876,21 +876,21 @@
|
||||
<div class="lt-section-body">
|
||||
<div>
|
||||
<div class="lt-accordion">
|
||||
<button class="lt-accordion-header" aria-expanded="false" aria-controls="acc-body-1" data-accordion>
|
||||
<button type="button" class="lt-accordion-header" aria-expanded="false" aria-controls="acc-body-1" data-accordion>
|
||||
SYSTEM OVERVIEW
|
||||
<svg class="lt-accordion-icon" viewBox="0 0 10 6" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path d="M1 1l4 4 4-4"/></svg>
|
||||
</button>
|
||||
<div class="lt-accordion-body" id="acc-body-1"><div class="lt-accordion-content">Node running at 72% CPU. 12 active processes. Last restart: 3d ago.</div></div>
|
||||
</div>
|
||||
<div class="lt-accordion">
|
||||
<button class="lt-accordion-header" aria-expanded="false" aria-controls="acc-body-2" data-accordion>
|
||||
<button type="button" class="lt-accordion-header" aria-expanded="false" aria-controls="acc-body-2" data-accordion>
|
||||
NETWORK CONFIG
|
||||
<svg class="lt-accordion-icon" viewBox="0 0 10 6" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path d="M1 1l4 4 4-4"/></svg>
|
||||
</button>
|
||||
<div class="lt-accordion-body" id="acc-body-2"><div class="lt-accordion-content">eth0: 10.0.0.7 — MTU 1500 — RX 4.2 GB — TX 1.1 GB</div></div>
|
||||
</div>
|
||||
<div class="lt-accordion">
|
||||
<button class="lt-accordion-header" aria-expanded="false" aria-controls="acc-body-3" data-accordion>
|
||||
<button type="button" class="lt-accordion-header" aria-expanded="false" aria-controls="acc-body-3" data-accordion>
|
||||
FIREWALL RULES
|
||||
<svg class="lt-accordion-icon" viewBox="0 0 10 6" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path d="M1 1l4 4 4-4"/></svg>
|
||||
</button>
|
||||
@@ -1039,15 +1039,15 @@
|
||||
<div style="display:flex;flex-direction:column;gap:var(--space-md)">
|
||||
<div style="display:flex;gap:var(--space-md);align-items:center;flex-wrap:wrap">
|
||||
<div class="lt-badge-wrap">
|
||||
<button class="lt-btn lt-btn-sm">Alerts</button>
|
||||
<button type="button" class="lt-btn lt-btn-sm">Alerts</button>
|
||||
<span class="lt-badge">3</span>
|
||||
</div>
|
||||
<div class="lt-badge-wrap">
|
||||
<button class="lt-btn lt-btn-sm">Messages</button>
|
||||
<button type="button" class="lt-btn lt-btn-sm">Messages</button>
|
||||
<span class="lt-badge lt-badge--orange">12</span>
|
||||
</div>
|
||||
<div class="lt-badge-wrap">
|
||||
<button class="lt-btn lt-btn-sm">Updates</button>
|
||||
<button type="button" class="lt-btn lt-btn-sm">Updates</button>
|
||||
<span class="lt-badge lt-badge--cyan">5</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1175,7 +1175,7 @@
|
||||
</div>
|
||||
<div class="lt-section-body">
|
||||
<div class="lt-grid lt-grid-3" style="gap:1rem">
|
||||
<div class="lt-card"><div class="lt-empty-state"><div class="lt-empty-state-icon">📭</div><div class="lt-empty-state-title">No Tickets Found</div><div class="lt-empty-state-body">No tickets match your current filters.</div><button class="lt-btn lt-btn-sm lt-btn-primary">Clear Filters</button></div></div>
|
||||
<div class="lt-card"><div class="lt-empty-state"><div class="lt-empty-state-icon">📭</div><div class="lt-empty-state-title">No Tickets Found</div><div class="lt-empty-state-body">No tickets match your current filters.</div><button type="button" class="lt-btn lt-btn-sm lt-btn-primary">Clear Filters</button></div></div>
|
||||
<div class="lt-card"><div class="lt-empty-state"><div class="lt-empty-state-icon">🔌</div><div class="lt-empty-state-title">No Workers Online</div><div class="lt-empty-state-body">All workers are offline or unreachable.</div><button type="button" class="lt-btn lt-btn-sm" onclick="lt.toast.warning('Checking workers…')">Retry</button></div></div>
|
||||
<div class="lt-card"><div class="lt-empty-state lt-empty-state--sm"><div class="lt-empty-state-icon">🗂</div><div class="lt-empty-state-title">No Results</div><div class="lt-empty-state-body">Try a different search term.</div></div></div>
|
||||
</div>
|
||||
@@ -1204,7 +1204,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="lt-flex lt-gap-md lt-align-center lt-wrap">
|
||||
<span class="lt-notif-wrap" id="demo-notif-btn"><button class="lt-btn lt-btn-sm">🔔 Alerts</button></span>
|
||||
<span class="lt-notif-wrap" id="demo-notif-btn"><button type="button" class="lt-btn lt-btn-sm">🔔 Alerts</button></span>
|
||||
<button type="button" class="lt-btn lt-btn-sm" onclick="lt.notif.inc('#demo-notif-btn')">+1 Badge</button>
|
||||
<button type="button" class="lt-btn lt-btn-sm" onclick="lt.notif.clear('#demo-notif-btn')">Clear</button>
|
||||
<button type="button" class="lt-btn lt-btn-sm" onclick="lt.notif.inc('#lt-notif-bell')">+1 Header Bell</button>
|
||||
@@ -1271,7 +1271,7 @@
|
||||
<div data-context-menu="demo-ctx" class="lt-card" style="padding:0.75rem 1.25rem;cursor:context-menu;border-style:dashed;display:inline-block">
|
||||
Right-click this card ›
|
||||
</div>
|
||||
<button class="lt-btn lt-btn-sm" id="demo-ctx-btn">Show Context Menu</button>
|
||||
<button type="button" class="lt-btn lt-btn-sm" id="demo-ctx-btn">Show Context Menu</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1433,9 +1433,9 @@
|
||||
</div>
|
||||
<!-- Nav -->
|
||||
<div class="lt-wizard-nav">
|
||||
<button class="lt-btn lt-btn-sm" data-wizard-prev disabled>← Back</button>
|
||||
<button class="lt-btn lt-btn-primary lt-btn-sm" data-wizard-next>Next →</button>
|
||||
<button class="lt-btn lt-btn-primary lt-btn-sm" data-wizard-done style="display:none">Submit ✓</button>
|
||||
<button type="button" class="lt-btn lt-btn-sm" data-wizard-prev disabled>← Back</button>
|
||||
<button type="button" class="lt-btn lt-btn-primary lt-btn-sm" data-wizard-next>Next →</button>
|
||||
<button type="button" class="lt-btn lt-btn-primary lt-btn-sm" data-wizard-done style="display:none">Submit ✓</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1484,8 +1484,8 @@
|
||||
<div class="lt-stat-label">Stopwatch</div>
|
||||
<div class="lt-countdown lt-num" id="demo-stopwatch">00:00:00</div>
|
||||
<div class="lt-flex lt-gap-xs" style="margin-top:0.5rem">
|
||||
<button class="lt-btn lt-btn-sm" id="sw-pause">Pause</button>
|
||||
<button class="lt-btn lt-btn-sm" id="sw-reset">Reset</button>
|
||||
<button type="button" class="lt-btn lt-btn-sm" id="sw-pause">Pause</button>
|
||||
<button type="button" class="lt-btn lt-btn-sm" id="sw-reset">Reset</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="lt-stat-card">
|
||||
|
||||
@@ -232,9 +232,14 @@
|
||||
_unlockScroll();
|
||||
// Remove trap handler
|
||||
if (el._ltTrapHandler) { el.removeEventListener('keydown', el._ltTrapHandler); delete el._ltTrapHandler; }
|
||||
// Return focus to trigger
|
||||
// Return focus to trigger (only if no other modal remains open)
|
||||
const trigger = _modalTriggers.get(el);
|
||||
if (trigger) { trigger.focus(); _modalTriggers.delete(el); }
|
||||
if (trigger) {
|
||||
_modalTriggers.delete(el);
|
||||
if (!document.querySelector('.lt-modal-overlay.is-open') && document.contains(trigger)) {
|
||||
trigger.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function closeAllModals() {
|
||||
@@ -1672,9 +1677,10 @@
|
||||
lt.contextMenu.register(selector, items)
|
||||
items = [{ label, icon, kbd, danger, divider, action }]
|
||||
================================================================ */
|
||||
let _ctxMenu = null;
|
||||
let _ctxMenu = null, _ctxTrigger = null;
|
||||
const _ctxItems = {};
|
||||
function _ctxShow(x, y, items) {
|
||||
function _ctxShow(x, y, items, trigger) {
|
||||
_ctxTrigger = trigger || (document.activeElement !== document.body ? document.activeElement : null);
|
||||
if (!_ctxMenu) {
|
||||
_ctxMenu = document.createElement('div');
|
||||
_ctxMenu.className = 'lt-context-menu';
|
||||
@@ -1706,6 +1712,8 @@
|
||||
}
|
||||
function _ctxHide() {
|
||||
if (_ctxMenu) _ctxMenu.classList.remove('is-open');
|
||||
if (_ctxTrigger && document.contains(_ctxTrigger)) { _ctxTrigger.focus(); }
|
||||
_ctxTrigger = null;
|
||||
}
|
||||
document.addEventListener('click', () => _ctxHide());
|
||||
document.addEventListener('contextmenu', e => {
|
||||
@@ -1714,7 +1722,7 @@
|
||||
e.preventDefault();
|
||||
const menuId = target.dataset.contextMenu;
|
||||
const items = _ctxItems[menuId];
|
||||
if (items) _ctxShow(e.clientX, e.clientY, items);
|
||||
if (items) _ctxShow(e.clientX, e.clientY, items, target);
|
||||
});
|
||||
document.addEventListener('keydown', e => { if (e.key === 'Escape') _ctxHide(); });
|
||||
const contextMenu = {
|
||||
@@ -2724,12 +2732,12 @@
|
||||
// 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>`;
|
||||
return `<a href="${safeUrl}" target="_blank" rel="noopener noreferrer">${escHtml(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%">`;
|
||||
return `<img src="${safeSrc}" alt="${escHtml(alt)}" style="max-width:100%">`;
|
||||
})
|
||||
// Blockquote
|
||||
.replace(/^>\s(.+)$/gm, '<blockquote>$1</blockquote>')
|
||||
@@ -2775,7 +2783,7 @@
|
||||
const pages = _pages();
|
||||
let html = '';
|
||||
// Prev
|
||||
html += `<button class="lt-page-btn" ${page <= 1 ? 'disabled' : ''} data-page="${page - 1}">«</button>`;
|
||||
html += `<button class="lt-page-btn" ${page <= 1 ? 'disabled' : ''} data-page="${page - 1}" aria-label="Previous page">«</button>`;
|
||||
// Page buttons with ellipsis
|
||||
const half = Math.floor((maxBtns - 2) / 2);
|
||||
let start = Math.max(2, page - half);
|
||||
@@ -2784,15 +2792,17 @@
|
||||
if (start === 2) end = Math.min(pages - 1, start + maxBtns - 3);
|
||||
else start = Math.max(2, end - maxBtns + 3);
|
||||
}
|
||||
html += `<button class="lt-page-btn${page === 1 ? ' active' : ''}" data-page="1">1</button>`;
|
||||
if (start > 2) html += `<button class="lt-page-btn" disabled>…</button>`;
|
||||
html += `<button class="lt-page-btn${page === 1 ? ' active' : ''}" data-page="1"${page === 1 ? ' aria-current="page"' : ''} aria-label="Page 1">1</button>`;
|
||||
if (start > 2) html += `<button class="lt-page-btn" disabled aria-hidden="true">…</button>`;
|
||||
for (let i = start; i <= end; i++) {
|
||||
html += `<button class="lt-page-btn${page === i ? ' active' : ''}" data-page="${i}">${i}</button>`;
|
||||
html += `<button class="lt-page-btn${page === i ? ' active' : ''}" data-page="${i}"${page === i ? ' aria-current="page"' : ''} aria-label="Page ${i}">${i}</button>`;
|
||||
}
|
||||
if (end < pages - 1) html += `<button class="lt-page-btn" disabled>…</button>`;
|
||||
if (pages > 1) html += `<button class="lt-page-btn${page === pages ? ' active' : ''}" data-page="${pages}">${pages}</button>`;
|
||||
if (end < pages - 1) html += `<button class="lt-page-btn" disabled aria-hidden="true">…</button>`;
|
||||
if (pages > 1) html += `<button class="lt-page-btn${page === pages ? ' active' : ''}" data-page="${pages}"${page === pages ? ' aria-current="page"' : ''} aria-label="Page ${pages}">${pages}</button>`;
|
||||
// Next
|
||||
html += `<button class="lt-page-btn" ${page >= pages ? 'disabled' : ''} data-page="${page + 1}">»</button>`;
|
||||
html += `<button class="lt-page-btn" ${page >= pages ? 'disabled' : ''} data-page="${page + 1}" aria-label="Next page">»</button>`;
|
||||
if (!nav.getAttribute('role')) nav.setAttribute('role', 'navigation');
|
||||
if (!nav.getAttribute('aria-label')) nav.setAttribute('aria-label', 'Pagination');
|
||||
nav.innerHTML = html;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user