Skip to content

Commit f55147e

Browse files
tailetanemanoylovEmanoil Manoylovtai.letan
authored
Questionnaire\Accessibility\Slider: The slider values should be associated with the labels and the slider should be programmatically associated with the question (#495)
* Questionnaire: More meaningful error than nopermissions (#488) Co-authored-by: Emanoil Manoylov <[email protected]> * Questionnaire\Accessibility\Slider: The slider values should be associated with the labels #674064 --------- Co-authored-by: emanoylov <[email protected]> Co-authored-by: Emanoil Manoylov <[email protected]> Co-authored-by: tai.letan <[email protected]>
1 parent 64457dd commit f55147e

File tree

8 files changed

+187
-46
lines changed

8 files changed

+187
-46
lines changed

classes/question/question.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -946,7 +946,7 @@ public function questionstart_survey_display($qnum, $response=null) {
946946
$this->content = '';
947947
}
948948
$pagetags->skippedclass = $skippedclass;
949-
if ($this->type_id == QUESNUMERIC || $this->type_id == QUESTEXT) {
949+
if ($this->type_id == QUESNUMERIC || $this->type_id == QUESTEXT || $this->type_id == QUESSLIDER) {
950950
$pagetags->label = (object)['for' => self::qtypename($this->type_id) . $this->id];
951951
} else if ($this->type_id == QUESDROP) {
952952
$pagetags->label = (object)['for' => self::qtypename($this->type_id) . $this->name];

lang/en/questionnaire.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,8 @@
275275
$string['lastrespondent'] = 'Last Respondent';
276276
$string['length'] = 'Length';
277277
$string['leftlabel'] = 'Left label';
278+
$string['leftpart'] = '{$a->min} is {$a->leftlabel}';
279+
$string['leftpartdefault'] = '{$a->min} is minimum slider range';
278280
$string['managequestions'] = 'Manage questions';
279281
$string['managequestions_help'] = 'In the Manage questions section of the Edit Questions page, you can conduct a number of operations on a Questionnaire\'s questions.';
280282
$string['managequestions_link'] = 'mod/questionnaire/questions#Manage_questions';
@@ -294,6 +296,10 @@
294296
Default values are 20 characters for the Input Box width and 25 characters for the maximum length of text entered.';
295297
$string['messageprovider:message'] = 'Questionnaire reminder';
296298
$string['messageprovider:notification'] = 'Questionnaire submission';
299+
$string['middlepart'] = ', {$a->centreval} is {$a->middlelabel}';
300+
$string['middlepartdefault'] = ', {$a->centreval} is average';
301+
$string['middlepartwithtwovalues'] = ', {$a->centreval1} and {$a->centreval2} are {$a->middlelabel}';
302+
$string['middlepartwithtwovaluesdefault'] = ', {$a->centreval1} and {$a->centreval2} are average';
297303
$string['minforcedresponses'] = 'Min. forced responses';
298304
$string['minforcedresponses_help'] = 'Use these parameters to force respondent to tick a minimum of **Min.** boxes and a maximum of **Max.** check boxes. To
299305
force an exact number of check boxes to be ticked, set **Min.** and **Max.** to the same value. If only a min or a max value is desired, just leave the other
@@ -559,6 +565,8 @@
559565
$string['resumesurvey'] = 'Resume questionnaire';
560566
$string['return'] = 'Return';
561567
$string['rightlabel'] = 'Right label';
568+
$string['rightpart'] = ' and {$a->max} is {$a->rightlabel}';
569+
$string['rightpartdefault'] = ' and {$a->max} is maximum slider range';
562570
$string['save'] = 'Save';
563571
$string['save_and_exit'] = 'Save and exit';
564572
$string['saveasnew'] = 'Save as New Question';
@@ -662,6 +670,7 @@
662670
$string['viewresponses'] = 'All responses ({$a})';
663671
$string['viewyourresponses'] = 'View your response(s)';
664672
$string['warning'] = 'Warning, error encountered.';
673+
$string['where'] = 'where ';
665674
$string['wronganswers'] = 'There is something wrong with your answers (see below)';
666675
$string['wrongdateformat'] = 'The date entered: <strong>{$a}</strong> does not correspond to the format shown in the example.';
667676
$string['wrongdaterange'] = 'ERROR! The year must be set in the 1902 to 2037 range.';
@@ -672,3 +681,4 @@
672681
$string['yourresponse'] = 'View your response(s)';
673682
$string['yourresponses'] = 'View your response(s)';
674683
$string['crontask'] = 'Questionnaire cleanup job';
684+
$string['nopermissions'] = 'Sorry, but you do not currently have permissions to view this page or perform this action.';

locallib.php

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -134,20 +134,29 @@ function questionnaire_choice_values($content) {
134134
* @return array a standard jsmodule structure.
135135
*/
136136
function questionnaire_get_js_module() {
137-
return array(
137+
return [
138138
'name' => 'mod_questionnaire',
139139
'fullpath' => '/mod/questionnaire/module.js',
140-
'requires' => array('base', 'dom', 'event-delegate', 'event-key',
141-
'core_question_engine', 'moodle-core-formchangechecker'),
142-
'strings' => array(
143-
array('cancel', 'moodle'),
144-
array('flagged', 'question'),
145-
array('functiondisabledbysecuremode', 'quiz'),
146-
array('startattempt', 'quiz'),
147-
array('timesup', 'quiz'),
148-
array('changesmadereallygoaway', 'moodle'),
149-
),
150-
);
140+
'requires' => ['base', 'dom', 'event-delegate', 'event-key',
141+
'core_question_engine', 'moodle-core-formchangechecker'],
142+
'strings' => [
143+
['cancel', 'moodle'],
144+
['flagged', 'question'],
145+
['functiondisabledbysecuremode', 'quiz'],
146+
['startattempt', 'quiz'],
147+
['timesup', 'quiz'],
148+
['changesmadereallygoaway', 'moodle'],
149+
['leftpart', 'questionnaire'],
150+
['leftpartdefault', 'questionnaire'],
151+
['middlepart', 'questionnaire'],
152+
['middlepartdefault', 'questionnaire'],
153+
['middlepartwithtwovalues', 'questionnaire'],
154+
['middlepartwithtwovaluesdefault', 'questionnaire'],
155+
['rightpart', 'questionnaire'],
156+
['rightpartdefault', 'questionnaire'],
157+
['where', 'questionnaire'],
158+
],
159+
];
151160
}
152161

153162
/**

module.js

Lines changed: 89 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -267,15 +267,22 @@ M.mod_questionnaire.init_sendmessage = function(Y) {
267267

268268
};
269269
M.mod_questionnaire.init_slider = function() {
270-
const allRanges = document.querySelectorAll(".slider");
270+
const allRanges = document.querySelectorAll(".question-slider");
271271
allRanges.forEach(wrap => {
272272
const range = wrap.querySelector("input.questionnaire-slider");
273273
const bubble = wrap.querySelector(".bubble");
274+
const labels = {
275+
leftlabel: wrap.querySelector(".left-side-label"),
276+
middlelabel: wrap.querySelector(".middle-side-label"),
277+
rightlabel: wrap.querySelector(".right-side-label")
278+
};
274279

275280
range.addEventListener("input", () => {
276281
setBubble(range, bubble);
282+
createAccessibilityHeading(range, bubble, labels);
277283
});
278284
setBubble(range, bubble);
285+
createAccessibilityHeading(range, bubble, labels);
279286
});
280287

281288
function setBubble(range, bubble) {
@@ -296,4 +303,85 @@ M.mod_questionnaire.init_slider = function() {
296303
// Sorta magic numbers based on size of the native UI thumb
297304
bubble.style.left = `calc(${newVal}% + (${8 - newVal * 0.15}px))`;
298305
}
306+
307+
/**
308+
* Adds accessibility support by generating an h2 element with relevant text.
309+
* @param {HTMLElement} range - The range input element.
310+
* @param {HTMLElement} bubble - The bubble element.
311+
* @param {Object} labels - The object containing label elements.
312+
*/
313+
function createAccessibilityHeading(range, bubble, labels) {
314+
const min = range.min ? range.min : 0;
315+
const max = range.max ? range.max : 100;
316+
const step = range.step ? range.step : 1;
317+
const centerValues = calculateCenterValues(range);
318+
319+
const accesshideElement = document.createElement('h2');
320+
accesshideElement.classList.add('accesshide');
321+
322+
const a = {
323+
min,
324+
max,
325+
leftlabel: labels.leftlabel.innerHTML,
326+
rightlabel: labels.rightlabel.innerHTML,
327+
middlelabel: labels.middlelabel.innerHTML,
328+
centreval: centerValues[0],
329+
centreval1: centerValues[0],
330+
centreval2: centerValues[1],
331+
};
332+
333+
const rangeNum = max - min;
334+
const numSteps = rangeNum / step;
335+
336+
const middleLabel = (numSteps % 2 !== 0)
337+
? (labels.middlelabel.innerHTML
338+
? M.util.get_string('middlepartwithtwovalues', 'questionnaire', a)
339+
: M.util.get_string('middlepartwithtwovaluesdefault', 'questionnaire', a))
340+
: (labels.middlelabel.innerHTML
341+
? M.util.get_string('middlepart', 'questionnaire', a)
342+
: M.util.get_string('middlepartdefault', 'questionnaire', a));
343+
344+
const leftPart = labels.leftlabel.innerHTML ? M.util.get_string('leftpart', 'questionnaire', a)
345+
: M.util.get_string('leftpartdefault', 'questionnaire', a);
346+
347+
const middlePart = middleLabel ? middleLabel : '';
348+
349+
const rightPart = labels.rightlabel.innerHTML
350+
? M.util.get_string('rightpart', 'questionnaire', a) : M.util.get_string('rightpartdefault', 'questionnaire', a);
351+
352+
// Include a whitespace to accommodate a setting of the NVDA screen reader.
353+
const whitespace = '\xa0';
354+
accesshideElement.textContent = whitespace + M.util.get_string('where', 'questionnaire', a) + leftPart + middlePart + rightPart;
355+
bubble.appendChild(accesshideElement);
356+
}
357+
358+
/**
359+
* Calculates the center value(s) based on the given range.
360+
* @param {Object} range - The range object containing properties: min, max, step.
361+
* @returns {Array} - An array containing the center value(s).
362+
*/
363+
function calculateCenterValues(range) {
364+
const min = parseInt(range.min);
365+
const max = parseInt(range.max);
366+
const step = parseInt(range.step);
367+
const rangeNum = max - min;
368+
369+
// Calculate the number of steps.
370+
const numSteps = rangeNum / step;
371+
372+
// Calculate the center value(s).
373+
const centerValues = [];
374+
if (numSteps % 2 === 0) {
375+
// Even number of steps, return single center value.
376+
const center = (min + max) / 2;
377+
centerValues.push(center);
378+
} else {
379+
// Odd number of steps, calculate lower and upper center values.
380+
const lowerCenter = min + Math.floor(numSteps / 2) * step;
381+
const upperCenter = lowerCenter + step;
382+
centerValues.push(lowerCenter, upperCenter);
383+
}
384+
385+
return centerValues;
386+
}
299387
};

templates/question_slider.mustache

Lines changed: 23 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@
1515
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
1616
}}
1717
{{!
18-
@template mod_questionnaire/question_yesno
18+
@template mod_questionnaire/question_slider
1919
20-
Template which defines a yes/no type question survey display.
20+
Template which defines a slider type question survey display.
2121
2222
Classes required for JS:
2323
* /mod/questionnaire/module.js
@@ -30,44 +30,37 @@
3030
Example context (json):
3131
{
3232
"qelements": {
33-
"choice": [
33+
"extradata":
3434
{
35-
"id": "choice1",
36-
"value": "y",
37-
"name": "q23",
38-
"checked": 1,
39-
"disabled": "",
40-
"onclick": "dosomething()",
41-
"label": "Yes"
42-
},
43-
{
44-
"id": "choice2",
45-
"value": "n",
46-
"name": "q23",
47-
"checked": 0,
48-
"disabled": "",
49-
"onclick": "dosomething()",
50-
"label": "No"
35+
"id": "slider1",
36+
"name": "q1",
37+
"minrange": "1",
38+
"maxrange": "10",
39+
"startingvalue": 5,
40+
"stepvalue": "1",
41+
"leftlabel": "Left label",
42+
"rightlabel": "Right label",
43+
"centerlabel": "Center label"
5144
}
52-
]
5345
}
5446
}
5547
}}
56-
<!-- Begin HTML generated from question_yesno template. -->
57-
<div class="question-slider">
58-
{{#qelements}}
59-
{{#extradata}}
48+
<!-- Begin HTML generated from question_slider template. -->
49+
{{#qelements}}
50+
{{#extradata}}
51+
<div class="question-slider">
6052
<div class="left-side-label">{{extradata.leftlabel}}</div>
6153
<div class="middle-side-content">
6254
<div class="slider">
63-
<input name="{{extradata.name}}" type="range" min="{{extradata.minrange}}" max="{{extradata.maxrange}}" step="{{extradata.stepvalue}}"
64-
value="{{extradata.startingvalue}}" class="questionnaire-slider" id="slider-{{extradata.id}}">
55+
<input name="{{extradata.name}}" type="range" min="{{extradata.minrange}}"
56+
max="{{extradata.maxrange}}" step="{{extradata.stepvalue}}"
57+
value="{{extradata.startingvalue}}" class="questionnaire-slider" id="{{extradata.id}}">
6558
<output class="bubble"></output>
6659
</div>
6760
<div class="middle-side-label">{{extradata.centerlabel}}</div>
6861
</div>
6962
<div class="right-side-label">{{extradata.rightlabel}}</div>
70-
{{/extradata}}
71-
{{/qelements}}
72-
</div>
73-
<!-- End HTML generated from question_yesno template. -->
63+
</div>
64+
{{/extradata}}
65+
{{/qelements}}
66+
<!-- End HTML generated from question_slider template. -->

templates/response_slider.mustache

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
<div class="middle-side-content">
4141
<div class="slider">
4242
<input disabled type="range" min="{{extradata.minrange}}" max="{{extradata.maxrange}}" step="{{extradata.stepvalue}}" value="{{content}}"
43-
class="questionnaire-slider" id="slider-{{extradata.id}}">
43+
class="questionnaire-slider" id="{{extradata.id}}">
4444
<output class="bubble"></output>
4545
</div>
4646
<div class="middle-side-label">{{extradata.centerlabel}}</div>

tests/behat/slider_question.feature

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,3 +85,44 @@ Feature: Slider questions can add slider with range for users to choose
8585
| Slider starting value | 10 |
8686
| Slider increment value | 15 |
8787
And I should see "This question type supports an absolute maximum range of -100 to +100. We expect the vast majority of questionnaire designs to use a range of 1-10 or -10 to +10."
88+
89+
@javascript
90+
Scenario: Test accessibility for slider question type.
91+
Given I log in as "teacher1"
92+
And I am on "Course 1" course homepage
93+
And I am on the "Test questionnaire" "questionnaire activity" page
94+
And I navigate to "Questions" in current page administration
95+
And I add a "Slider" question and I fill the form with:
96+
| Question Name | Q2 |
97+
| Question Text | Slider question test normal case |
98+
| Left label | Left |
99+
| Right label | Right |
100+
| Centre label | Center |
101+
| Minimum slider range (left) | 1 |
102+
| Maximum slider range (right) | 9 |
103+
| Slider starting value | 5 |
104+
| Slider increment value | 1 |
105+
And I add a "Slider" question and I fill the form with:
106+
| Question Name | Q3 |
107+
| Question Text | Slider question test Left label only |
108+
| Left label | Left |
109+
| Minimum slider range (left) | -5 |
110+
| Maximum slider range (right) | 5 |
111+
| Slider starting value | 1 |
112+
| Slider increment value | 1 |
113+
And I add a "Slider" question and I fill the form with:
114+
| Question Name | Q4 |
115+
| Question Text | Slider question test no label |
116+
| Minimum slider range (left) | 1 |
117+
| Maximum slider range (right) | 9 |
118+
| Slider starting value | 1 |
119+
| Slider increment value | 1 |
120+
And I navigate to "Preview" in current page administration
121+
Then "//legend[@class='accesshide' and contains(text(), 'Question #1')]" "xpath_element" should exist
122+
Then "//output[@class='bubble' and contains(text(), '5')]/h2[contains(text(), 'where 5 is Left, 50 and 55 are Center and 100 is Right')]" "xpath_element" should exist
123+
Then "//legend[@class='accesshide' and contains(text(), 'Question #2')]" "xpath_element" should exist
124+
Then "//output[@class='bubble' and contains(text(), '5')]/h2[contains(text(), 'where 1 is Left, 5 is Center and 9 is Right')]" "xpath_element" should exist
125+
Then "//legend[@class='accesshide' and contains(text(), 'Question #3')]" "xpath_element" should exist
126+
Then "//output[@class='bubble' and contains(text(), '1')]/h2[contains(text(), 'where -5 is Left, 0 is average and 5 is maximum slider range')]" "xpath_element" should exist
127+
Then "//legend[@class='accesshide' and contains(text(), 'Question #4')]" "xpath_element" should exist
128+
Then "//output[@class='bubble' and contains(text(), '1')]/h2[contains(text(), 'where 1 is minimum slider range, 5 is average and 9 is maximum slider range')]" "xpath_element" should exist

version.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525

2626
defined('MOODLE_INTERNAL') || die();
2727

28-
$plugin->version = 2022121600; // The current module version (Date: YYYYMMDDXX).
28+
$plugin->version = 2022121600.01; // The current module version (Date: YYYYMMDDXX).
2929
$plugin->requires = 2022112800.00; // Moodle version (4.1.0).
3030

3131
$plugin->component = 'mod_questionnaire';

0 commit comments

Comments
 (0)