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 %} {% block content %}
<!-- Drop overlay --> <!-- Drop overlay -->
<div id="dropOverlay" class="drop-overlay"> <div id="dropOverlay" class="drop-overlay" aria-hidden="true">
<div class="drop-message"> <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> <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 class="drop-hint">Supports PDF, Office docs, images, HTML, Markdown, and more folders are processed recursively</div>
</div> </div>
</div> </div>
<!-- Upload progress modal --> <!-- 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"> <div class="upload-modal-header">
<span><i class="fas fa-upload"></i> Uploading Files</span> <span><i class="fas fa-upload" aria-hidden="true"></i> Uploading Files</span>
<button class="close-modal-btn" onclick="closeUploadModal()"> <button class="close-modal-btn" onclick="closeUploadModal()" aria-label="Close upload progress">
<i class="fas fa-times"></i> <i class="fas fa-times" aria-hidden="true"></i>
</button> </button>
</div> </div>
<div class="upload-modal-body"> <div class="upload-modal-body">
<div id="uploadStatusMessage" class="text-sm text-gray-700 mb-2"></div> <div id="uploadStatusMessage" class="text-sm text-gray-700 mb-2" aria-live="polite"></div>
<div id="uploadProgressContainer" class="space-y-2"></div> <div id="uploadProgressContainer" class="space-y-2" role="status" aria-live="polite"></div>
</div> </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 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 justify-between">
<div class="flex items-center"> <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> <span>
<strong id="queueBannerCount">0</strong> item(s) are currently queued or being processed. <strong id="queueBannerCount">0</strong> item(s) are currently queued or being processed.
Files will appear here once processing completes. Files will appear here once processing completes.
@@ -382,7 +382,7 @@
</div> </div>
<a href="/admin/queue" class="text-blue-600 hover:text-blue-800 text-sm font-medium whitespace-nowrap ml-4" <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;"> 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> </a>
</div> </div>
</div> </div>
@@ -445,7 +445,7 @@
<div class="filters-section" style="margin-top: 0.75rem;"> <div class="filters-section" style="margin-top: 0.75rem;">
<div style="width: 100%;"> <div style="width: 100%;">
<label for="fulltext-search" style="font-weight: 600; display: block; margin-bottom: 0.4rem;"> <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> </label>
<div style="display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap;"> <div style="display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap;">
<input <input
@@ -490,10 +490,10 @@
</div> </div>
<div class="flex flex-wrap gap-2"> <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;"> <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>
<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;"> <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>
<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;"> <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 Clear Selection
@@ -504,13 +504,13 @@
<!-- File table (desktop) --> <!-- File table (desktop) -->
<div class="hidden md:block overflow-x-auto"> <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> <thead>
<tr> <tr>
<th style="width: 40px;"> <th scope="col" style="width: 40px;">
<input type="checkbox" id="selectAll" onclick="toggleSelectAll()" title="Select all files on this page"> <input type="checkbox" id="selectAll" onclick="toggleSelectAll()" title="Select all files on this page" aria-label="Select all files on this page">
</th> </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 ID
<span class="sort-indicator {% if sort_by == 'id' %}active{% endif %}"> <span class="sort-indicator {% if sort_by == 'id' %}active{% endif %}">
{% if sort_by == 'id' %} {% if sort_by == 'id' %}
@@ -520,7 +520,7 @@
{% endif %} {% endif %}
</span> </span>
</th> </th>
<th class="sortable" onclick="sortTable('original_filename')"> <th scope="col" class="sortable" onclick="sortTable('original_filename')">
Original Filename Original Filename
<span class="sort-indicator {% if sort_by == 'original_filename' %}active{% endif %}"> <span class="sort-indicator {% if sort_by == 'original_filename' %}active{% endif %}">
{% if sort_by == 'original_filename' %} {% if sort_by == 'original_filename' %}
@@ -530,7 +530,7 @@
{% endif %} {% endif %}
</span> </span>
</th> </th>
<th class="sortable" onclick="sortTable('file_size')"> <th scope="col" class="sortable" onclick="sortTable('file_size')">
File Size File Size
<span class="sort-indicator {% if sort_by == 'file_size' %}active{% endif %}"> <span class="sort-indicator {% if sort_by == 'file_size' %}active{% endif %}">
{% if sort_by == 'file_size' %} {% if sort_by == 'file_size' %}
@@ -540,7 +540,7 @@
{% endif %} {% endif %}
</span> </span>
</th> </th>
<th class="sortable" onclick="sortTable('mime_type')"> <th scope="col" class="sortable" onclick="sortTable('mime_type')">
MIME Type MIME Type
<span class="sort-indicator {% if sort_by == 'mime_type' %}active{% endif %}"> <span class="sort-indicator {% if sort_by == 'mime_type' %}active{% endif %}">
{% if sort_by == 'mime_type' %} {% if sort_by == 'mime_type' %}
@@ -550,8 +550,8 @@
{% endif %} {% endif %}
</span> </span>
</th> </th>
<th>Status</th> <th scope="col">Status</th>
<th class="sortable" onclick="sortTable('created_at')"> <th scope="col" class="sortable" onclick="sortTable('created_at')">
Created At Created At
<span class="sort-indicator {% if sort_by == 'created_at' %}active{% endif %}"> <span class="sort-indicator {% if sort_by == 'created_at' %}active{% endif %}">
{% if sort_by == 'created_at' %} {% if sort_by == 'created_at' %}
@@ -561,7 +561,7 @@
{% endif %} {% endif %}
</span> </span>
</th> </th>
<th>Actions</th> <th scope="col">Actions</th>
</tr> </tr>
</thead> </thead>
<tbody> <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>{{ file.created_at.strftime('%Y-%m-%d %H:%M:%S') if file.created_at else 'N/A' }}</td>
<td> <td>
<div style="display: flex; align-items: center;"> <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;"> <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"></i> <i class="fas fa-info-circle" aria-hidden="true"></i>
</button> </button>
<button onclick="showDeleteModal({{ file.id }}, event)" class="action-btn delete" title="Delete file" style="min-height:44px;min-width:44px;"> <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"></i> <i class="fas fa-trash" aria-hidden="true"></i>
</button> </button>
</div> </div>
</td> </td>
@@ -621,11 +621,11 @@
<div class="flex flex-col items-end gap-2 flex-shrink-0"> <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> <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();"> <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;"> <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"></i> <i class="fas fa-info-circle" aria-hidden="true"></i>
</button> </button>
<button onclick="showDeleteModal({{ file.id }}, event)" class="action-btn delete" title="Delete file" style="min-height:44px;min-width:44px;"> <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"></i> <i class="fas fa-trash" aria-hidden="true"></i>
</button> </button>
</div> </div>
</div> </div>
@@ -638,36 +638,36 @@
<!-- Pagination --> <!-- Pagination -->
{% if pagination.total_pages > 1 %} {% if pagination.total_pages > 1 %}
<div class="pagination flex-wrap gap-2"> <nav class="pagination flex-wrap gap-2" aria-label="File list pagination">
<div class="pagination-info"> <div class="pagination-info" aria-live="polite">
Showing {{ ((pagination.page - 1) * pagination.per_page + 1) }} - Showing {{ ((pagination.page - 1) * pagination.per_page + 1) }} -
{{ min(pagination.page * pagination.per_page, pagination.total_items) }} {{ min(pagination.page * pagination.per_page, pagination.total_items) }}
of {{ pagination.total_items }} files of {{ pagination.total_items }} files
</div> </div>
<div class="pagination-buttons flex-wrap"> <div class="pagination-buttons flex-wrap">
{% if pagination.page > 1 %} {% if pagination.page > 1 %}
<button class="pagination-button" onclick="goToPage(1)" style="min-height:44px;">First</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 }})" style="min-height:44px;">Previous</button> <button class="pagination-button" onclick="goToPage({{ pagination.page - 1 }})" aria-label="Go to previous page" style="min-height:44px;">Previous</button>
{% endif %} {% endif %}
{% for p in range(max(1, pagination.page - 2), min(pagination.total_pages + 1, pagination.page + 3)) %} {% 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 }} {{ p }}
</button> </button>
{% endfor %} {% endfor %}
{% if pagination.page < pagination.total_pages %} {% 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.page + 1 }})" aria-label="Go to next page" 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.total_pages }})" aria-label="Go to last page" style="min-height:44px;">Last</button>
{% endif %} {% endif %}
</div> </div>
</div> </nav>
{% endif %} {% endif %}
<!-- Delete confirmation modal --> <!-- 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-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> <p>Are you sure you want to delete this file?</p>
<div class="modal-buttons"> <div class="modal-buttons">
<button id="cancelDelete" class="modal-btn modal-btn-cancel" style="min-height:44px;min-width:80px;">Cancel</button> <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" /> crossorigin="anonymous" referrerpolicy="no-referrer" />
</head> </head>
<body class="bg-gray-100 h-screen flex items-center justify-center"> <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"> <div class="flex justify-center mb-6">
<img src="/static/images/logo_writing.svg" alt="DocuElevate Logo" class="h-16"> <img src="/static/images/logo_writing.svg" alt="DocuElevate Logo" class="h-16">
</div> </div>
@@ -67,7 +67,7 @@
<a href="/oauth-login" <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"> 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> <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 }} {{ oauth_provider_name }}
</a> </a>
</div> </div>
@@ -78,9 +78,7 @@
Return to Home Return to Home
</a> </a>
</div> </div>
</div> </main>
<!-- Optional footer with version info -->
<div class="fixed bottom-4 text-center w-full text-xs text-gray-500"> <div class="fixed bottom-4 text-center w-full text-xs text-gray-500">
DocuElevate {{ app_version|default('', true) }} DocuElevate {{ app_version|default('', true) }}
</div> </div>
+8 -7
View File
@@ -84,27 +84,28 @@
{% block content %} {% block content %}
<div class="container mx-auto px-4 py-8"> <div class="container mx-auto px-4 py-8">
<div class="search-container"> <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 --> <!-- Search box -->
<div class="search-box"> <div class="search-box" role="search">
<label for="search-input" class="sr-only">Search documents</label>
<input <input
type="text" type="search"
id="search-input" id="search-input"
placeholder="Search documents by content, sender, tags, type..." placeholder="Search documents by content, sender, tags, type..."
value="{{ query }}" value="{{ query }}"
autofocus autofocus
> >
<button type="button" id="search-btn"> <button type="button" id="search-btn" aria-label="Search documents">
<i class="fas fa-search"></i> Search <i class="fas fa-search" aria-hidden="true"></i> Search
</button> </button>
</div> </div>
<!-- Summary --> <!-- 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 --> <!-- Results -->
<div id="search-results"></div> <div id="search-results" aria-live="polite" aria-relevant="additions removals"></div>
<!-- Pagination --> <!-- Pagination -->
<div id="search-pagination" class="search-pagination" style="display:none;"></div> <div id="search-pagination" class="search-pagination" style="display:none;"></div>
+10 -5
View File
@@ -12,6 +12,9 @@
<div <div
id="dropZone" id="dropZone"
class="border-4 border-dashed border-gray-300 rounded-lg p-6 sm:p-8 bg-white text-center w-full" 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)" ondragover="handleDragOver(event)"
ondragleave="handleDragLeave(event)" ondragleave="handleDragLeave(event)"
> >
@@ -28,6 +31,7 @@
onchange="handleFileSelect(event)" onchange="handleFileSelect(event)"
name="files" name="files"
multiple 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/*" 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) --> <!-- 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" 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;" 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> </button>
<div class="text-sm text-gray-500 mt-2"> <div class="text-sm text-gray-500 mt-2">
<p>Allowed types: PDF, Office documents (Word, Excel, PowerPoint, etc.), Images</p> <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" 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;" 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 <input
id="cameraInput" id="cameraInput"
type="file" type="file"
@@ -59,6 +63,7 @@
onchange="handleFileSelect(event)" onchange="handleFileSelect(event)"
accept="image/*" accept="image/*"
capture="environment" capture="environment"
aria-label="Take photo or scan document"
/> />
</label> </label>
</div> </div>
@@ -111,12 +116,12 @@
Download and Process Download and Process
</button> </button>
</form> </form>
<div id="urlStatusMessage" class="mt-4"></div> <div id="urlStatusMessage" class="mt-4" aria-live="polite"></div>
</div> </div>
</div> </div>
<div id="statusMessage" class="mt-4 text-gray-700"></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"></div> <div id="uploadProgress" class="mt-4 w-full max-w-2xl" role="status" aria-live="polite"></div>
</div> </div>
{% endblock %} {% endblock %}