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>
+3 -5
View File
@@ -10,7 +10,7 @@
crossorigin="anonymous" referrerpolicy="no-referrer" />
</head>
<body class="bg-gray-100 h-screen flex items-center justify-center">
<div class="bg-white rounded-lg shadow-lg p-8 max-w-md w-full">
<main class="bg-white rounded-lg shadow-lg p-8 max-w-md w-full" role="main">
<div class="flex justify-center mb-6">
<img src="/static/images/logo_writing.svg" alt="DocuElevate Logo" class="h-16">
</div>
@@ -67,7 +67,7 @@
<a href="/oauth-login"
class="w-full inline-flex justify-center py-2 px-4 border border-gray-300 rounded-md shadow-sm bg-white text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
<span class="sr-only">Sign in with SSO</span>
<i class="fas fa-lock mr-2"></i>
<i class="fas fa-lock mr-2" aria-hidden="true"></i>
{{ oauth_provider_name }}
</a>
</div>
@@ -78,9 +78,7 @@
Return to Home
</a>
</div>
</div>
<!-- Optional footer with version info -->
</main>
<div class="fixed bottom-4 text-center w-full text-xs text-gray-500">
DocuElevate {{ app_version|default('', true) }}
</div>
+8 -7
View File
@@ -84,27 +84,28 @@
{% block content %}
<div class="container mx-auto px-4 py-8">
<div class="search-container">
<h1 class="text-3xl font-bold mb-6"><i class="fas fa-search text-blue-500"></i> Document Search</h1>
<h1 class="text-3xl font-bold mb-6"><i class="fas fa-search text-blue-500" aria-hidden="true"></i> Document Search</h1>
<!-- Search box -->
<div class="search-box">
<div class="search-box" role="search">
<label for="search-input" class="sr-only">Search documents</label>
<input
type="text"
type="search"
id="search-input"
placeholder="Search documents by content, sender, tags, type..."
value="{{ query }}"
autofocus
>
<button type="button" id="search-btn">
<i class="fas fa-search"></i> Search
<button type="button" id="search-btn" aria-label="Search documents">
<i class="fas fa-search" aria-hidden="true"></i> Search
</button>
</div>
<!-- Summary -->
<div id="search-summary" class="search-summary" style="display:none;"></div>
<div id="search-summary" class="search-summary" style="display:none;" aria-live="polite"></div>
<!-- Results -->
<div id="search-results"></div>
<div id="search-results" aria-live="polite" aria-relevant="additions removals"></div>
<!-- Pagination -->
<div id="search-pagination" class="search-pagination" style="display:none;"></div>
+10 -5
View File
@@ -12,6 +12,9 @@
<div
id="dropZone"
class="border-4 border-dashed border-gray-300 rounded-lg p-6 sm:p-8 bg-white text-center w-full"
role="button"
tabindex="0"
aria-label="File upload area. Drag and drop files here, or press Enter to browse files."
ondragover="handleDragOver(event)"
ondragleave="handleDragLeave(event)"
>
@@ -28,6 +31,7 @@
onchange="handleFileSelect(event)"
name="files"
multiple
aria-label="Select files to upload"
accept="application/pdf,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document,application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.ms-powerpoint,application/vnd.openxmlformats-officedocument.presentationml.presentation,image/*"
/>
<!-- Browse Files button (all devices) -->
@@ -37,7 +41,7 @@
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-3 px-6 rounded-lg transition duration-200 mb-4"
style="min-height:44px;"
>
<i class="fas fa-folder-open mr-2"></i> Browse Files
<i class="fas fa-folder-open mr-2" aria-hidden="true"></i> Browse Files
</button>
<div class="text-sm text-gray-500 mt-2">
<p>Allowed types: PDF, Office documents (Word, Excel, PowerPoint, etc.), Images</p>
@@ -51,7 +55,7 @@
class="w-full flex items-center justify-center gap-2 bg-green-500 hover:bg-green-700 text-white font-bold py-3 px-6 rounded-lg transition duration-200 cursor-pointer"
style="min-height:44px;"
>
<i class="fas fa-camera"></i> Take Photo / Scan Document
<i class="fas fa-camera" aria-hidden="true"></i> Take Photo / Scan Document
<input
id="cameraInput"
type="file"
@@ -59,6 +63,7 @@
onchange="handleFileSelect(event)"
accept="image/*"
capture="environment"
aria-label="Take photo or scan document"
/>
</label>
</div>
@@ -111,12 +116,12 @@
Download and Process
</button>
</form>
<div id="urlStatusMessage" class="mt-4"></div>
<div id="urlStatusMessage" class="mt-4" aria-live="polite"></div>
</div>
</div>
<div id="statusMessage" class="mt-4 text-gray-700"></div>
<div id="uploadProgress" class="mt-4 w-full max-w-2xl"></div>
<div id="statusMessage" class="mt-4 text-gray-700" aria-live="polite"></div>
<div id="uploadProgress" class="mt-4 w-full max-w-2xl" role="status" aria-live="polite"></div>
</div>
{% endblock %}