Commit 3cef87b9 by Matjaz Gregoric Committed by GitHub

Merge pull request #16 from open-craft/mtyaka/a11y-fixes

A11y fixes
parents 84580b6f 7ef8e5d1
...@@ -123,6 +123,19 @@ class PollBase(XBlock, ResourceMixin, PublishEventMixin): ...@@ -123,6 +123,19 @@ class PollBase(XBlock, ResourceMixin, PublishEventMixin):
return [(key, {'label': markdown(value['label']), 'img': value['img'], 'img_alt': value.get('img_alt')}) return [(key, {'label': markdown(value['label']), 'img': value['img'], 'img_alt': value.get('img_alt')})
for key, value in items] for key, value in items]
def _get_block_id(self):
"""
Return unique ID of this block. Useful for HTML ID attributes.
Works both in LMS/Studio and workbench runtimes:
- In LMS/Studio, use the location.html_id method.
- In the workbench, use the usage_id.
"""
if hasattr(self, 'location'):
return self.location.html_id() # pylint: disable=no-member
else:
return unicode(self.scope_ids.usage_id)
def img_alt_mandatory(self): def img_alt_mandatory(self):
""" """
Determine whether alt attributes for images are configured to be mandatory. Defaults to True. Determine whether alt attributes for images are configured to be mandatory. Defaults to True.
...@@ -404,13 +417,13 @@ class PollBlock(PollBase): ...@@ -404,13 +417,13 @@ class PollBlock(PollBase):
'feedback': markdown(self.feedback) or False, 'feedback': markdown(self.feedback) or False,
'js_template': js_template, 'js_template': js_template,
'any_img': self.any_image(self.answers), 'any_img': self.any_image(self.answers),
# The SDK doesn't set url_name.
'url_name': getattr(self, 'url_name', ''),
'display_name': self.display_name, 'display_name': self.display_name,
'can_vote': self.can_vote(), 'can_vote': self.can_vote(),
'max_submissions': self.max_submissions, 'max_submissions': self.max_submissions,
'submissions_count': self.submissions_count, 'submissions_count': self.submissions_count,
'can_view_private_results': self.can_view_private_results(), 'can_view_private_results': self.can_view_private_results(),
# a11y: Transfer block ID to enable creating unique ids for questions and answers in the template
'block_id': self._get_block_id(),
}) })
if self.choice: if self.choice:
...@@ -465,6 +478,8 @@ class PollBlock(PollBase): ...@@ -465,6 +478,8 @@ class PollBlock(PollBase):
'plural': total > 1, 'plural': total > 1,
'display_name': self.display_name, 'display_name': self.display_name,
'any_img': self.any_image(self.answers), 'any_img': self.any_image(self.answers),
# a11y: Transfer block ID to enable creating unique ids for questions and answers in the template
'block_id': self._get_block_id(),
} }
@XBlock.json_handler @XBlock.json_handler
...@@ -613,19 +628,6 @@ class SurveyBlock(PollBase): ...@@ -613,19 +628,6 @@ class SurveyBlock(PollBase):
choices = Dict(help=_("The user's answers"), scope=Scope.user_state) choices = Dict(help=_("The user's answers"), scope=Scope.user_state)
event_namespace = 'xblock.survey' event_namespace = 'xblock.survey'
def _get_block_id(self):
"""
Return ID of this Survey block.
Take into account the needs of both LMS/Studio and workbench runtimes:
- In LMS/Studio, usage_id is a UsageKey object.
- In the workbench, usage_id is a string.
"""
usage_id = self.scope_ids.usage_id
# Try accessing block ID. If usage_id does not have it, return usage_id itself:
return unicode(getattr(usage_id, 'block_id', usage_id))
def student_view(self, context=None): def student_view(self, context=None):
""" """
The primary view of the SurveyBlock, shown to students The primary view of the SurveyBlock, shown to students
...@@ -649,8 +651,6 @@ class SurveyBlock(PollBase): ...@@ -649,8 +651,6 @@ class SurveyBlock(PollBase):
'any_img': self.any_image(self.questions), 'any_img': self.any_image(self.questions),
# Mustache is treating an empty string as true. # Mustache is treating an empty string as true.
'feedback': markdown(self.feedback) or False, 'feedback': markdown(self.feedback) or False,
# The SDK doesn't set url_name.
'url_name': getattr(self, 'url_name', ''),
'block_name': self.block_name, 'block_name': self.block_name,
'can_vote': self.can_vote(), 'can_vote': self.can_vote(),
'submissions_count': self.submissions_count, 'submissions_count': self.submissions_count,
...@@ -820,9 +820,15 @@ class SurveyBlock(PollBase): ...@@ -820,9 +820,15 @@ class SurveyBlock(PollBase):
detail, total = self.tally_detail() detail, total = self.tally_detail()
return { return {
'answers': [ 'answers': [
value for value in OrderedDict(self.answers).values()], {'key': key, 'label': label} for key, label in self.answers
'tally': detail, 'total': total, 'feedback': markdown(self.feedback), ],
'plural': total > 1, 'block_name': self.block_name, 'tally': detail,
'total': total,
'feedback': markdown(self.feedback),
'plural': total > 1,
'block_name': self.block_name,
# a11y: Transfer block ID to enable creating unique ids for questions and answers in the template
'block_id': self._get_block_id()
} }
@XBlock.json_handler @XBlock.json_handler
......
<script id="poll-results-template" type="text/html"> <script class="poll-results-template" type="text/html">
<h3 class="poll-header">{{display_name}}</h3> <h3 class="poll-header">{{display_name}}</h3>
<div class="poll-question-container">{{{question}}}</div> <div class="poll-question-container">{{{question}}}</div>
<ul class="poll-answers-results poll-results {{~#if any_img}} has-images{{/if}}"> <div class="poll-results-wrapper" role="radiogroup" tabindex="0">
{{#each tally}} <h4 class="poll-header">{{i18n "Results" }}</h4>
<li class="poll-result"> <ul class="poll-answers-results poll-results {{~#if any_img}} has-images{{/if}}">
<div class="poll-result-input-container"> {{#each tally}}
<input id="answer-{{key}}" type="radio" disabled {{#if choice}}checked{{/if}} /> <li class="poll-result">
</div> <div class="poll-result-input-container">
{{~#if ../any_img~}} <input id="answer-{{key}}-{{../block_id}}" type="radio" disabled {{#if choice}}checked{{/if}} />
<div class="poll-image result-image"> </div>
<label for="answer-{{key}}" class="poll-image-label"> {{~#if ../any_img~}}
{{#if img}} <div class="poll-image result-image">
<img src="{{img}}" alt="{{img_alt}}"/> <label for="answer-{{key}}-{{../block_id}}" class="poll-image-label">
{{/if}} {{#if img}}
</label> <img src="{{img}}" alt="{{img_alt}}"/>
</div><div class="percentage-gauge-background"></div> {{/if}}
{{~/if~}} </label>
<div class="percentage-gauge-container"> </div><div class="percentage-gauge-background"></div>
<div class="percentage-gauge" style="width:{{percent}}%;"></div> {{~/if~}}
<label class="poll-answer-label" for="answer-{{key}}">{{{answer}}}</label> <div class="percentage-gauge-container">
</div> <div class="percentage-gauge" style="width:{{percent}}%;"></div>
<div class="poll-percent-container"> <label class="poll-answer-label" for="answer-{{key}}-{{../block_id}}">{{{answer}}}</label>
<span class="poll-percent-display{{#if first}} poll-top-choice{{/if}}">{{percent}}%</span> </div>
<div class="poll-percent-container">
<span class="poll-percent-display{{#if first}} poll-top-choice{{/if}}">{{percent}}%</span>
</div>
</li>
{{/each}}
</ul>
<input class="input-main" type="button" name="poll-submit" value="Submit" disabled>
<div class="poll-footnote">
{{interpolate (i18n_ngettext "Results gathered from {total} respondent." "Results gathered from {total} respondents." total) total=total}}
</div>
{{#if feedback}}
<hr />
<h3 class="poll-header">{{i18n "Feedback" }}</h3>
<div class="poll-feedback">
{{{feedback}}}
</div> </div>
</li> {{/if}}
{{/each}}
</ul>
<input class="input-main" type="button" name="poll-submit" value="Submit" disabled>
<div class="poll-footnote">
{{interpolate (i18n_ngettext "Results gathered from {total} respondent." "Results gathered from {total} respondents." total) total=total}}
</div> </div>
{{#if feedback}}
<hr />
<h3 class="poll-header">{{i18n "Feedback" }}</h3>
<div class="poll-feedback">
{{{feedback}}}
</div>
{{/if}}
</script> </script>
<script id="poll-form-component" type="text/html"> <script class="poll-form-component" type="text/html">
{{#each items}} {{#each items}}
<li class="field comp-setting-entry is-set poll-{{noun}}-studio-item"> <li class="field comp-setting-entry is-set poll-{{noun}}-studio-item">
<div class="wrapper-comp-setting"> <div class="wrapper-comp-setting">
......
<script id="survey-results-template" type="text/html"> <script class="survey-results-template" type="text/html">
<h3 class="poll-header">{{block_name}}</h3> <h3 class="poll-header">{{block_name}}</h3>
<table class="survey-table poll-results"> <div class="poll-results-wrapper" tabindex="0">
<thead> <h4 class="poll-header">{{i18n "Results" }}</h4>
<tr> <table class="survey-table poll-results">
<td></td> <thead>
{{#each answers}} <tr>
<th class="survey-answer">{{{this}}}</th> <td></td>
{{/each}} {{#each answers}}
</tr> <th id="answer-{{key}}-{{../block_id}}" class="survey-answer">{{{label}}}</th>
</thead> {{/each}}
{{#each tally}} </tr>
<tr class="survey-row"> </thead>
<th class="survey-question"> {{#each tally}}
{{#if img}} <tr class="survey-row">
<div class="poll-image-td"> <th class="survey-question">
<img src="{{img}}" alt="img_alt"/> {{#if img}}
</div> <div class="poll-image-td">
{{/if}} <img src="{{img}}" alt="img_alt"/>
{{{label}}} </div>
</th> {{/if}}
{{#each answers}} {{{label}}}
<td class="survey-percentage survey-option{{#if choice}} survey-choice{{/if}}{{#if top}} poll-top-choice{{/if}}">{{percent}}%</td> </th>
{{/each}} {{#each answers}}
</tr> <td class="survey-percentage survey-option{{#if choice}} survey-choice{{/if}}{{#if top}} poll-top-choice{{/if}}" aria-labelledby="answer-{{key}}-{{../../block_id}}">{{percent}}%</td>
{{/each}} {{/each}}
</table> </tr>
<input class="input-main" type="button" name="poll-submit" value="Submit" disabled> {{/each}}
<div class="poll-footnote"> </table>
{{interpolate (i18n_ngettext "Results gathered from {total} respondent." "Results gathered from {total} respondents." total) total=total}} <input class="input-main" type="button" name="poll-submit" value="Submit" disabled>
</div> <div class="poll-footnote">
{{interpolate (i18n_ngettext "Results gathered from {total} respondent." "Results gathered from {total} respondents." total) total=total}}
{{#if feedback}}
<hr />
<h3 class="poll-header">{{i18n "Feedback" }}</h3>
<div class="poll-feedback">
{{{feedback}}}
</div> </div>
{{/if}}
{{#if feedback}}
<hr />
<h3 class="poll-header">{{i18n "Feedback" }}</h3>
<div class="poll-feedback">
{{{feedback}}}
</div>
{{/if}}
</div>
</script> </script>
...@@ -5,29 +5,31 @@ ...@@ -5,29 +5,31 @@
<h3 class="poll-header">{{ display_name }}</h3> <h3 class="poll-header">{{ display_name }}</h3>
<form> <form>
<div class="poll-question-container"> <div role="group" aria-labelledby="{{ block_id }}-question">
{{ question|safe }} <div id="{{ block_id }}-question" class="poll-question-container">
</div> {{ question|safe }}
<ul class="poll-answers"> </div>
{% for key, value in answers %} <ul class="poll-answers">
<li class="poll-answer"> {% for key, value in answers %}
<div class="poll-input-container"> <li class="poll-answer">
<input type="radio" name="choice" id="{{ url_name }}-answer-{{ key }}" value="{{ key }}" <div class="poll-input-container">
{% if choice == key %}checked{% endif %}/> <input type="radio" name="choice" id="{{ block_id }}-answer-{{ key }}" value="{{ key }}"
</div> {% if choice == key %}checked{% endif %}/>
{% if any_img %}
<div class="poll-image">
<label for="{{ url_name }}-answer-{{ key }}" class="poll-image-label">
{% if value.img %}
<img src="{{ value.img }}"/>
{% endif %}
</label>
</div> </div>
{% endif %} {% if any_img %}
<label class="poll-answer-text" for="{{ url_name }}-answer-{{ key }}">{{ value.label|safe }}</label> <div class="poll-image">
</li> <label for="{{ block_id }}-answer-{{ key }}" class="poll-image-label">
{% endfor %} {% if value.img %}
</ul> <img src="{{ value.img }}"/>
{% endif %}
</label>
</div>
{% endif %}
<label class="poll-answer-text" for="{{ block_id }}-answer-{{ key }}">{{ value.label|safe }}</label>
</li>
{% endfor %}
</ul>
</div>
<input class="input-main" type="button" name="poll-submit" <input class="input-main" type="button" name="poll-submit"
value="{% if choice %}Resubmit{% else %}Submit{% endif %}" disabled/> value="{% if choice %}Resubmit{% else %}Submit{% endif %}" disabled/>
</form> </form>
......
{{ js_template|safe }} {{ js_template|safe }}
<div class="poll-block themed-xblock" data-private="{% if private_results %}1{% endif %}" data-can-vote="{% if can_vote %}1{% endif %}"> <div class="poll-block themed-xblock" data-private="{% if private_results %}1{% endif %}"
data-can-vote="{% if can_vote %}1{% endif %}">
<div class="poll-block-form-wrapper"> <div class="poll-block-form-wrapper">
<h3 class="poll-header">{{block_name}}</h3> <h3 class="poll-header">{{block_name}}</h3>
<form> <form>
...@@ -13,7 +14,7 @@ ...@@ -13,7 +14,7 @@
</tr> </tr>
</thead> </thead>
{% for key, question in questions %} {% for key, question in questions %}
<tr class="survey-row"> <tr class="survey-row" role="group" aria-labelledby="{{block_id}}-{{key}}">
<th id="{{block_id}}-{{key}}" class="survey-question"> <th id="{{block_id}}-{{key}}" class="survey-question">
{% if question.img %} {% if question.img %}
<div class="poll-image-td"> <div class="poll-image-td">
...@@ -28,9 +29,8 @@ ...@@ -28,9 +29,8 @@
<input type="radio" <input type="radio"
name="{{key}}" name="{{key}}"
value="{{answer}}"{% if question.choice == answer %} checked{% endif %} value="{{answer}}"{% if question.choice == answer %} checked{% endif %}
aria-labelledby="{{block_id}}-{{key}} {{block_id}}-{{answer}}" aria-labelledby="{{block_id}}-{{answer}}"
/> />
<span class="sr">{{label}}</span>
</label> </label>
</td> </td>
{% endfor %} {% endfor %}
......
...@@ -37,7 +37,7 @@ function PollUtil (runtime, element, pollType) { ...@@ -37,7 +37,7 @@ function PollUtil (runtime, element, pollType) {
}); });
}); });
this.resultsTemplate = Handlebars.compile($("#" + pollType + "-results-template", element).html()); this.resultsTemplate = Handlebars.compile($("." + pollType + "-results-template", element).html());
this.viewResultsButton = $('.view-results-button', element); this.viewResultsButton = $('.view-results-button', element);
this.viewResultsButton.click(this.getResults); this.viewResultsButton.click(this.getResults);
...@@ -204,12 +204,13 @@ function PollUtil (runtime, element, pollType) { ...@@ -204,12 +204,13 @@ function PollUtil (runtime, element, pollType) {
data: JSON.stringify({}), data: JSON.stringify({}),
success: function (data) { success: function (data) {
$('div.poll-block', element).html(self.resultsTemplate(data)); $('div.poll-block', element).html(self.resultsTemplate(data));
$('.poll-results-wrapper').focus();
whenImagesLoaded(adjustGaugeBackground); whenImagesLoaded(adjustGaugeBackground);
} }
}); });
}; };
this.enableSubmit = function () { this.enableSubmit = function () {
// Enable the submit button. // Enable the submit button.
self.submit.removeAttr("disabled"); self.submit.removeAttr("disabled");
self.answers.unbind("change.enableSubmit"); self.answers.unbind("change.enableSubmit");
......
...@@ -12,7 +12,7 @@ function PollEditUtil(runtime, element, pollType) { ...@@ -12,7 +12,7 @@ function PollEditUtil(runtime, element, pollType) {
this.init = function () { this.init = function () {
// Set up the editing form for a Poll or Survey. // Set up the editing form for a Poll or Survey.
var temp = $('#poll-form-component', element).html(); var temp = $('.poll-form-component', element).html();
// Set up gettext in case it isn't available in the client runtime: // Set up gettext in case it isn't available in the client runtime:
if (typeof gettext == "undefined") { if (typeof gettext == "undefined") {
......
...@@ -44,7 +44,7 @@ def package_data(pkg, roots): ...@@ -44,7 +44,7 @@ def package_data(pkg, roots):
setup( setup(
name='xblock-poll', name='xblock-poll',
version='1.2', version='1.2.1',
description='An XBlock for polling users.', description='An XBlock for polling users.',
packages=[ packages=[
'poll', 'poll',
......
...@@ -25,8 +25,6 @@ Tests a realistic, configured Poll to make sure that everything works as it ...@@ -25,8 +25,6 @@ Tests a realistic, configured Poll to make sure that everything works as it
should. should.
""" """
import itertools
from .base_test import PollBaseTest from .base_test import PollBaseTest
...@@ -165,15 +163,15 @@ class TestSurveyFunctions(PollBaseTest): ...@@ -165,15 +163,15 @@ class TestSurveyFunctions(PollBaseTest):
answers = self.browser.find_elements_by_css_selector('.survey-answer') answers = self.browser.find_elements_by_css_selector('.survey-answer')
question_ids = [question.get_attribute('id') for question in questions] question_ids = [question.get_attribute('id') for question in questions]
answer_ids = [answer.get_attribute('id') for answer in answers] answer_ids = [answer.get_attribute('id') for answer in answers]
id_pairs = [ rows = self.browser.find_elements_by_css_selector('.survey-row')
"{question_id} {answer_id}".format(question_id=question_id, answer_id=answer_id) self.assertEqual(len(rows), len(questions))
for question_id, answer_id in itertools.product(question_ids, answer_ids) for i, row in enumerate(rows):
] self.assertEqual(row.get_attribute('role'), 'group')
options = self.browser.find_elements_by_css_selector('.survey-option input') self.assertEqual(row.get_attribute('aria-labelledby'), question_ids[i])
self.assertEqual(len(options), len(id_pairs)) options = row.find_elements_by_css_selector('.survey-option input')
for option in options: self.assertEqual(len(options), len(answers))
labelledby = option.get_attribute('aria-labelledby') for j, option in enumerate(options):
self.assertIn(labelledby, id_pairs) self.assertEqual(option.get_attribute('aria-labelledby'), answer_ids[j])
def fill_survey(self, assert_submit=False): def fill_survey(self, assert_submit=False):
""" """
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment