MDL-68612 user: Participants filter row accessibility improvements
authorMichael Hawkins <michaelh@moodle.com>
Mon, 18 May 2020 08:07:05 +0000 (16:07 +0800)
committerMichael Hawkins <michaelh@moodle.com>
Fri, 29 May 2020 04:18:39 +0000 (12:18 +0800)
More clearly defining each filter row and its ability to remove
filter selections for screen readers.

lang/en/user.php
user/amd/build/local/participantsfilter/selectors.min.js
user/amd/build/local/participantsfilter/selectors.min.js.map
user/amd/build/participantsfilter.min.js
user/amd/build/participantsfilter.min.js.map
user/amd/src/local/participantsfilter/selectors.js
user/amd/src/participantsfilter.js
user/classes/output/participants_filter.php
user/templates/local/participantsfilter/autocomplete_selection_items.mustache
user/templates/local/participantsfilter/filterrow.mustache

index 5eee2cb..ee3c7fd 100644 (file)
@@ -29,7 +29,9 @@ $string['adverbfor_or'] = 'or';
 $string['applyfilters'] = 'Apply filters';
 $string['clearfilterrow'] = 'Remove filter row';
 $string['clearfilters'] = 'Clear filters';
+$string['clearfilterselection'] = 'Remove "{$a}" from filter';
 $string['countparticipantsfound'] = '{$a} participants found';
+$string['filterrowlegend'] = 'Filter {$a}';
 $string['filtersetmatchdescription'] = 'How multiple filters should be combined';
 $string['match'] = 'Match';
 $string['matchofthefollowing'] = 'of the following:';
index 21be665..69b63a7 100644 (file)
Binary files a/user/amd/build/local/participantsfilter/selectors.min.js and b/user/amd/build/local/participantsfilter/selectors.min.js differ
index 023c1a6..15c6dd7 100644 (file)
Binary files a/user/amd/build/local/participantsfilter/selectors.min.js.map and b/user/amd/build/local/participantsfilter/selectors.min.js.map differ
index bbb20c6..0269fe3 100644 (file)
Binary files a/user/amd/build/participantsfilter.min.js and b/user/amd/build/participantsfilter.min.js differ
index 214f7dc..7094330 100644 (file)
Binary files a/user/amd/build/participantsfilter.min.js.map and b/user/amd/build/participantsfilter.min.js.map differ
index 3d0ef29..27c9419 100644 (file)
@@ -63,5 +63,6 @@ export default {
             all: `${getFilterRegion('filtertypedata')} [data-field-name]`,
         },
         typeList: getFilterRegion('filtertypelist'),
+        typeListSelect: `select${getFilterRegion('filtertypelist')}`,
     },
 };
index 3dcdb88..087beb0 100644 (file)
@@ -25,6 +25,7 @@
 import CourseFilter from './local/participantsfilter/filtertypes/courseid';
 import * as DynamicTable from 'core_table/dynamic';
 import GenericFilter from './local/participantsfilter/filter';
+import {get_strings as getStrings} from 'core/str';
 import Notification from 'core/notification';
 import Selectors from './local/participantsfilter/selectors';
 import Templates from 'core/templates';
@@ -56,7 +57,8 @@ export const init = participantsRegionId => {
      * @return {Promise}
      */
     const addFilterRow = () => {
-        return Templates.renderForPromise('core_user/local/participantsfilter/filterrow', {})
+        const rownum = 1 + getFilterRegion().querySelectorAll(Selectors.filter.region).length;
+        return Templates.renderForPromise('core_user/local/participantsfilter/filterrow', {"rownumber": rownum})
         .then(({html, js}) => {
             const newContentNodes = Templates.appendNodeContents(getFilterRegion(), html, js);
 
@@ -157,7 +159,7 @@ export const init = participantsRegionId => {
      *
      * @param {HTMLElement} filterRow
      */
-    const removeFilterRow = filterRow => {
+    const removeFilterRow = async filterRow => {
         // Remove the filter object.
         removeFilterObject(filterRow.dataset.filterType);
 
@@ -169,19 +171,28 @@ export const init = participantsRegionId => {
 
         // Update the list of available filter types.
         updateFiltersOptions();
+
+        // Update filter fieldset legends.
+        const filterLegends = await getAvailableFilterLegends();
+
+        getFilterRegion().querySelectorAll(Selectors.filter.region).forEach((filterRow, index) => {
+            filterRow.querySelector('legend').innerText = filterLegends[index];
+        });
+
     };
 
     /**
      * Replace the specified filter row with a new one.
      *
      * @param {HTMLElement} filterRow
+     * @param {Number} rowNum The number used to label the filter fieldset legend (eg Row 1). Defaults to 1 (the first filter).
      * @return {Promise}
      */
-    const replaceFilterRow = filterRow => {
+    const replaceFilterRow = (filterRow, rowNum = 1) => {
         // Remove the filter object.
         removeFilterObject(filterRow.dataset.filterType);
 
-        return Templates.renderForPromise('core_user/local/participantsfilter/filterrow', {})
+        return Templates.renderForPromise('core_user/local/participantsfilter/filterrow', {"rownumber": rowNum})
         .then(({html, js}) => {
             const newContentNodes = Templates.replaceNode(filterRow, html, js);
 
@@ -302,6 +313,33 @@ export const init = participantsRegionId => {
         );
     };
 
+    /**
+     * Fetch the strings used to populate the fieldset legends for the maximum number of filters possible.
+     *
+     * @return {array}
+     */
+    const getAvailableFilterLegends = async() => {
+        const maxFilters = document.querySelector(Selectors.data.typeListSelect).length - 1;
+        let requests = [];
+
+        [...Array(maxFilters)].forEach((_, rowIndex) => {
+            requests.push({
+                "key": "filterrowlegend",
+                "component": "core_user",
+                // Add 1 since rows begin at 1 (index begins at zero).
+                "param": rowIndex + 1
+            });
+        });
+
+        const legendStrings = await getStrings(requests)
+        .then(fetchedStrings => {
+            return fetchedStrings;
+        })
+        .catch(Notification.exception);
+
+        return legendStrings;
+    };
+
     // Add listeners for the main actions.
     filterSet.querySelector(Selectors.filterset.region).addEventListener('click', e => {
         if (e.target.closest(Selectors.filterset.actions.addRow)) {
index 0ab5ed8..443c5a2 100644 (file)
@@ -350,6 +350,7 @@ class participants_filter implements renderable, templatable {
             'tableregionid' => $this->tableregionid,
             'courseid' => $this->context->instanceid,
             'filtertypes' => $this->get_filtertypes(),
+            'rownumber' => 1,
         ];
 
         return $data;
index 9f358b6..732bb23 100644 (file)
 {{#items}}
     <span role="listitem" data-value="{{value}}" aria-selected="true"
             class="badge badge-secondary clickable text-wrap text-break line-height-4 mr-2 my-1">
-        {{label}}<i class="icon fa fa-times pl-2 mr-0"></i>
+        {{label}}
+        <button class="btn btn-link text-reset p-0" aria-label='{{#str}}clearfilterselection, core_user, {{label}}{{/str}}'>
+            <i class="icon fa fa-times pl-2 mr-0"></i>
+        </button>
     </span>
 {{/items}}
 {{^items}}
index 9d2bdc6..2ae3c42 100644 (file)
                 "name": "status",
                 "title": "Status"
             }
-        ]
+        ],
+        "rownumber": 1
     }
 }}
 <div data-filterregion="filter">
-    <div class="border-radius my-2 p-2 bg-white border d-flex flex-column flex-md-row align-items-md-start">
-        <div class="d-flex flex-column flex-md-row align-items-md-center">
-            <label for="core_user-local-participantsfilter-filterrow-jointype-{{uniqid}}" class="mr-md-2 mb-md-0">{{#str}}match, core_user{{/str}}</label>
-            <select class="custom-select mb-1 mb-md-0 mr-md-2" data-filterfield="join" id="core_user-local-participantsfilter-filterrow-jointype-{{uniqid}}">
-                <option value="0">{{#str}}none{{/str}}</option>
-                <option selected=selected value="1">{{#str}}any{{/str}}</option>
-                <option value="2">{{#str}}all{{/str}}</option>
-            </select>
-        </div>
+    <fieldset>
+        <legend class="sr-only">{{#str}}filterrowlegend, core_user, {{rownumber}}{{/str}}</legend>
+        <div class="border-radius my-2 p-2 bg-white border d-flex flex-column flex-md-row align-items-md-start">
+            <div class="d-flex flex-column flex-md-row align-items-md-center">
+                <label for="core_user-local-participantsfilter-filterrow-jointype-{{uniqid}}" class="mr-md-2 mb-md-0">{{#str}}match, core_user{{/str}}</label>
+                <select class="custom-select mb-1 mb-md-0 mr-md-2" data-filterfield="join" id="core_user-local-participantsfilter-filterrow-jointype-{{uniqid}}">
+                    <option value="0">{{#str}}none{{/str}}</option>
+                    <option selected=selected value="1">{{#str}}any{{/str}}</option>
+                    <option value="2">{{#str}}all{{/str}}</option>
+                </select>
+            </div>
 
-        <label class="sr-only pt-2" for="core_user-local-participantsfilter-filterrow-filtertype-{{uniqid}}">filtertype</label>
-        <select class="custom-select mb-1 mb-md-0 mr-md-2" data-filterfield="type" id="core_user-local-participantsfilter-filterrow-filtertype-{{uniqid}}">
-            <option value="">{{#str}}selectfiltertype, core_user{{/str}}</option>
-            {{#filtertypes}}
-            <option value="{{name}}">{{title}}</option>
-            {{/filtertypes}}
-        </select>
+            <label class="sr-only pt-2" for="core_user-local-participantsfilter-filterrow-filtertype-{{uniqid}}">filtertype</label>
+            <select class="custom-select mb-1 mb-md-0 mr-md-2" data-filterfield="type" id="core_user-local-participantsfilter-filterrow-filtertype-{{uniqid}}">
+                <option value="">{{#str}}selectfiltertype, core_user{{/str}}</option>
+                {{#filtertypes}}
+                <option value="{{name}}">{{title}}</option>
+                {{/filtertypes}}
+            </select>
 
-        <div data-filterregion="value" class="d-md-flex flex-column align-items-start flex-lg-row"></div>
+            <div data-filterregion="value" class="d-md-flex flex-column align-items-start flex-lg-row"></div>
 
-        <button data-filteraction="remove" class="ml-auto icon-no-margin icon-size-4 btn text-reset" aria-label="{{#str}}clearfilterrow, core_user{{/str}}">
-            <i class="icon fa fa-times-circle"></i>
-        </button>
-    </div>
-    <div data-filterregion="joinadverb" class="pl-1 text-uppercase font-weight-bold">
-        <div data-filterverbfor="0">{{#str}}adverbfor_andnot, core_user{{/str}}</div>
-        <div data-filterverbfor="1">{{#str}}adverbfor_or, core_user{{/str}}</div>
-        <div data-filterverbfor="2">{{#str}}adverbfor_and, core_user{{/str}}</div>
-    </div>
+            <button data-filteraction="remove" class="ml-auto icon-no-margin icon-size-4 btn text-reset" aria-label="{{#str}}clearfilterrow, core_user{{/str}}">
+                <i class="icon fa fa-times-circle"></i>
+            </button>
+        </div>
+        <div data-filterregion="joinadverb" class="pl-1 text-uppercase font-weight-bold">
+            <div data-filterverbfor="0">{{#str}}adverbfor_andnot, core_user{{/str}}</div>
+            <div data-filterverbfor="1">{{#str}}adverbfor_or, core_user{{/str}}</div>
+            <div data-filterverbfor="2">{{#str}}adverbfor_and, core_user{{/str}}</div>
+        </div>
+    </fieldset>
 </div>