# `Drafter.Widget.TabbedContent`
[🔗](https://github.com/jaman/drafter/blob/main/lib/drafter/widget/tabbed_content.ex#L1)

Renders a bordered tabbed panel where each tab displays independent content.

Tabs are switched with `←`/`→` or by clicking the tab label. The active tab
label is wrapped in `[brackets]`; hovered tabs are highlighted. Tab content
can be a list of strings, a single `{:label, text}` tuple, or an
`{:option_list, items, opts}` tuple that embeds a fully interactive
`OptionList` widget inside the tab body.

An optional `:title` string is rendered in the top border, aligned according
to `:title_align`.

## Component tag

Tag `:tabbed_content`, built by `Drafter.App` as `{:tabbed_content, tabs, opts}`:

    tabbed_content(tabs, opts)

The positional `tabs` list is used when non-empty, falling back to
`opts[:tabs]`. `from_component_opts/2` wraps `:on_tab_change` with
`Drafter.Widget.Callback`, so it may be given as an atom event name. `:width`
defaults to the rect the parent allocated.

## Options

  * `:tabs` - list of tab descriptors. Default `[]`. Each is normalised into
    `%{id: id, label: label, content: list}`:
      * `"label"` — a string used as both id and label, with empty content
      * `{label, content}` — content that is a list is kept, a string or a tuple
        is wrapped in a one-element list
      * `%{id: id, label: label}` — content defaults to `[]`
      * `%{id: id, label: label, content: content}` — a tuple content is wrapped
        in a list
    Anything else raises `FunctionClauseError`.
  * `:active_tab` - `t:non_neg_integer/0` zero-based index of the initially
    active tab. Default `0`. Not bounds-checked at mount.
  * `:title` - `t:String.t/0` shown in the top border, or `nil`. Default `nil`.
  * `:title_align` - `:left | :center | :right`. Default `:left`.
  * `:width` - `t:pos_integer/0` explicit width in columns. Default `nil` when
    mounting directly, which makes `render/2` use the rect width; through the
    element it defaults to the width of `opts[:__rect__]`, itself defaulting to
    `%{width: 80}`.
  * `:on_tab_change` - the app callback name fired with the newly active tab's
    `:id` when the tab changes. Default `nil`. Through the element it is set to
    the one-argument function `Drafter.Widget.Callback.wrap_1/1` returns, which
    the widget passes on as a callback name rather than calling.
  * `:on_item_select` - one-arity function receiving the highlighted item when
    `enter` is pressed on a tab whose content is a plain list. Default `nil`.
    Read by `mount/1` only — the `tabbed_content/2` element does not forward it.
  * `:focused` - `t:boolean/0` read by `mount/1`. Default `false`.
  * `:height` - `t:pos_integer/0` read only by `preferred_height/2`, never by
    `mount/1`. Default `8`.

`update/2` accepts `:tabs`, `:active_tab`, `:on_tab_change`, `:on_item_select`,
`:title`, `:title_align` and `:width`, resetting the item highlight whenever the
active tab changes and remounting the embedded child widgets only when the number
of tabs changes. Through the component tree `update_props_from_mount/3` narrows
that to `:tabs`, `:title_align`, `:width` and `:classes` — so `:active_tab`,
`:title`, `:on_tab_change` and `:on_item_select` are mount-only and the tab the
user switched to survives a re-render.

## Key bindings

  * `left` / `right` - switch tabs, returning `{:noreply, state}` at either end
  * `up` / `down` - move the highlight inside the active tab, or forward the key
    to the tab's embedded widget
  * `enter` - call `:on_item_select` with the highlighted item, or forward the
    key to the tab's embedded widget
  * `tab` - ignored, returning `{:noreply, state}`

A mouse up on row `0` or `1` selects a tab, on row `3` or below goes to the
content, and anywhere else just focuses the widget. A mouse move on row `0` or
`1` sets `:hovered_tab`, and clears it elsewhere.

## Widget value

`Drafter.get_widget_value/1` returns `:active_tab`, the zero-based index of the
active tab, not its id.

## Usage

    tabbed_content(tabs: [
      %{id: :overview, label: "Overview", content: ["Line 1", "Line 2"]},
      %{id: :details,  label: "Details",  content: ["More info"]}
    ])

    tabbed_content(
      tabs: [{"Files", {:option_list, file_list, on_select: :file_selected}}],
      title: "Browser"
    )

# `t`

```elixir
@type t() :: %Drafter.Widget.TabbedContent{
  active_tab: non_neg_integer(),
  child_widgets: [term() | nil],
  focused: boolean(),
  highlighted_item: non_neg_integer(),
  hovered_tab: non_neg_integer() | nil,
  on_item_select: (term() -&gt; term()) | nil,
  on_tab_change: term(),
  tabs: [tab()],
  title: String.t() | nil,
  title_align: :left | :center | :right,
  width: pos_integer() | nil
}
```

# `tab`

```elixir
@type tab() :: %{id: term(), label: String.t(), content: list()}
```

# `component_tag`

```elixir
@spec component_tag() :: :tabbed_content
```

The component tag this widget registers under.

    iex> Drafter.Widget.TabbedContent.component_tag()
    :tabbed_content

# `focused`

# `from_component_opts`

```elixir
@spec from_component_opts(
  list() | nil,
  keyword()
) :: Drafter.Widget.props()
```

Builds the props map for a `{:tabbed_content, tabs, opts}` element.

`tabs` is used when it is a non-empty list, otherwise `opts[:tabs]`, defaulting
to `[]`; the descriptors are passed through as given and `mount/1` normalises
them. `:on_tab_change` goes through `Drafter.Widget.Callback.wrap_1/1`.
`:width` falls back to the width of `opts[:__rect__]`, itself defaulting to
`%{width: 80}`. `:on_item_select` is not forwarded and the emitted `:classes`
key is not read by `mount/1`.

    iex> props = Drafter.Widget.TabbedContent.from_component_opts(["One"], title: "Browser")
    iex> {props.tabs, props.active_tab, props.title, props.title_align, props.width}
    {["One"], 0, "Browser", :left, 80}

    iex> props = Drafter.Widget.TabbedContent.from_component_opts(nil, tabs: ["A"], on_tab_change: :switched)
    iex> {props.tabs, is_function(props.on_tab_change, 1)}
    {["A"], true}

# `handle_event`

```elixir
@spec handle_event(term(), t()) :: {:ok, t()} | {:ok, t(), list()} | {:noreply, t()}
```

Handles the panel's own events, replacing the dispatch `use Drafter.Widget`
would otherwise generate.

Recognised events are the key bindings and mouse handling listed in the module
doc, plus `{:focus}` and `{:blur}`; blurring also clears `:hovered_tab`. A tab
change resets `:highlighted_item` to `0` and notifies `:on_tab_change` with the
new tab's `:id`. Anything unrecognised returns `{:noreply, state}`.

    iex> state = Drafter.Widget.TabbedContent.mount(%{tabs: ["One", "Two"]})
    iex> {:ok, switched} = Drafter.Widget.TabbedContent.handle_event({:key, :right}, state)
    iex> switched.active_tab
    1

    iex> state = Drafter.Widget.TabbedContent.mount(%{tabs: ["One", "Two"]})
    iex> Drafter.Widget.TabbedContent.handle_event({:key, :left}, state) == {:noreply, state}
    true

    iex> state = Drafter.Widget.TabbedContent.mount(%{tabs: [{"One", ["a", "b"]}]})
    iex> {:ok, moved} = Drafter.Widget.TabbedContent.handle_event({:key, :down}, state)
    iex> moved.highlighted_item
    1

    iex> state = Drafter.Widget.TabbedContent.mount(%{tabs: ["One"]})
    iex> Drafter.Widget.TabbedContent.handle_event({:key, :tab}, state) == {:noreply, state}
    true

# `mount`

```elixir
@spec mount(Drafter.Widget.props()) :: t()
```

Builds the widget state from `props`.

Tabs are normalised into `%{id: _, label: _, content: _}` maps, and a tab whose
content is a single widget tuple has that widget mounted into `:child_widgets`
at the same index; every other tab gets `nil` there. `:hovered_tab` starts as
`nil` and `:highlighted_item` at `0`.

    iex> state = Drafter.Widget.TabbedContent.mount(%{tabs: ["One", {"Two", ["a", "b"]}]})
    iex> state.tabs
    [%{id: "One", label: "One", content: []}, %{id: "Two", label: "Two", content: ["a", "b"]}]

    iex> state = Drafter.Widget.TabbedContent.mount(%{tabs: ["One"]})
    iex> {state.active_tab, state.highlighted_item, state.hovered_tab, state.title_align, state.width}
    {0, 0, nil, :left, nil}

    iex> Drafter.Widget.TabbedContent.mount(%{tabs: ["One"]}).child_widgets
    [nil]

# `preferred_height`

```elixir
@spec preferred_height(
  term(),
  keyword()
) :: pos_integer()
```

The number of rows the element asks for: `opts[:height]`, default `8`.

    iex> Drafter.Widget.TabbedContent.preferred_height(nil, [])
    8

    iex> Drafter.Widget.TabbedContent.preferred_height(nil, height: 20)
    20

# `render`

```elixir
@spec render(t(), Drafter.Widget.rect()) :: [Drafter.Draw.Strip.t()]
```

Draws the panel into `rect`.

The layout is a title row, the tab bar, a separator, then the active tab's
content and the bottom border. The panel is `state.width` columns wide, falling
back to `rect.width` when that is `nil`.

# `unmount`

# `update`

```elixir
@spec update(Drafter.Widget.props(), t()) :: t()
```

Replaces the state fields named in `props`, keeping the current value for any key
that is absent.

New `:tabs` are normalised. `:highlighted_item` resets to `0` when
`:active_tab` changes and is otherwise kept. The embedded child widgets are
remounted only when the number of tabs changes, so replacing a tab's widget
content in place does not take effect.

    iex> state = Drafter.Widget.TabbedContent.mount(%{tabs: ["One", "Two"]})
    iex> state = %{state | highlighted_item: 3}
    iex> updated = Drafter.Widget.TabbedContent.update(%{active_tab: 1}, state)
    iex> {updated.active_tab, updated.highlighted_item}
    {1, 0}

    iex> state = Drafter.Widget.TabbedContent.mount(%{tabs: ["One"]})
    iex> Drafter.Widget.TabbedContent.update(%{title: "Browser"}, state).title
    "Browser"

# `update_props_from_mount`

```elixir
@spec update_props_from_mount(Drafter.Widget.props(), term(), keyword()) ::
  Drafter.Widget.props()
```

Narrows a re-render to `:tabs`, `:title_align`, `:width` and `:classes`.

`:active_tab`, `:title`, `:on_tab_change` and `:on_item_select` are dropped, so
they are mount-only through the component tree and the tab the user switched to
survives a re-render.

    iex> props = Drafter.Widget.TabbedContent.from_component_opts(["One"], title: "Browser")
    iex> Drafter.Widget.TabbedContent.update_props_from_mount(props, %{}, []) |> Map.keys() |> Enum.sort()
    [:classes, :tabs, :title_align, :width]

---

*Consult [api-reference.md](api-reference.md) for complete listing*
