Skip to content

Commit 812fe01

Browse files
authored
allow correct answer/hide correct answer buttons to be keyboard/screen reader accessible (#3051)
* allow for 'correct answer' & 'my answer' functionality to work with screen readers * add new 'correct/user answer' functionality in for components like MCQ/GMCQ that use itemsQuestionModel.js * refactor buttonsView to reduce amount of else & else..if code blocks * remove unused parameters from updateAttemptsCount
1 parent a4a1023 commit 812fe01

File tree

3 files changed

+97
-41
lines changed

3 files changed

+97
-41
lines changed

src/core/js/models/itemsQuestionModel.js

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import Adapt from 'core/js/adapt';
12
import QuestionModel from 'core/js/models/questionModel';
23
import ItemsComponentModel from 'core/js/models/itemsComponentModel';
34

@@ -215,4 +216,51 @@ export default class ItemsQuestionModel extends BlendedItemsComponentQuestionMod
215216
return 'choice';
216217
}
217218

219+
/**
220+
* Creates a string explaining the answer(s) the learner should have chosen
221+
* Used by ButtonsView to retrieve question-specific correct answer text for the ARIA
222+
* 'live region' that gets updated when the learner selects the 'show correct answer' button
223+
* @return {string}
224+
*/
225+
getCorrectAnswerAsText() {
226+
const globals = Adapt.course.get('_globals')._components['_' + this.get('_component')];
227+
let ariaAnswer;
228+
let correctAnswer;
229+
230+
if (this.isSingleSelect()) {
231+
ariaAnswer = globals.ariaCorrectAnswer;
232+
const correctOption = this.getChildren().findWhere({ _shouldBeSelected: true });
233+
correctAnswer = correctOption.get('text');
234+
} else {
235+
ariaAnswer = globals.ariaCorrectAnswers;
236+
const correctOptions = this.getChildren().where({ _shouldBeSelected: true });
237+
correctAnswer = correctOptions.map(correctOption => correctOption.get('text')).join('<br>');
238+
}
239+
240+
return Handlebars.compile(ariaAnswer)({ correctAnswer });
241+
}
242+
243+
/**
244+
* Creates a string listing the answer(s) the learner chose
245+
* Used by ButtonsView to retrieve question-specific user answer text for the ARIA
246+
* 'live region' that gets updated when the learner selects the 'hide correct answer' button
247+
* @return {string}
248+
*/
249+
getUserAnswerAsText() {
250+
const globals = Adapt.course.get('_globals')._components['_' + this.get('_component')];
251+
let ariaAnswer;
252+
let userAnswer;
253+
254+
const selectedItems = this.getActiveItems();
255+
if (selectedItems.length === 1) {
256+
ariaAnswer = globals.ariaUserAnswer;
257+
userAnswer = selectedItems[0].get('text');
258+
} else {
259+
ariaAnswer = globals.ariaUserAnswers;
260+
userAnswer = selectedItems.map(selectedItem => selectedItem.get('text')).join('<br>');
261+
}
262+
263+
return Handlebars.compile(ariaAnswer)({ userAnswer });
264+
}
265+
218266
}

src/core/js/views/buttonsView.js

Lines changed: 45 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export default class ButtonsView extends Backbone.View {
3535

3636
render() {
3737
const data = this.model.toJSON();
38-
const template = Handlebars.templates['buttons'];
38+
const template = Handlebars.templates.buttons;
3939
_.defer(() => {
4040
this.postRender();
4141
Adapt.trigger('buttonsView:postRender', this);
@@ -49,21 +49,31 @@ export default class ButtonsView extends Backbone.View {
4949

5050
checkResetSubmittedState() {
5151
const isSubmitted = this.model.get('_isSubmitted');
52-
53-
if (!isSubmitted) {
54-
this.$('.js-btn-marking').removeClass('is-incorrect is-correct').addClass('u-display-none');
55-
this.$el.removeClass('is-submitted');
56-
this.model.set('feedbackMessage', undefined);
57-
Adapt.a11y.toggleEnabled(this.$('.js-btn-feedback'), false);
58-
} else {
52+
if (isSubmitted) {
5953
this.$el.addClass('is-submitted');
54+
return;
6055
}
56+
57+
this.$('.js-btn-marking').removeClass('is-incorrect is-correct').addClass('u-display-none');
58+
this.$el.removeClass('is-submitted');
59+
this.model.set('feedbackMessage', undefined);
60+
Adapt.a11y.toggleEnabled(this.$('.js-btn-feedback'), false);
6161
}
6262

6363
onActionClicked() {
64-
const buttonState = this.model.get('_buttonState');
65-
this.trigger('buttons:stateUpdate', BUTTON_STATE(buttonState));
64+
const buttonState = BUTTON_STATE(this.model.get('_buttonState'));
65+
this.trigger('buttons:stateUpdate', buttonState);
6666
this.checkResetSubmittedState();
67+
68+
if (buttonState === BUTTON_STATE.SHOW_CORRECT_ANSWER) {
69+
const correctAnswer = this.model.getCorrectAnswerAsText?.();
70+
this.updateAnswerLiveRegion(correctAnswer);
71+
}
72+
73+
if (buttonState === BUTTON_STATE.HIDE_CORRECT_ANSWER) {
74+
const userAnswer = this.model.getUserAnswerAsText?.();
75+
this.updateAnswerLiveRegion(userAnswer);
76+
}
6777
}
6878

6979
onFeedbackClicked() {
@@ -74,10 +84,10 @@ export default class ButtonsView extends Backbone.View {
7484
if (changedAttribute && this.model.get('_canShowFeedback')) {
7585
// enable feedback button
7686
Adapt.a11y.toggleEnabled(this.$('.js-btn-feedback'), true);
77-
} else {
78-
// disable feedback button
79-
Adapt.a11y.toggleEnabled(this.$('.js-btn-feedback'), false);
87+
return;
8088
}
89+
// disable feedback button
90+
Adapt.a11y.toggleEnabled(this.$('.js-btn-feedback'), false);
8191
}
8292

8393
onCanSubmitChange() {
@@ -93,30 +103,17 @@ export default class ButtonsView extends Backbone.View {
93103
const buttonState = BUTTON_STATE(changedAttribute);
94104
if (changedAttribute === BUTTON_STATE.CORRECT || changedAttribute === BUTTON_STATE.INCORRECT) {
95105
// Both 'correct' and 'incorrect' states have no model answer, so disable the submit button
96-
97106
Adapt.a11y.toggleEnabled($buttonsAction, false);
107+
return;
108+
}
98109

99-
} else {
100-
101-
const propertyName = textPropertyName[buttonState.asString];
102-
const ariaLabel = this.model.get('_buttons')['_' + propertyName].ariaLabel;
103-
const buttonText = this.model.get('_buttons')['_' + propertyName].buttonText;
104-
105-
// Enable the button, make accessible and update aria labels and text
106-
107-
Adapt.a11y.toggleEnabled($buttonsAction, this.model.get('_canSubmit'));
108-
$buttonsAction.html(buttonText).attr('aria-label', ariaLabel);
109-
110-
// Make model answer button inaccessible (but still enabled) for visual users due to
111-
// the inability to represent selected incorrect/correct answers to a screen reader, may need revisiting
112-
switch (changedAttribute) {
113-
case BUTTON_STATE.SHOW_CORRECT_ANSWER:
114-
case BUTTON_STATE.HIDE_CORRECT_ANSWER:
115-
116-
Adapt.a11y.toggleAccessible($buttonsAction, false);
117-
}
110+
const propertyName = textPropertyName[buttonState.asString];
111+
const ariaLabel = this.model.get('_buttons')['_' + propertyName].ariaLabel;
112+
const buttonText = this.model.get('_buttons')['_' + propertyName].buttonText;
118113

119-
}
114+
// Enable the button, make accessible and update aria labels and text
115+
Adapt.a11y.toggleEnabled($buttonsAction, this.model.get('_canSubmit'));
116+
$buttonsAction.html(buttonText).attr('aria-label', ariaLabel);
120117
}
121118

122119
checkFeedbackState() {
@@ -127,7 +124,7 @@ export default class ButtonsView extends Backbone.View {
127124
this.$('.js-btn-marking').toggleClass('is-full-width u-display-none', !canShowFeedback);
128125
}
129126

130-
updateAttemptsCount(model, changedAttribute) {
127+
updateAttemptsCount() {
131128
const isInteractionComplete = this.model.get('_isInteractionComplete');
132129
const attemptsLeft = (this.model.get('_attemptsLeft')) ? this.model.get('_attemptsLeft') : this.model.get('_attempts');
133130
const shouldDisplayAttempts = this.model.get('_shouldDisplayAttempts');
@@ -137,12 +134,9 @@ export default class ButtonsView extends Backbone.View {
137134

138135
if (!isInteractionComplete && attemptsLeft !== 0) {
139136
attemptsString = attemptsLeft + ' ';
140-
if (attemptsLeft > 1) {
141-
attemptsString += this.model.get('_buttons').remainingAttemptsText;
142-
} else if (attemptsLeft === 1) {
143-
attemptsString += this.model.get('_buttons').remainingAttemptText;
144-
}
145-
137+
attemptsString += attemptsLeft === 1 ?
138+
this.model.get('_buttons').remainingAttemptText :
139+
this.model.get('_buttons').remainingAttemptsText;
146140
} else {
147141
this.$('.js-display-attempts').addClass('u-visibility-hidden');
148142
this.showMarking();
@@ -154,6 +148,16 @@ export default class ButtonsView extends Backbone.View {
154148

155149
}
156150

151+
/**
152+
* Updates the ARIA 'live region' with the correct/user answer when that button
153+
* is selected by the learner.
154+
* @param {string} answer Textual representation of the correct/user answer
155+
*/
156+
updateAnswerLiveRegion(answer) {
157+
if (!answer) return;
158+
this.$('.js-answer-live-region').html(answer);
159+
}
160+
157161
showMarking() {
158162
if (!this.model.shouldShowMarking) return;
159163

src/core/templates/buttons.hbs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44
<div class="icon"></div>
55
</div>
66

7+
{{#if _canShowModelAnswer}}
8+
<div class="js-answer-live-region aria-label" aria-live="assertive" aria-atomic="true"></div>
9+
{{/if}}
10+
711
<button class="btn-text btn__action js-btn-action" aria-label="{{_buttons._submit.ariaLabel}}">
812
{{{_buttons._submit.buttonText}}}
913
</button>

0 commit comments

Comments
 (0)