Merge branch 'MDL-82625-500' of https://github.com/cameron1729/moodle into MOODLE_500_STABLE

This commit is contained in:
Shamim Rezaie
2026-03-16 18:28:19 +11:00
20 changed files with 392 additions and 27 deletions
+1 -1
View File
@@ -411,7 +411,7 @@ abstract class backup_helper {
}
$selectmenu = new \core\output\select_menu('coursereusetype', $menuarray, $activeurl);
$selectmenu = new \core\output\select_menu('coursereusetype', $menuarray, $activeurl, true);
$selectmenu->set_label(get_string('coursereusenavigationmenu'), ['class' => 'visually-hidden']);
$options = \html_writer::tag(
'div',
@@ -61,7 +61,8 @@ class completion_action_bar implements templatable, renderable {
$selectmenu = new select_menu(
'coursecompletionnavigation',
manager::get_available_completion_options($this->courseid),
$this->currenturl->out(false)
$this->currenturl->out(false),
true
);
$selectmenu->set_label(
get_string('coursecompletionnavigation', 'completion'),
+1 -1
View File
@@ -94,7 +94,7 @@ class export_action_bar extends action_bar {
}
// This navigation selector menu will contain the links to all available grade export plugin pages.
$exportsurlselect = new \core\output\select_menu('exportas', $exportsmenu, $exportactiveurl);
$exportsurlselect = new \core\output\select_menu('exportas', $exportsmenu, $exportactiveurl, true);
$exportsurlselect->set_label(get_string('exportas', 'grades'));
$data['exportselector'] = $exportsurlselect->export_for_template($output);
+1 -1
View File
@@ -187,7 +187,7 @@ class general_action_bar extends action_bar {
$menu[][get_string('moremenu')] = $moregroup;
}
$selectmenu = new select_menu('gradesactionselect', $menu, $this->activeurl->out(false));
$selectmenu = new select_menu('gradesactionselect', $menu, $this->activeurl->out(false), true);
$selectmenu->set_label(get_string('gradebooknavigationmenu', 'grades'), ['class' => 'visually-hidden']);
return $selectmenu;
+1 -1
View File
@@ -94,7 +94,7 @@ class import_action_bar extends action_bar {
}
// This navigation selector menu will contain the links to all available grade export plugin pages.
$importsurlselect = new \core\output\select_menu('importas', $importsmenu, $importactiveurl);
$importsurlselect = new \core\output\select_menu('importas', $importsmenu, $importactiveurl, true);
$importsurlselect->set_label(get_string('importas', 'grades'));
$data['importselector'] = $importsurlselect->export_for_template($output);
@@ -127,7 +127,7 @@ class action_bar extends \core_grades\output\action_bar {
$selectoractiveurl = $this->userview === GRADE_REPORT_USER_VIEW_USER ? $viewasotheruser : $viewasmyself;
$viewasselect = new \core\output\select_menu('viewas', $selectoroptions, $selectoractiveurl->out(false));
$viewasselect = new \core\output\select_menu('viewas', $selectoroptions, $selectoractiveurl->out(false), true);
$viewasselect->set_label(get_string('viewas', 'core_grades'));
$data['viewasselector'] = $viewasselect->export_for_template($output);
+1 -1
View File
@@ -190,7 +190,7 @@ class gradereport_user_renderer extends plugin_renderer_base {
$selectoractiveurl = $userview === GRADE_REPORT_USER_VIEW_USER ? $viewasotheruser : $viewasmyself;
$viewasselect = new \core\output\select_menu('viewas', $selectoroptions, $selectoractiveurl->out(false));
$viewasselect = new \core\output\select_menu('viewas', $selectoroptions, $selectoractiveurl->out(false), true);
$viewasselect->set_label(get_string('viewas', 'core_grades'));
return $this->render_from_template('gradereport_user/view_mode_selector',
@@ -206,7 +206,7 @@ class participants_action_bar implements renderable {
$activeurl = $this->find_active_page($urlselectcontent);
$activeurl = $activeurl ?: $this->find_active_page($urlselectcontent, URL_MATCH_BASE);
$selectmenu = new select_menu('participantsnavigation', $urlselectcontent, $activeurl);
$selectmenu = new select_menu('participantsnavigation', $urlselectcontent, $activeurl, true);
$selectmenu->set_label(get_string('participantsnavigation', 'course'), ['class' => 'visually-hidden']);
return $selectmenu->export_for_template($output);
+15 -1
View File
@@ -45,6 +45,9 @@ class select_menu implements renderable, templatable {
/** @var string Name of the combobox element */
protected $name;
/** @var bool A flag indicating whether the active state should be disabled in the dropdown. */
protected $disableactive;
/**
* select_menu constructor.
*
@@ -52,11 +55,19 @@ class select_menu implements renderable, templatable {
* @param array $options List of options in an associative array format like ['val' => 'Option'].
* Supports grouped options as well. Empty string or null values will be rendered as dividers.
* @param string|null $selected The value of the preselected option.
* @param bool $disableactive A flag that indicates whether the active state should be disabled in the dropdown.
* This is useful when the dropdown items result in navigation to another page,
* as it makes it unnecessary to mark the selected item as active. If the flag
* is set to true, the checkmark indicating the active menu item will not be displayed,
* as the user is redirected. However, in cases where no redirection occurs and
* it is valid to display the active state, this flag should remain false,
* allowing the checkmark to appear beside the active item.
*/
public function __construct(string $name, array $options, ?string $selected = null) {
public function __construct(string $name, array $options, ?string $selected = null, bool $disableactive = false) {
$this->name = $name;
$this->options = $options;
$this->selected = $selected;
$this->disableactive = $disableactive;
}
/**
@@ -100,6 +111,7 @@ class select_menu implements renderable, templatable {
'value' => $optvalue,
'selected' => $this->selected == $optvalue,
'id' => \html_writer::random_id('select-menu-option'),
'disableactive' => $this->disableactive,
];
}
}
@@ -113,6 +125,7 @@ class select_menu implements renderable, templatable {
'value' => $value,
'selected' => $this->selected == $value,
'id' => \html_writer::random_id('select-menu-option'),
'disableactive' => $this->disableactive,
];
}
}
@@ -171,6 +184,7 @@ class select_menu implements renderable, templatable {
$data->selectedoption = $this->get_selected_option();
$data->name = $this->name;
$data->value = $this->selected;
$data->disableactive = $this->disableactive;
// Label attributes.
$data->labelattributes = [];
+1 -1
View File
@@ -69,7 +69,7 @@ class report_helper {
}
}
$selectmenu = new \core\output\select_menu('reporttype', $menuarray, $activeurl);
$selectmenu = new \core\output\select_menu('reporttype', $menuarray, $activeurl, true);
$selectmenu->set_label(get_string('reporttype'), ['class' => 'visually-hidden']);
$options = \html_writer::tag(
'div',
+9 -3
View File
@@ -27,6 +27,7 @@
* labelattributes - Label attributes.
* selectedoption - Text of the selected option
* options - Array of options for the select with value, name, selected, isgroup and id properites.
* disableactive - A flag indicating whether the active state should be disabled in the dropdown.
Example context (json):
{
@@ -82,7 +83,8 @@
"id": "select-menu-option5",
"selected": false
}
]
],
"disableactive": false
}
}}
<div class="dropdown select-menu" id="{{baseid}}">
@@ -141,7 +143,9 @@
<li role="separator" class="dropdown-divider"></li>
{{/isdivider}}
{{^isdivider}}
<li class="dropdown-item" role="option" id="{{id}}" data-value="{{value}}" {{#selected}}aria-selected="true"{{/selected}}>
<li class="dropdown-item" role="option" id="{{id}}" data-value="{{value}}"
{{#disableactive}}data-disableactive="true"{{/disableactive}}
{{#selected}}aria-selected="true"{{/selected}}>
{{name}}
</li>
{{/isdivider}}
@@ -154,7 +158,9 @@
<li role="separator" class="dropdown-divider"></li>
{{/isdivider}}
{{^isdivider}}
<li class="dropdown-item" role="option" id="{{id}}" data-value="{{value}}" {{#selected}}aria-selected="true"{{/selected}}>
<li class="dropdown-item" role="option" id="{{id}}" data-value="{{value}}"
{{#disableactive}}data-disableactive="true"{{/disableactive}}
{{#selected}}aria-selected="true"{{/selected}}>
{{name}}
</li>
{{/isdivider}}
+48
View File
@@ -1694,4 +1694,52 @@ class behat_navigation extends behat_base {
$this->getSession()->executeScript($script);
}
/**
* Checks if a dropdown item is active.
*
* @Then dropdown item :dropdownitem should be active
* @param string $dropdownitem The dropdown item name.
*/
public function dropdown_item_should_be_active(string $dropdownitem): void {
$elementselector = "//li[contains(text(), '$dropdownitem') and @aria-selected='true']";
$params = [$elementselector, "xpath_element"];
$this->execute("behat_general::should_exist", $params);
}
/**
* Checks if a dropdown item is not active.
*
* @Then dropdown item :dropdownitem should not be active
* @param string $dropdownitem The dropdown item name.
*/
public function dropdown_item_should_not_be_active(string $dropdownitem): void {
$elementselector = "//li[contains(text(), '$dropdownitem') and @aria-selected='true']";
$params = [$elementselector, "xpath_element"];
$this->execute("behat_general::should_not_exist", $params);
}
/**
* Selects the specified item from the dropdown menu.
*
* @When /^I select "([^"]*)" from the dropdown$/
* @param string $selecteditem THe dropdown item selected.
* @throws ExpectationException
*/
public function i_select_from_the_dropdown(string $selecteditem): void {
$isdropdownvisible = $this->getSession()->getPage()->find('css', '.dropdown-menu.show');
if (!$isdropdownvisible) {
throw new ExpectationException("Dropdown menu is not visible.", $this->getSession());
}
$dropdownitem = $this->getSession()->getPage()->find(
'xpath',
"//li[contains(@class, 'dropdown-item') and contains(text(), '$selecteditem')]"
);
if (!$dropdownitem) {
throw new ExpectationException("Dropdown item '$selecteditem' not found.", $this->getSession());
}
$dropdownitem->click();
}
}
@@ -0,0 +1,54 @@
<?php
// This file is part of Moodle - https://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <https://www.gnu.org/licenses/>.
/**
* Test page for select_menu output component disableactive behaviour.
*
* @copyright 2026 Monash University
* @author Cameron Ball <[email protected]>
* @package core
* @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
declare(strict_types=1);
require_once(__DIR__ . '/../../../../config.php');
defined('BEHAT_SITE_RUNNING') || die();
global $PAGE, $OUTPUT;
require_login();
$PAGE->set_url('/lib/tests/behat/fixtures/select_menu_disableactive_testpage.php');
$PAGE->add_body_class('limitedwidth');
$PAGE->set_context(core\context\system::instance());
$PAGE->set_title('Select menu disableactive fixture');
$options = [
'opt1' => 'Option 1',
'opt2' => 'Option 2',
'opt3' => 'Option 3',
];
$selectmenu = new core\output\select_menu('fixtureselect', $options, 'opt1', true);
$selectmenu->set_label('Test combobox');
echo $OUTPUT->header();
echo '<h2>Select menu disableactive fixture</h2>';
echo $OUTPUT->render($selectmenu);
echo $OUTPUT->footer();
@@ -175,7 +175,7 @@ class grading_actionmenu implements templatable, renderable {
if ($this->assign->is_any_submission_plugin_enabled()) {
['statusmenu' => $statusmenu, 'currentvalue' => $currentvalue] = $this->get_status_menu();
$statusselect = new \core\output\select_menu('status', $statusmenu, $currentvalue);
$statusselect = new \core\output\select_menu('status', $statusmenu, $currentvalue, true);
$statusselect->set_label(get_string('status', 'mod_assign'), [], true);
$data['statusselector'] = $statusselect->export_for_template($output);
}
+1 -1
View File
@@ -187,7 +187,7 @@ class action_bar {
$rsstemplatelink->out(false) => get_string('rsstemplate', 'mod_data'),
];
$selectmenu = new \core\output\select_menu('presetsactions', $menu, $this->currenturl->out(false));
$selectmenu = new \core\output\select_menu('presetsactions', $menu, $this->currenturl->out(false), true);
$selectmenu->set_label(get_string('templatesnavigation', 'mod_data'), ['class' => 'visually-hidden']);
$renderer = $PAGE->get_renderer('mod_data');
+1 -1
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+12 -8
View File
@@ -369,6 +369,18 @@ const comboboxFix = () => {
});
const selectOption = (combobox, option) => {
if (combobox.dataset.inputElement) {
const inputElement = document.getElementById(combobox.dataset.inputElement);
if (inputElement && (inputElement.value != option.dataset.value)) {
inputElement.value = option.dataset.value;
inputElement.dispatchEvent(new Event('change', {bubbles: true}));
}
}
if (option.dataset.disableactive) {
return;
}
const listbox = option.closest('[role="listbox"]');
const oldSelectedOption = listbox.querySelector('[role="option"][aria-selected="true"]');
@@ -389,14 +401,6 @@ const comboboxFix = () => {
combobox.textContent = option.dataset.shortText || option.textContent;
}
}
if (combobox.dataset.inputElement) {
const inputElement = document.getElementById(combobox.dataset.inputElement);
if (inputElement && (inputElement.value != option.dataset.value)) {
inputElement.value = option.dataset.value;
inputElement.dispatchEvent(new Event('change', {bubbles: true}));
}
}
};
};
@@ -53,8 +53,87 @@ class behat_theme_boost_behat_navigation extends behat_navigation {
* @return void
*/
public function i_should_see_is_active_in_secondary_navigation($element) {
$this->execute("behat_general::assert_element_contains_text",
[$element, '.secondary-navigation .nav-link.active', 'css_element']);
$page = $this->getSession()->getPage();
$expectedexception = new ExpectationException(
"\"{$element}\" is not active in secondary navigation",
$this->getSession(),
);
$this->spin(
function () use ($page, $element) {
$secondarynav = $page->find('css', '.secondary-navigation');
if (!$secondarynav) {
return false;
}
// Special case: if the active item is inside the secondary navigation 'More' menu, then the 'More'
// toggle itself is the only visible active element.
$moretoggle = $page->find('css', '.secondary-navigation [data-region="morebutton"] > .dropdown-toggle');
if ($moretoggle && trim($moretoggle->getText()) === $element) {
$ariacurrent = $moretoggle->getAttribute('aria-current');
return $moretoggle->hasClass('active') || $ariacurrent === 'true' || $ariacurrent === 'page';
}
// First, check the visible secondary navigation items (excluding the 'More' toggle).
$selectors = [
'.nav-item:not(.dropdownmoremenu) .nav-link.active',
'.nav-item:not(.dropdownmoremenu) .nav-link[aria-current="true"]',
'.nav-item:not(.dropdownmoremenu) .nav-link[aria-current="page"]',
];
foreach ($selectors as $selector) {
if ($this->node_list_contains_text($secondarynav->findAll('css', $selector), $element)) {
return true;
}
}
// If not found, the active item may be inside the secondary navigation 'More' menu.
if (!$moretoggle) {
return false;
}
// Ensure the dropdown is open so its items are visible.
if ($moretoggle->getAttribute('aria-expanded') !== 'true') {
$moretoggle->click();
}
$selectors = [
'.dropdownmoremenu .dropdown-item.active',
'.dropdownmoremenu .dropdown-item[aria-current="true"]',
'.dropdownmoremenu .dropdown-item[aria-current="page"]',
];
foreach ($selectors as $selector) {
if ($this->node_list_contains_text($secondarynav->findAll('css', $selector), $element)) {
// Close the menu to avoid it obscuring subsequent steps.
if ($moretoggle->getAttribute('aria-expanded') === 'true') {
$moretoggle->click();
}
return true;
}
}
return false;
},
false,
behat_base::get_reduced_timeout(),
$expectedexception,
);
}
/**
* Check whether any of the provided nodes contains the given text and is visible.
*
* @param array $nodes
* @param string $text
* @return bool
*/
protected function node_list_contains_text(array $nodes, string $text): bool {
foreach ($nodes as $node) {
if ($node->isVisible() && strpos($node->getText(), $text) !== false) {
return true;
}
}
return false;
}
/**
@@ -0,0 +1,159 @@
@core @javascript @theme_boost
Feature: Menu navigation accurately updates checkmarks in tertiary navigation
In order to correctly navigate the menu items
As a teacher
I need to see accurate checkmarks when navigating back and forward
Background:
Given the following "courses" exist:
| fullname | shortname | newsitems |
| Course 1 | C1 | 5 |
And the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
Scenario: Ensure the tertiary navigation checkmark updates correctly when navigating in the Grades page
Given I log in as "teacher1"
When I am on "Course 1" course homepage
And I navigate to "Grades" in current page administration
Then dropdown item "Grader report" should be active
When I click on "Grader report" "combobox"
And I select "Scales" from the dropdown
Then dropdown item "Scales" should be active
And dropdown item "Grader report" should not be active
Scenario: Ensure the tertiary navigation checkmark updates correctly after pressing browser back button in the Grades page
Given I log in as "teacher1"
When I am on "Course 1" course homepage
And I navigate to "Grades" in current page administration
Then I should see "Grades" is active in secondary navigation
When I click on "Grader report" "combobox"
And I select "Scales" from the dropdown
And I click on "Scales" "combobox"
And I select "Grade letters" from the dropdown
And I click on "Grade letters" "combobox"
And I select "Import" from the dropdown
And I click on "Import" "combobox"
And I select "Export" from the dropdown
And I click on "Export" "combobox"
And I press the "back" button in the browser
Then dropdown item "Import" should be active
When I press the "back" button in the browser
Then dropdown item "Grade letters" should be active
When I press the "back" button in the browser
Then dropdown item "Scales" should be active
When I press the "back" button in the browser
Then dropdown item "Grader report" should be active
Scenario: Ensure the tertiary navigation checkmark updates correctly after pressing browser forward button in the Grades page
Given I log in as "teacher1"
When I am on "Course 1" course homepage
And I navigate to "Participants" in current page administration
Then I should see "Participants" is active in secondary navigation
When I click on "Enrolled users" "combobox"
And I select "Groups" from the dropdown
And I click on "Groups" "combobox"
And I select "Permissions" from the dropdown
And I press the "back" button in the browser
And I press the "back" button in the browser
And I press the "forward" button in the browser
Then dropdown item "Groups" should be active
Scenario: Ensure the tertiary navigation checkmark updates correctly after pressing browser back button in the Participants page
Given I log in as "teacher1"
When I am on "Course 1" course homepage
And I navigate to "Participants" in current page administration
Then I should see "Participants" is active in secondary navigation
When I click on "Enrolled users" "combobox"
And I select "Permissions" from the dropdown
And I click on "Permissions" "combobox"
And I select "Groups" from the dropdown
And I click on "Groups" "combobox"
And I select "Role renaming" from the dropdown
And I click on "Role renaming" "combobox"
And I press the "back" button in the browser
Then dropdown item "Groups" should be active
When I press the "back" button in the browser
Then dropdown item "Permissions" should be active
Scenario: Ensure the tertiary navigation checkmark updates correctly after pressing browser forward button in the Participants page
Given I log in as "teacher1"
When I am on "Course 1" course homepage
And I navigate to "Participants" in current page administration
Then I should see "Participants" is active in secondary navigation
When I click on "Enrolled users" "combobox"
And I select "Groups" from the dropdown
And I click on "Groups" "combobox"
And I select "Permissions" from the dropdown
And I press the "back" button in the browser
And I press the "back" button in the browser
And I press the "forward" button in the browser
Then dropdown item "Groups" should be active
Scenario: Ensure the tertiary navigation checkmark updates correctly after pressing browser back button in the Reports page
Given I log in as "teacher1"
When I am on "Course 1" course homepage
And I navigate to "Reports" in current page administration
And I click on "Competency breakdown" "link"
Then I should see "Reports" is active in secondary navigation
When I click on "Competency breakdown" "combobox"
And I select "Logs" from the dropdown
And I click on "Logs" "combobox"
And I select "Course participation" from the dropdown
And I click on "Course participation" "combobox"
And I select "Activity report" from the dropdown
And I click on "Activity report" "combobox"
And I press the "back" button in the browser
Then dropdown item "Course participation" should be active
When I press the "back" button in the browser
Then dropdown item "Logs" should be active
Scenario: Ensure the tertiary navigation checkmark updates correctly after pressing browser forward button in the Reports page
Given I log in as "teacher1"
When I am on "Course 1" course homepage
And I navigate to "Reports" in current page administration
And I click on "Competency breakdown" "link"
Then I should see "Reports" is active in secondary navigation
When I click on "Competency breakdown" "combobox"
And I select "Activity report" from the dropdown
And I click on "Activity report" "combobox"
And I select "Course participation" from the dropdown
And I press the "back" button in the browser
And I press the "forward" button in the browser
Then dropdown item "Course participation" should be active
And dropdown item "Competency breakdown" should not be active
And dropdown item "Logs" should not be active
And dropdown item "Live logs" should not be active
And dropdown item "Activity report" should not be active
Scenario: Admin can see checkmark beside menu item they are currently on after pressing browser back button when
jumping between secondary navigation menu
Given I log in as "teacher1"
When I am on "Course 1" course homepage
And I navigate to "Participants" in current page administration
Then I should see "Participants" is active in secondary navigation
When I click on "Enrolled users" "combobox"
And I navigate to "Grades" in current page administration
And I press the "back" button in the browser
Then I should see "Participants" is active in secondary navigation
And dropdown item "Enrolled users" should be active
When I navigate to "Reports" in current page administration
And I click on "Competency breakdown" "link"
And I navigate to "Competencies" in current page administration
And I press the "back" button in the browser
Then I should see "Reports" is active in secondary navigation
And dropdown item "Competency breakdown" should be active
Scenario: Ensure checkmark is not updated when disableactive is enabled
Given I log in as "teacher1"
When I am on fixture page "/lib/tests/behat/fixtures/select_menu_disableactive_testpage.php"
Then the field with xpath "//input[@name='fixtureselect']" matches value "opt1"
When I click on "//*[@role='combobox']" "xpath_element"
Then dropdown item "Option 1" should be active
And I select "Option 2" from the dropdown
Then dropdown item "Option 2" should not be active
And dropdown item "Option 1" should be active
And the field with xpath "//input[@name='fixtureselect']" matches value "opt2"