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