# 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.
# Ships zero CSS.
#
# Usage:
#   Menu
#     span "Actions"
#     div $item: "edit", "Edit"
#     div $item: "delete", "Delete"
#     div $item: "archive", "Archive"

export Menu = component
  @disabled := false

  open := false
  highlightedIndex := -1
  typeaheadBuffer := ''
  typeaheadTimer := null

  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 =>
      @_position()
      @_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()

  _position: ->
    return unless @_trigger and @_list
    tr = @_trigger.getBoundingClientRect()
    @_list.style.position = 'fixed'
    @_list.style.left = "#{tr.left}px"
    @_list.style.top = "#{tr.bottom + 4}px"
    @_list.style.minWidth = "#{tr.width}px"
    fl = @_list.getBoundingClientRect()
    if fl.bottom > window.innerHeight
      @_list.style.top = "#{tr.top - fl.height - 4}px"
    if fl.right > window.innerWidth
      @_list.style.left = "#{window.innerWidth - fl.width - 4}px"
    @_list.style.visibility = 'visible'

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

  onMenuKeydown: (e) ->
    len = items.length
    return unless len

    switch e.key
      when 'ArrowDown'
        e.preventDefault()
        highlightedIndex = (highlightedIndex + 1) %% len
        @_list?.querySelectorAll('[role="menuitem"]')[highlightedIndex]?.focus()
      when 'ArrowUp'
        e.preventDefault()
        highlightedIndex = (highlightedIndex - 1) %% len
        @_list?.querySelectorAll('[role="menuitem"]')[highlightedIndex]?.focus()
      when 'Home'
        e.preventDefault()
        highlightedIndex = 0
        @_list?.querySelectorAll('[role="menuitem"]')[0]?.focus()
      when 'End'
        e.preventDefault()
        highlightedIndex = len - 1
        @_list?.querySelectorAll('[role="menuitem"]')[len - 1]?.focus()
      when 'Enter', ' '
        e.preventDefault()
        @selectIndex(highlightedIndex)
      when 'Escape'
        e.preventDefault()
        @close()
      when 'Tab'
        @close()
      else
        if e.key.length is 1
          @_typeahead(e.key)

  ~>
    if open
      onDown = (e) =>
        unless @_trigger?.contains(e.target) or @_list?.contains(e.target)
          @close()
      document.addEventListener 'mousedown', onDown
      return -> document.removeEventListener 'mousedown', onDown

  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

      if open
        div ref: "_list", role: "menu", $open: true, style: "position:fixed;visibility:hidden", @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
