# Tabs — accessible headless tab widget
#
# Keyboard: ArrowLeft/Right (horizontal) or ArrowUp/Down (vertical) to navigate,
# Home/End for first/last. Manages focus via roving tabindex.
# Exposes $active on tabs and panels. Ships zero CSS.
#
# Props:
#   active       — currently active tab id (two-way bindable)
#   orientation  — 'horizontal' (default) or 'vertical'
#   activation   — 'automatic' (default, selects on focus) or 'manual' (Enter/Space to select)
#
# Usage:
#   Tabs active <=> currentTab
#     div $tab: "one", "Tab One"
#     div $tab: "two", "Tab Two"
#     div $panel: "one"
#       p "Content for tab one"
#     div $panel: "two"
#       p "Content for tab two"

export Tabs = component
  @active:: any := null
  @orientation:: "horizontal" | "vertical" := "horizontal"
  @activation:: "automatic" | "manual" := "automatic"
  _ready       := false
  _id =! "tabs-#{Math.random().toString(36).slice(2, 8)}"
  activationDirection := 'none'

  tabs ~=
    return [] unless _ready
    Array.from(@_content?.querySelectorAll('[data-tab]') or [])

  panels ~=
    return [] unless _ready
    Array.from(@_content?.querySelectorAll('[data-panel]') or [])

  mounted: ->
    _ready = true
    unless @active
      @active = tabs[0]?.dataset.tab

  ~>
    return unless _ready
    tabs.forEach (el) -> el.hidden = true
    panels.forEach (el) =>
      id = el.dataset.panel
      isActive = id is @active
      el.id = "#{_id}-panel-#{id}"
      el.setAttribute 'role', 'tabpanel'
      el.setAttribute 'aria-labelledby', "#{_id}-tab-#{id}"
      el.toggleAttribute 'hidden', not isActive
      el.toggleAttribute 'data-active', isActive

  _isDisabled: (el) -> el?.hasAttribute('data-disabled')

  select: (id) ->
    prev = @active
    horiz = @orientation is 'horizontal'
    if prev and id isnt prev
      oldTab = tabs.find (t) -> t.dataset.tab is prev
      newTab = tabs.find (t) -> t.dataset.tab is id
      if oldTab and newTab
        oldRect = oldTab.getBoundingClientRect()
        newRect = newTab.getBoundingClientRect()
        activationDirection = if horiz
          if newRect.left > oldRect.left then 'right' else 'left'
        else
          if newRect.top > oldRect.top then 'down' else 'up'
    @active = id
    @emit 'change', id

  _nextEnabled: (ids, from, dir) ->
    len = ids.length
    i = from
    loop len
      i = (i + dir) %% len
      tab = tabs.find (t) -> t.dataset.tab is ids[i]
      return ids[i] unless @_isDisabled(tab)
    ids[from]

  onKeydown: (e) ->
    ids = tabs.map (t) -> t.dataset.tab
    idx = ids.indexOf @active
    return if idx is -1
    move = (nextId) =>
      return unless nextId
      tabs.find((t) -> t.dataset.tab is nextId)?.focus()
      @select(nextId) if @activation is 'automatic'
    ARIA.rovingNav e, {
      next:   => move(@_nextEnabled(ids, idx,              1))
      prev:   => move(@_nextEnabled(ids, idx,             -1))
      first:  => move(@_nextEnabled(ids, ids.length - 1,   1))
      last:   => move(@_nextEnabled(ids, 0,               -1))
      select: => @select(ids[idx]) if @activation is 'manual'
    }, @orientation

  render
    .
      div role: "tablist", aria-orientation: @orientation, data-activation-direction: activationDirection, @keydown: @onKeydown
        for tab in tabs
          button role: "tab"
            id: "#{_id}-tab-#{tab.dataset.tab}"
            aria-selected: tab.dataset.tab is @active
            aria-controls: "#{_id}-panel-#{tab.dataset.tab}"
            aria-disabled: @_isDisabled(tab)?!
            tabindex: if @_isDisabled(tab) then '-1' else (tab.dataset.tab is @active ? '0' : '-1')
            $active: (tab.dataset.tab is @active)?!
            $disabled: @_isDisabled(tab)?!
            @click: (=> @select(tab.dataset.tab) unless @_isDisabled(tab))
            = tab.textContent

      . ref: "_content"
        slot
