diff --git a/blocks/site_main_menu/tests/behat/edit_activities.feature b/blocks/site_main_menu/tests/behat/edit_activities.feature index 0152ff3a2af..336c14bc652 100644 --- a/blocks/site_main_menu/tests/behat/edit_activities.feature +++ b/blocks/site_main_menu/tests/behat/edit_activities.feature @@ -35,7 +35,6 @@ Feature: Edit activities in main menu block | activity | course | section | name | | forum | Acceptance test site | 0 | Visible forum | | forum | Acceptance test site | 0 | My forum name | - And I change window size to "large" And I log in as "admin" And I am on site homepage And I turn editing mode on diff --git a/course/tests/behat/activities_visibility_icons.feature b/course/tests/behat/activities_visibility_icons.feature index c9d8328d112..a35da5cbade 100644 --- a/course/tests/behat/activities_visibility_icons.feature +++ b/course/tests/behat/activities_visibility_icons.feature @@ -99,7 +99,6 @@ Feature: Toggle activities visibility from the course page Scenario: Activities can be made available but not visible on a course page Given the following config values are set as admin: | allowstealth | 1 | - And I change window size to "large" And I log in as "teacher1" And I am on "Course 1" course homepage with editing mode on When I open "Test assignment name" actions menu diff --git a/grade/report/grader/tests/behat/switch_views.feature b/grade/report/grader/tests/behat/switch_views.feature index 55125d48a08..340d85dc8b4 100644 --- a/grade/report/grader/tests/behat/switch_views.feature +++ b/grade/report/grader/tests/behat/switch_views.feature @@ -39,7 +39,6 @@ Feature: We can change what we are viewing on the grader report @javascript Scenario: View and minimise the grader report containing hidden activities When I am on "Course 1" course homepage with editing mode on - And I change window size to "large" And I open "Test assignment name 2" actions menu And I choose "Availability > Hide on course page" in the open action menu And I am on "Course 1" course homepage with editing mode off diff --git a/lib/amd/build/local/action_menu/subpanel.min.js b/lib/amd/build/local/action_menu/subpanel.min.js index fab800ed52e..f8fa228f0e3 100644 --- a/lib/amd/build/local/action_menu/subpanel.min.js +++ b/lib/amd/build/local/action_menu/subpanel.min.js @@ -5,6 +5,6 @@ define("core/local/action_menu/subpanel",["exports","jquery","core/utils","core/ * @module core/local/action_menu/subpanel * @copyright 2023 Mikel Martín * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_jquery=_interopRequireDefault(_jquery),_pending=_interopRequireDefault(_pending);const Selectors_mainMenu='[role="menu"]',Selectors_dropdownRight=".dropdown-menu-right",Selectors_subPanel=".dropdown-subpanel",Selectors_subPanelMenuItem=".dropdown-subpanel > .dropdown-item",Selectors_subPanelContent=".dropdown-subpanel > .dropdown-menu",Selectors_drawer='[data-region="fixed-drawer"]',Classes_dropRight="dropright",Classes_dropLeft="dropleft",Classes_dropDown="dropdown",Classes_forceLeft="downleft",Classes_contentDisplayed="content-displayed",BootstrapEvents_hideDropdown="hidden.bs.dropdown";let initialized=!1;const updateAllPanelsPosition=()=>{document.querySelectorAll(Selectors_subPanel).forEach((dropdown=>{new SubPanel(dropdown).updatePosition()}))};class SubPanel{constructor(element){this.element=element,this.menuItem=element.querySelector(Selectors_subPanelMenuItem),this.panelContent=element.querySelector(Selectors_subPanelContent)}init(){this.element.dataset.subPanelInitialized||(this.updatePosition(),this.element.addEventListener("focusin",this._mainElementFocusInHandler.bind(this)),this.menuItem.addEventListener("click",this._menuItemClickHandler.bind(this)),this.menuItem.addEventListener("keydown",this._menuItemKeyHandler.bind(this)),this.menuItem.addEventListener("mouseover",this._menuItemHoverHandler.bind(this)),this.menuItem.addEventListener("mouseout",this._menuItemHoverOutHandler.bind(this)),this.panelContent.addEventListener("keydown",this._panelContentKeyHandler.bind(this)),this.element.dataset.subPanelInitialized=!0)}_needSmallSpaceBehaviour(){return(0,_pagehelpers.isExtraSmall)()||null!==this.element.closest(Selectors_drawer)}_mainElementFocusInHandler(){this._needSmallSpaceBehaviour()||this.setVisibility(!0)}_menuItemClickHandler(event){event.stopPropagation(),event.preventDefault(),this._needSmallSpaceBehaviour()&&this.setVisibility(!this.getVisibility())}_menuItemHoverHandler(){this._needSmallSpaceBehaviour()||this.setVisibility(!0)}_menuItemHoverOutHandler(){this._needSmallSpaceBehaviour()||this._hideOtherSubPanels()}_menuItemKeyHandler(event){if("ArrowUp"===event.key||"ArrowDown"===event.key&&!this._needSmallSpaceBehaviour())return void this.setVisibility(!1);let focusPanel=!1;"ArrowRight"!==event.key&&"ArrowLeft"!==event.key||(focusPanel=!0),"Enter"!==event.key&&" "!==event.key||this._needSmallSpaceBehaviour()||(focusPanel=!0),"ArrowDown"===event.key&&this._needSmallSpaceBehaviour()&&this.getVisibility()&&(focusPanel=!0),focusPanel&&(event.stopPropagation(),event.preventDefault(),this.setVisibility(!0),this._focusPanelContent())}_panelContentKeyHandler(event){const canLoop=!this._needSmallSpaceBehaviour();let isBrowsingSubPanel=!1,newFocus=null;"ArrowRight"!==event.key&&"ArrowLeft"!==event.key||(newFocus=this.menuItem),"ArrowUp"===event.key&&(newFocus=(0,_pagehelpers.previousFocusableElement)(this.panelContent,canLoop),isBrowsingSubPanel=!0),"ArrowDown"===event.key&&(newFocus=(0,_pagehelpers.nextFocusableElement)(this.panelContent,canLoop),isBrowsingSubPanel=!0),null===newFocus&&isBrowsingSubPanel&&!canLoop&&(newFocus=this.menuItem),null!==newFocus&&(event.stopPropagation(),event.preventDefault(),newFocus.focus())}_focusPanelContent(){const pendingPromise=new _pending.default("core/action_menu/subpanel:focuscontent");setTimeout((()=>{const firstFocusable=(0,_pagehelpers.firstFocusableElement)(this.panelContent);firstFocusable&&firstFocusable.focus(),pendingPromise.resolve()}),100)}setVisibility(visible){visible&&this._hideOtherSubPanels(),this.menuItem.setAttribute("aria-expanded",visible?"true":"false"),this.panelContent.classList.toggle("show",visible),this.element.classList.toggle(Classes_contentDisplayed,visible)}_hideOtherSubPanels(){this.element.closest(Selectors_mainMenu).querySelectorAll("".concat(Selectors_subPanelContent,".show")).forEach((visibleSubPanel=>{const dropdownSubPanel=visibleSubPanel.closest(Selectors_subPanel);if(dropdownSubPanel===this.element)return;new SubPanel(dropdownSubPanel).setVisibility(!1)}))}getVisibility(){return"true"===this.menuItem.getAttribute("aria-expanded")}updatePosition(){const dropdownRight=this.element.closest(Selectors_dropdownRight);if(this._needSmallSpaceBehaviour())return this.element.classList.remove(Classes_dropRight),this.element.classList.remove(Classes_dropLeft),this.element.classList.add(Classes_dropDown),void this.element.classList.toggle(Classes_forceLeft,null!==dropdownRight);this.element.classList.remove(Classes_dropDown),this.element.classList.remove(Classes_forceLeft),this.element.classList.toggle(Classes_dropRight,null===dropdownRight),this.element.classList.toggle(Classes_dropLeft,null!==dropdownRight)}}_exports.init=selector=>{initialized||((0,_jquery.default)(document).on(BootstrapEvents_hideDropdown,(()=>{document.querySelectorAll("".concat(Selectors_subPanelContent,".show")).forEach((visibleSubPanel=>{const dropdownSubPanel=visibleSubPanel.closest(Selectors_subPanel);new SubPanel(dropdownSubPanel).setVisibility(!1)}))})),window.addEventListener("resize",(0,_utils.debounce)(updateAllPanelsPosition,400)),initialized=!0);const subMenu=document.querySelector(selector);if(!subMenu)throw new Error("Sub panel element not found: ".concat(selector));new SubPanel(subMenu).init()}})); + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_jquery=_interopRequireDefault(_jquery),_pending=_interopRequireDefault(_pending);const Selectors_mainMenu='[role="menu"]',Selectors_dropdownRight=".dropdown-menu-right",Selectors_subPanel=".dropdown-subpanel",Selectors_subPanelMenuItem=".dropdown-subpanel > .dropdown-item",Selectors_subPanelContent=".dropdown-subpanel > .dropdown-menu",Selectors_drawer='[data-region="fixed-drawer"]',Classes_dropRight="dropright",Classes_dropLeft="dropleft",Classes_dropDown="dropdown",Classes_forceLeft="downleft",Classes_contentDisplayed="content-displayed",BootstrapEvents_hideDropdown="hidden.bs.dropdown";let initialized=!1;const updateAllPanelsPosition=()=>{document.querySelectorAll(Selectors_subPanel).forEach((dropdown=>{new SubPanel(dropdown).updatePosition()}))};class SubPanel{constructor(element){this.element=element,this.menuItem=element.querySelector(Selectors_subPanelMenuItem),this.panelContent=element.querySelector(Selectors_subPanelContent)}init(){this.element.dataset.subPanelInitialized||(this.updatePosition(),this.element.addEventListener("focusin",this._mainElementFocusInHandler.bind(this)),this.menuItem.addEventListener("click",this._menuItemClickHandler.bind(this)),this.menuItem.addEventListener("keydown",this._menuItemKeyHandler.bind(this)),(0,_pagehelpers.isBehatSite)()||(this.menuItem.addEventListener("mouseover",this._menuItemHoverHandler.bind(this)),this.menuItem.addEventListener("mouseout",this._menuItemHoverOutHandler.bind(this))),this.panelContent.addEventListener("keydown",this._panelContentKeyHandler.bind(this)),this.element.dataset.subPanelInitialized=!0)}_needSmallSpaceBehaviour(){return(0,_pagehelpers.isExtraSmall)()||null!==this.element.closest(Selectors_drawer)}_mainElementFocusInHandler(){this._needSmallSpaceBehaviour()||this.setVisibility(!0)}_menuItemClickHandler(event){event.stopPropagation(),event.preventDefault(),this._needSmallSpaceBehaviour()&&this.setVisibility(!this.getVisibility())}_menuItemHoverHandler(){this._needSmallSpaceBehaviour()||this.setVisibility(!0)}_menuItemHoverOutHandler(){this._needSmallSpaceBehaviour()||this._hideOtherSubPanels()}_menuItemKeyHandler(event){if("ArrowUp"===event.key||"ArrowDown"===event.key&&!this._needSmallSpaceBehaviour())return void this.setVisibility(!1);let focusPanel=!1;"ArrowRight"!==event.key&&"ArrowLeft"!==event.key||(focusPanel=!0),"Enter"!==event.key&&" "!==event.key||this._needSmallSpaceBehaviour()||(focusPanel=!0),"ArrowDown"===event.key&&this._needSmallSpaceBehaviour()&&this.getVisibility()&&(focusPanel=!0),focusPanel&&(event.stopPropagation(),event.preventDefault(),this.setVisibility(!0),this._focusPanelContent())}_panelContentKeyHandler(event){const canLoop=!this._needSmallSpaceBehaviour();let isBrowsingSubPanel=!1,newFocus=null;"ArrowRight"!==event.key&&"ArrowLeft"!==event.key||(newFocus=this.menuItem),"ArrowUp"===event.key&&(newFocus=(0,_pagehelpers.previousFocusableElement)(this.panelContent,canLoop),isBrowsingSubPanel=!0),"ArrowDown"===event.key&&(newFocus=(0,_pagehelpers.nextFocusableElement)(this.panelContent,canLoop),isBrowsingSubPanel=!0),null===newFocus&&isBrowsingSubPanel&&!canLoop&&(newFocus=this.menuItem),null!==newFocus&&(event.stopPropagation(),event.preventDefault(),newFocus.focus())}_focusPanelContent(){const pendingPromise=new _pending.default("core/action_menu/subpanel:focuscontent");setTimeout((()=>{const firstFocusable=(0,_pagehelpers.firstFocusableElement)(this.panelContent);firstFocusable&&firstFocusable.focus(),pendingPromise.resolve()}),100)}setVisibility(visible){visible&&this._hideOtherSubPanels(),this.menuItem.setAttribute("aria-expanded",visible?"true":"false"),this.panelContent.classList.toggle("show",visible),this.element.classList.toggle(Classes_contentDisplayed,visible)}_hideOtherSubPanels(){this.element.closest(Selectors_mainMenu).querySelectorAll("".concat(Selectors_subPanelContent,".show")).forEach((visibleSubPanel=>{const dropdownSubPanel=visibleSubPanel.closest(Selectors_subPanel);if(dropdownSubPanel===this.element)return;new SubPanel(dropdownSubPanel).setVisibility(!1)}))}getVisibility(){return"true"===this.menuItem.getAttribute("aria-expanded")}updatePosition(){const dropdownRight=this.element.closest(Selectors_dropdownRight);if(this._needSmallSpaceBehaviour())return this.element.classList.remove(Classes_dropRight),this.element.classList.remove(Classes_dropLeft),this.element.classList.add(Classes_dropDown),void this.element.classList.toggle(Classes_forceLeft,null!==dropdownRight);this.element.classList.remove(Classes_dropDown),this.element.classList.remove(Classes_forceLeft),this.element.classList.toggle(Classes_dropRight,null===dropdownRight),this.element.classList.toggle(Classes_dropLeft,null!==dropdownRight)}}_exports.init=selector=>{initialized||((0,_jquery.default)(document).on(BootstrapEvents_hideDropdown,(()=>{document.querySelectorAll("".concat(Selectors_subPanelContent,".show")).forEach((visibleSubPanel=>{const dropdownSubPanel=visibleSubPanel.closest(Selectors_subPanel);new SubPanel(dropdownSubPanel).setVisibility(!1)}))})),window.addEventListener("resize",(0,_utils.debounce)(updateAllPanelsPosition,400)),initialized=!0);const subMenu=document.querySelector(selector);if(!subMenu)throw new Error("Sub panel element not found: ".concat(selector));new SubPanel(subMenu).init()}})); //# sourceMappingURL=subpanel.min.js.map \ No newline at end of file diff --git a/lib/amd/build/local/action_menu/subpanel.min.js.map b/lib/amd/build/local/action_menu/subpanel.min.js.map index d793f92798f..a2628bb2700 100644 --- a/lib/amd/build/local/action_menu/subpanel.min.js.map +++ b/lib/amd/build/local/action_menu/subpanel.min.js.map @@ -1 +1 @@ -{"version":3,"file":"subpanel.min.js","sources":["../../../src/local/action_menu/subpanel.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Action menu subpanel JS controls.\n *\n * @module core/local/action_menu/subpanel\n * @copyright 2023 Mikel Martín \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport jQuery from 'jquery';\nimport {debounce} from 'core/utils';\nimport {\n isExtraSmall,\n firstFocusableElement,\n previousFocusableElement,\n nextFocusableElement,\n} from 'core/pagehelpers';\nimport Pending from 'core/pending';\n\nconst Selectors = {\n mainMenu: '[role=\"menu\"]',\n dropdownRight: '.dropdown-menu-right',\n subPanel: '.dropdown-subpanel',\n subPanelMenuItem: '.dropdown-subpanel > .dropdown-item',\n subPanelContent: '.dropdown-subpanel > .dropdown-menu',\n drawer: '[data-region=\"fixed-drawer\"]',\n};\n\nconst Classes = {\n dropRight: 'dropright',\n dropLeft: 'dropleft',\n dropDown: 'dropdown',\n forceLeft: 'downleft',\n contentDisplayed: 'content-displayed',\n};\n\nconst BootstrapEvents = {\n hideDropdown: 'hidden.bs.dropdown',\n};\n\nlet initialized = false;\n\n/**\n * Initialize all delegated events into the page.\n */\nconst initPageEvents = () => {\n if (initialized) {\n return;\n }\n // Hide all subpanels when hidind a dropdown.\n // This is using JQuery because of BS4 events. JQuery won't be needed with BS5.\n jQuery(document).on(BootstrapEvents.hideDropdown, () => {\n document.querySelectorAll(`${Selectors.subPanelContent}.show`).forEach(visibleSubPanel => {\n const dropdownSubPanel = visibleSubPanel.closest(Selectors.subPanel);\n const subPanel = new SubPanel(dropdownSubPanel);\n subPanel.setVisibility(false);\n });\n });\n\n window.addEventListener('resize', debounce(updateAllPanelsPosition, 400));\n\n initialized = true;\n};\n\n/**\n * Update all the panels position.\n */\nconst updateAllPanelsPosition = () => {\n document.querySelectorAll(Selectors.subPanel).forEach(dropdown => {\n const subpanel = new SubPanel(dropdown);\n subpanel.updatePosition();\n });\n};\n\n/**\n * Subpanel class.\n * @private\n */\nclass SubPanel {\n /**\n * Constructor.\n * @param {HTMLElement} element The element to initialize.\n */\n constructor(element) {\n this.element = element;\n this.menuItem = element.querySelector(Selectors.subPanelMenuItem);\n this.panelContent = element.querySelector(Selectors.subPanelContent);\n }\n\n /**\n * Initialize the subpanel element.\n *\n * This method adds the event listeners to the subpanel and the position classes.\n */\n init() {\n if (this.element.dataset.subPanelInitialized) {\n return;\n }\n\n this.updatePosition();\n\n // Full element events.\n this.element.addEventListener('focusin', this._mainElementFocusInHandler.bind(this));\n // Menu Item events.\n this.menuItem.addEventListener('click', this._menuItemClickHandler.bind(this));\n this.menuItem.addEventListener('keydown', this._menuItemKeyHandler.bind(this));\n this.menuItem.addEventListener('mouseover', this._menuItemHoverHandler.bind(this));\n this.menuItem.addEventListener('mouseout', this._menuItemHoverOutHandler.bind(this));\n // Subpanel content events.\n this.panelContent.addEventListener('keydown', this._panelContentKeyHandler.bind(this));\n\n this.element.dataset.subPanelInitialized = true;\n }\n\n /**\n * Checks if the subpanel has enough space.\n *\n * In general there are two scenarios were the subpanel must be interacted differently:\n * - Extra small screens: The subpanel is displayed below the menu item.\n * - Drawer: The subpanel is displayed one of the drawers.\n *\n * @returns {Boolean} true if the subpanel should be displayed in small screens.\n */\n _needSmallSpaceBehaviour() {\n return isExtraSmall() || this.element.closest(Selectors.drawer) !== null;\n }\n\n /**\n * Main element focus in handler.\n */\n _mainElementFocusInHandler() {\n if (this._needSmallSpaceBehaviour()) {\n return;\n }\n this.setVisibility(true);\n }\n\n /**\n * Menu item click handler.\n * @param {Event} event\n */\n _menuItemClickHandler(event) {\n // Avoid dropdowns being closed after clicking a subemnu.\n // This won't be needed with BS5 (data-bs-auto-close handles it).\n event.stopPropagation();\n event.preventDefault();\n if (this._needSmallSpaceBehaviour()) {\n this.setVisibility(!this.getVisibility());\n }\n }\n\n /**\n * Menu item hover handler.\n * @private\n */\n _menuItemHoverHandler() {\n if (this._needSmallSpaceBehaviour()) {\n return;\n }\n this.setVisibility(true);\n }\n\n /**\n * Menu item hover out handler.\n * @private\n */\n _menuItemHoverOutHandler() {\n if (this._needSmallSpaceBehaviour()) {\n return;\n }\n this._hideOtherSubPanels();\n }\n\n /**\n * Menu item key handler.\n * @param {Event} event\n * @private\n */\n _menuItemKeyHandler(event) {\n // In small sizes te down key will focus on the panel.\n if (event.key === 'ArrowUp' || (event.key === 'ArrowDown' && !this._needSmallSpaceBehaviour())) {\n this.setVisibility(false);\n return;\n }\n\n // Keys to move focus to the panel.\n let focusPanel = false;\n\n if (event.key === 'ArrowRight' || event.key === 'ArrowLeft') {\n focusPanel = true;\n }\n if ((event.key === 'Enter' || event.key === ' ') && !this._needSmallSpaceBehaviour()) {\n focusPanel = true;\n }\n // In extra small screen the panel is shown below the item.\n if (event.key === 'ArrowDown' && this._needSmallSpaceBehaviour() && this.getVisibility()) {\n focusPanel = true;\n }\n if (focusPanel) {\n event.stopPropagation();\n event.preventDefault();\n this.setVisibility(true);\n this._focusPanelContent();\n }\n\n }\n\n /**\n * Sub panel content key handler.\n * @param {Event} event\n * @private\n */\n _panelContentKeyHandler(event) {\n // In extra small devices the panel is displayed under the menu item\n // so the arrow up/down switch between subpanel and the menu item.\n const canLoop = !this._needSmallSpaceBehaviour();\n let isBrowsingSubPanel = false;\n let newFocus = null;\n if (event.key === 'ArrowRight' || event.key === 'ArrowLeft') {\n newFocus = this.menuItem;\n }\n if (event.key === 'ArrowUp') {\n newFocus = previousFocusableElement(this.panelContent, canLoop);\n isBrowsingSubPanel = true;\n }\n if (event.key === 'ArrowDown') {\n newFocus = nextFocusableElement(this.panelContent, canLoop);\n isBrowsingSubPanel = true;\n }\n // If the user cannot loop and arrive to the start/end of the subpanel\n // we focus on the menu item.\n if (newFocus === null && isBrowsingSubPanel && !canLoop) {\n newFocus = this.menuItem;\n }\n if (newFocus !== null) {\n event.stopPropagation();\n event.preventDefault();\n newFocus.focus();\n }\n }\n\n /**\n * Focus on the first focusable element of the subpanel.\n * @private\n */\n _focusPanelContent() {\n const pendingPromise = new Pending('core/action_menu/subpanel:focuscontent');\n // Some Bootstrap events are triggered after the click event.\n // To prevent this from affecting the focus we wait a bit.\n setTimeout(() => {\n const firstFocusable = firstFocusableElement(this.panelContent);\n if (firstFocusable) {\n firstFocusable.focus();\n }\n pendingPromise.resolve();\n }, 100);\n }\n\n /**\n * Set the visibility of a subpanel.\n * @param {Boolean} visible true if the subpanel should be visible.\n */\n setVisibility(visible) {\n if (visible) {\n this._hideOtherSubPanels();\n }\n this.menuItem.setAttribute('aria-expanded', visible ? 'true' : 'false');\n this.panelContent.classList.toggle('show', visible);\n this.element.classList.toggle(Classes.contentDisplayed, visible);\n }\n\n /**\n * Hide all other subpanels in the parent menu.\n * @private\n */\n _hideOtherSubPanels() {\n const dropdown = this.element.closest(Selectors.mainMenu);\n dropdown.querySelectorAll(`${Selectors.subPanelContent}.show`).forEach(visibleSubPanel => {\n const dropdownSubPanel = visibleSubPanel.closest(Selectors.subPanel);\n if (dropdownSubPanel === this.element) {\n return;\n }\n const subPanel = new SubPanel(dropdownSubPanel);\n subPanel.setVisibility(false);\n });\n }\n\n /**\n * Get the visibility of a subpanel.\n * @returns {Boolean} true if the subpanel is visible.\n */\n getVisibility() {\n return this.menuItem.getAttribute('aria-expanded') === 'true';\n }\n\n /**\n * Update the panels position depending on the screen size and panel position.\n */\n updatePosition() {\n const dropdownRight = this.element.closest(Selectors.dropdownRight);\n if (this._needSmallSpaceBehaviour()) {\n this.element.classList.remove(Classes.dropRight);\n this.element.classList.remove(Classes.dropLeft);\n this.element.classList.add(Classes.dropDown);\n this.element.classList.toggle(Classes.forceLeft, dropdownRight !== null);\n return;\n }\n this.element.classList.remove(Classes.dropDown);\n this.element.classList.remove(Classes.forceLeft);\n this.element.classList.toggle(Classes.dropRight, dropdownRight === null);\n this.element.classList.toggle(Classes.dropLeft, dropdownRight !== null);\n }\n}\n\n/**\n * Initialise module for given report\n *\n * @method\n * @param {string} selector The query selector to init.\n */\nexport const init = (selector) => {\n initPageEvents();\n const subMenu = document.querySelector(selector);\n if (!subMenu) {\n throw new Error(`Sub panel element not found: ${selector}`);\n }\n const subPanel = new SubPanel(subMenu);\n subPanel.init();\n};\n"],"names":["Selectors","Classes","BootstrapEvents","initialized","updateAllPanelsPosition","document","querySelectorAll","forEach","dropdown","SubPanel","updatePosition","constructor","element","menuItem","querySelector","panelContent","init","this","dataset","subPanelInitialized","addEventListener","_mainElementFocusInHandler","bind","_menuItemClickHandler","_menuItemKeyHandler","_menuItemHoverHandler","_menuItemHoverOutHandler","_panelContentKeyHandler","_needSmallSpaceBehaviour","closest","setVisibility","event","stopPropagation","preventDefault","getVisibility","_hideOtherSubPanels","key","focusPanel","_focusPanelContent","canLoop","isBrowsingSubPanel","newFocus","focus","pendingPromise","Pending","setTimeout","firstFocusable","resolve","visible","setAttribute","classList","toggle","visibleSubPanel","dropdownSubPanel","getAttribute","dropdownRight","remove","add","selector","on","window","subMenu","Error"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"subpanel.min.js","sources":["../../../src/local/action_menu/subpanel.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Action menu subpanel JS controls.\n *\n * @module core/local/action_menu/subpanel\n * @copyright 2023 Mikel Martín \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport jQuery from 'jquery';\nimport {debounce} from 'core/utils';\nimport {\n isBehatSite,\n isExtraSmall,\n firstFocusableElement,\n previousFocusableElement,\n nextFocusableElement,\n} from 'core/pagehelpers';\nimport Pending from 'core/pending';\n\nconst Selectors = {\n mainMenu: '[role=\"menu\"]',\n dropdownRight: '.dropdown-menu-right',\n subPanel: '.dropdown-subpanel',\n subPanelMenuItem: '.dropdown-subpanel > .dropdown-item',\n subPanelContent: '.dropdown-subpanel > .dropdown-menu',\n drawer: '[data-region=\"fixed-drawer\"]',\n};\n\nconst Classes = {\n dropRight: 'dropright',\n dropLeft: 'dropleft',\n dropDown: 'dropdown',\n forceLeft: 'downleft',\n contentDisplayed: 'content-displayed',\n};\n\nconst BootstrapEvents = {\n hideDropdown: 'hidden.bs.dropdown',\n};\n\nlet initialized = false;\n\n/**\n * Initialize all delegated events into the page.\n */\nconst initPageEvents = () => {\n if (initialized) {\n return;\n }\n // Hide all subpanels when hidind a dropdown.\n // This is using JQuery because of BS4 events. JQuery won't be needed with BS5.\n jQuery(document).on(BootstrapEvents.hideDropdown, () => {\n document.querySelectorAll(`${Selectors.subPanelContent}.show`).forEach(visibleSubPanel => {\n const dropdownSubPanel = visibleSubPanel.closest(Selectors.subPanel);\n const subPanel = new SubPanel(dropdownSubPanel);\n subPanel.setVisibility(false);\n });\n });\n\n window.addEventListener('resize', debounce(updateAllPanelsPosition, 400));\n\n initialized = true;\n};\n\n/**\n * Update all the panels position.\n */\nconst updateAllPanelsPosition = () => {\n document.querySelectorAll(Selectors.subPanel).forEach(dropdown => {\n const subpanel = new SubPanel(dropdown);\n subpanel.updatePosition();\n });\n};\n\n/**\n * Subpanel class.\n * @private\n */\nclass SubPanel {\n /**\n * Constructor.\n * @param {HTMLElement} element The element to initialize.\n */\n constructor(element) {\n this.element = element;\n this.menuItem = element.querySelector(Selectors.subPanelMenuItem);\n this.panelContent = element.querySelector(Selectors.subPanelContent);\n }\n\n /**\n * Initialize the subpanel element.\n *\n * This method adds the event listeners to the subpanel and the position classes.\n */\n init() {\n if (this.element.dataset.subPanelInitialized) {\n return;\n }\n\n this.updatePosition();\n\n // Full element events.\n this.element.addEventListener('focusin', this._mainElementFocusInHandler.bind(this));\n // Menu Item events.\n this.menuItem.addEventListener('click', this._menuItemClickHandler.bind(this));\n this.menuItem.addEventListener('keydown', this._menuItemKeyHandler.bind(this));\n if (!isBehatSite()) {\n // Behat in Chrome usually move the mouse over the page when trying clicking a subpanel element.\n // If the menu has more than one subpanel this could cause closing the subpanel by mistake.\n this.menuItem.addEventListener('mouseover', this._menuItemHoverHandler.bind(this));\n this.menuItem.addEventListener('mouseout', this._menuItemHoverOutHandler.bind(this));\n }\n // Subpanel content events.\n this.panelContent.addEventListener('keydown', this._panelContentKeyHandler.bind(this));\n\n this.element.dataset.subPanelInitialized = true;\n }\n\n /**\n * Checks if the subpanel has enough space.\n *\n * In general there are two scenarios were the subpanel must be interacted differently:\n * - Extra small screens: The subpanel is displayed below the menu item.\n * - Drawer: The subpanel is displayed one of the drawers.\n *\n * @returns {Boolean} true if the subpanel should be displayed in small screens.\n */\n _needSmallSpaceBehaviour() {\n return isExtraSmall() || this.element.closest(Selectors.drawer) !== null;\n }\n\n /**\n * Main element focus in handler.\n */\n _mainElementFocusInHandler() {\n if (this._needSmallSpaceBehaviour()) {\n return;\n }\n this.setVisibility(true);\n }\n\n /**\n * Menu item click handler.\n * @param {Event} event\n */\n _menuItemClickHandler(event) {\n // Avoid dropdowns being closed after clicking a subemnu.\n // This won't be needed with BS5 (data-bs-auto-close handles it).\n event.stopPropagation();\n event.preventDefault();\n if (this._needSmallSpaceBehaviour()) {\n this.setVisibility(!this.getVisibility());\n }\n }\n\n /**\n * Menu item hover handler.\n * @private\n */\n _menuItemHoverHandler() {\n if (this._needSmallSpaceBehaviour()) {\n return;\n }\n this.setVisibility(true);\n }\n\n /**\n * Menu item hover out handler.\n * @private\n */\n _menuItemHoverOutHandler() {\n if (this._needSmallSpaceBehaviour()) {\n return;\n }\n this._hideOtherSubPanels();\n }\n\n /**\n * Menu item key handler.\n * @param {Event} event\n * @private\n */\n _menuItemKeyHandler(event) {\n // In small sizes te down key will focus on the panel.\n if (event.key === 'ArrowUp' || (event.key === 'ArrowDown' && !this._needSmallSpaceBehaviour())) {\n this.setVisibility(false);\n return;\n }\n\n // Keys to move focus to the panel.\n let focusPanel = false;\n\n if (event.key === 'ArrowRight' || event.key === 'ArrowLeft') {\n focusPanel = true;\n }\n if ((event.key === 'Enter' || event.key === ' ') && !this._needSmallSpaceBehaviour()) {\n focusPanel = true;\n }\n // In extra small screen the panel is shown below the item.\n if (event.key === 'ArrowDown' && this._needSmallSpaceBehaviour() && this.getVisibility()) {\n focusPanel = true;\n }\n if (focusPanel) {\n event.stopPropagation();\n event.preventDefault();\n this.setVisibility(true);\n this._focusPanelContent();\n }\n\n }\n\n /**\n * Sub panel content key handler.\n * @param {Event} event\n * @private\n */\n _panelContentKeyHandler(event) {\n // In extra small devices the panel is displayed under the menu item\n // so the arrow up/down switch between subpanel and the menu item.\n const canLoop = !this._needSmallSpaceBehaviour();\n let isBrowsingSubPanel = false;\n let newFocus = null;\n if (event.key === 'ArrowRight' || event.key === 'ArrowLeft') {\n newFocus = this.menuItem;\n }\n if (event.key === 'ArrowUp') {\n newFocus = previousFocusableElement(this.panelContent, canLoop);\n isBrowsingSubPanel = true;\n }\n if (event.key === 'ArrowDown') {\n newFocus = nextFocusableElement(this.panelContent, canLoop);\n isBrowsingSubPanel = true;\n }\n // If the user cannot loop and arrive to the start/end of the subpanel\n // we focus on the menu item.\n if (newFocus === null && isBrowsingSubPanel && !canLoop) {\n newFocus = this.menuItem;\n }\n if (newFocus !== null) {\n event.stopPropagation();\n event.preventDefault();\n newFocus.focus();\n }\n }\n\n /**\n * Focus on the first focusable element of the subpanel.\n * @private\n */\n _focusPanelContent() {\n const pendingPromise = new Pending('core/action_menu/subpanel:focuscontent');\n // Some Bootstrap events are triggered after the click event.\n // To prevent this from affecting the focus we wait a bit.\n setTimeout(() => {\n const firstFocusable = firstFocusableElement(this.panelContent);\n if (firstFocusable) {\n firstFocusable.focus();\n }\n pendingPromise.resolve();\n }, 100);\n }\n\n /**\n * Set the visibility of a subpanel.\n * @param {Boolean} visible true if the subpanel should be visible.\n */\n setVisibility(visible) {\n if (visible) {\n this._hideOtherSubPanels();\n }\n this.menuItem.setAttribute('aria-expanded', visible ? 'true' : 'false');\n this.panelContent.classList.toggle('show', visible);\n this.element.classList.toggle(Classes.contentDisplayed, visible);\n }\n\n /**\n * Hide all other subpanels in the parent menu.\n * @private\n */\n _hideOtherSubPanels() {\n const dropdown = this.element.closest(Selectors.mainMenu);\n dropdown.querySelectorAll(`${Selectors.subPanelContent}.show`).forEach(visibleSubPanel => {\n const dropdownSubPanel = visibleSubPanel.closest(Selectors.subPanel);\n if (dropdownSubPanel === this.element) {\n return;\n }\n const subPanel = new SubPanel(dropdownSubPanel);\n subPanel.setVisibility(false);\n });\n }\n\n /**\n * Get the visibility of a subpanel.\n * @returns {Boolean} true if the subpanel is visible.\n */\n getVisibility() {\n return this.menuItem.getAttribute('aria-expanded') === 'true';\n }\n\n /**\n * Update the panels position depending on the screen size and panel position.\n */\n updatePosition() {\n const dropdownRight = this.element.closest(Selectors.dropdownRight);\n if (this._needSmallSpaceBehaviour()) {\n this.element.classList.remove(Classes.dropRight);\n this.element.classList.remove(Classes.dropLeft);\n this.element.classList.add(Classes.dropDown);\n this.element.classList.toggle(Classes.forceLeft, dropdownRight !== null);\n return;\n }\n this.element.classList.remove(Classes.dropDown);\n this.element.classList.remove(Classes.forceLeft);\n this.element.classList.toggle(Classes.dropRight, dropdownRight === null);\n this.element.classList.toggle(Classes.dropLeft, dropdownRight !== null);\n }\n}\n\n/**\n * Initialise module for given report\n *\n * @method\n * @param {string} selector The query selector to init.\n */\nexport const init = (selector) => {\n initPageEvents();\n const subMenu = document.querySelector(selector);\n if (!subMenu) {\n throw new Error(`Sub panel element not found: ${selector}`);\n }\n const subPanel = new SubPanel(subMenu);\n subPanel.init();\n};\n"],"names":["Selectors","Classes","BootstrapEvents","initialized","updateAllPanelsPosition","document","querySelectorAll","forEach","dropdown","SubPanel","updatePosition","constructor","element","menuItem","querySelector","panelContent","init","this","dataset","subPanelInitialized","addEventListener","_mainElementFocusInHandler","bind","_menuItemClickHandler","_menuItemKeyHandler","_menuItemHoverHandler","_menuItemHoverOutHandler","_panelContentKeyHandler","_needSmallSpaceBehaviour","closest","setVisibility","event","stopPropagation","preventDefault","getVisibility","_hideOtherSubPanels","key","focusPanel","_focusPanelContent","canLoop","isBrowsingSubPanel","newFocus","focus","pendingPromise","Pending","setTimeout","firstFocusable","resolve","visible","setAttribute","classList","toggle","visibleSubPanel","dropdownSubPanel","getAttribute","dropdownRight","remove","add","selector","on","window","subMenu","Error"],"mappings":"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"} \ No newline at end of file diff --git a/lib/amd/build/pagehelpers.min.js b/lib/amd/build/pagehelpers.min.js index 49a34967e43..d368e2bfe8a 100644 --- a/lib/amd/build/pagehelpers.min.js +++ b/lib/amd/build/pagehelpers.min.js @@ -1,4 +1,4 @@ -define("core/pagehelpers",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.previousFocusableElement=_exports.nextFocusableElement=_exports.lastFocusableElement=_exports.isSmall=_exports.isLarge=_exports.isExtraSmall=_exports.getCurrentWidth=_exports.focusableElements=_exports.firstFocusableElement=void 0; +define("core/pagehelpers",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.previousFocusableElement=_exports.nextFocusableElement=_exports.lastFocusableElement=_exports.isSmall=_exports.isLarge=_exports.isExtraSmall=_exports.isBehatSite=_exports.getCurrentWidth=_exports.focusableElements=_exports.firstFocusableElement=void 0; /** * Page utility helpers. * @@ -6,6 +6,6 @@ define("core/pagehelpers",["exports"],(function(_exports){Object.defineProperty( * @copyright 2023 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -const Sizes_small=576,Sizes_medium=991,Sizes_large=1400,Selectors_focusable='a, button, input, select, textarea, [tabindex]:not([tabindex="-1"])',getCurrentWidth=()=>{const DomRect=document.body.getBoundingClientRect();return DomRect.x+DomRect.width};_exports.getCurrentWidth=getCurrentWidth;_exports.isExtraSmall=()=>getCurrentWidth()getCurrentWidth()getCurrentWidth()>=Sizes_large;_exports.firstFocusableElement=container=>(container||document).querySelector(Selectors_focusable);_exports.lastFocusableElement=container=>{var _focusableElements;const focusableElements=(container||document).querySelectorAll(Selectors_focusable);return null!==(_focusableElements=focusableElements[focusableElements.length-1])&&void 0!==_focusableElements?_focusableElements:null};const focusableElements=container=>(container||document).querySelectorAll(Selectors_focusable);_exports.focusableElements=focusableElements;_exports.previousFocusableElement=(container,loopSelection)=>getRelativeFocusableElement(container,loopSelection,-1);_exports.nextFocusableElement=(container,loopSelection)=>getRelativeFocusableElement(container,loopSelection,1);const getRelativeFocusableElement=(container,loopSelection,direction)=>{var _focusables;const focusedElement=document.activeElement,focusables=[...focusableElements(container)],focusedIndex=focusables.indexOf(focusedElement);if(-1===focusedIndex)return null;const newIndex=focusedIndex+direction;return void 0!==focusables[newIndex]?focusables[newIndex]:1!=loopSelection?null:direction>0?null!==(_focusables$=focusables[0])&&void 0!==_focusables$?_focusables$:null:null!==(_focusables=focusables[focusables.length-1])&&void 0!==_focusables?_focusables:null;var _focusables$}})); +const Sizes_small=576,Sizes_medium=991,Sizes_large=1400,Selectors_focusable='a, button, input, select, textarea, [tabindex]:not([tabindex="-1"])',Classes_behatSite="behat-site";_exports.isBehatSite=()=>document.body.classList.contains(Classes_behatSite);const getCurrentWidth=()=>{const DomRect=document.body.getBoundingClientRect();return DomRect.x+DomRect.width};_exports.getCurrentWidth=getCurrentWidth;_exports.isExtraSmall=()=>getCurrentWidth()getCurrentWidth()getCurrentWidth()>=Sizes_large;_exports.firstFocusableElement=container=>(container||document).querySelector(Selectors_focusable);_exports.lastFocusableElement=container=>{var _focusableElements;const focusableElements=(container||document).querySelectorAll(Selectors_focusable);return null!==(_focusableElements=focusableElements[focusableElements.length-1])&&void 0!==_focusableElements?_focusableElements:null};const focusableElements=container=>(container||document).querySelectorAll(Selectors_focusable);_exports.focusableElements=focusableElements;_exports.previousFocusableElement=(container,loopSelection)=>getRelativeFocusableElement(container,loopSelection,-1);_exports.nextFocusableElement=(container,loopSelection)=>getRelativeFocusableElement(container,loopSelection,1);const getRelativeFocusableElement=(container,loopSelection,direction)=>{var _focusables;const focusedElement=document.activeElement,focusables=[...focusableElements(container)],focusedIndex=focusables.indexOf(focusedElement);if(-1===focusedIndex)return null;const newIndex=focusedIndex+direction;return void 0!==focusables[newIndex]?focusables[newIndex]:1!=loopSelection?null:direction>0?null!==(_focusables$=focusables[0])&&void 0!==_focusables$?_focusables$:null:null!==(_focusables=focusables[focusables.length-1])&&void 0!==_focusables?_focusables:null;var _focusables$}})); //# sourceMappingURL=pagehelpers.min.js.map \ No newline at end of file diff --git a/lib/amd/build/pagehelpers.min.js.map b/lib/amd/build/pagehelpers.min.js.map index 7d183d5e1eb..9e1c56a27e1 100644 --- a/lib/amd/build/pagehelpers.min.js.map +++ b/lib/amd/build/pagehelpers.min.js.map @@ -1 +1 @@ -{"version":3,"file":"pagehelpers.min.js","sources":["../src/pagehelpers.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Page utility helpers.\n *\n * @module core/pagehelpers\n * @copyright 2023 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n/**\n * Maximum sizes for breakpoints. This needs to correspond with Bootstrap\n * Breakpoints\n *\n * @private\n */\nconst Sizes = {\n small: 576,\n medium: 991,\n large: 1400\n};\n\nconst Selectors = {\n focusable: 'a, button, input, select, textarea, [tabindex]:not([tabindex=\"-1\"])'\n};\n\n/**\n * Get the current body width.\n * @returns {number} the current body width.\n */\nexport const getCurrentWidth = () => {\n const DomRect = document.body.getBoundingClientRect();\n return DomRect.x + DomRect.width;\n};\n\n/**\n * Check if the user uses an extra small size browser.\n *\n * @returns {boolean} true if the body is smaller than sizes.small max size.\n */\nexport const isExtraSmall = () => {\n const browserWidth = getCurrentWidth();\n return browserWidth < Sizes.small;\n};\n\n/**\n * Check if the user uses a small size browser.\n *\n * @returns {boolean} true if the body is smaller than sizes.medium max size.\n */\nexport const isSmall = () => {\n const browserWidth = getCurrentWidth();\n return browserWidth < Sizes.medium;\n};\n\n/**\n * Check if the user uses a large size browser.\n *\n * @returns {boolean} true if the body is smaller than sizes.large max size.\n */\nexport const isLarge = () => {\n const browserWidth = getCurrentWidth();\n return browserWidth >= Sizes.large;\n};\n\n/**\n * Get the first focusable element inside a container.\n * @param {HTMLElement} [container] Container to search in. Defaults to document.\n * @returns {HTMLElement|null}\n */\nexport const firstFocusableElement = (container) => {\n const containerElement = container || document;\n return containerElement.querySelector(Selectors.focusable);\n};\n\n/**\n * Get the last focusable element inside a container.\n * @param {HTMLElement} [container] Container to search in. Defaults to document.\n * @returns {HTMLElement|null}\n */\nexport const lastFocusableElement = (container) => {\n const containerElement = container || document;\n const focusableElements = containerElement.querySelectorAll(Selectors.focusable);\n return focusableElements[focusableElements.length - 1] ?? null;\n};\n\n/**\n * Get all focusable elements inside a container.\n * @param {HTMLElement} [container] Container to search in. Defaults to document.\n * @returns {HTMLElement[]}\n */\nexport const focusableElements = (container) => {\n const containerElement = container || document;\n return containerElement.querySelectorAll(Selectors.focusable);\n};\n\n/**\n * Get the previous focusable element in a container.\n * It uses the current focused element to know where to start the search.\n * @param {HTMLElement} [container] Container to search in. Defaults to document.\n * @param {Boolean} [loopSelection] Whether to loop selection or not. Default to false.\n * @returns {HTMLElement|null}\n */\nexport const previousFocusableElement = (container, loopSelection) => {\n return getRelativeFocusableElement(container, loopSelection, -1);\n};\n\n/**\n * Get the next focusable element in a container.\n * It uses the current focused element to know where to start the search.\n * @param {HTMLElement} [container] Container to search in. Defaults to document.\n * @param {Boolean} [loopSelection] Whether to loop selection or not. Default to false.\n * @returns {HTMLElement|null}\n */\nexport const nextFocusableElement = (container, loopSelection) => {\n return getRelativeFocusableElement(container, loopSelection, 1);\n};\n\n/**\n * Internal function to get the next or previous focusable element.\n * @param {HTMLElement} [container] Container to search in. Defaults to document.\n * @param {Boolean} [loopSelection] Whether to loop selection or not.\n * @param {Number} [direction] Direction to search in. 1 for next, -1 for previous.\n * @returns {HTMLElement|null}\n * @private\n */\nconst getRelativeFocusableElement = (container, loopSelection, direction) => {\n const focusedElement = document.activeElement;\n const focusables = [...focusableElements(container)];\n const focusedIndex = focusables.indexOf(focusedElement);\n\n if (focusedIndex === -1) {\n return null;\n }\n\n const newIndex = focusedIndex + direction;\n\n if (focusables[newIndex] !== undefined) {\n return focusables[newIndex];\n }\n if (loopSelection != true) {\n return null;\n }\n if (direction > 0) {\n return focusables[0] ?? null;\n }\n return focusables[focusables.length - 1] ?? null;\n};\n"],"names":["Sizes","Selectors","getCurrentWidth","DomRect","document","body","getBoundingClientRect","x","width","container","querySelector","focusableElements","querySelectorAll","length","loopSelection","getRelativeFocusableElement","direction","focusedElement","activeElement","focusables","focusedIndex","indexOf","newIndex","undefined"],"mappings":";;;;;;;;MA6BMA,YACK,IADLA,aAEM,IAFNA,YAGK,KAGLC,oBACS,sEAOFC,gBAAkB,WACrBC,QAAUC,SAASC,KAAKC,+BACvBH,QAAQI,EAAIJ,QAAQK,sEAQH,IACHN,kBACCF,6BAQH,IACEE,kBACCF,8BAQH,IACEE,mBACEF,2CAQWS,YACTA,WAAaL,UACdM,cAAcT,mDAQLQ,yCAE3BE,mBADmBF,WAAaL,UACKQ,iBAAiBX,uDACrDU,kBAAkBA,kBAAkBE,OAAS,oDAAM,YAQjDF,kBAAqBF,YACLA,WAAaL,UACdQ,iBAAiBX,oGAUL,CAACQ,UAAWK,gBACzCC,4BAA4BN,UAAWK,eAAgB,iCAU9B,CAACL,UAAWK,gBACrCC,4BAA4BN,UAAWK,cAAe,SAW3DC,4BAA8B,CAACN,UAAWK,cAAeE,mCACrDC,eAAiBb,SAASc,cAC1BC,WAAa,IAAIR,kBAAkBF,YACnCW,aAAeD,WAAWE,QAAQJ,oBAElB,IAAlBG,oBACO,WAGLE,SAAWF,aAAeJ,sBAEHO,IAAzBJ,WAAWG,UACJH,WAAWG,UAED,GAAjBR,cACO,KAEPE,UAAY,uBACLG,WAAW,wCAAM,yBAErBA,WAAWA,WAAWN,OAAS,sCAAM"} \ No newline at end of file +{"version":3,"file":"pagehelpers.min.js","sources":["../src/pagehelpers.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Page utility helpers.\n *\n * @module core/pagehelpers\n * @copyright 2023 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n/**\n * Maximum sizes for breakpoints. This needs to correspond with Bootstrap\n * Breakpoints\n *\n * @private\n */\nconst Sizes = {\n small: 576,\n medium: 991,\n large: 1400\n};\n\nconst Selectors = {\n focusable: 'a, button, input, select, textarea, [tabindex]:not([tabindex=\"-1\"])',\n};\n\nconst Classes = {\n behatSite: 'behat-site',\n};\n\n/**\n * Check fi the current page is a Behat site.\n * @returns {boolean} true if the current page is a Behat site.\n */\nexport const isBehatSite = () => {\n return document.body.classList.contains(Classes.behatSite);\n};\n\n/**\n * Get the current body width.\n * @returns {number} the current body width.\n */\nexport const getCurrentWidth = () => {\n const DomRect = document.body.getBoundingClientRect();\n return DomRect.x + DomRect.width;\n};\n\n/**\n * Check if the user uses an extra small size browser.\n *\n * @returns {boolean} true if the body is smaller than sizes.small max size.\n */\nexport const isExtraSmall = () => {\n const browserWidth = getCurrentWidth();\n return browserWidth < Sizes.small;\n};\n\n/**\n * Check if the user uses a small size browser.\n *\n * @returns {boolean} true if the body is smaller than sizes.medium max size.\n */\nexport const isSmall = () => {\n const browserWidth = getCurrentWidth();\n return browserWidth < Sizes.medium;\n};\n\n/**\n * Check if the user uses a large size browser.\n *\n * @returns {boolean} true if the body is smaller than sizes.large max size.\n */\nexport const isLarge = () => {\n const browserWidth = getCurrentWidth();\n return browserWidth >= Sizes.large;\n};\n\n/**\n * Get the first focusable element inside a container.\n * @param {HTMLElement} [container] Container to search in. Defaults to document.\n * @returns {HTMLElement|null}\n */\nexport const firstFocusableElement = (container) => {\n const containerElement = container || document;\n return containerElement.querySelector(Selectors.focusable);\n};\n\n/**\n * Get the last focusable element inside a container.\n * @param {HTMLElement} [container] Container to search in. Defaults to document.\n * @returns {HTMLElement|null}\n */\nexport const lastFocusableElement = (container) => {\n const containerElement = container || document;\n const focusableElements = containerElement.querySelectorAll(Selectors.focusable);\n return focusableElements[focusableElements.length - 1] ?? null;\n};\n\n/**\n * Get all focusable elements inside a container.\n * @param {HTMLElement} [container] Container to search in. Defaults to document.\n * @returns {HTMLElement[]}\n */\nexport const focusableElements = (container) => {\n const containerElement = container || document;\n return containerElement.querySelectorAll(Selectors.focusable);\n};\n\n/**\n * Get the previous focusable element in a container.\n * It uses the current focused element to know where to start the search.\n * @param {HTMLElement} [container] Container to search in. Defaults to document.\n * @param {Boolean} [loopSelection] Whether to loop selection or not. Default to false.\n * @returns {HTMLElement|null}\n */\nexport const previousFocusableElement = (container, loopSelection) => {\n return getRelativeFocusableElement(container, loopSelection, -1);\n};\n\n/**\n * Get the next focusable element in a container.\n * It uses the current focused element to know where to start the search.\n * @param {HTMLElement} [container] Container to search in. Defaults to document.\n * @param {Boolean} [loopSelection] Whether to loop selection or not. Default to false.\n * @returns {HTMLElement|null}\n */\nexport const nextFocusableElement = (container, loopSelection) => {\n return getRelativeFocusableElement(container, loopSelection, 1);\n};\n\n/**\n * Internal function to get the next or previous focusable element.\n * @param {HTMLElement} [container] Container to search in. Defaults to document.\n * @param {Boolean} [loopSelection] Whether to loop selection or not.\n * @param {Number} [direction] Direction to search in. 1 for next, -1 for previous.\n * @returns {HTMLElement|null}\n * @private\n */\nconst getRelativeFocusableElement = (container, loopSelection, direction) => {\n const focusedElement = document.activeElement;\n const focusables = [...focusableElements(container)];\n const focusedIndex = focusables.indexOf(focusedElement);\n\n if (focusedIndex === -1) {\n return null;\n }\n\n const newIndex = focusedIndex + direction;\n\n if (focusables[newIndex] !== undefined) {\n return focusables[newIndex];\n }\n if (loopSelection != true) {\n return null;\n }\n if (direction > 0) {\n return focusables[0] ?? null;\n }\n return focusables[focusables.length - 1] ?? null;\n};\n"],"names":["Sizes","Selectors","Classes","document","body","classList","contains","getCurrentWidth","DomRect","getBoundingClientRect","x","width","container","querySelector","focusableElements","querySelectorAll","length","loopSelection","getRelativeFocusableElement","direction","focusedElement","activeElement","focusables","focusedIndex","indexOf","newIndex","undefined"],"mappings":";;;;;;;;MA6BMA,YACK,IADLA,aAEM,IAFNA,YAGK,KAGLC,oBACS,sEAGTC,kBACS,kCAOY,IAChBC,SAASC,KAAKC,UAAUC,SAASJ,yBAO/BK,gBAAkB,WACrBC,QAAUL,SAASC,KAAKK,+BACvBD,QAAQE,EAAIF,QAAQG,sEAQH,IACHJ,kBACCP,6BAQH,IACEO,kBACCP,8BAQH,IACEO,mBACEP,2CAQWY,YACTA,WAAaT,UACdU,cAAcZ,mDAQLW,yCAE3BE,mBADmBF,WAAaT,UACKY,iBAAiBd,uDACrDa,kBAAkBA,kBAAkBE,OAAS,oDAAM,YAQjDF,kBAAqBF,YACLA,WAAaT,UACdY,iBAAiBd,oGAUL,CAACW,UAAWK,gBACzCC,4BAA4BN,UAAWK,eAAgB,iCAU9B,CAACL,UAAWK,gBACrCC,4BAA4BN,UAAWK,cAAe,SAW3DC,4BAA8B,CAACN,UAAWK,cAAeE,mCACrDC,eAAiBjB,SAASkB,cAC1BC,WAAa,IAAIR,kBAAkBF,YACnCW,aAAeD,WAAWE,QAAQJ,oBAElB,IAAlBG,oBACO,WAGLE,SAAWF,aAAeJ,sBAEHO,IAAzBJ,WAAWG,UACJH,WAAWG,UAED,GAAjBR,cACO,KAEPE,UAAY,uBACLG,WAAW,wCAAM,yBAErBA,WAAWA,WAAWN,OAAS,sCAAM"} \ No newline at end of file diff --git a/lib/amd/src/local/action_menu/subpanel.js b/lib/amd/src/local/action_menu/subpanel.js index 98096c0d454..9106b45f7f8 100644 --- a/lib/amd/src/local/action_menu/subpanel.js +++ b/lib/amd/src/local/action_menu/subpanel.js @@ -24,6 +24,7 @@ import jQuery from 'jquery'; import {debounce} from 'core/utils'; import { + isBehatSite, isExtraSmall, firstFocusableElement, previousFocusableElement, @@ -118,8 +119,12 @@ class SubPanel { // Menu Item events. this.menuItem.addEventListener('click', this._menuItemClickHandler.bind(this)); this.menuItem.addEventListener('keydown', this._menuItemKeyHandler.bind(this)); - this.menuItem.addEventListener('mouseover', this._menuItemHoverHandler.bind(this)); - this.menuItem.addEventListener('mouseout', this._menuItemHoverOutHandler.bind(this)); + if (!isBehatSite()) { + // Behat in Chrome usually move the mouse over the page when trying clicking a subpanel element. + // If the menu has more than one subpanel this could cause closing the subpanel by mistake. + this.menuItem.addEventListener('mouseover', this._menuItemHoverHandler.bind(this)); + this.menuItem.addEventListener('mouseout', this._menuItemHoverOutHandler.bind(this)); + } // Subpanel content events. this.panelContent.addEventListener('keydown', this._panelContentKeyHandler.bind(this)); diff --git a/lib/amd/src/pagehelpers.js b/lib/amd/src/pagehelpers.js index 4aef45f0ca8..7c5e1896644 100644 --- a/lib/amd/src/pagehelpers.js +++ b/lib/amd/src/pagehelpers.js @@ -34,7 +34,19 @@ const Sizes = { }; const Selectors = { - focusable: 'a, button, input, select, textarea, [tabindex]:not([tabindex="-1"])' + focusable: 'a, button, input, select, textarea, [tabindex]:not([tabindex="-1"])', +}; + +const Classes = { + behatSite: 'behat-site', +}; + +/** + * Check fi the current page is a Behat site. + * @returns {boolean} true if the current page is a Behat site. + */ +export const isBehatSite = () => { + return document.body.classList.contains(Classes.behatSite); }; /** diff --git a/theme/boost/scss/moodle/core.scss b/theme/boost/scss/moodle/core.scss index efc1c555d99..bdcb9da25ff 100644 --- a/theme/boost/scss/moodle/core.scss +++ b/theme/boost/scss/moodle/core.scss @@ -45,6 +45,10 @@ $font-size-xs: ($font-size-base * .75) !default; @media (prefers-reduced-motion: reduce) { animation: none; } + + body.behat-site & { + animation: none; + } } .context-header-settings-menu .dropdown-toggle > .icon, diff --git a/theme/boost/style/moodle.css b/theme/boost/style/moodle.css index 2d8d34e1837..891e67e5403 100644 --- a/theme/boost/style/moodle.css +++ b/theme/boost/style/moodle.css @@ -26036,6 +26036,9 @@ blockquote { animation: none; } } +body.behat-site .action-menu .dropdown-subpanel-content.show { + animation: none; +} .action-menu .dropdown-subpanel .dropdown-item::after { border: 0; content: "\f054"; diff --git a/theme/classic/style/moodle.css b/theme/classic/style/moodle.css index 56735d5c411..73d1febeb1b 100644 --- a/theme/classic/style/moodle.css +++ b/theme/classic/style/moodle.css @@ -26036,6 +26036,9 @@ blockquote { animation: none; } } +body.behat-site .action-menu .dropdown-subpanel-content.show { + animation: none; +} .action-menu .dropdown-subpanel .dropdown-item::after { border: 0; content: "\f054";