MDL-78885 comboboxsearch: Enable aria.js for auto handling

- Added the `dropdown` class to ensure toggle element focus when the
  dropdown is closed.
- Improved keyboard handling by adding the `dropdown` class.
  aria.js will automatically handle keyboard interactions.
- Removed redundant keyboard handling.
- The "view all results" option is just a normal option in a combobox.
  It should not be treated as the default action for a combobox.
- Ensured correct markup for 'Esc' key handling. aria.js automatically
  focuses on the toggle element if the dropdown's toggle and the
  dropdown menu are wrapped within a .dropdown element.
- Implemented menu closure for outside clicks and when leaving the edit
  box.
- Manually focused on the user search element when opening the search
 dropdown due to a focusLock issue.
- Fix the issue of another dropdown staying open
- Clicking on the clearSearchButton should not close the dropdown
This commit is contained in:
Shamim Rezaie 2023-10-03 04:20:40 +11:00
parent 2f023f9ebe
commit d886cba9d3
27 changed files with 106 additions and 307 deletions

View File

@ -1,3 +1,3 @@
define("core_grades/comboboxsearch/grade",["exports","core/comboboxsearch/search_combobox","core_grades/searchwidget/repository","core/templates","core/utils"],(function(_exports,_search_combobox,Repository,_templates,_utils){var obj;function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_search_combobox=(obj=_search_combobox)&&obj.__esModule?obj:{default:obj},Repository=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(Repository);class GradeItemSearch extends _search_combobox.default{constructor(){super(),function(obj,key,value){key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value}(this,"courseID",void 0),this.selectors={...this.selectors,courseid:'[data-region="courseid"]',placeholder:'.gradesearchdropdown [data-region="searchplaceholder"]'};const component=document.querySelector(this.componentSelector());this.courseID=component.querySelector(this.selectors.courseid).dataset.courseid,this.renderDefault()}static init(){return new GradeItemSearch}componentSelector(){return".grade-search"}dropdownSelector(){return".gradesearchdropdown"}triggerSelector(){return".gradesearchwidget"}async renderDropdown(){const{html:html,js:js}=await(0,_templates.renderForPromise)("core/local/comboboxsearch/resultset",{results:this.getMatchedResults(),hasresults:this.getMatchedResults().length>0,searchterm:this.getSearchTerm()});(0,_templates.replaceNodeContents)(this.selectors.placeholder,html,js)}async renderDefault(){this.setMatchedResults(await this.filterDataset(await this.getDataset())),this.filterMatchDataset(),await this.renderDropdown(),this.updateNodes(),this.registerInputEvents()}async fetchDataset(){return await Repository.gradeitemFetch(this.courseID).then((r=>r.gradeitems))}async filterDataset(filterableData){return""===this.getPreppedSearchTerm()?filterableData:filterableData.filter((grade=>Object.keys(grade).some((key=>""!==grade[key]&&grade[key].toString().toLowerCase().includes(this.getPreppedSearchTerm())))))}filterMatchDataset(){this.setMatchedResults(this.getMatchedResults().map((grade=>({id:grade.id,name:grade.name,link:this.selectOneLink(grade.id)}))))}registerInputEvents(){this.searchInput.addEventListener("input",(0,_utils.debounce)((async()=>{this.setSearchTerms(this.searchInput.value),""===this.searchInput.value?this.clearSearchButton.classList.add("d-none"):this.clearSearchButton.classList.remove("d-none"),await this.filterrenderpipe()}),300))}async clickHandler(e){e.target.closest(this.selectors.dropdown)&&e.stopImmediatePropagation(),this.clearSearchButton.addEventListener("click",(async()=>{this.searchInput.value="",this.setSearchTerms(this.searchInput.value),await this.filterrenderpipe()})),e.target.closest(".dropdown-item")&&0===e.button&&(window.location=e.target.closest(".dropdown-item").href)}keyHandler(e){if(super.keyHandler(e),"Escape"===e.key)if("option"===document.activeElement.getAttribute("role"))e.stopPropagation(),this.searchInput.focus({preventScroll:!0});else if(e.target.closest(this.selectors.input)){this.component.querySelector(this.selectors.trigger).focus({preventScroll:!0})}}registerInputHandlers(){this.searchInput.addEventListener("input",(0,_utils.debounce)((()=>{this.setSearchTerms(this.searchInput.value),""===this.getSearchTerm()?this.clearSearchButton.classList.add("d-none"):this.clearSearchButton.classList.remove("d-none")}),300))}selectOneLink(gradeID){throw new Error("selectOneLink(".concat(gradeID,") must be implemented in ").concat(this.constructor.name))}}return _exports.default=GradeItemSearch,_exports.default}));
define("core_grades/comboboxsearch/grade",["exports","core/comboboxsearch/search_combobox","core_grades/searchwidget/repository","core/templates","core/utils"],(function(_exports,_search_combobox,Repository,_templates,_utils){var obj;function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_search_combobox=(obj=_search_combobox)&&obj.__esModule?obj:{default:obj},Repository=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(Repository);class GradeItemSearch extends _search_combobox.default{constructor(){super(),function(obj,key,value){key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value}(this,"courseID",void 0),this.selectors={...this.selectors,courseid:'[data-region="courseid"]',placeholder:'.gradesearchdropdown [data-region="searchplaceholder"]'};const component=document.querySelector(this.componentSelector());this.courseID=component.querySelector(this.selectors.courseid).dataset.courseid,this.renderDefault()}static init(){return new GradeItemSearch}componentSelector(){return".grade-search"}dropdownSelector(){return".gradesearchdropdown"}triggerSelector(){return".gradesearchwidget"}async renderDropdown(){const{html:html,js:js}=await(0,_templates.renderForPromise)("core/local/comboboxsearch/resultset",{results:this.getMatchedResults(),hasresults:this.getMatchedResults().length>0,searchterm:this.getSearchTerm()});(0,_templates.replaceNodeContents)(this.selectors.placeholder,html,js)}async renderDefault(){this.setMatchedResults(await this.filterDataset(await this.getDataset())),this.filterMatchDataset(),await this.renderDropdown(),this.updateNodes(),this.registerInputEvents()}async fetchDataset(){return await Repository.gradeitemFetch(this.courseID).then((r=>r.gradeitems))}async filterDataset(filterableData){return""===this.getPreppedSearchTerm()?filterableData:filterableData.filter((grade=>Object.keys(grade).some((key=>""!==grade[key]&&grade[key].toString().toLowerCase().includes(this.getPreppedSearchTerm())))))}filterMatchDataset(){this.setMatchedResults(this.getMatchedResults().map((grade=>({id:grade.id,name:grade.name,link:this.selectOneLink(grade.id)}))))}registerInputEvents(){this.searchInput.addEventListener("input",(0,_utils.debounce)((async()=>{this.setSearchTerms(this.searchInput.value),""===this.searchInput.value?this.clearSearchButton.classList.add("d-none"):this.clearSearchButton.classList.remove("d-none"),await this.filterrenderpipe()}),300))}async clickHandler(e){e.target.closest(this.selectors.clearSearch)&&(e.stopPropagation(),this.searchInput.value="",this.setSearchTerms(this.searchInput.value),this.searchInput.focus(),this.clearSearchButton.classList.add("d-none"),await this.filterrenderpipe()),e.target.closest(".dropdown-item")&&0===e.button&&(window.location=e.target.closest(".dropdown-item").href)}registerInputHandlers(){this.searchInput.addEventListener("input",(0,_utils.debounce)((()=>{this.setSearchTerms(this.searchInput.value),""===this.getSearchTerm()?this.clearSearchButton.classList.add("d-none"):this.clearSearchButton.classList.remove("d-none")}),300))}selectOneLink(gradeID){throw new Error("selectOneLink(".concat(gradeID,") must be implemented in ").concat(this.constructor.name))}}return _exports.default=GradeItemSearch,_exports.default}));
//# sourceMappingURL=grade.min.js.map

File diff suppressed because one or more lines are too long

View File

@ -169,42 +169,22 @@ export default class GradeItemSearch extends search_combobox {
* @param {MouseEvent} e The triggering event that we are working with.
*/
async clickHandler(e) {
if (e.target.closest(this.selectors.dropdown)) {
// Forcibly prevent BS events so that we can control the open and close.
// Really needed because by default input elements cant trigger a dropdown.
e.stopImmediatePropagation();
}
this.clearSearchButton.addEventListener('click', async() => {
if (e.target.closest(this.selectors.clearSearch)) {
e.stopPropagation();
// Clear the entered search query in the search bar.
this.searchInput.value = '';
this.setSearchTerms(this.searchInput.value);
this.searchInput.focus();
this.clearSearchButton.classList.add('d-none');
// Display results.
await this.filterrenderpipe();
});
}
// Prevent normal key presses activating this.
if (e.target.closest('.dropdown-item') && e.button === 0) {
window.location = e.target.closest('.dropdown-item').href;
}
}
/**
* The handler for when a user presses a key within the component.
*
* @param {KeyboardEvent} e The triggering event that we are working with.
*/
keyHandler(e) {
super.keyHandler(e);
// Switch the key presses to handle keyboard nav.
switch (e.key) {
case 'Escape':
if (document.activeElement.getAttribute('role') === 'option') {
e.stopPropagation();
this.searchInput.focus({preventScroll: true});
} else if (e.target.closest(this.selectors.input)) {
const trigger = this.component.querySelector(this.selectors.trigger);
trigger.focus({preventScroll: true});
}
}
}
/**
* Override the input event listener for the text input area.
*/

View File

@ -95,7 +95,7 @@ class core_grades_renderer extends plugin_renderer_base {
$sbody,
'group-search',
'groupsearchwidget',
'groupsearchdropdown overflow-auto w-100',
'groupsearchdropdown overflow-auto',
);
return $this->render_from_template($groupdropdown->get_template(), $groupdropdown->export_for_template($this));
}

View File

@ -103,7 +103,7 @@ class action_bar extends \core_grades\output\action_bar {
true,
$searchinput,
null,
'user-search dropdown d-flex',
'user-search d-flex',
null,
'usersearchdropdown overflow-auto',
null,

View File

@ -185,16 +185,14 @@ Feature: Within the grader report, test that we can collapse columns
# Move onto general keyboard navigation testing.
Then the focused element is "Search collapsed columns" "field"
And I press the down key
And the focused element is "Email address" "option_role"
And I press the end key
And the focused element is "Country" "option_role"
And I press the home key
And the focused element is "Email address" "option_role"
And the focused element is "Search collapsed columns" "field"
And ".active" "css_element" should exist in the "Email address" "option_role"
And I press the up key
And the focused element is "Country" "option_role"
And the focused element is "Search collapsed columns" "field"
And ".active" "css_element" should exist in the "Country" "option_role"
And I press the down key
And the focused element is "Email address" "option_role"
And I press the end key
And the focused element is "Search collapsed columns" "field"
And ".active" "css_element" should exist in the "Email address" "option_role"
And I press the tab key
And the focused element is "Select all" "checkbox"
And I press the escape key

View File

@ -80,17 +80,14 @@ Feature: Group searching functionality within the grader report.
And I click on "Search groups" "field"
And I wait until "Default group" "option_role" exists
And I press the down key
And the focused element is "All participants" "option_role"
And I press the end key
And the focused element is "Tutor group" "option_role"
And I press the home key
And the focused element is "All participants" "option_role"
And I press the up key
And the focused element is "Tutor group" "option_role"
And I press the down key
And the focused element is "All participants" "option_role"
And I press the escape key
And the focused element is "Search groups" "field"
And ".active" "css_element" should exist in the "All participants" "option_role"
And I press the up key
And the focused element is "Search groups" "field"
And ".active" "css_element" should exist in the "Tutor group" "option_role"
And I press the down key
And the focused element is "Search groups" "field"
And ".active" "css_element" should exist in the "All participants" "option_role"
Then I set the field "Search groups" to "Goodmeme"
And I wait until "Tutor group" "option_role" does not exist
And I press the down key
@ -101,7 +98,8 @@ Feature: Group searching functionality within the grader report.
And I set the field "Search groups" to "Tutor"
And I wait until "All participants" "option_role" does not exist
And I press the down key
And the focused element is "Tutor group" "option_role"
And the focused element is "Search groups" "field"
And ".active" "css_element" should exist in the "Tutor group" "option_role"
# Lets check the tabbing order.
And I set the field "Search groups" to "Marker"

View File

@ -120,6 +120,7 @@ Feature: Within the grader report, test that we can open our generic filter drop
# Click off the drop down
And I click on "Filter by name" "combobox"
And "input[data-action=save]" "css_element" should be visible
And I change window size to "large"
And I click on user profile field menu "fullname"
And "input[data-action=save]" "css_element" should not be visible

View File

@ -271,6 +271,7 @@ Feature: Within the grader report, test that we can search for users
# Ensure we can interact with the input & clear search options with the keyboard.
# Space & Enter have the same handling for triggering the two functionalities.
And I set the field "Search users" to "User"
And I press the up key
And I press the enter key
And I wait to be redirected
And the following should exist in the "user-grades" table:

View File

@ -57,7 +57,7 @@ class gradereport_singleview_renderer extends plugin_renderer_base {
true,
$this->render_from_template('core_user/comboboxsearch/user_selector', $data),
null,
'user-search dropdown d-flex',
'user-search d-flex',
null,
'usersearchdropdown overflow-auto',
null,
@ -99,7 +99,7 @@ class gradereport_singleview_renderer extends plugin_renderer_base {
$sbody,
'grade-search h-100',
'gradesearchwidget h-100',
'gradesearchdropdown overflow-auto w-100',
'gradesearchdropdown overflow-auto',
);
return $this->render_from_template($dropdown->get_template(), $dropdown->export_for_template($this));
}

View File

@ -47,17 +47,14 @@ Feature: Given we have opted to search for a grade item, Lets find and search th
And I click on "Search items" "field"
And I wait until "Test assignment one" "option_role" exists
And I press the down key
And the focused element is "Test assignment one" "option_role"
And I press the end key
And the focused element is "Course total" "option_role"
And I press the home key
And the focused element is "Test assignment one" "option_role"
And I press the up key
And the focused element is "Course total" "option_role"
And I press the down key
And the focused element is "Test assignment one" "option_role"
And I press the escape key
And the focused element is "Search items" "field"
And ".active" "css_element" should exist in the "Test assignment one" "option_role"
And I press the up key
And the focused element is "Search items" "field"
And ".active" "css_element" should exist in the "Course total" "option_role"
And I press the down key
And the focused element is "Search items" "field"
And ".active" "css_element" should exist in the "Test assignment one" "option_role"
Then I set the field "Search items" to "Goodmeme"
And I wait until "Test assignment one" "option_role" does not exist
And I press the down key

View File

@ -105,7 +105,7 @@ class gradereport_user_renderer extends plugin_renderer_base {
true,
$this->render_from_template('core_user/comboboxsearch/user_selector', $data),
null,
'user-search dropdown d-flex',
'user-search d-flex',
null,
'usersearchdropdown overflow-auto',
null,

View File

@ -82,17 +82,14 @@ Feature: Group searching functionality within the user report.
And I click on "Search groups" "field"
And I wait until "Default group" "option_role" exists
And I press the down key
And the focused element is "All participants" "option_role"
And I press the end key
And the focused element is "Tutor group" "option_role"
And I press the home key
And the focused element is "All participants" "option_role"
And I press the up key
And the focused element is "Tutor group" "option_role"
And I press the down key
And the focused element is "All participants" "option_role"
And I press the escape key
And the focused element is "Search groups" "field"
And ".active" "css_element" should exist in the "All participants" "option_role"
And I press the up key
And the focused element is "Search groups" "field"
And ".active" "css_element" should exist in the "Tutor group" "option_role"
And I press the down key
And the focused element is "Search groups" "field"
And ".active" "css_element" should exist in the "All participants" "option_role"
Then I set the field "Search groups" to "Goodmeme"
And I wait until "Tutor group" "option_role" does not exist
And I press the down key
@ -103,7 +100,8 @@ Feature: Group searching functionality within the user report.
And I set the field "Search groups" to "Tutor"
And I wait until "All participants" "option_role" does not exist
And I press the down key
And the focused element is "Tutor group" "option_role"
And the focused element is "Search groups" "field"
And ".active" "css_element" should exist in the "Tutor group" "option_role"
# Lets check the tabbing order.
And I set the field "Search groups" to "Marker"

View File

@ -187,6 +187,7 @@ Feature: Within the User report, a teacher can search for users.
And I confirm "User Example" in "user" search within the gradebook widget exists
And I confirm "User Test" in "user" search within the gradebook widget exists
And I confirm "Student 1" in "user" search within the gradebook widget exists
And I press the up key
And I press the enter key
And I wait until the page is ready
And "Student 1" "heading" should exist

View File

@ -1,3 +1,3 @@
define("core_group/comboboxsearch/group",["exports","core/comboboxsearch/search_combobox","core_group/comboboxsearch/repository","core/templates","core/utils","core/notification"],(function(_exports,_search_combobox,_repository,_templates,_utils,_notification){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_search_combobox=_interopRequireDefault(_search_combobox),_notification=_interopRequireDefault(_notification);class GroupSearch extends _search_combobox.default{constructor(){super(),_defineProperty(this,"courseID",void 0),_defineProperty(this,"bannedFilterFields",["id","link","groupimageurl"]),this.selectors={...this.selectors,courseid:'[data-region="courseid"]',placeholder:'.groupsearchdropdown [data-region="searchplaceholder"]'};const component=document.querySelector(this.componentSelector());this.courseID=component.querySelector(this.selectors.courseid).dataset.courseid,this.renderDefault().catch(_notification.default.exception)}static init(){return new GroupSearch}componentSelector(){return".group-search"}dropdownSelector(){return".groupsearchdropdown"}triggerSelector(){return".groupsearchwidget"}async renderDropdown(){const{html:html,js:js}=await(0,_templates.renderForPromise)("core_group/comboboxsearch/resultset",{groups:this.getMatchedResults(),hasresults:this.getMatchedResults().length>0,searchterm:this.getSearchTerm()});(0,_templates.replaceNodeContents)(this.selectors.placeholder,html,js)}async renderDefault(){this.setMatchedResults(await this.filterDataset(await this.getDataset())),this.filterMatchDataset(),await this.renderDropdown(),this.updateNodes(),this.registerInputEvents()}async fetchDataset(){return await(0,_repository.groupFetch)(this.courseID).then((r=>r.groups))}async filterDataset(filterableData){return""===this.getPreppedSearchTerm()?filterableData:filterableData.filter((group=>Object.keys(group).some((key=>""!==group[key]&&!this.bannedFilterFields.includes(key)&&group[key].toString().toLowerCase().includes(this.getPreppedSearchTerm())))))}filterMatchDataset(){this.setMatchedResults(this.getMatchedResults().map((group=>({id:group.id,name:group.name,link:this.selectOneLink(group.id),groupimageurl:group.groupimageurl}))))}registerInputEvents(){this.searchInput.addEventListener("input",(0,_utils.debounce)((async()=>{this.setSearchTerms(this.searchInput.value),""===this.searchInput.value?this.clearSearchButton.classList.add("d-none"):this.clearSearchButton.classList.remove("d-none"),await this.filterrenderpipe()}),300))}async clickHandler(e){e.target.closest(this.selectors.dropdown)&&e.stopImmediatePropagation(),this.clearSearchButton.addEventListener("click",(async()=>{this.searchInput.value="",this.setSearchTerms(this.searchInput.value),await this.filterrenderpipe()})),e.target.closest(".dropdown-item")&&0===e.button&&(window.location=e.target.closest(".dropdown-item").href)}keyHandler(e){if(super.keyHandler(e),"Escape"===e.key)if("option"===document.activeElement.getAttribute("role"))e.stopPropagation(),this.searchInput.focus({preventScroll:!0});else if(e.target.closest(this.selectors.input)){this.component.querySelector(this.selectors.trigger).focus({preventScroll:!0})}}registerInputHandlers(){this.searchInput.addEventListener("input",(0,_utils.debounce)((()=>{this.setSearchTerms(this.searchInput.value),""===this.getSearchTerm()?this.clearSearchButton.classList.add("d-none"):this.clearSearchButton.classList.remove("d-none")}),300))}selectOneLink(groupID){throw new Error("selectOneLink(".concat(groupID,") must be implemented in ").concat(this.constructor.name))}}return _exports.default=GroupSearch,_exports.default}));
define("core_group/comboboxsearch/group",["exports","core/comboboxsearch/search_combobox","core_group/comboboxsearch/repository","core/templates","core/utils","core/notification"],(function(_exports,_search_combobox,_repository,_templates,_utils,_notification){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_search_combobox=_interopRequireDefault(_search_combobox),_notification=_interopRequireDefault(_notification);class GroupSearch extends _search_combobox.default{constructor(){super(),_defineProperty(this,"courseID",void 0),_defineProperty(this,"bannedFilterFields",["id","link","groupimageurl"]),this.selectors={...this.selectors,courseid:'[data-region="courseid"]',placeholder:'.groupsearchdropdown [data-region="searchplaceholder"]'};const component=document.querySelector(this.componentSelector());this.courseID=component.querySelector(this.selectors.courseid).dataset.courseid,this.renderDefault().catch(_notification.default.exception)}static init(){return new GroupSearch}componentSelector(){return".group-search"}dropdownSelector(){return".groupsearchdropdown"}triggerSelector(){return".groupsearchwidget"}async renderDropdown(){const{html:html,js:js}=await(0,_templates.renderForPromise)("core_group/comboboxsearch/resultset",{groups:this.getMatchedResults(),hasresults:this.getMatchedResults().length>0,searchterm:this.getSearchTerm()});(0,_templates.replaceNodeContents)(this.selectors.placeholder,html,js)}async renderDefault(){this.setMatchedResults(await this.filterDataset(await this.getDataset())),this.filterMatchDataset(),await this.renderDropdown(),this.updateNodes(),this.registerInputEvents()}async fetchDataset(){return await(0,_repository.groupFetch)(this.courseID).then((r=>r.groups))}async filterDataset(filterableData){return""===this.getPreppedSearchTerm()?filterableData:filterableData.filter((group=>Object.keys(group).some((key=>""!==group[key]&&!this.bannedFilterFields.includes(key)&&group[key].toString().toLowerCase().includes(this.getPreppedSearchTerm())))))}filterMatchDataset(){this.setMatchedResults(this.getMatchedResults().map((group=>({id:group.id,name:group.name,link:this.selectOneLink(group.id),groupimageurl:group.groupimageurl}))))}registerInputEvents(){this.searchInput.addEventListener("input",(0,_utils.debounce)((async()=>{this.setSearchTerms(this.searchInput.value),""===this.searchInput.value?this.clearSearchButton.classList.add("d-none"):this.clearSearchButton.classList.remove("d-none"),await this.filterrenderpipe()}),300))}async clickHandler(e){e.target.closest(this.selectors.clearSearch)&&(e.stopPropagation(),this.searchInput.value="",this.setSearchTerms(this.searchInput.value),this.searchInput.focus(),this.clearSearchButton.classList.add("d-none"),await this.filterrenderpipe()),e.target.closest(".dropdown-item")&&0===e.button&&(window.location=e.target.closest(".dropdown-item").href)}registerInputHandlers(){this.searchInput.addEventListener("input",(0,_utils.debounce)((()=>{this.setSearchTerms(this.searchInput.value),""===this.getSearchTerm()?this.clearSearchButton.classList.add("d-none"):this.clearSearchButton.classList.remove("d-none")}),300))}selectOneLink(groupID){throw new Error("selectOneLink(".concat(groupID,") must be implemented in ").concat(this.constructor.name))}}return _exports.default=GroupSearch,_exports.default}));
//# sourceMappingURL=group.min.js.map

File diff suppressed because one or more lines are too long

View File

@ -168,43 +168,22 @@ export default class GroupSearch extends search_combobox {
* @param {MouseEvent} e The triggering event that we are working with.
*/
async clickHandler(e) {
if (e.target.closest(this.selectors.dropdown)) {
// Forcibly prevent BS events so that we can control the open and close.
// Really needed because by default input elements cant trigger a dropdown.
e.stopImmediatePropagation();
}
this.clearSearchButton.addEventListener('click', async() => {
if (e.target.closest(this.selectors.clearSearch)) {
e.stopPropagation();
// Clear the entered search query in the search bar.
this.searchInput.value = '';
this.setSearchTerms(this.searchInput.value);
this.searchInput.focus();
this.clearSearchButton.classList.add('d-none');
// Display results.
await this.filterrenderpipe();
});
}
// Prevent normal key presses activating this.
if (e.target.closest('.dropdown-item') && e.button === 0) {
window.location = e.target.closest('.dropdown-item').href;
}
}
/**
* The handler for when a user presses a key within the component.
*
* @param {KeyboardEvent} e The triggering event that we are working with.
*/
keyHandler(e) {
super.keyHandler(e);
// Switch the key presses to handle keyboard nav.
switch (e.key) {
case 'Escape':
if (document.activeElement.getAttribute('role') === 'option') {
e.stopPropagation();
this.searchInput.focus({preventScroll: true});
} else if (e.target.closest(this.selectors.input)) {
const trigger = this.component.querySelector(this.selectors.trigger);
trigger.focus({preventScroll: true});
}
break;
}
}
/**
* Override the input event listener for the text input area.
*/

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -14,7 +14,6 @@
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
import $ from 'jquery';
import CustomEvents from "core/custom_interaction_events";
import {debounce} from 'core/utils';
import Pending from 'core/pending';
@ -25,14 +24,6 @@ import Pending from 'core/pending';
* @copyright 2023 Mathew May <mathew.solutions>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
// Reused variables for the class.
const events = [
'keydown',
CustomEvents.events.activate,
CustomEvents.events.keyboardActivate
];
const UP = -1;
const DOWN = 1;
export default class {
// Define our standard lookups.
@ -80,7 +71,6 @@ export default class {
this.setSearchTerms(this.searchInput?.value ?? '');
// Begin handling the base search component.
this.registerClickHandlers();
this.registerKeyHandlers();
// Conditionally set up the input handler since we don't know exactly how we were called.
if (this.searchInput !== null) {
this.registerInputHandlers();
@ -284,18 +274,6 @@ export default class {
this.component.addEventListener('click', this.clickHandler.bind(this));
}
/**
* Register key event listeners.
*/
registerKeyHandlers() {
CustomEvents.define(document, events);
// Register click events.
events.forEach((event) => {
this.component.addEventListener(event, this.keyHandler.bind(this));
});
}
/**
* Register input event listener for the text input area.
*/
@ -349,48 +327,6 @@ export default class {
this.toggleDropdown(true);
}
/**
* Set the current focus either on the preceding or next result item.
*
* @param {Number} direction Is the user moving up or down the resultset?
* @param {KeyboardEvent} e The JS event from the event handler.
*/
keyUpDown(direction, e) {
e.preventDefault();
// Stop Bootstrap from being clever.
e.stopPropagation();
// Current focus is on the input box so depending on direction, go to the top or the bottom of the displayed results.
if (document.activeElement === this.searchInput && this.resultNodes.length > 0) {
if (direction === UP) {
this.moveToLastNode();
} else {
this.moveToFirstNode();
}
}
const index = this.resultNodes.indexOf(this.currentNode);
if (this.currentNode) {
if (direction === UP) {
if (index === 0) {
this.moveToLastNode();
} else {
this.moveToNode(index - 1);
}
} else {
if (index + 1 >= this.resultNodes.length) {
this.moveToFirstNode();
} else {
this.moveToNode(index + 1);
}
}
} else {
if (direction === UP) {
this.moveToLastNode();
} else {
this.moveToFirstNode();
}
}
}
/**
* The handler for when a user interacts with the component.
*
@ -413,69 +349,4 @@ export default class {
await this.renderAndShow();
}
}
/**
* The handler for when a user presses a key within the component.
*
* @param {KeyboardEvent} e The triggering event that we are working with.
*/
keyHandler(e) {
this.updateNodes();
// Switch the key presses to handle keyboard nav.
switch (e.key) {
case 'ArrowUp':
this.keyUpDown(UP, e);
break;
case 'ArrowDown':
this.keyUpDown(DOWN, e);
break;
case 'Home':
e.preventDefault();
this.moveToFirstNode();
break;
case 'End':
e.preventDefault();
this.moveToLastNode();
break;
}
}
/**
* Set focus on a given node after parsed through the calling functions.
*
* @param {HTMLElement} node The node to set focus upon.
*/
selectNode = (node) => {
node.focus({preventScroll: true});
this.searchDropdown.scrollTop = node.offsetTop - (node.clientHeight / 2);
};
/**
* Set the focus on the first node within the array.
*/
moveToFirstNode = () => {
if (this.resultNodes.length > 0) {
this.selectNode(this.resultNodes[0]);
}
};
/**
* Set the focus to the final node within the array.
*/
moveToLastNode = () => {
if (this.resultNodes.length > 0) {
this.selectNode(this.resultNodes[this.resultNodes.length - 1]);
}
};
/**
* Set focus on any given specified node within the node array.
*
* @param {Number} index Which item within the array to set focus upon.
*/
moveToNode = (index) => {
if (this.resultNodes.length > 0) {
this.selectNode(this.resultNodes[index]);
}
};
}

View File

@ -43,7 +43,7 @@
{{#buttonheader}}
<small>{{.}}</small>
{{/buttonheader}}
<div class="{{#parentclasses}}{{.}}{{/parentclasses}}"
<div class="{{#parentclasses}}{{.}}{{/parentclasses}} dropdown"
{{^usebutton}}
data-input-element="input-{{uniqid}}"
{{/usebutton}}>

View File

@ -3079,14 +3079,16 @@ blockquote {
.usersearchdropdown,
.gradesearchdropdown,
.groupsearchdropdown {
max-width: 350px;
.searchresultitemscontainer {
max-height: 170px;
overflow: auto;
/* stylelint-disable declaration-no-important */
img {
height: 48px !important;
width: 48px !important;
&.dropdown-menu {
width: 350px;
.searchresultitemscontainer {
max-height: 170px;
overflow: auto;
/* stylelint-disable declaration-no-important */
img {
height: 48px !important;
width: 48px !important;
}
}
}
}

View File

@ -25928,21 +25928,21 @@ blockquote {
}
/* Combobox search dropdowns */
.usersearchdropdown,
.gradesearchdropdown,
.groupsearchdropdown {
max-width: 350px;
.usersearchdropdown.dropdown-menu,
.gradesearchdropdown.dropdown-menu,
.groupsearchdropdown.dropdown-menu {
width: 350px;
}
.usersearchdropdown .searchresultitemscontainer,
.gradesearchdropdown .searchresultitemscontainer,
.groupsearchdropdown .searchresultitemscontainer {
.usersearchdropdown.dropdown-menu .searchresultitemscontainer,
.gradesearchdropdown.dropdown-menu .searchresultitemscontainer,
.groupsearchdropdown.dropdown-menu .searchresultitemscontainer {
max-height: 170px;
overflow: auto;
/* stylelint-disable declaration-no-important */
}
.usersearchdropdown .searchresultitemscontainer img,
.gradesearchdropdown .searchresultitemscontainer img,
.groupsearchdropdown .searchresultitemscontainer img {
.usersearchdropdown.dropdown-menu .searchresultitemscontainer img,
.gradesearchdropdown.dropdown-menu .searchresultitemscontainer img,
.groupsearchdropdown.dropdown-menu .searchresultitemscontainer img {
height: 48px !important;
width: 48px !important;
}

View File

@ -25928,21 +25928,21 @@ blockquote {
}
/* Combobox search dropdowns */
.usersearchdropdown,
.gradesearchdropdown,
.groupsearchdropdown {
max-width: 350px;
.usersearchdropdown.dropdown-menu,
.gradesearchdropdown.dropdown-menu,
.groupsearchdropdown.dropdown-menu {
width: 350px;
}
.usersearchdropdown .searchresultitemscontainer,
.gradesearchdropdown .searchresultitemscontainer,
.groupsearchdropdown .searchresultitemscontainer {
.usersearchdropdown.dropdown-menu .searchresultitemscontainer,
.gradesearchdropdown.dropdown-menu .searchresultitemscontainer,
.groupsearchdropdown.dropdown-menu .searchresultitemscontainer {
max-height: 170px;
overflow: auto;
/* stylelint-disable declaration-no-important */
}
.usersearchdropdown .searchresultitemscontainer img,
.gradesearchdropdown .searchresultitemscontainer img,
.groupsearchdropdown .searchresultitemscontainer img {
.usersearchdropdown.dropdown-menu .searchresultitemscontainer img,
.gradesearchdropdown.dropdown-menu .searchresultitemscontainer img,
.groupsearchdropdown.dropdown-menu .searchresultitemscontainer img {
height: 48px !important;
width: 48px !important;
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -36,14 +36,19 @@ export default class UserSearch extends search_combobox {
constructor() {
super();
// Register a small click event onto the document since we need to check if they are clicking off the component.
document.addEventListener('click', (e) => {
// Since we are handling dropdowns manually, ensure we can close it when clicking off.
if (!e.target.closest(this.selectors.component) && this.searchDropdown.classList.contains('show')) {
this.toggleDropdown();
}
// Register a couple of events onto the document since we need to check if they are moving off the component.
['click', 'focus'].forEach(eventType => {
// Since we are handling dropdowns manually, ensure we can close it when moving off.
document.addEventListener(eventType, e => {
if (this.searchDropdown.classList.contains('show') && !this.combobox.contains(e.target)) {
this.toggleDropdown();
}
}, true);
});
// Register keyboard events.
this.component.addEventListener('keydown', this.keyHandler.bind(this));
// Define our standard lookups.
this.selectors = {...this.selectors,
courseid: '[data-region="courseid"]',
@ -191,11 +196,6 @@ export default class UserSearch extends search_combobox {
*/
clickHandler(e) {
super.clickHandler(e).catch(Notification.exception);
if (e.target.closest(this.selectors.component)) {
// Forcibly prevent BS events so that we can control the open and close.
// Really needed because by default input elements cant trigger a dropdown.
e.stopImmediatePropagation();
}
if (e.target === this.getHTMLElements().currentViewAll && e.button === 0) {
window.location = this.selectAllResultsLink();
}
@ -210,48 +210,20 @@ export default class UserSearch extends search_combobox {
* @param {KeyboardEvent} e The triggering event that we are working with.
*/
keyHandler(e) {
// We don't call the super here because we want to let aria.js handle the key presses mostly.
if (e.target === this.getHTMLElements().currentViewAll && (e.key === 'Enter' || e.key === 'Space')) {
window.location = this.selectAllResultsLink();
}
// Switch the key presses to handle keyboard nav.
switch (e.key) {
case 'Enter':
case ' ':
e.stopPropagation();
if (document.activeElement === this.getHTMLElements().searchInput) {
if (e.key === 'Enter' && this.selectAllResultsLink() !== null) {
window.location = this.selectAllResultsLink();
}
}
if (document.activeElement === this.getHTMLElements().clearSearchButton) {
this.closeSearch(true);
break;
}
if (e.target.closest(this.selectors.resetPageButton)) {
e.stopPropagation();
window.location = e.target.closest(this.selectors.resetPageButton).href;
break;
}
if (e.target.closest('.dropdown-item')) {
e.preventDefault();
window.location = e.target.closest('.dropdown-item').href;
break;
}
break;
case 'Escape':
this.toggleDropdown();
this.searchInput.focus({preventScroll: true});
break;
case 'Tab':
// If the current focus is on clear search, then check if viewall exists then around tab to it.
if (e.target.closest(this.selectors.clearSearch)) {
if (this.currentViewAll && !e.shiftKey) {
this.closeSearch();
}
}
break;
}
}
@ -265,6 +237,7 @@ export default class UserSearch extends search_combobox {
this.searchDropdown.classList.add('show');
$(this.searchDropdown).show();
this.getHTMLElements().searchInput.setAttribute('aria-expanded', 'true');
this.searchInput.focus({preventScroll: true});
} else {
this.searchDropdown.classList.remove('show');
$(this.searchDropdown).hide();