mirror of
https://github.com/moodle/moodle.git
synced 2025-01-18 22:08:20 +01:00
MDL-76246 output: combobox support improvements
- It was a mistake to assume the listbox is always within combobox.parentElement - Take into account that the popup of the combobox is not necessarily a listbox - Update combobox fix so that it also work with comboboxes that are not select-menu - Update combobox fix so that it also support editable comboboxes - Update click listener to take into account that the event's target might be one of the option element - Having a hidden input element for comboboxes was not an ARIA requirement and was added by us. I added data-input-element to the combobox element to specify the input element related to it.
This commit is contained in:
parent
775872450c
commit
c4f33ceb59
@ -96,6 +96,7 @@
|
||||
aria-haspopup="listbox"
|
||||
aria-expanded="false"
|
||||
aria-controls="{{baseid}}-listbox"
|
||||
data-input-element="{{baseid}}-input"
|
||||
tabindex="0"
|
||||
>
|
||||
{{selectedoption}}
|
||||
@ -121,7 +122,7 @@
|
||||
{{/isgroup}}
|
||||
{{/options}}
|
||||
</ul>
|
||||
<input type="hidden" name="{{name}}" value="{{value}}" />
|
||||
<input type="hidden" name="{{name}}" value="{{value}}" id="{{baseid}}-input" />
|
||||
</div>
|
||||
{{#js}}
|
||||
var label = document.getElementById('{{baseid}}-label');
|
||||
|
2
theme/boost/amd/build/aria.min.js
vendored
2
theme/boost/amd/build/aria.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -206,53 +206,60 @@ 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"]');
|
||||
const listbox = document.querySelector(`#${combobox.getAttribute('aria-controls')}[role="listbox"]`);
|
||||
|
||||
// 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);
|
||||
if (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"]');
|
||||
const listbox = document.querySelector(`#${combobox.getAttribute('aria-controls')}[role="listbox"]`);
|
||||
|
||||
combobox.removeAttribute('aria-activedescendant');
|
||||
|
||||
setTimeout(() => {
|
||||
// Undo all previously highlighted options.
|
||||
listbox.querySelectorAll('.active[role="option"]').forEach(option => {
|
||||
option.classList.remove('active');
|
||||
});
|
||||
}, 0);
|
||||
if (listbox) {
|
||||
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"]')) {
|
||||
if (e.target.matches('[role="combobox"][aria-controls]:not([aria-haspopup=dialog])')) {
|
||||
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"]');
|
||||
const listbox = document.querySelector(`#${combobox.getAttribute('aria-controls')}[role="listbox"]`);
|
||||
const options = listbox.querySelectorAll('[role="option"]');
|
||||
const activeOption = listbox.querySelector('.active[role="option"]');
|
||||
const editable = combobox.hasAttribute('aria-autocomplete');
|
||||
|
||||
// Under the special case that the dropdown menu is being shown as a result of they key press (like when the user
|
||||
// Under the special case that the dropdown menu is being shown as a result of the 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 (options && (activeOption || editable)) {
|
||||
if (trigger == 'ArrowDown') {
|
||||
for (let i = 0; i < options.length - 1; i++) {
|
||||
if (options[i] == activeOption) {
|
||||
@ -260,6 +267,9 @@ const comboboxFix = () => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (editable && !next) {
|
||||
next = options[0];
|
||||
}
|
||||
} if (trigger == 'ArrowUp') {
|
||||
for (let i = 1; i < options.length; i++) {
|
||||
if (options[i] == activeOption) {
|
||||
@ -267,13 +277,17 @@ const comboboxFix = () => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (editable && !next) {
|
||||
next = options[options.length - 1];
|
||||
}
|
||||
} else if (trigger == 'Home') {
|
||||
next = options[0];
|
||||
} else if (trigger == 'End') {
|
||||
next = options[options.length - 1];
|
||||
} else if (trigger == ' ' || trigger == 'Enter') {
|
||||
} else if ((trigger == ' ' && !editable) || trigger == 'Enter') {
|
||||
e.preventDefault();
|
||||
selectOption(combobox, activeOption);
|
||||
} else {
|
||||
} else if (!editable) {
|
||||
// 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++) {
|
||||
@ -290,27 +304,33 @@ const comboboxFix = () => {
|
||||
// Variable next is set if we do want to act on the keypress.
|
||||
if (next) {
|
||||
e.preventDefault();
|
||||
activeOption.classList.remove('active');
|
||||
if (activeOption) {
|
||||
activeOption.classList.remove('active');
|
||||
}
|
||||
next.classList.add('active');
|
||||
combobox.setAttribute('aria-activedescendant', next.id);
|
||||
next.scrollIntoView({block: 'nearest'});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
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);
|
||||
const option = e.target.closest('[role="listbox"] [role="option"]');
|
||||
if (option) {
|
||||
const listbox = option.closest('[role="listbox"]');
|
||||
const combobox = document.querySelector(`[role="combobox"][aria-controls="${listbox.id}"]`);
|
||||
if (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"]');
|
||||
if (e.target.matches('input[type="hidden"][id]')) {
|
||||
const combobox = document.querySelector(`[role="combobox"][data-input-element="${e.target.id}"]`);
|
||||
const option = e.target.parentElement.querySelector(`[role="option"][data-value="${e.target.value}"]`);
|
||||
|
||||
if (combobox && option) {
|
||||
@ -320,8 +340,8 @@ const comboboxFix = () => {
|
||||
});
|
||||
|
||||
const selectOption = (combobox, option) => {
|
||||
const oldSelectedOption = combobox.parentElement.querySelector('[role="listbox"] [role="option"][aria-selected="true"]');
|
||||
const inputElement = combobox.parentElement.querySelector('input[type="hidden"]');
|
||||
const listbox = option.closest('[role="listbox"]');
|
||||
const oldSelectedOption = listbox.querySelector('[role="option"][aria-selected="true"]');
|
||||
|
||||
if (oldSelectedOption != option) {
|
||||
if (oldSelectedOption) {
|
||||
@ -329,10 +349,19 @@ const comboboxFix = () => {
|
||||
}
|
||||
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}));
|
||||
|
||||
if (combobox.hasAttribute('value')) {
|
||||
combobox.value = option.textContent.replace(/[\n\r]+|[\s]{2,}/g, ' ').trim();
|
||||
} else {
|
||||
combobox.textContent = 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}));
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user