All files / packages/theme-selector/src/dropdown state.ts

100% Statements 31/31
100% Branches 15/15
100% Functions 5/5
100% Lines 30/30

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79                                                                  26x   26x 44x     26x 20x   18x 18x 18x 18x 18x     26x 8x 8x 8x 8x 8x 8x 6x       26x 6x 6x   6x 3x 3x 3x 20x 20x   3x 1x       26x    
// SPDX-License-Identifier: MIT
/**
 * Dropdown state management
 */
 
import { setItemActiveState, setTabindexBatch } from './helpers.js';
 
export interface DropdownElements {
  dropdownMenu: HTMLElement;
  trigger: HTMLButtonElement;
  dropdown: HTMLElement;
  selectEl: HTMLSelectElement | null;
}
 
export interface DropdownState {
  currentIndex: number;
  menuItems: HTMLElement[];
}
 
export interface DropdownStateManager {
  updateAriaExpanded: (expanded: boolean) => void;
  focusMenuItem: (index: number) => void;
  closeDropdown: (options?: { restoreFocus?: boolean }) => void;
  toggleDropdown: (focusFirst?: boolean) => void;
}
 
/**
 * Creates dropdown state management functions.
 */
export function createDropdownStateManager(
  elements: DropdownElements,
  state: DropdownState
): DropdownStateManager {
  const { trigger, dropdown } = elements;
 
  const updateAriaExpanded = (expanded: boolean): void => {
    trigger.setAttribute('aria-expanded', String(expanded));
  };
 
  const focusMenuItem = (index: number): void => {
    if (index < 0 || index >= state.menuItems.length) return;
 
    const item = state.menuItems[index]!;
    setTabindexBatch(state.menuItems, '-1');
    item.setAttribute('tabindex', '0');
    item.focus();
    state.currentIndex = index;
  };
 
  const closeDropdown = (options: { restoreFocus?: boolean } = {}): void => {
    const { restoreFocus = true } = options;
    dropdown.classList.remove('is-active');
    updateAriaExpanded(false);
    setTabindexBatch(state.menuItems, '-1');
    state.currentIndex = -1;
    if (restoreFocus) {
      trigger.focus();
    }
  };
 
  const toggleDropdown = (focusFirst = false): void => {
    const isActive = dropdown.classList.toggle('is-active');
    updateAriaExpanded(isActive);
 
    if (!isActive) {
      state.currentIndex = -1;
      setTabindexBatch(state.menuItems, '-1');
      for (const menuItem of state.menuItems) {
        const isActiveItem = menuItem.classList.contains('is-active');
        setItemActiveState(menuItem, isActiveItem);
      }
    } else if (focusFirst && state.menuItems.length > 0) {
      focusMenuItem(0);
    }
  };
 
  return { updateAriaExpanded, focusMenuItem, closeDropdown, toggleDropdown };
}