feat(ui): fix accessibility in upload, search, files, and login templates

- upload.html: Add role/tabindex/aria-label to drop zone, aria-live to status messages, aria-label to file inputs
- search.html: Add role="search", sr-only label, aria-live for results, input type="search"
- files.html: Add table aria-label, scope="col" to headers, aria-sort on sortable columns, aria-labels to action buttons, dialog roles to modals, aria-live to status areas, pagination nav with aria-labels
- login.html: Wrap form in main landmark, aria-hidden on decorative icons

Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-02-28 15:59:12 +00:00
parent 364ff9375a
commit bf6bbe3512
4 changed files with 62 additions and 58 deletions
+41 -41
View File
@@ -345,25 +345,25 @@
{% block content %}
<!-- Drop overlay -->
<div id="dropOverlay" class="drop-overlay">
<div id="dropOverlay" class="drop-overlay" aria-hidden="true">
<div class="drop-message">
<i class="fas fa-cloud-upload-alt"></i>
<i class="fas fa-cloud-upload-alt" aria-hidden="true"></i>
<p>Drop files or folders anywhere to upload</p>
<div class="drop-hint">Supports PDF, Office docs, images, HTML, Markdown, and more folders are processed recursively</div>
</div>
</div>
<!-- Upload progress modal -->
<div id="uploadModal" class="upload-modal">
<div id="uploadModal" class="upload-modal" role="dialog" aria-modal="true" aria-label="Upload progress">
<div class="upload-modal-header">
<span><i class="fas fa-upload"></i> Uploading Files</span>
<button class="close-modal-btn" onclick="closeUploadModal()">
<i class="fas fa-times"></i>
<span><i class="fas fa-upload" aria-hidden="true"></i> Uploading Files</span>
<button class="close-modal-btn" onclick="closeUploadModal()" aria-label="Close upload progress">
<i class="fas fa-times" aria-hidden="true"></i>
</button>
</div>
<div class="upload-modal-body">
<div id="uploadStatusMessage" class="text-sm text-gray-700 mb-2"></div>
<div id="uploadProgressContainer" class="space-y-2"></div>
<div id="uploadStatusMessage" class="text-sm text-gray-700 mb-2" aria-live="polite"></div>
<div id="uploadProgressContainer" class="space-y-2" role="status" aria-live="polite"></div>
</div>
</div>
@@ -374,7 +374,7 @@
<div id="queueBanner" class="hidden bg-blue-50 border-l-4 border-blue-400 text-blue-800 p-4 mb-6 rounded-r-lg" role="status">
<div class="flex items-center justify-between">
<div class="flex items-center">
<i class="fas fa-spinner fa-spin mr-3 text-blue-500"></i>
<i class="fas fa-spinner fa-spin mr-3 text-blue-500" aria-hidden="true"></i>
<span>
<strong id="queueBannerCount">0</strong> item(s) are currently queued or being processed.
Files will appear here once processing completes.
@@ -382,7 +382,7 @@
</div>
<a href="/admin/queue" class="text-blue-600 hover:text-blue-800 text-sm font-medium whitespace-nowrap ml-4"
id="queueBannerLink" style="display:none;">
<i class="fas fa-external-link-alt mr-1"></i>View Queue
<i class="fas fa-external-link-alt mr-1" aria-hidden="true"></i>View Queue
</a>
</div>
</div>
@@ -445,7 +445,7 @@
<div class="filters-section" style="margin-top: 0.75rem;">
<div style="width: 100%;">
<label for="fulltext-search" style="font-weight: 600; display: block; margin-bottom: 0.4rem;">
<i class="fas fa-search"></i> Full-Text Search (OCR text, metadata, tags)
<i class="fas fa-search" aria-hidden="true"></i> Full-Text Search (OCR text, metadata, tags)
</label>
<div style="display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap;">
<input
@@ -490,10 +490,10 @@
</div>
<div class="flex flex-wrap gap-2">
<button type="button" onclick="bulkReprocess()" style="padding: 0.5rem 1rem; background-color: #3182ce; color: white; border: none; border-radius: 0.25rem; cursor: pointer; font-weight: 600; min-height: 44px;">
<i class="fas fa-sync"></i> Reprocess Selected
<i class="fas fa-sync" aria-hidden="true"></i> Reprocess Selected
</button>
<button type="button" onclick="bulkDelete()" style="padding: 0.5rem 1rem; background-color: #e53e3e; color: white; border: none; border-radius: 0.25rem; cursor: pointer; font-weight: 600; min-height: 44px;">
<i class="fas fa-trash"></i> Delete Selected
<i class="fas fa-trash" aria-hidden="true"></i> Delete Selected
</button>
<button type="button" onclick="clearSelection()" style="padding: 0.5rem 1rem; background-color: #718096; color: white; border: none; border-radius: 0.25rem; cursor: pointer; font-weight: 600; min-height: 44px;">
Clear Selection
@@ -504,13 +504,13 @@
<!-- File table (desktop) -->
<div class="hidden md:block overflow-x-auto">
<table class="file-table" id="fileTable">
<table class="file-table" id="fileTable" aria-label="File records">
<thead>
<tr>
<th style="width: 40px;">
<input type="checkbox" id="selectAll" onclick="toggleSelectAll()" title="Select all files on this page">
<th scope="col" style="width: 40px;">
<input type="checkbox" id="selectAll" onclick="toggleSelectAll()" title="Select all files on this page" aria-label="Select all files on this page">
</th>
<th class="sortable" onclick="sortTable('id')">
<th scope="col" class="sortable" onclick="sortTable('id')" role="columnheader" aria-sort="{% if sort_by == 'id' %}{% if sort_order == 'asc' %}ascending{% else %}descending{% endif %}{% else %}none{% endif %}">
ID
<span class="sort-indicator {% if sort_by == 'id' %}active{% endif %}">
{% if sort_by == 'id' %}
@@ -520,7 +520,7 @@
{% endif %}
</span>
</th>
<th class="sortable" onclick="sortTable('original_filename')">
<th scope="col" class="sortable" onclick="sortTable('original_filename')">
Original Filename
<span class="sort-indicator {% if sort_by == 'original_filename' %}active{% endif %}">
{% if sort_by == 'original_filename' %}
@@ -530,7 +530,7 @@
{% endif %}
</span>
</th>
<th class="sortable" onclick="sortTable('file_size')">
<th scope="col" class="sortable" onclick="sortTable('file_size')">
File Size
<span class="sort-indicator {% if sort_by == 'file_size' %}active{% endif %}">
{% if sort_by == 'file_size' %}
@@ -540,7 +540,7 @@
{% endif %}
</span>
</th>
<th class="sortable" onclick="sortTable('mime_type')">
<th scope="col" class="sortable" onclick="sortTable('mime_type')">
MIME Type
<span class="sort-indicator {% if sort_by == 'mime_type' %}active{% endif %}">
{% if sort_by == 'mime_type' %}
@@ -550,8 +550,8 @@
{% endif %}
</span>
</th>
<th>Status</th>
<th class="sortable" onclick="sortTable('created_at')">
<th scope="col">Status</th>
<th scope="col" class="sortable" onclick="sortTable('created_at')">
Created At
<span class="sort-indicator {% if sort_by == 'created_at' %}active{% endif %}">
{% if sort_by == 'created_at' %}
@@ -561,7 +561,7 @@
{% endif %}
</span>
</th>
<th>Actions</th>
<th scope="col">Actions</th>
</tr>
</thead>
<tbody>
@@ -582,11 +582,11 @@
<td>{{ file.created_at.strftime('%Y-%m-%d %H:%M:%S') if file.created_at else 'N/A' }}</td>
<td>
<div style="display: flex; align-items: center;">
<button onclick="viewFileDetail({{ file.id }}, event)" class="action-btn" title="View details" style="min-height:44px;min-width:44px;">
<i class="fas fa-info-circle"></i>
<button onclick="viewFileDetail({{ file.id }}, event)" class="action-btn" title="View details" aria-label="View details for {{ file.original_filename }}" style="min-height:44px;min-width:44px;">
<i class="fas fa-info-circle" aria-hidden="true"></i>
</button>
<button onclick="showDeleteModal({{ file.id }}, event)" class="action-btn delete" title="Delete file" style="min-height:44px;min-width:44px;">
<i class="fas fa-trash"></i>
<button onclick="showDeleteModal({{ file.id }}, event)" class="action-btn delete" title="Delete file" aria-label="Delete {{ file.original_filename }}" style="min-height:44px;min-width:44px;">
<i class="fas fa-trash" aria-hidden="true"></i>
</button>
</div>
</td>
@@ -621,11 +621,11 @@
<div class="flex flex-col items-end gap-2 flex-shrink-0">
<span class="status-badge status-{{ file.processing_status }}">{{ file.processing_status | title }}</span>
<div class="flex gap-1" role="group" aria-label="File actions" onclick="event.stopPropagation();" onkeydown="event.stopPropagation();">
<button onclick="viewFileDetail({{ file.id }}, event)" class="action-btn" title="View details" style="min-height:44px;min-width:44px;">
<i class="fas fa-info-circle"></i>
<button onclick="viewFileDetail({{ file.id }}, event)" class="action-btn" title="View details" aria-label="View details for {{ file.original_filename }}" style="min-height:44px;min-width:44px;">
<i class="fas fa-info-circle" aria-hidden="true"></i>
</button>
<button onclick="showDeleteModal({{ file.id }}, event)" class="action-btn delete" title="Delete file" style="min-height:44px;min-width:44px;">
<i class="fas fa-trash"></i>
<button onclick="showDeleteModal({{ file.id }}, event)" class="action-btn delete" title="Delete file" aria-label="Delete {{ file.original_filename }}" style="min-height:44px;min-width:44px;">
<i class="fas fa-trash" aria-hidden="true"></i>
</button>
</div>
</div>
@@ -638,36 +638,36 @@
<!-- Pagination -->
{% if pagination.total_pages > 1 %}
<div class="pagination flex-wrap gap-2">
<div class="pagination-info">
<nav class="pagination flex-wrap gap-2" aria-label="File list pagination">
<div class="pagination-info" aria-live="polite">
Showing {{ ((pagination.page - 1) * pagination.per_page + 1) }} -
{{ min(pagination.page * pagination.per_page, pagination.total_items) }}
of {{ pagination.total_items }} files
</div>
<div class="pagination-buttons flex-wrap">
{% if pagination.page > 1 %}
<button class="pagination-button" onclick="goToPage(1)" style="min-height:44px;">First</button>
<button class="pagination-button" onclick="goToPage({{ pagination.page - 1 }})" style="min-height:44px;">Previous</button>
<button class="pagination-button" onclick="goToPage(1)" aria-label="Go to first page" style="min-height:44px;">First</button>
<button class="pagination-button" onclick="goToPage({{ pagination.page - 1 }})" aria-label="Go to previous page" style="min-height:44px;">Previous</button>
{% endif %}
{% for p in range(max(1, pagination.page - 2), min(pagination.total_pages + 1, pagination.page + 3)) %}
<button class="pagination-button {% if p == pagination.page %}active{% endif %}" onclick="goToPage({{ p }})" style="min-height:44px;">
<button class="pagination-button {% if p == pagination.page %}active{% endif %}" onclick="goToPage({{ p }})" aria-label="Page {{ p }}" {% if p == pagination.page %}aria-current="page"{% endif %} style="min-height:44px;">
{{ p }}
</button>
{% endfor %}
{% if pagination.page < pagination.total_pages %}
<button class="pagination-button" onclick="goToPage({{ pagination.page + 1 }})" style="min-height:44px;">Next</button>
<button class="pagination-button" onclick="goToPage({{ pagination.total_pages }})" style="min-height:44px;">Last</button>
<button class="pagination-button" onclick="goToPage({{ pagination.page + 1 }})" aria-label="Go to next page" style="min-height:44px;">Next</button>
<button class="pagination-button" onclick="goToPage({{ pagination.total_pages }})" aria-label="Go to last page" style="min-height:44px;">Last</button>
{% endif %}
</div>
</div>
</nav>
{% endif %}
<!-- Delete confirmation modal -->
<div id="deleteModal" class="modal">
<div id="deleteModal" class="modal" role="dialog" aria-modal="true" aria-labelledby="deleteModalTitle">
<div class="modal-content">
<div class="modal-title">Confirm Deletion</div>
<div class="modal-title" id="deleteModalTitle">Confirm Deletion</div>
<p>Are you sure you want to delete this file?</p>
<div class="modal-buttons">
<button id="cancelDelete" class="modal-btn modal-btn-cancel" style="min-height:44px;min-width:80px;">Cancel</button>