mirror of
https://github.com/moodle/moodle.git
synced 2025-04-13 12:32:08 +02:00
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:
parent
2f023f9ebe
commit
d886cba9d3
2
grade/amd/build/comboboxsearch/grade.min.js
vendored
2
grade/amd/build/comboboxsearch/grade.min.js
vendored
@ -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
@ -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.
|
||||
*/
|
||||
|
@ -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));
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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));
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
2
group/amd/build/comboboxsearch/group.min.js
vendored
2
group/amd/build/comboboxsearch/group.min.js
vendored
@ -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
@ -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
@ -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]);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -43,7 +43,7 @@
|
||||
{{#buttonheader}}
|
||||
<small>{{.}}</small>
|
||||
{{/buttonheader}}
|
||||
<div class="{{#parentclasses}}{{.}}{{/parentclasses}}"
|
||||
<div class="{{#parentclasses}}{{.}}{{/parentclasses}} dropdown"
|
||||
{{^usebutton}}
|
||||
data-input-element="input-{{uniqid}}"
|
||||
{{/usebutton}}>
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
2
user/amd/build/comboboxsearch/user.min.js
vendored
2
user/amd/build/comboboxsearch/user.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -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();
|
||||
|
Loading…
x
Reference in New Issue
Block a user