Merge branch 'MDL-75155-master' of https://github.com/rezaies/moodle

This commit is contained in:
Ilya Tregubov 2022-09-06 15:44:09 +04:00
commit c9a108973e
27 changed files with 620 additions and 71 deletions

View File

@ -27,9 +27,6 @@ use moodle_url;
*/
class export_action_bar extends action_bar {
/** @var moodle_url $exportactiveurl The URL that should be set as active in the exports URL selector element. */
protected $exportactiveurl;
/** @var string $activeplugin The plugin of the current export grades page (xml, ods, ...). */
protected $activeplugin;
@ -37,12 +34,14 @@ class export_action_bar extends action_bar {
* The class constructor.
*
* @param \context $context The context object.
* @param moodle_url $exportactiveurl The URL that should be set as active in the exports URL selector element.
* @param null $unused This parameter has been deprecated since 4.1 and should not be used anymore.
* @param string $activeplugin The plugin of the current export grades page (xml, ods, ...).
*/
public function __construct(\context $context, moodle_url $exportactiveurl, string $activeplugin) {
public function __construct(\context $context, $unused, string $activeplugin) {
if ($unused !== null) {
debugging('Deprecated argument passed to ' . __FUNCTION__, DEBUG_DEVELOPER);
}
parent::__construct($context);
$this->exportactiveurl = $exportactiveurl;
$this->activeplugin = $activeplugin;
}
@ -85,14 +84,18 @@ class export_action_bar extends action_bar {
}
$exportsmenu = [];
$exportactiveurl = null;
// Generate the data for the exports navigation selector menu.
foreach ($exports as $export) {
$exportsmenu[$export->link->out()] = $export->string;
if ($export->id == $this->activeplugin) {
$exportactiveurl = $export->link->out();
}
}
// This navigation selector menu will contain the links to all available grade export plugin pages.
$exportsurlselect = new \url_select($exportsmenu, $this->exportactiveurl->out(false), null,
'gradesexportactionselect');
$exportsurlselect = new \core\output\select_menu('exportas', $exportsmenu, $exportactiveurl);
$exportsurlselect->set_label(get_string('exportas', 'grades'));
$data['exportselector'] = $exportsurlselect->export_for_template($output);
return $data;

View File

@ -48,8 +48,7 @@ class export_key_manager_action_bar extends action_bar {
}
$courseid = $this->context->instanceid;
// Get the data used to output the general navigation selector and exports navigation selector.
$exportnavselectors = new export_action_bar($this->context,
new moodle_url('/grade/export/keymanager.php', ['id' => $courseid]), 'keymanager');
$exportnavselectors = new export_action_bar($this->context, null, 'keymanager');
$data = $exportnavselectors->export_for_template($output);
// Add a button to the action bar with a link to the 'add user key' page.

View File

@ -27,9 +27,6 @@ use moodle_url;
*/
class import_action_bar extends action_bar {
/** @var moodle_url $importactiveurl The URL that should be set as active in the imports URL selector element. */
protected $importactiveurl;
/** @var string $activeplugin The plugin of the current import grades page (xml, csv, ...). */
protected $activeplugin;
@ -37,12 +34,14 @@ class import_action_bar extends action_bar {
* The class constructor.
*
* @param \context $context The context object.
* @param moodle_url $importactiveurl The URL that should be set as active in the imports URL selector element.
* @param null $unused This parameter has been deprecated since 4.1 and should not be used anymore.
* @param string $activeplugin The plugin of the current import grades page (xml, csv, ...).
*/
public function __construct(\context $context, moodle_url $importactiveurl, string $activeplugin) {
public function __construct(\context $context, $unused, string $activeplugin) {
if ($unused !== null) {
debugging('Deprecated argument passed to ' . __FUNCTION__, DEBUG_DEVELOPER);
}
parent::__construct($context);
$this->importactiveurl = $importactiveurl;
$this->activeplugin = $activeplugin;
}
@ -85,14 +84,18 @@ class import_action_bar extends action_bar {
}
$importsmenu = [];
$importactiveurl = null;
// Generate the data for the imports navigation selector menu.
foreach ($imports as $import) {
$importsmenu[$import->link->out()] = $import->string;
if ($import->id == $this->activeplugin) {
$importactiveurl = $import->link->out();
}
}
// This navigation selector menu will contain the links to all available grade export plugin pages.
$importsurlselect = new \url_select($importsmenu, $this->importactiveurl->out(false), null,
'gradesimportactionselect');
$importsurlselect = new \core\output\select_menu('importas', $importsmenu, $importactiveurl);
$importsurlselect->set_label(get_string('importas', 'grades'));
$data['importselector'] = $importsurlselect->export_for_template($output);
return $data;

View File

@ -48,8 +48,7 @@ class import_key_manager_action_bar extends action_bar {
}
$courseid = $this->context->instanceid;
// Get the data used to output the general navigation selector and imports navigation selector.
$importnavselectors = new import_action_bar($this->context,
new moodle_url('/grade/import/keymanager.php', ['id' => $courseid]), 'keymanager');
$importnavselectors = new import_action_bar($this->context, null, 'keymanager');
$data = $importnavselectors->export_for_template($output);
// Add a button to the action bar with a link to the 'add user key' page.

View File

@ -33,7 +33,7 @@ $context = context_course::instance($id);
require_capability('moodle/grade:export', $context);
require_capability('gradeexport/ods:view', $context);
$actionbar = new \core_grades\output\export_action_bar($context, $PAGE->url, 'ods');
$actionbar = new \core_grades\output\export_action_bar($context, null, 'ods');
print_grade_page_head($COURSE->id, 'export', 'ods',
get_string('exportto', 'grades') . ' ' . get_string('pluginname', 'gradeexport_ods'),
false, false, true, null, null, null, $actionbar);

View File

@ -33,7 +33,7 @@ $context = context_course::instance($id);
require_capability('moodle/grade:export', $context);
require_capability('gradeexport/txt:view', $context);
$actionbar = new \core_grades\output\export_action_bar($context, $PAGE->url, 'txt');
$actionbar = new \core_grades\output\export_action_bar($context, null, 'txt');
print_grade_page_head($COURSE->id, 'export', 'txt',
get_string('exportto', 'grades') . ' ' . get_string('pluginname', 'gradeexport_txt'),
false, false, true, null, null, null, $actionbar);

View File

@ -33,7 +33,7 @@ $context = context_course::instance($id);
require_capability('moodle/grade:export', $context);
require_capability('gradeexport/xls:view', $context);
$actionbar = new \core_grades\output\export_action_bar($context, $PAGE->url, 'xls');
$actionbar = new \core_grades\output\export_action_bar($context, null, 'xls');
print_grade_page_head($COURSE->id, 'export', 'xls',
get_string('exportto', 'grades') . ' ' . get_string('pluginname', 'gradeexport_xls'),
false, false, true, null, null, null, $actionbar);

View File

@ -33,7 +33,7 @@ $context = context_course::instance($id);
require_capability('moodle/grade:export', $context);
require_capability('gradeexport/xml:view', $context);
$actionbar = new \core_grades\output\export_action_bar($context, $PAGE->url, 'xml');
$actionbar = new \core_grades\output\export_action_bar($context, null, 'xml');
print_grade_page_head($COURSE->id, 'export', 'xml',
get_string('exportto', 'grades') . ' ' . get_string('pluginname', 'gradeexport_xml'),
false, false, true, null, null, null, $actionbar);

View File

@ -51,7 +51,7 @@ $separatemode = (groups_get_course_groupmode($COURSE) == SEPARATEGROUPS and
!has_capability('moodle/site:accessallgroups', $context));
$currentgroup = groups_get_course_group($course);
$actionbar = new \core_grades\output\import_action_bar($context, $PAGE->url, 'csv');
$actionbar = new \core_grades\output\import_action_bar($context, null, 'csv');
print_grade_page_head($course->id, 'import', 'csv', get_string('importcsv', 'grades'), false, false, true,
'importcsv', 'grades', null, $actionbar);

View File

@ -47,7 +47,7 @@ $separatemode = (groups_get_course_groupmode($COURSE) == SEPARATEGROUPS and
!has_capability('moodle/site:accessallgroups', $context));
$currentgroup = groups_get_course_group($course);
$actionbar = new \core_grades\output\import_action_bar($context, $PAGE->url, 'direct');
$actionbar = new \core_grades\output\import_action_bar($context, null, 'direct');
print_grade_page_head($course->id, 'import', 'direct', get_string('pluginname', 'gradeimport_direct'), false, false, true,
'userdata', 'gradeimport_direct', null, $actionbar);

View File

@ -87,7 +87,7 @@ if ($data = $mform->get_data()) {
}
}
$actionbar = new \core_grades\output\import_action_bar($context, $PAGE->url, 'xml');
$actionbar = new \core_grades\output\import_action_bar($context, null, 'xml');
print_grade_page_head($COURSE->id, 'import', 'xml', get_string('importxml', 'grades'),
false, false, true, 'importxml', 'gradeimport_xml', null, $actionbar);

View File

@ -44,23 +44,25 @@
"title": null
},
"exportselector": {
"id": "url_select56789",
"action": "https://example.com/get",
"formid": "gradesexportactionselect",
"sesskey": "sesskey",
"classes": "urlselect",
"label": "",
"helpicon": false,
"showbutton": null,
"name": "exportas",
"value": "https://example.com/grade/export/ods/index.php",
"baseid": "select-menu56789",
"label": "Export as",
"labelattributes": [
{
"name": "class",
"value": "font-weight-bold"
}
],
"selectedoption": "OpenDocument spreadsheet",
"options": [
{
"name": "OpenDocument spreadsheet",
"value": "/grade/export/ods/index.php",
"selected": true
"value": "https://example.com/grade/export/ods/index.php",
"selected": true,
"id": "select-menu-option56789"
}
],
"disabled": false,
"title": null
]
}
}
}}
@ -73,8 +75,13 @@
{{/generalnavselector}}
{{#exportselector}}
<div class="navitem">
{{>core/url_select}}
{{>core/select_menu}}
</div>
{{#js}}
document.querySelector('#{{baseid}}').addEventListener('change', function(e) {
window.location.href = e.target.value;
});
{{/js}}
{{/exportselector}}
</div>
</div>

View File

@ -44,23 +44,25 @@
"title": null
},
"importselector": {
"id": "url_select56789",
"action": "https://example.com/get",
"formid": "gradesimportactionselect",
"sesskey": "sesskey",
"classes": "urlselect",
"label": "",
"helpicon": false,
"showbutton": null,
"name": "importas",
"value": "https://example.com/grade/import/csv/index.php",
"baseid": "select-menu56789",
"label": "Import as",
"labelattributes": [
{
"name": "class",
"value": "font-weight-bold"
}
],
"selectedoption": "CSV file",
"options": [
{
"name": "CSV file",
"value": "/grade/import/csv/index.php",
"selected": true
"value": "https://example.com/grade/import/csv/index.php",
"selected": true,
"id": "select-menu-option56789"
}
],
"disabled": false,
"title": null
]
}
}
}}
@ -73,8 +75,13 @@
{{/generalnavselector}}
{{#importselector}}
<div class="navitem">
{{>core/url_select}}
{{>core/select_menu}}
</div>
{{#js}}
document.querySelector('#{{baseid}}').addEventListener('change', function(e) {
window.location.href = e.target.value;
});
{{/js}}
{{/importselector}}
</div>
</div>

View File

@ -349,7 +349,7 @@ class behat_grade extends behat_base {
*/
public function i_navigate_to_import_page_in_the_course_gradebook($gradeimportoption) {
$this->i_navigate_to_in_the_course_gradebook("More > Import");
$this->select_in_gradebook_navigation_selector($gradeimportoption, 'gradesimportactionselect');
$this->execute('behat_forms::i_set_the_field_to', [get_string('importas', 'grades'), $gradeimportoption]);
}
/**
@ -364,7 +364,7 @@ class behat_grade extends behat_base {
*/
public function i_navigate_to_export_page_in_the_course_gradebook($gradeexportoption) {
$this->i_navigate_to_in_the_course_gradebook("More > Export");
$this->select_in_gradebook_navigation_selector($gradeexportoption, 'gradesexportactionselect');
$this->execute('behat_forms::i_set_the_field_to', [get_string('exportas', 'grades'), $gradeexportoption]);
}
/**

View File

@ -1,6 +1,10 @@
This file describes API changes in /grade/* ;
Information provided here is intended especially for developers.
=== 4.1 ===
* The $importactiveurl parameter in the constructor of the core_grades\output\import_action_bar class has been deprecated and is not used anymore.
* The $exportactiveurl parameter in the constructor of the core_grades\output\export_action_bar class has been deprecated and is not used anymore.
=== 4.0 ===
* The select_in_gradebook_tabs() function in behat_grade.php has been deprecated. Please use the function

View File

@ -212,6 +212,7 @@ $string['excluded_help'] = 'If ticked, the grade will not be included in any agg
$string['expand'] = 'Expand category';
$string['expandcriterion'] = 'Expand criterion';
$string['export'] = 'Export';
$string['exportas'] = 'Export as';
$string['exportalloutcomes'] = 'Export all outcomes';
$string['exportfeedback'] = 'Include feedback in export';
$string['exportfeedback_desc'] = 'This can be overridden during export.';
@ -400,6 +401,7 @@ $string['identifier'] = 'Identify user by';
$string['idnumbers'] = 'ID numbers';
$string['ignore'] = 'Ignore';
$string['import'] = 'Import';
$string['importas'] = 'Import as';
$string['importcsv'] = 'Import CSV';
$string['importcsv_help'] = 'Grades can be imported via a CSV file with format as follows:

View File

@ -190,6 +190,12 @@ class behat_field_manager {
}
}
if ($tagname == 'div') {
if ($node->getAttribute('role') == 'combobox') {
return 'select_menu';
}
}
// We can not provide a closer field type.
return false;
}

View File

@ -279,6 +279,11 @@ XPATH
'date_time' => <<<XPATH
.//fieldset[(%idMatch% or ./legend[%exactTagTextMatch%]) and (@data-fieldtype='date' or @data-fieldtype='date_time')]
XPATH
,
'select_menu' => <<<XPATH
//*[@role='combobox'][@aria-labelledby = //label[contains(normalize-space(string(.)), %locator%)]/@id]
XPATH
,
],
];

View File

@ -0,0 +1,48 @@
<?php
// This file is part of Moodle - http://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 <http://www.gnu.org/licenses/>.
declare(strict_types=1);
require_once(__DIR__ . '/behat_form_field.php');
/**
* Custom interaction with select_menu elements
*
* @package core_form
* @copyright 2022 Shamim Rezaie <shamim@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class behat_form_select_menu extends behat_form_field {
public function set_value($value) {
self::require_javascript();
$rootnode = $this->field->getParent();
$options = $rootnode->findAll('css', '[role=option]');
$this->field->click();
foreach ($options as $option) {
if (trim($option->getHtml()) == $value) {
$option->click();
break;
}
}
}
public function get_value() {
$rootnode = $this->field->getParent();
$input = $rootnode->find('css', 'input');
return $input->getValue();
}
}

View File

@ -0,0 +1,145 @@
<?php
// This file is part of Moodle - http://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 <http://www.gnu.org/licenses/>.
declare(strict_types=1);
namespace core\output;
use renderer_base;
/**
* A single-select combobox widget that is functionally similar to an HTML select element.
*
* @package core
* @category output
* @copyright 2022 Shamim Rezaie <shamim@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class select_menu implements \renderable, \templatable {
/** @var array List of options. */
protected $options;
/** @var string|null The value of the preselected option. */
protected $selected;
/** @var string The combobox label */
protected $label;
/** @var array Button label's attributes */
protected $labelattributes;
/** @var string Name of the combobox element */
protected $name;
/**
* select_menu constructor.
*
* @param string $name Name of the combobox element
* @param array $options List of options in an associative array format like ['val' => 'Option'].
* Supports grouped options as well.
* @param string|null $selected The value of the preselected option.
*/
public function __construct(string $name, array $options, string $selected = null) {
$this->name = $name;
$this->options = $options;
$this->selected = $selected;
}
/**
* Sets the select menu's label.
*
* @param string $label The label.
* @param array $attributes List of attributes to apply on the label element.
*/
public function set_label(string $label, array $attributes = []) {
$this->label = $label;
$this->labelattributes = $attributes;
}
/**
* Flatten the options for Mustache.
*
* @return array
*/
protected function flatten_options(): array {
$flattened = [];
foreach ($this->options as $value => $option) {
if (is_array($option)) {
foreach ($option as $groupname => $optoptions) {
if (!isset($flattened[$groupname])) {
$flattened[$groupname] = [
'name' => $groupname,
'isgroup' => true,
'id' => \html_writer::random_id('select-menu-group'),
'options' => []
];
}
foreach ($optoptions as $optvalue => $optoption) {
$flattened[$groupname]['options'][$optvalue] = [
'name' => $optoption,
'value' => $optvalue,
'selected' => $this->selected == $optvalue,
'id' => \html_writer::random_id('select-menu-option'),
];
}
}
} else {
$flattened[$value] = [
'name' => $option,
'value' => $value,
'selected' => $this->selected == $value,
'id' => \html_writer::random_id('select-menu-option'),
];
}
}
// Make non-associative array.
foreach ($flattened as $key => $value) {
if (!empty($value['options'])) {
$flattened[$key]['options'] = array_values($value['options']);
}
}
$flattened = array_values($flattened);
return $flattened;
}
/**
* Export for template.
*
* @param renderer_base $output The renderer.
* @return \stdClass
*/
public function export_for_template(renderer_base $output): \stdClass {
$data = new \stdClass();
$data->baseid = \html_writer::random_id('select-menu');
$data->label = $this->label;
$data->options = $this->flatten_options($this->options);
$data->selectedoption = array_column($data->options, 'name', 'value')[$this->selected];
$data->name = $this->name;
$data->value = $this->selected;
// Label attributes.
$data->labelattributes = [];
// Map the label attributes.
foreach ($this->labelattributes as $key => $value) {
$data->labelattributes[] = ['name' => $key, 'value' => $value];
}
return $data;
}
}

View File

@ -0,0 +1,133 @@
{{!
This file is part of Moodle - http://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 <http://www.gnu.org/licenses/>.
}}
{{!
@template core/select_menu
Template for select_menu output component.
Context variables required for this template:
* name - name of the form element
* value - value of the form element
* baseid - id of the dropdown element and to be used to generate id for other elements used internally
* label - Element label
* labelattributes - Label attributes.
* selectedoption - Text of the selected option
* options - Array of options for the select with value, name, selected, isgroup and id properites.
Example context (json):
{
"name": "menuname",
"value": "opt2",
"baseid": "select-menu56789",
"label": "Select one option",
"labelattributes": [
{
"name": "class",
"value": "font-weight-bold"
}
],
"selectedoption": "Second option",
"options": [
{
"name": "First option",
"value": "opt1",
"id": "select-menu-option1",
"selected": false
},
{
"name": "Second option",
"value": "opt2",
"id": "select-menu-option2",
"selected": true
},
{
"selected": false,
"isgroup": {
"name": "First group",
"id": "select-menu-group1",
"options": [
{
"name": "Third option",
"value": "opt3",
"id": "select-menu-option3",
"selected": false
},
{
"name": "Fourth option",
"value": "opt4",
"id": "select-menu-option4",
"selected": false
}
]
}
},
{
"name": "Fifth option",
"value": "opt5",
"id": "select-menu-option5",
"selected": false
}
]
}
}}
<div class="dropdown select-menu" id="{{baseid}}">
{{#label}}
<label id="{{baseid}}-label"{{#labelattributes}} {{name}}="{{value}}"{{/labelattributes}}>{{label}}</label>
{{/label}}
<div
class="btn dropdown-toggle"
role="combobox"
data-toggle="dropdown"
{{#label}}aria-labelledby="{{baseid}}-label{{/label}}"
aria-haspopup="listbox"
aria-expanded="false"
aria-controls="{{baseid}}-listbox"
tabindex="0"
>
{{selectedoption}}
</div>
<ul class="dropdown-menu" role="listbox" id="{{baseid}}-listbox" {{#label}}aria-labelledby="{{baseid}}-label{{/label}}">
{{#options}}
{{#isgroup}}
<li role="none">
<ul role="group" aria-labelledby="{{id}}">
<li role="presentation" id="{{id}}">{{name}}</li>
{{#options}}
<li class="dropdown-item" role="option" id="{{id}}" data-value="{{value}}" {{#selected}}aria-selected="true"{{/selected}}>
{{name}}
</li>
{{/options}}
</ul>
</li>
{{/isgroup}}
{{^isgroup}}
<li class="dropdown-item" role="option" id="{{id}}" data-value="{{value}}" {{#selected}}aria-selected="true"{{/selected}}>
{{name}}
</li>
{{/isgroup}}
{{/options}}
</ul>
<input type="hidden" name="{{name}}" value="{{value}}" />
</div>
{{#js}}
var label = document.getElementById('{{baseid}}-label');
if (label) {
label.addEventListener('click', function() {
label.parentElement.querySelector('.dropdown-toggle').focus();
});
}
{{/js}}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -199,6 +199,144 @@ const dropdownFix = () => {
});
};
/**
* A lot of Bootstrap's out of the box features don't work if dropdown items are not focusable.
*/
const comboboxFix = () => {
$(document).on('show.bs.dropdown', e => {
if (e.relatedTarget.matches('[role="combobox"]')) {
const combobox = e.relatedTarget;
const listbox = combobox.parentElement.querySelector('[role="listbox"]');
const selectedOption = listbox.querySelector('[role="option"][aria-selected="true"]');
// To make sure ArrowDown doesn't move the active option afterwards.
setTimeout(() => {
if (selectedOption) {
selectedOption.classList.add('active');
combobox.setAttribute('aria-activedescendant', selectedOption.id);
} else {
const firstOption = listbox.querySelector('[role="option"]');
firstOption.setAttribute('aria-selected', 'true');
firstOption.classList.add('active');
combobox.setAttribute('aria-activedescendant', firstOption.id);
}
}, 0);
}
});
$(document).on('hidden.bs.dropdown', e => {
if (e.relatedTarget.matches('[role="combobox"]')) {
const combobox = e.relatedTarget;
const listbox = combobox.parentElement.querySelector('[role="listbox"]');
combobox.removeAttribute('aria-activedescendant');
setTimeout(() => {
// Undo all previously highlighted options.
listbox.querySelectorAll('.active[role="option"]').forEach(option => {
option.classList.remove('active');
});
}, 0);
}
});
// Handling keyboard events for both navigating through and selecting options.
document.addEventListener('keydown', e => {
if (e.target.matches('.select-menu [role="combobox"]')) {
const combobox = e.target;
const trigger = e.key;
let next = null;
const options = combobox.parentElement.querySelectorAll('[role="listbox"] [role="option"]');
const activeOption = combobox.parentElement.querySelector('[role="listbox"] .active[role="option"]');
// Under the special case that the dropdown menu is being shown as a result of they key press (like when the user
// presses ArrowDown or Enter or ... to open the dropdown menu), activeOption is not set yet.
// It's because of a race condition with show.bs.dropdown event handler.
if (options && activeOption) {
if (trigger == 'ArrowDown') {
for (let i = 0; i < options.length - 1; i++) {
if (options[i] == activeOption) {
next = options[i + 1];
break;
}
}
} if (trigger == 'ArrowUp') {
for (let i = 1; i < options.length; i++) {
if (options[i] == activeOption) {
next = options[i - 1];
break;
}
}
} else if (trigger == 'Home') {
next = options[0];
} else if (trigger == 'End') {
next = options[options.length - 1];
} else if (trigger == ' ' || trigger == 'Enter') {
selectOption(combobox, activeOption);
} else {
// Search for options by finding the first option that has
// text starting with the typed character (case insensitive).
for (let i = 0; i < options.length; i++) {
const option = options[i];
const optionText = option.textContent.trim().toLowerCase();
const keyPressed = e.key.toLowerCase();
if (optionText.indexOf(keyPressed) == 0) {
next = option;
break;
}
}
}
// Variable next is set if we do want to act on the keypress.
if (next) {
e.preventDefault();
activeOption.classList.remove('active');
next.classList.add('active');
combobox.setAttribute('aria-activedescendant', next.id);
}
}
}
});
document.addEventListener('click', e => {
if (e.target.matches('.select-menu [role="option"]')) {
const option = e.target;
const combobox = option.closest('.select-menu').querySelector('[role="combobox"]');
combobox.focus();
selectOption(combobox, option);
}
});
// In case some code somewhere else changes the value of the combobox.
document.addEventListener('change', e => {
if (e.target.matches('.select-menu input[type="hidden"]')) {
const combobox = e.target.parentElement.querySelector('[role="combobox"]');
const option = e.target.parentElement.querySelector(`[role="option"][data-value="${e.target.value}"]`);
if (combobox && option) {
selectOption(combobox, option);
}
}
});
const selectOption = (combobox, option) => {
const oldSelectedOption = combobox.parentElement.querySelector('[role="listbox"] [role="option"][aria-selected="true"]');
const inputElement = combobox.parentElement.querySelector('input[type="hidden"]');
if (oldSelectedOption != option) {
if (oldSelectedOption) {
oldSelectedOption.removeAttribute('aria-selected');
}
option.setAttribute('aria-selected', 'true');
}
combobox.textContent = option.textContent;
if (inputElement.value != option.dataset.value) {
inputElement.value = option.dataset.value;
inputElement.dispatchEvent(new Event('change', {bubbles: true}));
}
};
};
/**
* After page load, focus on any element with special autofocus attribute.
*/
@ -303,6 +441,7 @@ const collapseFix = () => {
export const init = () => {
dropdownFix();
comboboxFix();
autoFocus();
tabElementFix();
collapseFix();

View File

@ -2369,6 +2369,7 @@ $footer-link-color: $bg-inverse-link-color !default;
width: 100%;
color: $body-color;
}
&.active,
&:active,
&:hover,
&:focus,
@ -2380,13 +2381,14 @@ $footer-link-color: $bg-inverse-link-color !default;
color: $dropdown-link-active-color;
}
}
&[aria-current="true"] {
&[aria-current="true"],
&[aria-selected="true"] {
position: relative;
display: flex;
align-items: center;
&:before {
@include fa-icon();
content: $fa-var-circle;
content: $fa-var-check;
position: absolute;
left: 0.4rem;
font-size: 0.7rem;
@ -2943,3 +2945,22 @@ body.dragging {
width: 9px;
border: 0;
}
.select-menu {
ul[role="group"] {
padding: 0;
margin: 0;
li:first-child {
cursor: default;
font-weight: bold;
padding: 0.25rem 1.5rem;
display: block;
}
.dropdown-item {
padding-left: 3rem;
}
}
.dropdown-item {
cursor: pointer;
}
}

View File

@ -11758,25 +11758,25 @@ ul {
width: 100%;
color: #1d2125; }
.dropdown-item:active, .dropdown-item:hover, .dropdown-item:focus, .dropdown-item:focus-within {
.dropdown-item.active, .dropdown-item:active, .dropdown-item:hover, .dropdown-item:focus, .dropdown-item:focus-within {
outline: 0;
background-color: #0f6cbf;
color: #fff; }
.dropdown-item:active a, .dropdown-item:hover a, .dropdown-item:focus a, .dropdown-item:focus-within a {
.dropdown-item.active a, .dropdown-item:active a, .dropdown-item:hover a, .dropdown-item:focus a, .dropdown-item:focus-within a {
color: #fff; }
.dropdown-item[aria-current="true"] {
.dropdown-item[aria-current="true"], .dropdown-item[aria-selected="true"] {
position: relative;
display: flex;
align-items: center; }
.dropdown-item[aria-current="true"]:before {
.dropdown-item[aria-current="true"]:before, .dropdown-item[aria-selected="true"]:before {
display: inline-block;
font: normal normal normal 14px/1 FontAwesome;
font-size: inherit;
text-rendering: auto;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
content: "";
content: "";
position: absolute;
left: 0.4rem;
font-size: 0.7rem; }
@ -12260,6 +12260,20 @@ body.dragging .dragging {
width: 9px;
border: 0; }
.select-menu ul[role="group"] {
padding: 0;
margin: 0; }
.select-menu ul[role="group"] li:first-child {
cursor: default;
font-weight: bold;
padding: 0.25rem 1.5rem;
display: block; }
.select-menu ul[role="group"] .dropdown-item {
padding-left: 3rem; }
.select-menu .dropdown-item {
cursor: pointer; }
.icon {
font-size: 16px;
width: 16px;

View File

@ -11758,25 +11758,25 @@ ul {
width: 100%;
color: #1d2125; }
.dropdown-item:active, .dropdown-item:hover, .dropdown-item:focus, .dropdown-item:focus-within {
.dropdown-item.active, .dropdown-item:active, .dropdown-item:hover, .dropdown-item:focus, .dropdown-item:focus-within {
outline: 0;
background-color: #0f6cbf;
color: #fff; }
.dropdown-item:active a, .dropdown-item:hover a, .dropdown-item:focus a, .dropdown-item:focus-within a {
.dropdown-item.active a, .dropdown-item:active a, .dropdown-item:hover a, .dropdown-item:focus a, .dropdown-item:focus-within a {
color: #fff; }
.dropdown-item[aria-current="true"] {
.dropdown-item[aria-current="true"], .dropdown-item[aria-selected="true"] {
position: relative;
display: flex;
align-items: center; }
.dropdown-item[aria-current="true"]:before {
.dropdown-item[aria-current="true"]:before, .dropdown-item[aria-selected="true"]:before {
display: inline-block;
font: normal normal normal 14px/1 FontAwesome;
font-size: inherit;
text-rendering: auto;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
content: "";
content: "";
position: absolute;
left: 0.4rem;
font-size: 0.7rem; }
@ -12260,6 +12260,20 @@ body.dragging .dragging {
width: 9px;
border: 0; }
.select-menu ul[role="group"] {
padding: 0;
margin: 0; }
.select-menu ul[role="group"] li:first-child {
cursor: default;
font-weight: bold;
padding: 0.25rem 1.5rem;
display: block; }
.select-menu ul[role="group"] .dropdown-item {
padding-left: 3rem; }
.select-menu .dropdown-item {
cursor: pointer; }
.icon {
font-size: 16px;
width: 16px;