Skip to content

Commit 9ba9723

Browse files
committed
MDL-76849 qtype_multianswer: Include question number in answer fields
* Add the question number to the answer fields if it's available. * Improve multiple choice question accessibility: - Label the multiple choice question groups appropriately by enclosing them in fieldset tags and applying sr-only legend tags to label them. - Apply Bootstrap form-check classes to the radio buttons, so they are rendered better and become responsive as well. This also helps avoid the use of the table element for layout purposes when rendering horizontal multiple choice sub-questions.
1 parent 716c223 commit 9ba9723

File tree

2 files changed

+62
-12
lines changed

2 files changed

+62
-12
lines changed

question/type/multianswer/lang/en/qtype_multianswer.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
$string['layoutundefined'] = 'Undefined layout';
3939
$string['layoutvertical'] = 'Vertical column of radio buttons';
4040
$string['missingsubquestion'] = 'This subquestion is missing from your system and cannot be displayed.';
41+
$string['multichoicex'] = 'Multiple choice {$a}';
4142
$string['nooptionsforsubquestion'] = 'Unable to get options for question part # {$a->sub} (question->id={$a->id})';
4243
$string['noquestions'] = 'The Cloze(multianswer) question "<strong>{$a}</strong>" does not contain any question';
4344
$string['pleaseananswerallparts'] = 'Please answer all parts of the question.';

question/type/multianswer/renderer.php

Lines changed: 61 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ public function subquestion(question_attempt $qa,
128128
} else {
129129
throw new coding_exception('Unexpected subquestion type.', $subq);
130130
}
131+
/** @var qtype_multianswer_subq_renderer_base $renderer */
131132
$renderer = $this->page->get_renderer('qtype_multianswer', $subrenderer);
132133
return $renderer->subquestion($qa, $options, $index, $subq);
133134
}
@@ -147,6 +148,12 @@ public function correct_response(question_attempt $qa) {
147148
*/
148149
abstract class qtype_multianswer_subq_renderer_base extends qtype_renderer {
149150

151+
/** @var int[] Stores the counts of answer instances for questions. */
152+
protected static $answercount = [];
153+
154+
/** @var question_display_options Question display options instance for any necessary information for rendering the question. */
155+
protected $displayoptions;
156+
150157
abstract public function subquestion(question_attempt $qa,
151158
question_display_options $options, $index,
152159
question_graded_automatically $subq);
@@ -198,6 +205,36 @@ protected function feedback_popup(question_graded_automatically $subq,
198205
return html_writer::tag('span', implode('<br />', $feedback),
199206
array('class' => 'feedbackspan accesshide'));
200207
}
208+
209+
/**
210+
* Generates a label for an answer field.
211+
*
212+
* If the question number is set ({@see qtype_renderer::$questionnumber}), the label will
213+
* include the question number in order to indicate which question the answer field belongs to.
214+
*
215+
* @param string $langkey The lang string key for the lang string that does not include the question number.
216+
* @param string $component The Frankenstyle component name.
217+
* @return string
218+
* @throws coding_exception
219+
*/
220+
protected function get_answer_label(
221+
string $langkey = 'answerx',
222+
string $component = 'question'
223+
): string {
224+
// There may be multiple answer fields for a question, so we need to increment the answer fields in order to distinguish
225+
// them from one another.
226+
$questionnumber = $this->displayoptions->questionidentifier ?? '';
227+
$questionnumberindex = $questionnumber !== '' ? $questionnumber : 0;
228+
if (isset(self::$answercount[$questionnumberindex][$langkey])) {
229+
self::$answercount[$questionnumberindex][$langkey]++;
230+
} else {
231+
self::$answercount[$questionnumberindex][$langkey] = 1;
232+
}
233+
234+
$params = self::$answercount[$questionnumberindex][$langkey];
235+
236+
return $this->displayoptions->add_question_identifier_to_label(get_string($langkey, $component, $params));
237+
}
201238
}
202239

203240

@@ -213,6 +250,8 @@ class qtype_multianswer_textfield_renderer extends qtype_multianswer_subq_render
213250
public function subquestion(question_attempt $qa, question_display_options $options,
214251
$index, question_graded_automatically $subq) {
215252

253+
$this->displayoptions = $options;
254+
216255
$fieldprefix = 'sub' . $index . '_';
217256
$fieldname = $fieldprefix . 'answer';
218257

@@ -272,7 +311,8 @@ public function subquestion(question_attempt $qa, question_display_options $opti
272311
s($correctanswer->answer), $options);
273312

274313
$output = html_writer::start_tag('span', array('class' => 'subquestion form-inline d-inline'));
275-
$output .= html_writer::tag('label', get_string('answer'),
314+
315+
$output .= html_writer::tag('label', $this->get_answer_label(),
276316
array('class' => 'subq accesshide', 'for' => $inputattributes['id']));
277317
$output .= html_writer::empty_tag('input', $inputattributes);
278318
$output .= $feedbackimg;
@@ -296,6 +336,8 @@ class qtype_multianswer_multichoice_inline_renderer
296336
public function subquestion(question_attempt $qa, question_display_options $options,
297337
$index, question_graded_automatically $subq) {
298338

339+
$this->displayoptions = $options;
340+
299341
$fieldprefix = 'sub' . $index . '_';
300342
$fieldname = $fieldprefix . 'answer';
301343

@@ -340,7 +382,7 @@ public function subquestion(question_attempt $qa, question_display_options $opti
340382
$qa, 'question', 'answer', $rightanswer->id), $options);
341383

342384
$output = html_writer::start_tag('span', array('class' => 'subquestion'));
343-
$output .= html_writer::tag('label', get_string('answer'),
385+
$output .= html_writer::tag('label', $this->get_answer_label(),
344386
array('class' => 'subq accesshide', 'for' => $inputattributes['id']));
345387
$output .= $select;
346388
$output .= $feedbackimg;
@@ -364,13 +406,16 @@ class qtype_multianswer_multichoice_vertical_renderer extends qtype_multianswer_
364406
public function subquestion(question_attempt $qa, question_display_options $options,
365407
$index, question_graded_automatically $subq) {
366408

409+
$this->displayoptions = $options;
410+
367411
$fieldprefix = 'sub' . $index . '_';
368412
$fieldname = $fieldprefix . 'answer';
369413
$response = $qa->get_last_qt_var($fieldname);
370414

371415
$inputattributes = array(
372416
'type' => 'radio',
373417
'name' => $qa->get_qt_field_name($fieldname),
418+
'class' => 'form-check-input',
374419
);
375420
if ($options->readonly) {
376421
$inputattributes['disabled'] = 'disabled';
@@ -392,7 +437,7 @@ public function subquestion(question_attempt $qa, question_display_options $opti
392437
unset($inputattributes['checked']);
393438
}
394439

395-
$class = 'r' . ($value % 2);
440+
$class = 'form-check text-wrap text-break';
396441
if ($options->correctness && $isselected) {
397442
$feedbackimg = $this->feedback_image($ans->fraction);
398443
$class .= ' ' . $this->feedback_class($ans->fraction);
@@ -404,7 +449,7 @@ public function subquestion(question_attempt $qa, question_display_options $opti
404449
$result .= html_writer::empty_tag('input', $inputattributes);
405450
$result .= html_writer::tag('label', $subq->format_text($ans->answer,
406451
$ans->answerformat, $qa, 'question', 'answer', $ansid),
407-
array('for' => $inputattributes['id']));
452+
array('for' => $inputattributes['id'], 'class' => 'form-check-label text-body'));
408453
$result .= $feedbackimg;
409454

410455
if ($options->feedback && $isselected && trim($ans->feedback)) {
@@ -465,14 +510,17 @@ protected function choice_wrapper_end() {
465510
* @return string HTML to go before all the choices.
466511
*/
467512
protected function all_choices_wrapper_start() {
468-
return html_writer::start_tag('div', array('class' => 'answer'));
513+
$wrapperstart = html_writer::start_tag('fieldset', array('class' => 'answer'));
514+
$legendtext = $this->get_answer_label('multichoicex', 'qtype_multianswer');
515+
$wrapperstart .= html_writer::tag('legend', $legendtext, ['class' => 'sr-only']);
516+
return $wrapperstart;
469517
}
470518

471519
/**
472520
* @return string HTML to go after all the choices.
473521
*/
474522
protected function all_choices_wrapper_end() {
475-
return html_writer::end_tag('div');
523+
return html_writer::end_tag('fieldset');
476524
}
477525
}
478526

@@ -488,21 +536,22 @@ class qtype_multianswer_multichoice_horizontal_renderer
488536
extends qtype_multianswer_multichoice_vertical_renderer {
489537

490538
protected function choice_wrapper_start($class) {
491-
return html_writer::start_tag('td', array('class' => $class));
539+
return html_writer::start_tag('div', array('class' => $class . ' form-check-inline'));
492540
}
493541

494542
protected function choice_wrapper_end() {
495-
return html_writer::end_tag('td');
543+
return html_writer::end_tag('div');
496544
}
497545

498546
protected function all_choices_wrapper_start() {
499-
return html_writer::start_tag('table', array('class' => 'answer')) .
500-
html_writer::start_tag('tbody') . html_writer::start_tag('tr');
547+
$wrapperstart = html_writer::start_tag('fieldset', ['class' => 'answer']);
548+
$captiontext = $this->get_answer_label('multichoicex', 'qtype_multianswer');
549+
$wrapperstart .= html_writer::tag('legend', $captiontext, ['class' => 'sr-only']);
550+
return $wrapperstart;
501551
}
502552

503553
protected function all_choices_wrapper_end() {
504-
return html_writer::end_tag('tr') . html_writer::end_tag('tbody') .
505-
html_writer::end_tag('table');
554+
return html_writer::end_tag('fieldset');
506555
}
507556
}
508557

0 commit comments

Comments
 (0)