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.
This commit is contained in:
Jun Pataleta
2023-03-09 08:59:33 +08:00
parent 28c6b39640
commit 7f7ce0d199
2 changed files with 62 additions and 12 deletions
@@ -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 "<strong>{$a}</strong>" does not contain any question';
$string['pleaseananswerallparts'] = 'Please answer all parts of the question.';
+61 -12
View File
@@ -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('<br />', $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');
}
}