DEV Community

Niti Agrawal
Niti Agrawal

Posted on

Enhancing Accessibility: Managing Keyboard Navigation in Modals and Dropdowns

Keyboard navigation is a critical aspect of web accessibility, especially for users relying on assistive technologies. One common challenge is ensuring that focus remains within a modal or dropdown when it is open. Here’s how you can implement focus and Escape key cycling functionality to create an accessible and user-friendly experience.

The Problem:
When a modal or dropdown is open:

Tab Navigation: The focus should cycle within the component. Pressing the Tab key on the last focusable element should move focus back to the first, and Shift + Tab on the first element should return to the last.
Escape to Close: Pressing the Escape key should close the modal or dropdown.
The Solution:
We can solve this with a generic approach that dynamically manages focusable elements within the container and ensures smooth keyboard interactions.

Code Implementation:

function getFocusableElements(container) {
  const focusableSelector = [
    'a[href]',
    'button:not([disabled])',
    'input:not([disabled]):not([type="hidden"])',
    'textarea:not([disabled])',
    'select:not([disabled])',
    '[tabindex]:not([tabindex="-1"])',
  ].join(', ');

  return Array.from(container.querySelectorAll(focusableSelector));
}

function enableFocusCycling(container, onCloseCallback) {
  let focusableElements = getFocusableElements(container);

  if (focusableElements.length === 0) {
    console.warn('No focusable elements found in the container');
    return;
  }

  const updateFocusableElements = () => {
    focusableElements = getFocusableElements(container);
  };

  const first = () => focusableElements[0];
  const last = () => focusableElements[focusableElements.length - 1];

  container.addEventListener('keydown', (event) => {
    if (event.key === 'Tab') {
      if (document.activeElement === last() && !event.shiftKey) {
        event.preventDefault();
        first().focus();
        console.log('Focus cycled to the first element:', first());
      } else if (document.activeElement === first() && event.shiftKey) {
        event.preventDefault();
        last().focus();
        console.log('Focus cycled to the last element:', last());
      }
    } else if (event.key === 'Escape') {
      event.preventDefault();
      if (onCloseCallback) onCloseCallback();
      console.log('Container closed via Escape key');
    }
  });

  // Update focusable elements dynamically if the content changes
  new MutationObserver(updateFocusableElements).observe(container, {
    childList: true,
    subtree: true,
  });
}

// Usage example
const container = document.querySelector('#myDiv');
enableFocusCycling(container, () => {
  container.style.display = 'none'; // Replace with your hide logic
});

Enter fullscreen mode Exit fullscreen mode

Key Features:
Focus Cycling: Ensures the Tab key keeps focus within the modal or dropdown.
Escape Key Support: Adds a mechanism to close the modal or dropdown using the Escape key.
Dynamic Content Handling: Uses MutationObserver to update focusable elements when the container’s content changes.
Error Handling: Provides warnings for edge cases, such as no focusable elements in the container.
Accessibility Best Practices:
ARIA Attributes: Add proper ARIA roles (role="dialog", aria-modal="true", etc.) for better screen reader support.
Keyboard Testing: Test across browsers to ensure consistent behavior.
Graceful Degradation: Ensure functionality works even if JavaScript is disabled.
Final Thoughts:
This solution is a robust way to enhance accessibility for modals and dropdowns. By implementing seamless keyboard navigation, you not only meet accessibility standards but also improve the overall user experience for everyone.

Top comments (0)