# Menu — accessible headless dropdown menu
#
# Keyboard: ArrowDown/Up to navigate, Enter/Space to select, Escape to close,
# Home/End for first/last. Exposes $open on menu, $highlighted on items.
# Uses native `popover="auto"` + anchor positioning. Ships zero CSS.
#
# Usage:
#   Menu
#     span "Actions"
#     div $item: "edit", "Edit"
#     div $item: "delete", "Delete"
#     div $item: "archive", "Archive"

export Menu = component
  @disabled:: boolean := false

  open := false
  highlightedIndex := -1
  typeaheadBuffer := ''
  typeaheadTimer := null
  _id =! "menu-#{Math.random().toString(36).slice(2, 8)}"

  items ~=
    return [] unless @_slot
    Array.from(@_slot.querySelectorAll('[data-item]') or [])

  triggerLabel ~=
    return '' unless @_slot
    el = @_slot.querySelector(':not([data-item])')
    el?.textContent?.trim() or ''

  toggle: ->
    return if @disabled
    if open then @close() else @openMenu()

  openMenu: ->
    open = true
    highlightedIndex = 0
    requestAnimationFrame => @_list?.querySelectorAll('[role="menuitem"]')[0]?.focus()

  close: ->
    open = false
    highlightedIndex = -1
    @_trigger?.focus()

  selectIndex: (idx) ->
    item = items[idx]
    return unless item
    return if item.dataset.disabled?
    role = item.getAttribute('role')
    if role is 'menuitemcheckbox'
      checked = item.getAttribute('aria-checked') is 'true'
      item.setAttribute 'aria-checked', not checked
      @emit 'select', { id: item.dataset.item, checked: not checked }
    else if role is 'menuitemradio'
      group = item.closest('[data-radio-group]')
      if group
        group.querySelectorAll('[role="menuitemradio"]').forEach (r) ->
          r.setAttribute 'aria-checked', false
      item.setAttribute 'aria-checked', true
      @emit 'select', { id: item.dataset.item, value: item.dataset.item }
    else
      @emit 'select', item.dataset.item
      @close()

  _typeahead: (char) ->
    clearTimeout typeaheadTimer if typeaheadTimer
    typeaheadBuffer += char.toLowerCase()
    typeaheadTimer = setTimeout (-> typeaheadBuffer = ''), 500
    idx = items.findIndex (o) -> o.textContent.trim().toLowerCase().startsWith(typeaheadBuffer)
    if idx >= 0
      highlightedIndex = idx
      @_list?.querySelectorAll('[role="menuitem"]')[idx]?.focus()

  _applyPlacement: ->
    ARIA.position @_trigger, @_list, placement: 'bottom start', offset: 4

  onTriggerKeydown: (e) ->
    return if @disabled
    if e.key in ['ArrowDown', 'Enter', ' ']
      e.preventDefault()
      @openMenu()

  _focusItem: (idx) ->
    highlightedIndex = idx
    @_list?.querySelectorAll('[role="menuitem"]')[idx]?.focus()

  onMenuKeydown: (e) ->
    len = items.length
    return unless len
    ARIA.listNav e,
      next:    => @_focusItem((highlightedIndex + 1) %% len)
      prev:    => @_focusItem((highlightedIndex - 1) %% len)
      first:   => @_focusItem(0)
      last:    => @_focusItem(len - 1)
      select:  => @selectIndex(highlightedIndex)
      dismiss: => @close()
      tab:     => @close()
      char:    => @_typeahead(e.key)

  ~>
    if @_list
      @_list.id = _id
      @_list.setAttribute 'popover', 'auto'
      @_applyPlacement()
    if @_trigger
      @_trigger.setAttribute 'aria-controls', _id
    ARIA.bindPopover open, (=> @_list), ((isOpen) => open = isOpen), (=> @_trigger)

  render
    .
      button ref: "_trigger"
        aria-haspopup: "menu"
        aria-expanded: !!open
        $open: open?!
        $disabled: @disabled?!
        disabled: @disabled
        @click: @toggle
        @keydown: @onTriggerKeydown
        triggerLabel

      . ref: "_slot", style: "display:none"
        slot

      div ref: "_list", role: "menu", $open: open?!, style: "position:fixed;margin:0;inset:auto", @keydown: @onMenuKeydown
        for item, idx in items
          div role: item.getAttribute('role') or 'menuitem'
            tabindex: "-1"
            aria-checked: item.getAttribute('aria-checked')?!
            $highlighted: (idx is highlightedIndex)?!
            $disabled: item.dataset.disabled?!
            @click: (=> @selectIndex(idx))
            @mouseenter: (=> highlightedIndex = idx)
            = item.textContent
