From 7f7ce0d199089314d5857d1b623f2ea009ff5b68 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 0fc1391f688..72aaee9d53c 100644 --- a/question/type/multianswer/lang/en/qtype_multianswer.php +++ b/question/type/multianswer/lang/en/qtype_multianswer.php @@ -38,6 +38,7 @@ $string['layoutselectinline'] = 'Drop-down menu in-line in the text'; $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 a935ed9bd5a..4c612de461c 100644 --- a/question/type/multianswer/renderer.php +++ b/question/type/multianswer/renderer.php @@ -128,6 +128,7 @@ class qtype_multianswer_renderer extends qtype_renderer { } 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 @@ class qtype_multianswer_renderer extends qtype_renderer { */ 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 @@ abstract class qtype_multianswer_subq_renderer_base extends qtype_renderer { 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 @@ class qtype_multianswer_textfield_renderer extends qtype_multianswer_subq_render 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 @@ class qtype_multianswer_multichoice_inline_renderer $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 @@ class qtype_multianswer_multichoice_vertical_renderer extends qtype_multianswer_ $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 @@ class qtype_multianswer_multichoice_vertical_renderer extends qtype_multianswer_ 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 @@ class qtype_multianswer_multichoice_vertical_renderer extends qtype_multianswer_ $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 @@ class qtype_multianswer_multichoice_vertical_renderer extends qtype_multianswer_ * @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'); } }