Add comprehensive accessibility improvements
HTML Accessibility: - Add ARIA roles to tab navigation (role="tablist", role="tab", role="tabpanel") - Add aria-selected to tab buttons with JS toggle - Add aria-controls and aria-labelledby for tab/panel relationships - Add aria-label to emoji icon buttons (settings, reply, edit, delete) - Add aria-pressed to view toggle buttons - Add labels for form inputs (comment textarea, dependency inputs, file input) - Add .sr-only utility class for screen-reader-only content CSS Accessibility: - Add .sr-only class (visually hidden, accessible to screen readers) JavaScript: - Update showTab() to toggle aria-selected on tab buttons Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -201,6 +201,19 @@ select:focus {
|
|||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Screen reader only - visually hidden but accessible */
|
||||||
|
.sr-only {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
padding: 0;
|
||||||
|
margin: -1px;
|
||||||
|
overflow: hidden;
|
||||||
|
clip: rect(0, 0, 0, 0);
|
||||||
|
white-space: nowrap;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
/* Reduced motion for accessibility */
|
/* Reduced motion for accessibility */
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
*,
|
*,
|
||||||
|
|||||||
@@ -566,14 +566,17 @@ function showTab(tabName) {
|
|||||||
activityTab.style.display = 'none';
|
activityTab.style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove active class from all buttons
|
// Remove active class and aria-selected from all buttons
|
||||||
document.querySelectorAll('.tab-btn').forEach(btn => {
|
document.querySelectorAll('.tab-btn').forEach(btn => {
|
||||||
btn.classList.remove('active');
|
btn.classList.remove('active');
|
||||||
|
btn.setAttribute('aria-selected', 'false');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Show selected tab and activate its button
|
// Show selected tab and activate its button
|
||||||
document.getElementById(`${tabName}-tab`).style.display = 'block';
|
document.getElementById(`${tabName}-tab`).style.display = 'block';
|
||||||
document.querySelector(`.tab-btn[data-tab="${tabName}"]`).classList.add('active');
|
const activeBtn = document.querySelector(`.tab-btn[data-tab="${tabName}"]`);
|
||||||
|
activeBtn.classList.add('active');
|
||||||
|
activeBtn.setAttribute('aria-selected', 'true');
|
||||||
|
|
||||||
// Load attachments when tab is shown
|
// Load attachments when tab is shown
|
||||||
if (tabName === 'attachments') {
|
if (tabName === 'attachments') {
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<button class="settings-icon" title="Settings (Alt+S)" data-action="open-settings">⚙</button>
|
<button class="settings-icon" title="Settings (Alt+S)" data-action="open-settings" aria-label="Settings">⚙</button>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -284,8 +284,8 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
<!-- Center: Actions + Count -->
|
<!-- Center: Actions + Count -->
|
||||||
<div class="toolbar-center">
|
<div class="toolbar-center">
|
||||||
<div class="view-toggle">
|
<div class="view-toggle">
|
||||||
<button id="tableViewBtn" class="view-btn active" data-action="set-view-mode" data-mode="table" title="Table View">≡</button>
|
<button id="tableViewBtn" class="view-btn active" data-action="set-view-mode" data-mode="table" title="Table View" aria-label="Table view" aria-pressed="true">≡</button>
|
||||||
<button id="cardViewBtn" class="view-btn" data-action="set-view-mode" data-mode="card" title="Kanban View">▦</button>
|
<button id="cardViewBtn" class="view-btn" data-action="set-view-mode" data-mode="card" title="Kanban View" aria-label="Kanban view" aria-pressed="false">▦</button>
|
||||||
</div>
|
</div>
|
||||||
<button data-action="navigate" data-url="<?php echo $GLOBALS['config']['BASE_URL']; ?>/ticket/create" class="btn create-ticket">+ New Ticket</button>
|
<button data-action="navigate" data-url="<?php echo $GLOBALS['config']['BASE_URL']; ?>/ticket/create" class="btn create-ticket">+ New Ticket</button>
|
||||||
<div class="export-dropdown" id="exportDropdown" style="display: none;">
|
<div class="export-dropdown" id="exportDropdown" style="display: none;">
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
<?php if ($GLOBALS['currentUser']['is_admin']): ?>
|
<?php if ($GLOBALS['currentUser']['is_admin']): ?>
|
||||||
<span class="admin-badge">Admin</span>
|
<span class="admin-badge">Admin</span>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<button class="settings-icon" title="Settings (Alt+S)" id="settingsBtn">⚙</button>
|
<button class="settings-icon" title="Settings (Alt+S)" id="settingsBtn" aria-label="Settings">⚙</button>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -268,13 +268,13 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
<!-- SECTION 2: Tab Navigation -->
|
<!-- SECTION 2: Tab Navigation -->
|
||||||
<div class="ascii-section-header">Content Sections</div>
|
<div class="ascii-section-header">Content Sections</div>
|
||||||
<div class="ascii-content">
|
<div class="ascii-content">
|
||||||
<div class="ticket-tabs">
|
<nav class="ticket-tabs" role="tablist" aria-label="Ticket content sections">
|
||||||
<button class="tab-btn active" data-tab="description">Description</button>
|
<button class="tab-btn active" id="description-tab-btn" data-tab="description" role="tab" aria-selected="true" aria-controls="description-tab">Description</button>
|
||||||
<button class="tab-btn" data-tab="comments">Comments</button>
|
<button class="tab-btn" id="comments-tab-btn" data-tab="comments" role="tab" aria-selected="false" aria-controls="comments-tab">Comments</button>
|
||||||
<button class="tab-btn" data-tab="attachments">Attachments</button>
|
<button class="tab-btn" id="attachments-tab-btn" data-tab="attachments" role="tab" aria-selected="false" aria-controls="attachments-tab">Attachments</button>
|
||||||
<button class="tab-btn" data-tab="dependencies">Dependencies</button>
|
<button class="tab-btn" id="dependencies-tab-btn" data-tab="dependencies" role="tab" aria-selected="false" aria-controls="dependencies-tab">Dependencies</button>
|
||||||
<button class="tab-btn" data-tab="activity">Activity</button>
|
<button class="tab-btn" id="activity-tab-btn" data-tab="activity" role="tab" aria-selected="false" aria-controls="activity-tab">Activity</button>
|
||||||
</div>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- DIVIDER -->
|
<!-- DIVIDER -->
|
||||||
@@ -285,7 +285,7 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
<div class="ascii-content">
|
<div class="ascii-content">
|
||||||
<div class="ascii-frame-inner">
|
<div class="ascii-frame-inner">
|
||||||
<div class="ticket-details">
|
<div class="ticket-details">
|
||||||
<div id="description-tab" class="tab-content active">
|
<div id="description-tab" class="tab-content active" role="tabpanel" aria-labelledby="description-tab-btn">
|
||||||
<div class="ascii-subsection-header">Description</div>
|
<div class="ascii-subsection-header">Description</div>
|
||||||
<div class="detail-group full-width">
|
<div class="detail-group full-width">
|
||||||
<label>Description</label>
|
<label>Description</label>
|
||||||
@@ -293,13 +293,14 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="comments-tab" class="tab-content">
|
<div id="comments-tab" class="tab-content" role="tabpanel" aria-labelledby="comments-tab-btn">
|
||||||
<div class="ascii-subsection-header">Comments Section</div>
|
<div class="ascii-subsection-header">Comments Section</div>
|
||||||
<div class="comments-section">
|
<div class="comments-section">
|
||||||
<div class="ascii-frame-inner">
|
<div class="ascii-frame-inner">
|
||||||
<h2>Add Comment</h2>
|
<h2>Add Comment</h2>
|
||||||
<div class="comment-form">
|
<div class="comment-form">
|
||||||
<textarea id="newComment" placeholder="Add a comment..."></textarea>
|
<label for="newComment" class="sr-only">New comment</label>
|
||||||
|
<textarea id="newComment" placeholder="Add a comment..." aria-label="Add a comment"></textarea>
|
||||||
<div class="comment-controls">
|
<div class="comment-controls">
|
||||||
<div class="markdown-toggles">
|
<div class="markdown-toggles">
|
||||||
<div class="preview-toggle">
|
<div class="preview-toggle">
|
||||||
@@ -362,12 +363,12 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
echo "<div class='comment-actions'>";
|
echo "<div class='comment-actions'>";
|
||||||
// Reply button (max depth of 3)
|
// Reply button (max depth of 3)
|
||||||
if ($threadDepth < 3) {
|
if ($threadDepth < 3) {
|
||||||
echo "<button type='button' class='comment-action-btn reply-btn' data-action='reply-comment' data-comment-id='{$commentId}' data-user=\"" . htmlspecialchars($displayName, ENT_QUOTES) . "\" title='Reply'>↩</button>";
|
echo "<button type='button' class='comment-action-btn reply-btn' data-action='reply-comment' data-comment-id='{$commentId}' data-user=\"" . htmlspecialchars($displayName, ENT_QUOTES) . "\" title='Reply' aria-label='Reply to comment'>↩</button>";
|
||||||
}
|
}
|
||||||
// Edit/Delete buttons for owner or admin
|
// Edit/Delete buttons for owner or admin
|
||||||
if ($canModify) {
|
if ($canModify) {
|
||||||
echo "<button type='button' class='comment-action-btn edit-btn' data-action='edit-comment' data-comment-id='{$commentId}' title='Edit'>✏️</button>";
|
echo "<button type='button' class='comment-action-btn edit-btn' data-action='edit-comment' data-comment-id='{$commentId}' title='Edit' aria-label='Edit comment'>✏️</button>";
|
||||||
echo "<button type='button' class='comment-action-btn delete-btn' data-action='delete-comment' data-comment-id='{$commentId}' title='Delete'>🗑️</button>";
|
echo "<button type='button' class='comment-action-btn delete-btn' data-action='delete-comment' data-comment-id='{$commentId}' title='Delete' aria-label='Delete comment'>🗑️</button>";
|
||||||
}
|
}
|
||||||
echo "</div>";
|
echo "</div>";
|
||||||
|
|
||||||
@@ -408,7 +409,7 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="attachments-tab" class="tab-content">
|
<div id="attachments-tab" class="tab-content" role="tabpanel" aria-labelledby="attachments-tab-btn">
|
||||||
<div class="ascii-subsection-header">File Attachments</div>
|
<div class="ascii-subsection-header">File Attachments</div>
|
||||||
<div class="attachments-container">
|
<div class="attachments-container">
|
||||||
<!-- Upload Form -->
|
<!-- Upload Form -->
|
||||||
@@ -419,7 +420,7 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
<div class="upload-icon">📁</div>
|
<div class="upload-icon">📁</div>
|
||||||
<p>Drag and drop files here or click to browse</p>
|
<p>Drag and drop files here or click to browse</p>
|
||||||
<p class="upload-hint">Max file size: <?php echo $GLOBALS['config']['MAX_UPLOAD_SIZE'] ? number_format($GLOBALS['config']['MAX_UPLOAD_SIZE'] / 1048576, 0) . 'MB' : '10MB'; ?></p>
|
<p class="upload-hint">Max file size: <?php echo $GLOBALS['config']['MAX_UPLOAD_SIZE'] ? number_format($GLOBALS['config']['MAX_UPLOAD_SIZE'] / 1048576, 0) . 'MB' : '10MB'; ?></p>
|
||||||
<input type="file" id="fileInput" multiple style="display: none;">
|
<input type="file" id="fileInput" multiple style="display: none;" aria-label="Upload files">
|
||||||
<button type="button" id="browseFilesBtn" class="btn" style="margin-top: 1rem;">Browse Files</button>
|
<button type="button" id="browseFilesBtn" class="btn" style="margin-top: 1rem;">Browse Files</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -441,16 +442,18 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="dependencies-tab" class="tab-content">
|
<div id="dependencies-tab" class="tab-content" role="tabpanel" aria-labelledby="dependencies-tab-btn">
|
||||||
<div class="ascii-subsection-header">Ticket Dependencies</div>
|
<div class="ascii-subsection-header">Ticket Dependencies</div>
|
||||||
<div class="dependencies-container">
|
<div class="dependencies-container">
|
||||||
<!-- Add Dependency Form -->
|
<!-- Add Dependency Form -->
|
||||||
<div class="ascii-frame-inner" style="margin-bottom: 1rem;">
|
<div class="ascii-frame-inner" style="margin-bottom: 1rem;">
|
||||||
<h3>Add Dependency</h3>
|
<h3>Add Dependency</h3>
|
||||||
<div class="add-dependency-form" style="display: flex; gap: 0.5rem; flex-wrap: wrap; align-items: center;">
|
<div class="add-dependency-form" style="display: flex; gap: 0.5rem; flex-wrap: wrap; align-items: center;">
|
||||||
<input type="text" id="dependencyTicketId" placeholder="Ticket ID (e.g., 123456789)"
|
<label for="dependencyTicketId" class="sr-only">Ticket ID for dependency</label>
|
||||||
|
<input type="text" id="dependencyTicketId" placeholder="Ticket ID (e.g., 123456789)" aria-label="Ticket ID for dependency"
|
||||||
style="flex: 1; min-width: 150px; padding: 0.5rem; border: 2px solid var(--terminal-green); background: var(--bg-primary); color: var(--terminal-green); font-family: var(--font-mono);">
|
style="flex: 1; min-width: 150px; padding: 0.5rem; border: 2px solid var(--terminal-green); background: var(--bg-primary); color: var(--terminal-green); font-family: var(--font-mono);">
|
||||||
<select id="dependencyType" style="padding: 0.5rem; border: 2px solid var(--terminal-green); background: var(--bg-primary); color: var(--terminal-green); font-family: var(--font-mono);">
|
<label for="dependencyType" class="sr-only">Dependency type</label>
|
||||||
|
<select id="dependencyType" aria-label="Dependency type" style="padding: 0.5rem; border: 2px solid var(--terminal-green); background: var(--bg-primary); color: var(--terminal-green); font-family: var(--font-mono);">
|
||||||
<option value="blocks">Blocks</option>
|
<option value="blocks">Blocks</option>
|
||||||
<option value="blocked_by">Blocked By</option>
|
<option value="blocked_by">Blocked By</option>
|
||||||
<option value="relates_to">Relates To</option>
|
<option value="relates_to">Relates To</option>
|
||||||
@@ -478,7 +481,7 @@ $nonce = SecurityHeadersMiddleware::getNonce();
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="activity-tab" class="tab-content">
|
<div id="activity-tab" class="tab-content" role="tabpanel" aria-labelledby="activity-tab-btn">
|
||||||
<div class="ascii-subsection-header">Activity Timeline</div>
|
<div class="ascii-subsection-header">Activity Timeline</div>
|
||||||
<div class="timeline-container">
|
<div class="timeline-container">
|
||||||
<?php if (empty($timeline)): ?>
|
<?php if (empty($timeline)): ?>
|
||||||
|
|||||||
Reference in New Issue
Block a user