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);
							 |