NETFRAME_DOCS // DROPDOWN
GITHUB

Dropdown

Absolutely-positioned menus anchored to a trigger. Toggle visibility with aria-hidden or .nf-hidden on the menu.

Basic

OPEN DROPDOWN

<div class="nf-dropdown">
<button class="nf-button nf-button-ghost nf-dropdown-trigger" aria-expanded="true" aria-haspopup="menu">OPTIONS</button>
<ul class="nf-dropdown-menu" role="menu">
  <li role="none"><button class="nf-dropdown-item" role="menuitem">SCAN_NODE</button></li>
  <li role="none"><button class="nf-dropdown-item" role="menuitem">PURGE_CACHE</button></li>
  <li role="none"><button class="nf-dropdown-item" role="menuitem" aria-disabled="true">SYNC_DATA</button></li>
</ul>
</div>

With dividers and labels

GROUPED MENU

<div class="nf-dropdown">
<button class="nf-button nf-button-primary nf-dropdown-trigger" aria-expanded="true">NODE_ACTIONS</button>
<ul class="nf-dropdown-menu" role="menu">
  <li class="nf-dropdown-label" role="none">DIAGNOSTICS</li>
  <li role="none"><button class="nf-dropdown-item" role="menuitem">RUN_SCAN</button></li>
  <li role="none"><button class="nf-dropdown-item" role="menuitem">VIEW_LOGS</button></li>
  <li class="nf-dropdown-divider" role="separator"></li>
  <li role="none"><button class="nf-dropdown-item nf-dropdown-item-danger" role="menuitem">TERMINATE</button></li>
</ul>
</div>

Right-aligned

Add .nf-dropdown-menu-right to align the menu to the right edge of the trigger:

<ul class="nf-dropdown-menu nf-dropdown-menu-right" role="menu">
  ...
</ul>

JavaScript

document.querySelectorAll('.nf-dropdown').forEach((dropdown) => {
  const trigger = dropdown.querySelector('.nf-dropdown-trigger');
  const menu    = dropdown.querySelector('.nf-dropdown-menu');
  const items   = () => [...menu.querySelectorAll('.nf-dropdown-item:not([aria-disabled="true"]):not(:disabled)')];

  // The CSS hides the menu only for aria-hidden="true" (or .nf-hidden),
  // so always write the literal string rather than toggling the attribute.
  function setOpen(open, { focusTrigger = false, focusItem = open } = {}) {
    trigger.setAttribute('aria-expanded', String(open));
    menu.setAttribute('aria-hidden', String(!open));
    if (open && focusItem) items()[0]?.focus();
    else if (focusTrigger) trigger.focus();
  }

  // Synchronize initial ARIA state without moving focus on page load.
  setOpen(trigger.getAttribute('aria-expanded') === 'true', { focusItem: false });

  trigger.addEventListener('click', (e) => {
    e.stopPropagation();
    setOpen(trigger.getAttribute('aria-expanded') !== 'true');
  });

  // Arrow keys move between items; Escape closes and returns focus.
  menu.addEventListener('keydown', (e) => {
    const list = items();
    const i = list.indexOf(document.activeElement);
    if (e.key === 'ArrowDown') { e.preventDefault(); list[(i + 1) % list.length]?.focus(); }
    if (e.key === 'ArrowUp')   { e.preventDefault(); list[(i - 1 + list.length) % list.length]?.focus(); }
    if (e.key === 'Escape')    { setOpen(false, { focusTrigger: true }); }
  });

  // Close on outside click
  document.addEventListener('click', (e) => {
    if (!dropdown.contains(e.target)) setOpen(false);
  });
});

Class reference

Class Description
nf-dropdown position: relative wrapper
nf-dropdown-trigger Button with caret indicator
nf-dropdown-menu Absolute menu panel — hide with aria-hidden="true" or nf-hidden
nf-dropdown-menu-right Right-aligns the menu
nf-dropdown-item Menu row — button or anchor
nf-dropdown-item-danger Error color variant
nf-dropdown-divider <li> separator between item groups
nf-dropdown-label <li> group header — non-interactive