266 lines
8.6 KiB
JavaScript
266 lines
8.6 KiB
JavaScript
/**
|
|
* @file
|
|
* Keyboard navigation component.
|
|
*/
|
|
|
|
((Drupal, once, { focusable }) => {
|
|
/**
|
|
* Attaches the keyboard navigation functionality.
|
|
*
|
|
* @type {Drupal~behavior}
|
|
*
|
|
* @prop {Drupal~behaviorAttach} attach
|
|
* Attaches the behavior to the `.admin-toolbar` element.
|
|
*/
|
|
Drupal.behaviors.keyboardNavigation = {
|
|
attach: (context) => {
|
|
once('keyboard-processed', '.admin-toolbar', context).forEach(
|
|
(sidebar) => {
|
|
const IS_RTL = document.documentElement.dir === 'rtl';
|
|
|
|
const isInteractive = (element) =>
|
|
element.getAttribute('aria-expanded');
|
|
|
|
const getFocusableGroup = (element) =>
|
|
element.closest('[class*="toolbar-menu--level-"]') ||
|
|
element.closest('[data-toolbar-popover-wrapper]') ||
|
|
element.closest('.admin-toolbar');
|
|
|
|
const findFirstElementByChar = (focusableElements, targetChar) => {
|
|
const elementWIthChar = Array.prototype.find.call(
|
|
focusableElements,
|
|
(element) => {
|
|
const dataText = element.dataset.indexText;
|
|
return dataText && dataText[0] === targetChar;
|
|
},
|
|
);
|
|
|
|
return elementWIthChar;
|
|
};
|
|
|
|
const checkChar = ({ key, target }) => {
|
|
const currentGroup = getFocusableGroup(target);
|
|
const foundElementWithIndexChar = findFirstElementByChar(
|
|
focusable(currentGroup),
|
|
key,
|
|
);
|
|
if (foundElementWithIndexChar) {
|
|
foundElementWithIndexChar.focus();
|
|
}
|
|
};
|
|
|
|
const focusFirstInGroup = (focusableElements) => {
|
|
focusableElements[0].focus();
|
|
};
|
|
|
|
const focusLastInGroup = (focusableElements) => {
|
|
focusableElements[focusableElements.length - 1].focus();
|
|
};
|
|
|
|
const focusNextInGroup = (focusableElements, element) => {
|
|
const currentIndex = Array.prototype.indexOf.call(
|
|
focusableElements,
|
|
element,
|
|
);
|
|
|
|
if (currentIndex === focusableElements.length - 1) {
|
|
focusableElements[0].focus();
|
|
} else {
|
|
focusableElements[currentIndex + 1].focus();
|
|
}
|
|
};
|
|
|
|
const focusPreviousInGroup = (focusableElements, element) => {
|
|
const currentIndex = Array.prototype.indexOf.call(
|
|
focusableElements,
|
|
element,
|
|
);
|
|
|
|
if (currentIndex === 0) {
|
|
focusableElements[focusableElements.length - 1].focus();
|
|
} else {
|
|
focusableElements[currentIndex - 1].focus();
|
|
}
|
|
};
|
|
|
|
const toggleMenu = (element, state) =>
|
|
element.dispatchEvent(
|
|
new CustomEvent('toolbar-menu-set-toggle', {
|
|
bubbles: false,
|
|
detail: {
|
|
state,
|
|
},
|
|
}),
|
|
);
|
|
|
|
const closePopover = (element) =>
|
|
element.dispatchEvent(
|
|
new CustomEvent('toolbar-popover-close', { bubbles: true }),
|
|
);
|
|
|
|
const openPopover = (element) =>
|
|
element.dispatchEvent(
|
|
new CustomEvent('toolbar-popover-open', { bubbles: true }),
|
|
);
|
|
|
|
const focusClosestPopoverTrigger = (element) => {
|
|
element
|
|
.closest('[data-toolbar-popover]')
|
|
?.querySelector('[data-toolbar-popover-control]')
|
|
?.focus();
|
|
};
|
|
|
|
const focusFirstMenuElement = (element) => {
|
|
const elements = focusable(
|
|
element
|
|
.closest('.toolbar-menu__item')
|
|
?.querySelector('.toolbar-menu'),
|
|
);
|
|
if (elements?.length) {
|
|
elements[0].focus();
|
|
}
|
|
};
|
|
|
|
const focusFirstPopoverElement = (element) => {
|
|
// Zero is always popover trigger.
|
|
// And Popover header can be not interactive.
|
|
const elements = focusable(
|
|
element.closest('[data-toolbar-popover]'),
|
|
);
|
|
if (elements?.length >= 1) {
|
|
elements[1].focus();
|
|
}
|
|
};
|
|
|
|
const focusLastPopoverElement = (element) => {
|
|
const elements = focusable(
|
|
element.closest('[data-toolbar-popover]'),
|
|
);
|
|
if (elements?.length > 0) {
|
|
elements[elements.length - 1].focus();
|
|
}
|
|
};
|
|
|
|
const closeNonInteractiveElement = (element) => {
|
|
// If we are inside submenus.
|
|
if (element.closest('[class*="toolbar-menu--level-"]')) {
|
|
const trigger =
|
|
element.closest('.toolbar-menu')?.previousElementSibling;
|
|
toggleMenu(trigger, false);
|
|
trigger.focus();
|
|
} else {
|
|
closePopover(element);
|
|
focusClosestPopoverTrigger(element);
|
|
}
|
|
};
|
|
|
|
const openInteractiveElement = (element) => {
|
|
// If menu button.
|
|
if (element.hasAttribute('data-toolbar-menu-trigger')) {
|
|
toggleMenu(element, true);
|
|
focusFirstMenuElement(element);
|
|
}
|
|
// If popover trigger.
|
|
if (element.hasAttribute('data-toolbar-popover-control')) {
|
|
openPopover(element);
|
|
focusFirstPopoverElement(element);
|
|
}
|
|
};
|
|
|
|
const closeInteractiveElement = (element) => {
|
|
// If menu button.
|
|
if (element.hasAttribute('data-toolbar-menu-trigger')) {
|
|
if (element.getAttribute('aria-expanded') === 'false') {
|
|
closeNonInteractiveElement(element);
|
|
} else {
|
|
toggleMenu(element, false);
|
|
focusFirstMenuElement(element);
|
|
}
|
|
}
|
|
// If popover trigger.
|
|
if (element.hasAttribute('data-toolbar-popover-control')) {
|
|
openPopover(element);
|
|
focusLastPopoverElement(element);
|
|
}
|
|
};
|
|
|
|
const arrowsSideControl = ({ key, target }) => {
|
|
if (
|
|
(key === 'ArrowRight' && !IS_RTL) ||
|
|
(key === 'ArrowLeft' && IS_RTL)
|
|
) {
|
|
if (isInteractive(target)) {
|
|
openInteractiveElement(target);
|
|
// If also we want to care about expand button.
|
|
if (
|
|
target.getAttribute('aria-controls') === 'admin-toolbar' &&
|
|
target.getAttribute('aria-expanded') === 'false'
|
|
) {
|
|
target.click();
|
|
}
|
|
}
|
|
} else if (
|
|
(key === 'ArrowRight' && IS_RTL) ||
|
|
(key === 'ArrowLeft' && !IS_RTL)
|
|
) {
|
|
if (isInteractive(target)) {
|
|
closeInteractiveElement(target);
|
|
|
|
// If also we want to care about expand button.
|
|
if (
|
|
target.getAttribute('aria-controls') === 'admin-toolbar' &&
|
|
target.getAttribute('aria-expanded') !== 'false'
|
|
) {
|
|
target.click();
|
|
}
|
|
} else {
|
|
closeNonInteractiveElement(target);
|
|
}
|
|
}
|
|
};
|
|
|
|
const arrowsDirectionControl = ({ key, target }) => {
|
|
const focusableElements = focusable(getFocusableGroup(target));
|
|
if (key === 'ArrowUp') {
|
|
focusPreviousInGroup(focusableElements, target);
|
|
} else if (key === 'ArrowDown') {
|
|
focusNextInGroup(focusableElements, target);
|
|
}
|
|
};
|
|
|
|
sidebar.addEventListener('keydown', (e) => {
|
|
switch (e.key) {
|
|
case 'Escape':
|
|
closePopover(e.target);
|
|
focusClosestPopoverTrigger(e.target);
|
|
break;
|
|
|
|
case 'ArrowLeft':
|
|
case 'ArrowRight':
|
|
e.preventDefault();
|
|
arrowsSideControl(e);
|
|
break;
|
|
case 'ArrowDown':
|
|
case 'ArrowUp':
|
|
e.preventDefault();
|
|
arrowsDirectionControl(e);
|
|
break;
|
|
case 'Home':
|
|
e.preventDefault();
|
|
focusFirstInGroup(getFocusableGroup(e.target));
|
|
break;
|
|
case 'End':
|
|
e.preventDefault();
|
|
focusLastInGroup(getFocusableGroup(e.target));
|
|
break;
|
|
default:
|
|
checkChar(e);
|
|
break;
|
|
}
|
|
});
|
|
},
|
|
);
|
|
},
|
|
};
|
|
})(Drupal, once, window.tabbable);
|