From 9ba9723826b768af0561a680ff6eca14a01dba8d Mon Sep 17 00:00:00 2001 From: Jun Pataleta Date: Fri, 17 Feb 2023 15:31:45 +0800 Subject: [PATCH] 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. --- .../multianswer/lang/en/qtype_multianswer.php | 1 + question/type/multianswer/renderer.php | 73 ++++++++++++++++--- 2 files changed, 62 insertions(+), 12 deletions(-) diff --git a/question/type/multianswer/lang/en/qtype_multianswer.php b/question/type/multianswer/lang/en/qtype_multianswer.php index 0fc1391f68844..72aaee9d53ca1 100644 --- a/question/type/multianswer/lang/en/qtype_multianswer.php +++ b/question/type/multianswer/lang/en/qtype_multianswer.php @@ -38,6 +38,7 @@ $string['layoutundefined'] = 'Undefined layout'; $string['layoutvertical'] = 'Vertical column of radio buttons'; $string['missingsubquestion'] = 'This subquestion is missing from your system and cannot be displayed.'; +$string['multichoicex'] = 'Multiple choice {$a}'; $string['nooptionsforsubquestion'] = 'Unable to get options for question part # {$a->sub} (question->id={$a->id})'; $string['noquestions'] = 'The Cloze(multianswer) question "{$a}" does not contain any question'; $string['pleaseananswerallparts'] = 'Please answer all parts of the question.'; diff --git a/question/type/multianswer/renderer.php b/question/type/multianswer/renderer.php index 27c31e9a6fd1e..2f3408ca5c05f 100644 --- a/question/type/multianswer/renderer.php +++ b/question/type/multianswer/renderer.php @@ -128,6 +128,7 @@ public function subquestion(question_attempt $qa, } else { throw new coding_exception('Unexpected subquestion type.', $subq); } + /** @var qtype_multianswer_subq_renderer_base $renderer */ $renderer = $this->page->get_renderer('qtype_multianswer', $subrenderer); return $renderer->subquestion($qa, $options, $index, $subq); } @@ -147,6 +148,12 @@ public function correct_response(question_attempt $qa) { */ abstract class qtype_multianswer_subq_renderer_base extends qtype_renderer { + /** @var int[] Stores the counts of answer instances for questions. */ + protected static $answercount = []; + + /** @var question_display_options Question display options instance for any necessary information for rendering the question. */ + protected $displayoptions; + abstract public function subquestion(question_attempt $qa, question_display_options $options, $index, question_graded_automatically $subq); @@ -198,6 +205,36 @@ protected function feedback_popup(question_graded_automatically $subq, return html_writer::tag('span', implode('
', $feedback), array('class' => 'feedbackspan accesshide')); } + + /** + * Generates a label for an answer field. + * + * If the question number is set ({@see qtype_renderer::$questionnumber}), the label will + * include the question number in order to indicate which question the answer field belongs to. + * + * @param string $langkey The lang string key for the lang string that does not include the question number. + * @param string $component The Frankenstyle component name. + * @return string + * @throws coding_exception + */ + protected function get_answer_label( + string $langkey = 'answerx', + string $component = 'question' + ): string { + // There may be multiple answer fields for a question, so we need to increment the answer fields in order to distinguish + // them from one another. + $questionnumber = $this->displayoptions->questionidentifier ?? ''; + $questionnumberindex = $questionnumber !== '' ? $questionnumber : 0; + if (isset(self::$answercount[$questionnumberindex][$langkey])) { + self::$answercount[$questionnumberindex][$langkey]++; + } else { + self::$answercount[$questionnumberindex][$langkey] = 1; + } + + $params = self::$answercount[$questionnumberindex][$langkey]; + + return $this->displayoptions->add_question_identifier_to_label(get_string($langkey, $component, $params)); + } } @@ -213,6 +250,8 @@ class qtype_multianswer_textfield_renderer extends qtype_multianswer_subq_render public function subquestion(question_attempt $qa, question_display_options $options, $index, question_graded_automatically $subq) { + $this->displayoptions = $options; + $fieldprefix = 'sub' . $index . '_'; $fieldname = $fieldprefix . 'answer'; @@ -272,7 +311,8 @@ public function subquestion(question_attempt $qa, question_display_options $opti s($correctanswer->answer), $options); $output = html_writer::start_tag('span', array('class' => 'subquestion form-inline d-inline')); - $output .= html_writer::tag('label', get_string('answer'), + + $output .= html_writer::tag('label', $this->get_answer_label(), array('class' => 'subq accesshide', 'for' => $inputattributes['id'])); $output .= html_writer::empty_tag('input', $inputattributes); $output .= $feedbackimg; @@ -296,6 +336,8 @@ class qtype_multianswer_multichoice_inline_renderer public function subquestion(question_attempt $qa, question_display_options $options, $index, question_graded_automatically $subq) { + $this->displayoptions = $options; + $fieldprefix = 'sub' . $index . '_'; $fieldname = $fieldprefix . 'answer'; @@ -340,7 +382,7 @@ public function subquestion(question_attempt $qa, question_display_options $opti $qa, 'question', 'answer', $rightanswer->id), $options); $output = html_writer::start_tag('span', array('class' => 'subquestion')); - $output .= html_writer::tag('label', get_string('answer'), + $output .= html_writer::tag('label', $this->get_answer_label(), array('class' => 'subq accesshide', 'for' => $inputattributes['id'])); $output .= $select; $output .= $feedbackimg; @@ -364,6 +406,8 @@ class qtype_multianswer_multichoice_vertical_renderer extends qtype_multianswer_ public function subquestion(question_attempt $qa, question_display_options $options, $index, question_graded_automatically $subq) { + $this->displayoptions = $options; + $fieldprefix = 'sub' . $index . '_'; $fieldname = $fieldprefix . 'answer'; $response = $qa->get_last_qt_var($fieldname); @@ -371,6 +415,7 @@ public function subquestion(question_attempt $qa, question_display_options $opti $inputattributes = array( 'type' => 'radio', 'name' => $qa->get_qt_field_name($fieldname), + 'class' => 'form-check-input', ); if ($options->readonly) { $inputattributes['disabled'] = 'disabled'; @@ -392,7 +437,7 @@ public function subquestion(question_attempt $qa, question_display_options $opti unset($inputattributes['checked']); } - $class = 'r' . ($value % 2); + $class = 'form-check text-wrap text-break'; if ($options->correctness && $isselected) { $feedbackimg = $this->feedback_image($ans->fraction); $class .= ' ' . $this->feedback_class($ans->fraction); @@ -404,7 +449,7 @@ public function subquestion(question_attempt $qa, question_display_options $opti $result .= html_writer::empty_tag('input', $inputattributes); $result .= html_writer::tag('label', $subq->format_text($ans->answer, $ans->answerformat, $qa, 'question', 'answer', $ansid), - array('for' => $inputattributes['id'])); + array('for' => $inputattributes['id'], 'class' => 'form-check-label text-body')); $result .= $feedbackimg; if ($options->feedback && $isselected && trim($ans->feedback)) { @@ -465,14 +510,17 @@ protected function choice_wrapper_end() { * @return string HTML to go before all the choices. */ protected function all_choices_wrapper_start() { - return html_writer::start_tag('div', array('class' => 'answer')); + $wrapperstart = html_writer::start_tag('fieldset', array('class' => 'answer')); + $legendtext = $this->get_answer_label('multichoicex', 'qtype_multianswer'); + $wrapperstart .= html_writer::tag('legend', $legendtext, ['class' => 'sr-only']); + return $wrapperstart; } /** * @return string HTML to go after all the choices. */ protected function all_choices_wrapper_end() { - return html_writer::end_tag('div'); + return html_writer::end_tag('fieldset'); } } @@ -488,21 +536,22 @@ class qtype_multianswer_multichoice_horizontal_renderer extends qtype_multianswer_multichoice_vertical_renderer { protected function choice_wrapper_start($class) { - return html_writer::start_tag('td', array('class' => $class)); + return html_writer::start_tag('div', array('class' => $class . ' form-check-inline')); } protected function choice_wrapper_end() { - return html_writer::end_tag('td'); + return html_writer::end_tag('div'); } protected function all_choices_wrapper_start() { - return html_writer::start_tag('table', array('class' => 'answer')) . - html_writer::start_tag('tbody') . html_writer::start_tag('tr'); + $wrapperstart = html_writer::start_tag('fieldset', ['class' => 'answer']); + $captiontext = $this->get_answer_label('multichoicex', 'qtype_multianswer'); + $wrapperstart .= html_writer::tag('legend', $captiontext, ['class' => 'sr-only']); + return $wrapperstart; } protected function all_choices_wrapper_end() { - return html_writer::end_tag('tr') . html_writer::end_tag('tbody') . - html_writer::end_tag('table'); + return html_writer::end_tag('fieldset'); } }