From a1d81dc6d96b745d848c19cecfb6659030f8bfad Mon Sep 17 00:00:00 2001 From: Jun Pataleta Date: Thu, 29 Oct 2020 15:36:15 +0800 Subject: [PATCH] MDL-70032 qtype_multichoice: Fix answer labelling * Discard the use of the label element in order to be able to render multiple choice answers as they are and have these act as the radio button/checkbox' label through the aria-labelledby attribute. * New JS module qtype_multichoice/answers that listens for click events on the answer text container and selects the appropriate answer radio button/checkbox. --- .../type/multichoice/amd/build/answers.min.js | 2 + .../multichoice/amd/build/answers.min.js.map | 1 + question/type/multichoice/amd/src/answers.js | 57 +++++++++++++++++++ question/type/multichoice/renderer.php | 21 ++++--- 4 files changed, 73 insertions(+), 8 deletions(-) create mode 100644 question/type/multichoice/amd/build/answers.min.js create mode 100644 question/type/multichoice/amd/build/answers.min.js.map create mode 100644 question/type/multichoice/amd/src/answers.js diff --git a/question/type/multichoice/amd/build/answers.min.js b/question/type/multichoice/amd/build/answers.min.js new file mode 100644 index 00000000000..5f519e3f8e4 --- /dev/null +++ b/question/type/multichoice/amd/build/answers.min.js @@ -0,0 +1,2 @@ +define ("qtype_multichoice/answers",["exports"],function(a){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.default=void 0;var b={ANSWER_LABEL:"[data-region=answer-label]"};a.default={init:function init(a){var c=document.getElementById(a),d=c.querySelectorAll(b.ANSWER_LABEL);d.forEach(function(a){a.addEventListener("click",function(a){var b=a.currentTarget.id,d=c.querySelector("[aria-labelledby=\"".concat(b,"\"]"));d.click()})})}};return a.default}); +//# sourceMappingURL=answers.min.js.map diff --git a/question/type/multichoice/amd/build/answers.min.js.map b/question/type/multichoice/amd/build/answers.min.js.map new file mode 100644 index 00000000000..f93028e78ba --- /dev/null +++ b/question/type/multichoice/amd/build/answers.min.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["../src/answers.js"],"names":["SELECTORS","ANSWER_LABEL","init","rootId","root","document","getElementById","answerLabels","querySelectorAll","forEach","answerLabel","addEventListener","e","labelId","currentTarget","id","linkedOption","querySelector","click"],"mappings":"8IA6BMA,CAAAA,CAAS,CAAG,CACdC,YAAY,CAAE,4BADA,C,WAyBH,CACXC,IAAI,CAjBK,QAAPA,CAAAA,IAAO,CAACC,CAAD,CAAY,IACfC,CAAAA,CAAI,CAAGC,QAAQ,CAACC,cAAT,CAAwBH,CAAxB,CADQ,CAIfI,CAAY,CAAGH,CAAI,CAACI,gBAAL,CAAsBR,CAAS,CAACC,YAAhC,CAJA,CAKrBM,CAAY,CAACE,OAAb,CAAqB,SAACC,CAAD,CAAiB,CAClCA,CAAW,CAACC,gBAAZ,CAA6B,OAA7B,CAAsC,SAACC,CAAD,CAAO,IACnCC,CAAAA,CAAO,CAAGD,CAAC,CAACE,aAAF,CAAgBC,EADS,CAGnCC,CAAY,CAAGZ,CAAI,CAACa,aAAL,8BAAwCJ,CAAxC,QAHoB,CAKzCG,CAAY,CAACE,KAAb,EACH,CAND,CAOH,CARD,CASH,CAEc,C","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Handles events related to the multiple-choice question type answers.\n *\n * @module qtype_multichoice/answers\n * @package qtype_multichoice\n * @copyright 2020 Jun Pataleta \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n/**\n * Selectors for this module.\n *\n * @type {{ANSWER_LABEL: string}}\n */\nconst SELECTORS = {\n ANSWER_LABEL: '[data-region=answer-label]',\n};\n\n/**\n * Init method.\n *\n * @param {string} rootId The ID of the question container.\n */\nconst init = (rootId) => {\n const root = document.getElementById(rootId);\n\n // Add click event handlers for the divs containing the answer since these cannot be enclosed in a label element.\n const answerLabels = root.querySelectorAll(SELECTORS.ANSWER_LABEL);\n answerLabels.forEach((answerLabel) => {\n answerLabel.addEventListener('click', (e) => {\n const labelId = e.currentTarget.id;\n // Fetch the answer this label is assigned to.\n const linkedOption = root.querySelector(`[aria-labelledby=\"${labelId}\"]`);\n // Trigger the click event.\n linkedOption.click();\n });\n });\n};\n\nexport default {\n init: init\n};\n"],"file":"answers.min.js"} \ No newline at end of file diff --git a/question/type/multichoice/amd/src/answers.js b/question/type/multichoice/amd/src/answers.js new file mode 100644 index 00000000000..bd5cd27d530 --- /dev/null +++ b/question/type/multichoice/amd/src/answers.js @@ -0,0 +1,57 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * Handles events related to the multiple-choice question type answers. + * + * @module qtype_multichoice/answers + * @package qtype_multichoice + * @copyright 2020 Jun Pataleta + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * Selectors for this module. + * + * @type {{ANSWER_LABEL: string}} + */ +const SELECTORS = { + ANSWER_LABEL: '[data-region=answer-label]', +}; + +/** + * Init method. + * + * @param {string} rootId The ID of the question container. + */ +const init = (rootId) => { + const root = document.getElementById(rootId); + + // Add click event handlers for the divs containing the answer since these cannot be enclosed in a label element. + const answerLabels = root.querySelectorAll(SELECTORS.ANSWER_LABEL); + answerLabels.forEach((answerLabel) => { + answerLabel.addEventListener('click', (e) => { + const labelId = e.currentTarget.id; + // Fetch the answer this label is assigned to. + const linkedOption = root.querySelector(`[aria-labelledby="${labelId}"]`); + // Trigger the click event. + linkedOption.click(); + }); + }); +}; + +export default { + init: init +}; diff --git a/question/type/multichoice/renderer.php b/question/type/multichoice/renderer.php index c886cda6817..0c83b5e1f20 100644 --- a/question/type/multichoice/renderer.php +++ b/question/type/multichoice/renderer.php @@ -88,6 +88,7 @@ abstract class qtype_multichoice_renderer_base extends qtype_with_combined_feedb $inputattributes['name'] = $this->get_input_name($qa, $value); $inputattributes['value'] = $this->get_input_value($value); $inputattributes['id'] = $this->get_input_id($qa, $value); + $inputattributes['aria-labelledby'] = $inputattributes['id'] . '_label'; $isselected = $question->is_choice_selected($response, $value); if ($isselected) { $inputattributes['checked'] = 'checked'; @@ -102,15 +103,16 @@ abstract class qtype_multichoice_renderer_base extends qtype_with_combined_feedb 'value' => 0, )); } + + $questionnumber = html_writer::span($this->number_in_style($value, $question->answernumbering), 'answernumber'); + $answertext = $question->format_text($ans->answer, $ans->answerformat, $qa, 'question', 'answer', $ansid); + $questionanswer = html_writer::div($answertext, 'flex-fill ml-1'); + $radiobuttons[] = $hidden . html_writer::empty_tag('input', $inputattributes) . - html_writer::tag('label', - html_writer::span($this->number_in_style($value, $question->answernumbering), 'answernumber') . - html_writer::tag('div', - $question->format_text( - $ans->answer, $ans->answerformat, - $qa, 'question', 'answer', $ansid), - array('class' => 'flex-fill ml-1')), - array('for' => $inputattributes['id'], 'class' => 'd-flex w-100')); + html_writer::div($questionnumber . $questionanswer, 'd-flex w-100', [ + 'id' => $inputattributes['id'] . '_label', + 'data-region' => 'answer-label', + ]); // Param $options->suppresschoicefeedback is a hack specific to the // oumultiresponse question type. It would be good to refactor to @@ -151,6 +153,9 @@ abstract class qtype_multichoice_renderer_base extends qtype_with_combined_feedb } $result .= html_writer::end_tag('div'); // Answer. + // Load JS module for the question answers. + $this->page->requires->js_call_amd('qtype_multichoice/answers', 'init', + [$qa->get_outer_question_div_unique_id()]); $result .= $this->after_choices($qa, $options); $result .= html_writer::end_tag('div'); // Ablock.