MDL-67901 type_multichoice: clear my choice accessibility
authorBas Brands <bas@moodle.com>
Tue, 25 Feb 2020 14:32:43 +0000 (15:32 +0100)
committerBas Brands <bas@moodle.com>
Thu, 27 Feb 2020 09:24:12 +0000 (10:24 +0100)
Improve the accessibility for the clear my choice option. The
extra radio input controlling the reset feature was removed and
resetting the choice is now controlled by JavaScript.
This fixes the missing label reported in Accessibility audit and W3C
validation of the reset link

By this change this issue also fixes MDL-67280

question/type/multichoice/amd/build/clearchoice.min.js
question/type/multichoice/amd/build/clearchoice.min.js.map
question/type/multichoice/amd/src/clearchoice.js
question/type/multichoice/renderer.php
question/type/multichoice/tests/behat/clearanswers.feature [new file with mode: 0644]
question/type/multichoice/tests/walkthrough_test.php
theme/boost/scss/moodle/question.scss
theme/boost/style/moodle.css
theme/classic/style/moodle.css

index d68c688..61bd64b 100644 (file)
Binary files a/question/type/multichoice/amd/build/clearchoice.min.js and b/question/type/multichoice/amd/build/clearchoice.min.js differ
index a777477..75a3b58 100644 (file)
Binary files a/question/type/multichoice/amd/build/clearchoice.min.js.map and b/question/type/multichoice/amd/build/clearchoice.min.js.map differ
index 5ca09ce..850bbf7 100644 (file)
 define(['jquery', 'core/custom_interaction_events'], function($, CustomEvents) {
 
     var SELECTORS = {
-        CHOICE_ELEMENT: '.answer input',
-        CLEAR_CHOICE_ELEMENT: 'div[class="qtype_multichoice_clearchoice"]'
+        ANSWER_RADIOS: '.answer input',
+        CLEARRESULTS_BUTTON: 'button[data-action="clearresults"]'
     };
 
-    /**
-     * Mark clear choice radio as checked.
-     *
-     * @param {Object} clearChoiceContainer The clear choice option container.
-     */
-    var checkClearChoiceRadio = function(clearChoiceContainer) {
-        clearChoiceContainer.find('input[type="radio"]').prop('checked', true);
-    };
-
-    /**
-     * Get the clear choice div container.
-     *
-     * @param {Object} root The question root element.
-     * @param {string} fieldPrefix The question outer div prefix.
-     * @returns {Object} The clear choice div container.
-     */
-    var getClearChoiceElement = function(root, fieldPrefix) {
-        return root.find('div[id="' + fieldPrefix + '"]');
-    };
-
-    /**
-     * Hide clear choice option.
-     *
-     * @param {Object} clearChoiceContainer The clear choice option container.
-     */
-    var hideClearChoiceOption = function(clearChoiceContainer) {
-        clearChoiceContainer.addClass('sr-only');
-    };
-
-    /**
-     * Shows clear choice option.
-     *
-     * @param {Object} clearChoiceContainer The clear choice option container.
-     */
-    var showClearChoiceOption = function(clearChoiceContainer) {
-        clearChoiceContainer.removeClass('sr-only');
-    };
+    var CSSHIDDEN = 'd-none';
 
     /**
      * Register event listeners for the clear choice module.
      *
      * @param {Object} root The question outer div prefix.
-     * @param {string} fieldPrefix The "Clear choice" div prefix.
      */
-    var registerEventListeners = function(root, fieldPrefix) {
-        var clearChoiceContainer = getClearChoiceElement(root, fieldPrefix);
-
-        root.on(CustomEvents.events.activate, SELECTORS.CLEAR_CHOICE_ELEMENT, function(e, data) {
+    var registerEventListeners = function(root) {
 
-                // Mark the clear choice radio element as checked.
-                checkClearChoiceRadio(clearChoiceContainer);
-                // Now that the hidden radio has been checked, hide the clear choice option.
-                hideClearChoiceOption(clearChoiceContainer);
+        var clearChoiceButton = root.find(SELECTORS.CLEARRESULTS_BUTTON);
 
-                data.originalEvent.preventDefault();
+        root.on(CustomEvents.events.activate, SELECTORS.CLEARRESULTS_BUTTON, function(e, data) {
+            root.find(SELECTORS.ANSWER_RADIOS).each(function() {
+                $(this).prop('checked', false);
+            });
+            $(e.target).addClass(CSSHIDDEN);
+            data.originalEvent.preventDefault();
         });
 
-        root.on(CustomEvents.events.activate, SELECTORS.CHOICE_ELEMENT, function() {
-            // If the event has been triggered by any other choice, show the clear choice option.
-            showClearChoiceOption(clearChoiceContainer);
+        root.on(CustomEvents.events.activate, SELECTORS.ANSWER_RADIOS, function() {
+            clearChoiceButton.removeClass(CSSHIDDEN);
         });
     };
 
@@ -95,11 +56,10 @@ define(['jquery', 'core/custom_interaction_events'], function($, CustomEvents) {
      * Initialise clear choice module.
 
      * @param {string} root The question outer div prefix.
-     * @param {string} fieldPrefix The "Clear choice" div prefix.
      */
-    var init = function(root, fieldPrefix) {
+    var init = function(root) {
         root = $('#' + root);
-        registerEventListeners(root, fieldPrefix);
+        registerEventListeners(root);
     };
 
     return {
index 1213c8b..261a4c0 100644 (file)
@@ -282,35 +282,25 @@ class qtype_multichoice_single_renderer extends qtype_multichoice_renderer_base
             }
         }
 
-        $clearchoiceid = $this->get_input_id($qa, -1);
-        $clearchoicefieldname = $qa->get_qt_field_name('clearchoice');
-        $clearchoiceradioattrs = [
-            'type' => $this->get_input_type(),
-            'name' => $qa->get_qt_field_name('answer'),
-            'id' => $clearchoiceid,
-            'value' => -1,
-            'class' => 'sr-only'
-        ];
+        $questiondivid = $qa->get_outer_question_div_unique_id();
 
-        $cssclass = 'qtype_multichoice_clearchoice';
         // When no choice selected during rendering, then hide the clear choice option.
+        $cssclass = '';
         if (!$hascheckedchoice && $response == -1) {
-            $cssclass .= ' sr-only';
-            $clearchoiceradioattrs['checked'] = 'checked';
+            $cssclass = 'd-none';
         }
-        // Adds an hidden radio that will be checked to give the impression the choice has been cleared.
-        $clearchoiceradio = html_writer::empty_tag('input', $clearchoiceradioattrs);
-        $clearchoiceradio .= html_writer::link('', get_string('clearchoice', 'qtype_multichoice'),
-            ['for' => $clearchoiceid, 'role' => 'button']);
 
-        // Now wrap the radio and label inside a div.
-        $result = html_writer::tag('div', $clearchoiceradio, ['id' => $clearchoicefieldname, 'class' => $cssclass]);
+        $clearchoicebutton = html_writer::tag('button', get_string('clearchoice', 'qtype_multichoice'), [
+            'class' => 'btn btn-link ml-3 ' . $cssclass,
+            'data-action' => 'clearresults',
+            'data-target' => '#' . $questiondivid
+        ]);
 
         // Load required clearchoice AMD module.
         $this->page->requires->js_call_amd('qtype_multichoice/clearchoice', 'init',
-            [$qa->get_outer_question_div_unique_id(), $clearchoicefieldname]);
+            [$questiondivid]);
 
-        return $result;
+        return $clearchoicebutton;
     }
 
 }
diff --git a/question/type/multichoice/tests/behat/clearanswers.feature b/question/type/multichoice/tests/behat/clearanswers.feature
new file mode 100644 (file)
index 0000000..2facc00
--- /dev/null
@@ -0,0 +1,42 @@
+@qtype @qtype_multichoice
+Feature: Clear my answers
+  As a student
+  In order to reset Multiple choice ansers
+  I need to clear my choice
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email               |
+      | student1 | S1        | Student1 | student1@moodle.com |
+    And the following "courses" exist:
+      | fullname | shortname | category |
+      | Course 1 | C1        | 0        |
+    And the following "course enrolments" exist:
+      | user     | course | role           |
+      | student1 | C1     | student        |
+    And the following "question categories" exist:
+      | contextlevel | reference | name           |
+      | Course       | C1        | Test questions |
+    And the following "questions" exist:
+      | questioncategory | qtype       | name             | template    | questiontext    |
+      | Test questions   | multichoice | Multi-choice-001 | one_of_four | Question One  |
+    And the following "activities" exist:
+      | activity   | name   | intro              | course | idnumber | preferredbehaviour | canredoquestions |
+      | quiz       | Quiz 1 | Quiz 1 description | C1     | quiz1    | immediatefeedback  | 1                |
+    And quiz "Quiz 1" contains the following questions:
+      | question         | page |
+      | Multi-choice-001 | 1    |
+
+  @javascript
+  Scenario: Attempt a quiz and reset my chosen answer.
+    When I log in as "student1"
+    And I am on "Course 1" course homepage
+    And I follow "Quiz 1"
+    And I press "Attempt quiz now"
+    And I should see "Question One"
+    And I click on "Four" "radio" in the "Question One" "question"
+    And I should see "Clear my choice"
+    And I click on "Clear my choice" "button" in the "Question One" "question"
+    Then I should not see "Clear my choice"
+    And I click on "Check" "button" in the "Question One" "question"
+    And I should see "Please select an answer" in the "Question One" "question"
index 37bc62f..9eb2be4 100644 (file)
@@ -97,73 +97,4 @@ class qtype_multichoice_walkthrough_test extends qbehaviour_walkthrough_test_bas
                 new question_pattern_expectation('/class="r0 correct"/'),
                 new question_pattern_expectation('/class="r1"/'));
     }
-
-    /**
-     * Test for clear choice option.
-     */
-    public function test_deferredfeedback_feedback_multichoice_clearchoice() {
-
-        // Create a multichoice, single question.
-        $mc = test_question_maker::make_a_multichoice_single_question();
-        $mc->shuffleanswers = false;
-
-        $clearchoice = -1;
-        $rightchoice = 0;
-        $wrongchoice = 2;
-
-        $this->start_attempt_at_question($mc, 'deferredfeedback', 3);
-
-        // Let's first submit the wrong choice (2).
-        $this->process_submission(array('answer' => $wrongchoice));  // Wrong choice (2).
-
-        $this->check_current_mark(null);
-        // Clear choice radio should not be checked.
-        $this->check_current_output(
-            $this->get_contains_mc_radio_expectation($rightchoice, true, false), // Not checked.
-            $this->get_contains_mc_radio_expectation($rightchoice + 1, true, false), // Not checked.
-            $this->get_contains_mc_radio_expectation($rightchoice + 2, true, true), // Wrong choice (2) checked.
-            $this->get_contains_mc_radio_expectation($clearchoice, true, false), // Not checked.
-            $this->get_does_not_contain_correctness_expectation(),
-            $this->get_does_not_contain_feedback_expectation()
-        );
-
-        // Now, let's clear our previous choice.
-        $this->process_submission(array('answer' => $clearchoice)); // Clear choice (-1).
-        $this->check_current_mark(null);
-
-        // This time, the clear choice radio should be the only one checked.
-        $this->check_current_output(
-            $this->get_contains_mc_radio_expectation($rightchoice, true, false), // Not checked.
-            $this->get_contains_mc_radio_expectation($rightchoice + 1, true, false), // Not checked.
-            $this->get_contains_mc_radio_expectation($rightchoice + 2, true, false), // Not checked.
-            $this->get_contains_mc_radio_expectation($clearchoice, true, true), // Clear choice radio checked.
-            $this->get_does_not_contain_correctness_expectation(),
-            $this->get_does_not_contain_feedback_expectation()
-        );
-
-        // Finally, let's submit the right choice.
-        $this->process_submission(array('answer' => $rightchoice)); // Right choice (0).
-        $this->check_current_state(question_state::$complete);
-        $this->check_current_mark(null);
-        $this->check_current_output(
-            $this->get_contains_mc_radio_expectation($rightchoice, true, true),
-            $this->get_contains_mc_radio_expectation($rightchoice + 1, true, false),
-            $this->get_contains_mc_radio_expectation($rightchoice + 2, true, false),
-            $this->get_contains_mc_radio_expectation($clearchoice, true, false),
-            $this->get_does_not_contain_correctness_expectation(),
-            $this->get_does_not_contain_feedback_expectation()
-        );
-
-        // Finish the attempt.
-        $this->finish();
-
-        // Verify.
-        $this->check_current_state(question_state::$gradedright);
-        $this->check_current_mark(3);
-        $this->check_current_output(
-            $this->get_contains_mc_radio_expectation($rightchoice, false, true),
-            $this->get_contains_correct_expectation(),
-            new question_pattern_expectation('/class="r0 correct"/'),
-            new question_pattern_expectation('/class="r1"/'));
-    }
 }
index 4831fe5..3ba4e3f 100644 (file)
@@ -310,14 +310,6 @@ body.path-question-type {
 .que.multichoice .answer div.r1 .icon.fa-remove {
     text-indent: 0;
 }
-.qtype_multichoice_clearchoice {
-    padding-top: 10px;
-    a {
-        cursor: pointer;
-        text-decoration: underline;
-        padding-left: 30px;
-    }
-}
 
 .formulation input[type="text"],
 .formulation select {
index 330360e..325534a 100644 (file)
@@ -14964,13 +14964,6 @@ body.path-question-type {
 .que.multichoice .answer div.r1 .icon.fa-remove {
   text-indent: 0; }
 
-.qtype_multichoice_clearchoice {
-  padding-top: 10px; }
-  .qtype_multichoice_clearchoice a {
-    cursor: pointer;
-    text-decoration: underline;
-    padding-left: 30px; }
-
 .formulation input[type="text"],
 .formulation select {
   width: auto;
index 4802c14..995f28e 100644 (file)
@@ -15184,13 +15184,6 @@ body.path-question-type {
 .que.multichoice .answer div.r1 .icon.fa-remove {
   text-indent: 0; }
 
-.qtype_multichoice_clearchoice {
-  padding-top: 10px; }
-  .qtype_multichoice_clearchoice a {
-    cursor: pointer;
-    text-decoration: underline;
-    padding-left: 30px; }
-
 .formulation input[type="text"],
 .formulation select {
   width: auto;