# Menubar — accessible headless horizontal menu bar
#
# A horizontal bar of menu triggers. Each trigger opens a dropdown menu.
# Arrow keys navigate between triggers; open menus close when moving
# to the next trigger. Ships zero CSS.
#
# Usage:
#   Menubar
#     div $menu: "file"
#       div $item: "new", "New"
#       div $item: "open", "Open"
#       div $item: "save", "Save"
#     div $menu: "edit"
#       div $item: "undo", "Undo"
#       div $item: "redo", "Redo"

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

  activeMenu := null
  highlightedIndex := -1

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

  _menuItemsFor: (menu) ->
    return [] unless menu
    Array.from(menu.querySelectorAll('[data-item]') or [])

  _openMenu: (menuId) ->
    return if @disabled
    activeMenu = menuId
    highlightedIndex = 0
    requestAnimationFrame =>
      @_position(menuId)
      @_root?.querySelector("[data-menu-list=\"#{menuId}\"] [role=\"menuitem\"]")?.focus()

  _closeMenu: ->
    activeMenu = null
    highlightedIndex = -1

  _position: (menuId) ->
    ARIA.positionBelow @_root?.querySelector("[data-menu-trigger=\"#{menuId}\"]"),
                       @_root?.querySelector("[data-menu-list=\"#{menuId}\"]"), 2, false

  selectItem: (menuId, itemId) ->
    @emit 'select', { menu: menuId, item: itemId }
    @_closeMenu()
    @_root?.querySelector("[data-menu-trigger=\"#{menuId}\"]")?.focus()

  _onBarKeydown: (e) ->
    triggers = @_root?.querySelectorAll('[data-menu-trigger]')
    return unless triggers?.length
    focused = Array.from(triggers).indexOf(document.activeElement)
    return if focused < 0
    len = triggers.length
    switch e.key
      when 'ArrowRight'
        e.preventDefault()
        next = (focused + 1) %% len
        triggers[next]?.focus()
        if activeMenu
          @_openMenu(triggers[next]?.dataset.menuTrigger)
      when 'ArrowLeft'
        e.preventDefault()
        prev = (focused - 1) %% len
        triggers[prev]?.focus()
        if activeMenu
          @_openMenu(triggers[prev]?.dataset.menuTrigger)
      when 'ArrowDown', 'Enter', ' '
        e.preventDefault()
        menuId = triggers[focused]?.dataset.menuTrigger
        @_openMenu(menuId) if menuId
      when 'Escape'
        @_closeMenu()

  _onMenuKeydown: (e, menuId, menuItems) ->
    len = menuItems.length
    return unless len
    switch e.key
      when 'ArrowDown'
        e.preventDefault()
        highlightedIndex = (highlightedIndex + 1) %% len
        @_root?.querySelector("[data-menu-list=\"#{menuId}\"]")?.querySelectorAll('[role="menuitem"]')[highlightedIndex]?.focus()
      when 'ArrowUp'
        e.preventDefault()
        highlightedIndex = (highlightedIndex - 1) %% len
        @_root?.querySelector("[data-menu-list=\"#{menuId}\"]")?.querySelectorAll('[role="menuitem"]')[highlightedIndex]?.focus()
      when 'Enter', ' '
        e.preventDefault()
        item = menuItems[highlightedIndex]
        @selectItem(menuId, item?.dataset.item) if item
      when 'Escape', 'Tab'
        e.preventDefault() if e.key is 'Escape'
        @_closeMenu()
        @_root?.querySelector("[data-menu-trigger=\"#{menuId}\"]")?.focus()
      when 'ArrowRight'
        e.preventDefault()
        @_closeMenu()
        triggers = @_root?.querySelectorAll('[data-menu-trigger]')
        focused = Array.from(triggers).findIndex (t) -> t.dataset.menuTrigger is menuId
        next = (focused + 1) %% triggers.length
        triggers[next]?.focus()
        @_openMenu(triggers[next]?.dataset.menuTrigger)
      when 'ArrowLeft'
        e.preventDefault()
        @_closeMenu()
        triggers = @_root?.querySelectorAll('[data-menu-trigger]')
        focused = Array.from(triggers).findIndex (t) -> t.dataset.menuTrigger is menuId
        prev = (focused - 1) %% triggers.length
        triggers[prev]?.focus()
        @_openMenu(triggers[prev]?.dataset.menuTrigger)

  ~>
    if activeMenu
      onDown   = (e) => @_closeMenu() unless @_root?.contains(e.target)
      onScroll = => @_position(activeMenu)
      document.addEventListener 'mousedown', onDown
      window.addEventListener   'scroll',    onScroll, true
      return ->
        document.removeEventListener 'mousedown', onDown
        window.removeEventListener   'scroll',    onScroll, true

  render
    div ref: "_root", role: "menubar", $disabled: @disabled?!

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

      for menu in _menus
        button role: "menuitem", tabindex: "0"
          "data-menu-trigger": menu.dataset.menu
          aria-haspopup: "menu"
          aria-expanded: activeMenu is menu.dataset.menu
          $open: (activeMenu is menu.dataset.menu)?!
          @click: (=> if activeMenu is menu.dataset.menu then @_closeMenu() else @_openMenu(menu.dataset.menu))
          @keydown: @_onBarKeydown
          = menu.dataset.menu

        if activeMenu is menu.dataset.menu
          div role: "menu", $open: true, style: "position:fixed"
            "data-menu-list": menu.dataset.menu
            @keydown: (e) => @_onMenuKeydown(e, menu.dataset.menu, @_menuItemsFor(menu))
            for item, idx in @_menuItemsFor(menu)
              div role: "menuitem", tabindex: "-1"
                $highlighted: (idx is highlightedIndex)?!
                $value: item.dataset.item
                @click: (=> @selectItem(menu.dataset.menu, item.dataset.item))
                @mouseenter: (=> highlightedIndex = idx)
                = item.textContent
