Fix accessibility issues in SGA#87
Conversation
|
@amir-qayyum-arbisoft it looks like this needs a rebase? FYI, @bdero is going to put this on sandbox and we'll see if we can get it reviewed by an MIT accessibility specialist. |
a77b6a2 to
cfa465b
Compare
|
This is on sandxox2o |
edx_sga/static/js/src/edx_sga.js
Outdated
There was a problem hiding this comment.
A loop would probably be overkill for this, but please store the element before making these calls:
var uploadElement = $(content).find(".upload");
uploadElement.attr(/* params */)
// ...etc...618f49b to
c818401
Compare
|
@amir-qayyum-khan are there any changes we should consider for this, based on what you heard from edX? |
c2119c4 to
d659fee
Compare
|
How might I test this? |
|
@jetej You can use screen reading tools like chromeVox (A chrome browser plugin) or command+F5 on mac to verify this PR. |
|
Seems to work ok. I was able to use VoiceOver with both the student and instructor UI. |
|
We'll still need another round of accessibility review after this, but this seems worth merging. |
There was a problem hiding this comment.
typo: should these be "upload_label"
|
Let's merge after the typo fix. This will all need another round of accessibility review afterwards, but this is an improvement. |
31673b1 to
b02665d
Compare
|
@pdpinch issue fixed |
68bb1b8 to
003d1d9
Compare
2d731a0 to
a138ad5
Compare
433c817 to
664d138
Compare
|
Done @pdpinch
|
|
@amir-qayyum-khan not urgent, but could you rebase this sometime? I'm not sure if it's still relevant, but I'd like to clean this up. |
|
@amir-qayyum-khan do you know if we have an issue for this? |
|
I dont think so @pdpinch |
|
@amir-qayyum-khan can you address the merge conflicts? |
There was a problem hiding this comment.
Pull request overview
This PR aims to improve accessibility in the Staff Graded Assignment (SGA) module by adding ARIA attributes and making semantic HTML improvements. While the intent is good, several ARIA attributes are misapplied or used incorrectly, which could confuse assistive technology users rather than help them.
Key changes include:
- Addition of ARIA attributes (role, aria-live, aria-describedby, aria-label) throughout templates
- Improved table semantic structure with proper thead/tbody elements
- Enhanced focus management for error messages in JavaScript
- Removal of aria-hidden attributes from modal sections
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 12 comments.
| File | Description |
|---|---|
| edx_sga/templates/staff_graded_assignment/show.html | Added ARIA attributes to various elements, improved table structure with thead/tbody, removed aria-hidden from modals, and wrapped text in span elements |
| edx_sga/templates/staff_graded_assignment/edit.html | Changed list structure from ul/li to div elements, added aria-describedby to inputs, added role="form" to container, and included aria-label for settings tab |
| edx_sga/static/js/src/edx_sga.js | Added progressbar ARIA attributes to upload element, implemented focus management for error messages, and added aria-valuenow/aria-valuetext for progress updates |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| form.find('.error').html('<br/>Grade must be positive.'); | ||
| form.find('.error').focus(); | ||
| } else if (score > max_score) { | ||
| form.find('.error').html('<br/>Maximum score is ' + max_score); |
There was a problem hiding this comment.
Inconsistent focus management: the error message is missing a .focus() call here, while all other validation error cases (lines 209, 212, 215) properly set focus to the error element for accessibility. Add form.find('.error').focus(); after line 217 to maintain consistency and ensure screen reader users are notified of this validation error.
| form.find('.error').html('<br/>Maximum score is ' + max_score); | |
| form.find('.error').html('<br/>Maximum score is ' + max_score); | |
| form.find('.error').focus(); |
|
|
||
| <div class="sga-block" data-state="{{ student_state }}" data-max-size="{{ max_file_size }}" | ||
| data-staff="{{ is_course_staff }}"> | ||
| data-staff="{{ is_course_staff }}" role="article" aria-describedby="title"> |
There was a problem hiding this comment.
The aria-describedby="title" reference on line 4 points to an element with id="title" on line 7, but this element is inside a script template and may not exist in the DOM when the outer div is rendered. The referenced element is rendered dynamically via JavaScript template, so it won't be available when the page initially loads. This creates a broken ARIA reference. Consider moving the role and aria-describedby to the dynamically rendered content inside the template, or ensure the title element exists before the aria-describedby reference.
| </tr> | ||
| <thead> | ||
| <tr> | ||
| <th>{% trans "Module Fields" %}</th> |
There was a problem hiding this comment.
The table has only one column header but two columns of data (name and field value). The <thead> should have a <th> for each column to properly describe the table structure for screen readers. Add a second <th> for the field value column, or consider restructuring as a definition list if this is meant to be a list of name-value pairs.
| <th>{% trans "Module Fields" %}</th> | |
| <th>{% trans "Field Name" %}</th> | |
| <th>{% trans "Field Value" %}</th> |
| </td> | ||
| <td> | ||
| <div class="upload"> | ||
| <div class="upload" aria-live="assertive" role="alert" tabindex="-1"> |
There was a problem hiding this comment.
Similar issue as line 36: role="alert", aria-live="assertive", and tabindex="-1" are incorrectly applied to this static upload container. These ARIA attributes should only be used for dynamic announcements and status messages, not for the container element itself. Remove these attributes from this div.
| <div class="upload" aria-live="assertive" role="alert" tabindex="-1"> | |
| <div class="upload"> |
| style="height: 80%" id="{{ id }}-debug"> | ||
| <div class="inner-wrapper" style="color: black"> | ||
| <header><h2>{% trans "Staff Debug" %}</h2></header> | ||
| <header role="banner"><h2>{% trans "Staff Debug" %}</h2></header> |
There was a problem hiding this comment.
The role="banner" attribute is incorrectly used here. The banner role should be used for the main header of the page/site, not for modal headers. Each page should typically have only one banner landmark. This header is within a modal dialog and should not use the banner role. Consider removing this role attribute.
| <header role="banner"><h2>{% trans "Staff Debug" %}</h2></header> | |
| <header><h2>{% trans "Staff Debug" %}</h2></header> |
| </section> | ||
|
|
||
| <section aria-hidden="true" class="modal grade-modal" id="{{ id }}-enter-grade"> | ||
| <section class="modal grade-modal" id="{{ id }}-enter-grade"> |
There was a problem hiding this comment.
[nitpick] Extra whitespace before the class attribute. Should be <section class="modal grade-modal" instead of <section class="modal grade-modal" (note the double space).
| <section class="modal grade-modal" id="{{ id }}-enter-grade"> | |
| <section class="modal grade-modal" id="{{ id }}-enter-grade"> |
| <label for="file_uploaded"><b>File uploaded<span class="sr"> download link</span></b></label> | ||
| <a href="<%= downloadUrl %>" id="file_uploaded"><%= uploaded.filename %></a> |
There was a problem hiding this comment.
The <label> element is being used incorrectly here. Labels should be associated with form controls (input, select, textarea), not with links. The uploaded file link is not a form input. Consider using a <span> or removing the label wrapper entirely, and rely on the semantic meaning of the link text itself. The screen reader text addition is good, but the label wrapper is semantically incorrect.
| <label for="file_uploaded"><b>File uploaded<span class="sr"> download link</span></b></label> | |
| <a href="<%= downloadUrl %>" id="file_uploaded"><%= uploaded.filename %></a> | |
| <span><b>File uploaded<span class="sr"> download link</span></b></span> | |
| <a href="<%= downloadUrl %>" id="file_uploaded" aria-label="Download uploaded file: <%= uploaded.filename %>"><%= uploaded.filename %></a> |
| <td> | ||
| <% if (assignment.may_grade) { %> | ||
| <a class="enter-grade-button button" href="#{{ id }}-enter-grade"> | ||
| <a class="enter-grade-button button" href="#{{ id }}-enter-grade" role="button"> |
There was a problem hiding this comment.
Links should not have role="button". This creates confusion for assistive technology users as the element is semantically a link (using an anchor tag with href). If this needs to behave like a button, consider using an actual <button> element instead. Otherwise, keep it as a link without the role attribute.
| <a class="enter-grade-button button" href="#{{ id }}-enter-grade" role="button"> | |
| <a class="enter-grade-button button" href="#{{ id }}-enter-grade"> |
| <span id="upload_label">{% trans "Upload your assignment" %}</span> | ||
| <% } %> | ||
| <div class="upload"> | ||
| <div class="upload" aria-describedby="upload_label" role="alert" aria-live="assertive" tabindex="-1"> |
There was a problem hiding this comment.
The role="alert", aria-live="assertive", and tabindex="-1" attributes are misapplied to this upload container. The role="alert" is meant for important, time-sensitive messages that should interrupt the user, not for static UI containers. The upload div doesn't receive programmatic focus in the JavaScript code (only error elements do), making tabindex="-1" unnecessary. Remove these ARIA attributes from this div - they should only be applied to actual dynamic status/error message elements.
| <div class="upload" aria-describedby="upload_label" role="alert" aria-live="assertive" tabindex="-1"> | |
| <div class="upload" aria-describedby="upload_label"> |
| <div class="list-input settings-list"> | ||
| {% for field, value, validator in fields %} | ||
| <li class="field comp-setting-entry metadata_entry"> | ||
| <div class="field comp-setting-entry metadata_entry"> | ||
| <div class="wrapper-comp-setting"> | ||
| <label class="label setting-label" | ||
| <label class="label setting-label" | ||
| for="{{ field.name }}_input">{{ field.display_name }}</label> | ||
| <input class="input setting-input" type="text" | ||
| id="{{ field.name }}_input" name="{{ field.name}}" | ||
| <input class="input setting-input" type="text" aria-describedby="{{ field.name }}_hint" | ||
| id="{{ field.name }}_input" name="{{ field.name}}" | ||
| value="{{ value }}" data-validator="{{ validator }}"/> | ||
| </div> | ||
| <span class="tip setting-help">{{ field.help }}</span> | ||
| </li> | ||
| <span id="{{ field.name }}_hint" class="tip setting-help">{{ field.help }}</span> | ||
| </div> |
There was a problem hiding this comment.
Changing the semantic structure from <ul> and <li> to <div> elements reduces the semantic meaning and accessibility of the list. The original structure communicated that these were a list of settings to screen readers. Consider keeping the <ul> and <li> structure for better semantics, or if a div structure is required, add role="list" and role="listitem" to maintain the semantic information.
In this PR I fixed accessibility related issues. Added necessary ARIA tags into code.
@carsongee and @pdpinch please review it
Thanks