Accessibility pass: ARIA roles, label associations, CSS class migrations
- Add role=dialog/aria-modal/aria-labelledby to all 12 modal overlays (JS + PHP)
- Add aria-label="Close" to all 14 modal close buttons
- Add full ARIA combobox pattern to @mention autocomplete (listbox, option, aria-selected, aria-expanded)
- Add for= attributes to admin filter form labels (AuditLog, UserActivity, ApiKeys)
- Remove dead closeOnAdvancedSearchBackdropClick() from advanced-search.js
CSS/JS style cleanup:
- Move .ascii-banner static styles from JS inline to CSS class; add .ascii-banner--glow
- Add .ascii-banner-cursor, .loading-overlay--hiding, .has-overlay, tr[data-clickable]
- Add .animate-fadein/.animate-fadeout/.comment--deleting to ticket.css
- Add .lt-toast--hiding to base.css; remove opacity/transition inline JS
- Remove redundant cursor:pointer JS (already in th{} CSS rule)
- Remove trailing space in lt-select class attributes
Bug fixes:
- base.js: boot overlay opacity inline style was overriding .fade-out class opacity via
specificity (1000 vs 20), preventing the fade-out animation — removed
- ascii-banner.js: cursor used blink-caret (border-color only) instead of blink-cursor
(opacity-based), so the █ cursor never actually blinked — fixed
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -50,7 +50,7 @@ function saveTicket() {
|
||||
// Use the correct API path
|
||||
lt.api.post('/api/update_ticket.php', { ticket_id: ticketId, ...data })
|
||||
.then(data => {
|
||||
if(data.success) {
|
||||
if (data.success) {
|
||||
const statusDisplay = document.getElementById('statusDisplay');
|
||||
if (statusDisplay) {
|
||||
statusDisplay.className = `status-${data.status}`;
|
||||
@@ -58,9 +58,11 @@ function saveTicket() {
|
||||
}
|
||||
lt.toast.success('Ticket updated successfully');
|
||||
} else {
|
||||
lt.toast.error('Error saving ticket: ' + (data.error || 'Unknown error'));
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
lt.toast.error('Error saving ticket: ' + error.message);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -511,10 +513,10 @@ function showDependencyError(message) {
|
||||
const dependentsList = document.getElementById('dependentsList');
|
||||
|
||||
if (dependenciesList) {
|
||||
dependenciesList.innerHTML = `<p style="color: var(--terminal-amber);">${lt.escHtml(message)}</p>`;
|
||||
dependenciesList.innerHTML = `<p class="text-amber">${lt.escHtml(message)}</p>`;
|
||||
}
|
||||
if (dependentsList) {
|
||||
dependentsList.innerHTML = `<p style="color: var(--terminal-amber);">${lt.escHtml(message)}</p>`;
|
||||
dependentsList.innerHTML = `<p class="text-amber">${lt.escHtml(message)}</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -557,7 +559,7 @@ function renderDependencies(dependencies) {
|
||||
}
|
||||
|
||||
if (!hasAny) {
|
||||
html = '<p style="color: var(--terminal-green-dim);">No dependencies configured.</p>';
|
||||
html = '<p class="text-muted-green">No dependencies configured.</p>';
|
||||
}
|
||||
|
||||
container.innerHTML = html;
|
||||
@@ -568,21 +570,21 @@ function renderDependents(dependents) {
|
||||
if (!container) return;
|
||||
|
||||
if (dependents.length === 0) {
|
||||
container.innerHTML = '<p style="color: var(--terminal-green-dim);">No tickets depend on this one.</p>';
|
||||
container.innerHTML = '<p class="text-muted-green">No tickets depend on this one.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '';
|
||||
dependents.forEach(dep => {
|
||||
const statusClass = 'status-' + dep.status.toLowerCase().replace(/ /g, '-');
|
||||
html += `<div class="dependency-item" style="display: flex; justify-content: space-between; align-items: center; padding: 0.5rem; border-bottom: 1px dashed var(--terminal-green-dim);">
|
||||
html += `<div class="dependency-item">
|
||||
<div>
|
||||
<a href="/ticket/${lt.escHtml(dep.ticket_id)}" style="color: var(--terminal-green);">
|
||||
<a href="/ticket/${lt.escHtml(dep.ticket_id)}">
|
||||
#${lt.escHtml(dep.ticket_id)}
|
||||
</a>
|
||||
<span style="margin-left: 0.5rem;">${lt.escHtml(dep.title)}</span>
|
||||
<span class="status-badge ${statusClass}" style="margin-left: 0.5rem; font-size: 0.8rem;">${lt.escHtml(dep.status)}</span>
|
||||
<span style="margin-left: 0.5rem; color: var(--terminal-amber);">(${lt.escHtml(dep.dependency_type)})</span>
|
||||
<span class="dependency-title">${lt.escHtml(dep.title)}</span>
|
||||
<span class="status-badge ${statusClass}">${lt.escHtml(dep.status)}</span>
|
||||
<span class="dependency-title text-amber">(${lt.escHtml(dep.dependency_type)})</span>
|
||||
</div>
|
||||
</div>`;
|
||||
});
|
||||
@@ -623,22 +625,25 @@ function addDependency() {
|
||||
}
|
||||
|
||||
function removeDependency(dependencyId) {
|
||||
if (!confirm('Are you sure you want to remove this dependency?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
lt.api.delete('/api/ticket_dependencies.php', { dependency_id: dependencyId })
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
lt.toast.success('Dependency removed', 3000);
|
||||
loadDependencies();
|
||||
} else {
|
||||
lt.toast.error('Error: ' + (data.error || 'Unknown error'), 4000);
|
||||
showConfirmModal(
|
||||
'Remove Dependency',
|
||||
'Are you sure you want to remove this dependency?',
|
||||
'warning',
|
||||
function() {
|
||||
lt.api.delete('/api/ticket_dependencies.php', { dependency_id: dependencyId })
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
lt.toast.success('Dependency removed', 3000);
|
||||
loadDependencies();
|
||||
} else {
|
||||
lt.toast.error('Error: ' + (data.error || 'Unknown error'), 4000);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
lt.toast.error('Error removing dependency', 4000);
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
lt.toast.error('Error removing dependency', 4000);
|
||||
});
|
||||
);
|
||||
}
|
||||
|
||||
// ========================================
|
||||
@@ -789,11 +794,11 @@ function loadAttachments() {
|
||||
if (data.success) {
|
||||
renderAttachments(data.attachments || []);
|
||||
} else {
|
||||
container.innerHTML = '<p style="color: var(--terminal-green-dim);">Error loading attachments.</p>';
|
||||
container.innerHTML = '<p class="text-muted-green">Error loading attachments.</p>';
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
container.innerHTML = '<p style="color: var(--terminal-green-dim);">Error loading attachments.</p>';
|
||||
container.innerHTML = '<p class="text-muted-green">Error loading attachments.</p>';
|
||||
});
|
||||
}
|
||||
|
||||
@@ -802,7 +807,7 @@ function renderAttachments(attachments) {
|
||||
if (!container) return;
|
||||
|
||||
if (attachments.length === 0) {
|
||||
container.innerHTML = '<p style="color: var(--terminal-green-dim);">No files attached to this ticket.</p>';
|
||||
container.innerHTML = '<p class="text-muted-green">No files attached to this ticket.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -823,7 +828,7 @@ function renderAttachments(attachments) {
|
||||
<div class="attachment-icon">${lt.escHtml(att.icon || '[ f ]')}</div>
|
||||
<div class="attachment-info">
|
||||
<div class="attachment-name" title="${lt.escHtml(att.original_filename)}">
|
||||
<a href="/api/download_attachment.php?id=${att.attachment_id}" target="_blank" style="color: var(--terminal-green);">
|
||||
<a href="/api/download_attachment.php?id=${att.attachment_id}" target="_blank">
|
||||
${lt.escHtml(att.original_filename)}
|
||||
</a>
|
||||
</div>
|
||||
@@ -855,22 +860,25 @@ function formatFileSize(bytes) {
|
||||
}
|
||||
|
||||
function deleteAttachment(attachmentId) {
|
||||
if (!confirm('Are you sure you want to delete this attachment?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
lt.api.post('/api/delete_attachment.php', { attachment_id: attachmentId })
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
lt.toast.success('Attachment deleted', 3000);
|
||||
loadAttachments();
|
||||
} else {
|
||||
lt.toast.error('Error: ' + (data.error || 'Unknown error'), 4000);
|
||||
showConfirmModal(
|
||||
'Delete Attachment',
|
||||
'Are you sure you want to delete this attachment?',
|
||||
'warning',
|
||||
function() {
|
||||
lt.api.post('/api/delete_attachment.php', { attachment_id: attachmentId })
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
lt.toast.success('Attachment deleted', 3000);
|
||||
loadAttachments();
|
||||
} else {
|
||||
lt.toast.error('Error: ' + (data.error || 'Unknown error'), 4000);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
lt.toast.error('Error deleting attachment', 4000);
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
lt.toast.error('Error deleting attachment', 4000);
|
||||
});
|
||||
);
|
||||
}
|
||||
|
||||
// ========================================
|
||||
@@ -893,7 +901,12 @@ function initMentionAutocomplete() {
|
||||
mentionAutocomplete = document.createElement('div');
|
||||
mentionAutocomplete.className = 'mention-autocomplete';
|
||||
mentionAutocomplete.id = 'mentionAutocomplete';
|
||||
textarea.parentElement.style.position = 'relative';
|
||||
mentionAutocomplete.setAttribute('role', 'listbox');
|
||||
mentionAutocomplete.setAttribute('aria-label', 'User suggestions');
|
||||
textarea.setAttribute('aria-autocomplete', 'list');
|
||||
textarea.setAttribute('aria-controls', 'mentionAutocomplete');
|
||||
textarea.setAttribute('aria-expanded', 'false');
|
||||
textarea.parentElement.classList.add('has-overlay');
|
||||
textarea.parentElement.appendChild(mentionAutocomplete);
|
||||
|
||||
// Fetch users list
|
||||
@@ -990,7 +1003,9 @@ function handleMentionKeydown(e) {
|
||||
*/
|
||||
function updateMentionSelection(options) {
|
||||
options.forEach((opt, i) => {
|
||||
opt.classList.toggle('selected', i === selectedMentionIndex);
|
||||
const isSelected = i === selectedMentionIndex;
|
||||
opt.classList.toggle('selected', isSelected);
|
||||
opt.setAttribute('aria-selected', isSelected ? 'true' : 'false');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1012,7 +1027,8 @@ function showMentionSuggestions(query, textarea) {
|
||||
let html = '';
|
||||
filtered.forEach((user, index) => {
|
||||
const isSelected = index === 0 ? 'selected' : '';
|
||||
html += `<div class="mention-option ${isSelected}" data-username="${lt.escHtml(user.username)}" data-action="select-mention">
|
||||
const ariaSelected = index === 0 ? 'true' : 'false';
|
||||
html += `<div class="mention-option ${isSelected}" role="option" aria-selected="${ariaSelected}" data-username="${lt.escHtml(user.username)}" data-action="select-mention">
|
||||
<span class="mention-username">@${lt.escHtml(user.username)}</span>
|
||||
${user.display_name ? `<span class="mention-displayname">${lt.escHtml(user.display_name)}</span>` : ''}
|
||||
</div>`;
|
||||
@@ -1020,6 +1036,8 @@ function showMentionSuggestions(query, textarea) {
|
||||
|
||||
mentionAutocomplete.innerHTML = html;
|
||||
mentionAutocomplete.classList.add('active');
|
||||
const textarea = document.getElementById('newComment');
|
||||
if (textarea) textarea.setAttribute('aria-expanded', 'true');
|
||||
selectedMentionIndex = 0;
|
||||
|
||||
// Position dropdown below cursor
|
||||
@@ -1034,6 +1052,8 @@ function showMentionSuggestions(query, textarea) {
|
||||
function hideMentionAutocomplete() {
|
||||
if (mentionAutocomplete) {
|
||||
mentionAutocomplete.classList.remove('active');
|
||||
const textarea = document.getElementById('newComment');
|
||||
if (textarea) textarea.setAttribute('aria-expanded', 'false');
|
||||
}
|
||||
mentionStartPos = -1;
|
||||
}
|
||||
@@ -1257,29 +1277,29 @@ function cancelEditComment(commentId) {
|
||||
* Delete a comment
|
||||
*/
|
||||
function deleteComment(commentId) {
|
||||
if (!confirm('Are you sure you want to delete this comment? This cannot be undone.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
lt.api.post('/api/delete_comment.php', { comment_id: commentId })
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
// Remove the comment from the DOM
|
||||
const commentDiv = document.querySelector(`.comment[data-comment-id="${commentId}"]`);
|
||||
if (commentDiv) {
|
||||
commentDiv.style.transition = 'opacity 0.3s, transform 0.3s';
|
||||
commentDiv.style.opacity = '0';
|
||||
commentDiv.style.transform = 'translateX(-20px)';
|
||||
setTimeout(() => commentDiv.remove(), 300);
|
||||
}
|
||||
lt.toast.success('Comment deleted successfully');
|
||||
} else {
|
||||
lt.toast.error(data.error || 'Failed to delete comment');
|
||||
showConfirmModal(
|
||||
'Delete Comment',
|
||||
'Are you sure you want to delete this comment? This cannot be undone.',
|
||||
'warning',
|
||||
function() {
|
||||
lt.api.post('/api/delete_comment.php', { comment_id: commentId })
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
const commentDiv = document.querySelector(`.comment[data-comment-id="${commentId}"]`);
|
||||
if (commentDiv) {
|
||||
commentDiv.classList.add('comment--deleting');
|
||||
setTimeout(() => commentDiv.remove(), 300);
|
||||
}
|
||||
lt.toast.success('Comment deleted successfully');
|
||||
} else {
|
||||
lt.toast.error(data.error || 'Failed to delete comment');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
lt.toast.error('Failed to delete comment');
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
lt.toast.error('Failed to delete comment');
|
||||
});
|
||||
);
|
||||
}
|
||||
|
||||
// ========================================
|
||||
@@ -1331,7 +1351,7 @@ function showReplyForm(commentId, userName) {
|
||||
*/
|
||||
function closeReplyForm() {
|
||||
document.querySelectorAll('.reply-form-container').forEach(form => {
|
||||
form.style.animation = 'fadeIn 0.2s ease reverse';
|
||||
form.classList.add('animate-fadeout');
|
||||
setTimeout(() => form.remove(), 200);
|
||||
});
|
||||
}
|
||||
@@ -1420,7 +1440,7 @@ function submitReply(parentCommentId) {
|
||||
`;
|
||||
|
||||
// Add animation
|
||||
replyDiv.style.animation = 'fadeIn 0.3s ease';
|
||||
replyDiv.classList.add('animate-fadein');
|
||||
repliesContainer.appendChild(replyDiv);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user