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

An inline hyperlink widget that opens a URL in the system browser when activated.

Renders as underlined text. When focused or hovered the text is wrapped in square
brackets (`[label]`) for visibility. Activating via Enter or mouse click invokes the
platform's default browser opener (`open` on macOS, `xdg-open` on Linux,
`cmd /c start` on Windows).

## Component tag

Tag `:link`, built by `Drafter.App` as `{:link, text, opts}`:

    link(text, opts)
    link(text, url)

The positional argument becomes `:text`, falling back to `opts[:text]` when
`nil`. Passing a binary second argument is shorthand for `[url: binary]`.

## Options

  * `:text` - `t:String.t/0` display text. Default `nil`, in which case the `:url`
    is used as the label. Supplied positionally through the `link/2` element,
    falling back to `opts[:text]` when the positional value is `nil`
  * `:url` - `t:String.t/0` to open. Default `nil`; without it, activating the
    link does nothing
  * `:tooltip` - stored on the widget state; never rendered. Default `nil`
  * `:style` - `t:map/0` of style overrides. Default `%{}`
  * `:class` - theme class atom or list of them, reaching `mount/1` as
    `:classes`. Default `[]`
  * `:app_module` - module supplying a per-app theme, passed by the renderer as
    `:__app_module__`. Default `nil`

`mount/1` always starts `:focused` and `:hovered` at `false` and ignores props of
those names. `update/2` re-reads every option, but through the component tree only
`:text`, `:url` and `:app_module` are re-applied on a re-render, making `:style`,
`:classes` and `:tooltip` effectively mount-only there.

## Widget value

`Drafter.get_widget_value/1` returns the link's `:text`, which is `nil` when the
label falls back to the URL.

## Usage

    link("Elixir website", url: "https://elixir-lang.org")

# `t`

```elixir
@type t() :: %Drafter.Widget.Link{
  app_module: module() | nil,
  classes: [atom()],
  focused: boolean(),
  hovered: boolean(),
  style: map(),
  text: String.t() | nil,
  tooltip: String.t() | nil,
  url: String.t() | nil
}
```

# `component_tag`

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

The registry tag for this widget.

    iex> Drafter.Widget.Link.component_tag()
    :link

# `focused`

# `from_component_opts`

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

Turns the `{:link, text, opts}` element into a props map for `mount/1`.

A `nil` positional `text` falls back to `opts[:text]`. `:class` is normalised into
`:classes` and `:__app_module__` becomes `:app_module`.

    iex> Drafter.Widget.Link.from_component_opts("Elixir", url: "https://elixir-lang.org")
    %{text: "Elixir", url: "https://elixir-lang.org", style: %{}, classes: [], tooltip: nil, app_module: nil}

# `handle_event`

```elixir
@spec handle_event(Drafter.Event.t() | atom(), t() | Drafter.Widget.props()) ::
  {:ok, t()} | {:noreply, t()}
```

Handles events directly instead of going through `Drafter.Widget.EventRouter`.

`{:key, :enter}` and a mouse release run the platform browser opener for `:url`
and return `{:ok, state}` unchanged; with no `:url` nothing is run. `{:focus}`
sets both `:focused` and `:hovered`, `{:blur}` clears both, and
`:hover`/`:unhover` move `:hovered` alone. Everything else, including `Space`,
returns `{:noreply, state}`.

    iex> l = Drafter.Widget.Link.mount(%{text: "Elixir", url: "https://elixir-lang.org"})
    iex> {:ok, focused} = Drafter.Widget.Link.handle_event({:focus}, l)
    iex> {focused.focused, focused.hovered}
    {true, true}

    iex> l = Drafter.Widget.Link.mount(%{text: "Elixir"})
    iex> Drafter.Widget.Link.handle_event({:key, :" "}, l) |> elem(0)
    :noreply

# `mount`

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

Builds the link state from `props`.

    iex> l = Drafter.Widget.Link.mount(%{text: "Elixir", url: "https://elixir-lang.org"})
    iex> {l.text, l.url, l.focused, l.hovered}
    {"Elixir", "https://elixir-lang.org", false, false}

    iex> l = Drafter.Widget.Link.mount(%{})
    iex> {l.text, l.url, l.style, l.classes, l.tooltip}
    {nil, nil, %{}, [], nil}

# `preferred_height`

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

Always `1`: the link occupies a single row.

# `render`

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

Draws the link as a single underlined strip.

Accepts either a `t:t/0` or a raw props map, which is mounted first. The label is
`:text`, falling back to `:url`, and is wrapped in square brackets while focused
or hovered. `rect` is not consulted, so a label wider than the rect is neither
cropped nor padded. While hovered a `:hover` class and while focused a `:focus`
class are added to the computed style.

# `unmount`

# `update`

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

Folds fresh props into `state`, re-reading `:text`, `:url`, `:style`, `:classes`,
`:app_module` and `:tooltip`. `:focused` and `:hovered` are left alone.

    iex> l = Drafter.Widget.Link.mount(%{text: "Old", url: "https://a.example"})
    iex> Drafter.Widget.Link.update(%{text: "New"}, l).url
    "https://a.example"

# `update_props_from_mount`

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

Narrows the props a re-render feeds to `update/2` to `:text`, `:url` and
`:app_module`, so `:style`, `:classes` and `:tooltip` stay as mounted.

---

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