Drafter.Widget.Button (drafter v0.3.1)

Copy Markdown View Source

A clickable button widget that triggers a callback when pressed or activated via keyboard.

The button renders with a 3-line layout: a top border highlight, a centred label, and a bottom shadow. Visual state changes (hover, active, focused, disabled) are reflected through colour adjustments.

Component tag

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

button(text, opts)

The positional argument becomes :text. from_component_opts/2 wraps :on_click with Drafter.Widget.Callback, so it may be given as an atom event name, and drops it entirely when disabled: true.

Options

  • :text - String.t/0 button label. Default "". Supplied positionally through the button/2 element
  • :on_click - atom event name or zero-arity function called when the button is activated. Default nil
  • :variant - visual style atom: :default (default), :primary, :success, :warning, :error. Any value other than :default is also prepended to the theme classes. :type is accepted as an alias by the element, and :button_type is accepted directly by mount/1
  • :disabled - boolean/0. Default false. A disabled button consumes interaction without firing :on_click and gains the :disabled theme class; the element also drops :on_click entirely
  • :compact - boolean/0. Default false. Renders the label row only, without the highlight and shadow rows
  • :style - map/0 of style overrides applied on top of theme defaults. Default %{}
  • :class - theme class atom or list of them, reaching mount/1 as :classes. Default []
  • :focused - boolean/0 initial focus flag. Default false
  • :app_module - module supplying a per-app theme, passed by the renderer as :__app_module__. Default nil

:active and :hovered are state the widget owns; mount/1 always starts them at false and ignores props of those names. Every other option, :focused included, is live-updatable through update/2.

Widget value

Drafter.get_widget_value/1 returns the button's :text as a String.t/0, because the value extractor reads the :text field. Activation itself is reported through :on_click.

Key bindings and events

:enter and :" " activate the button; every other key bubbles. A mouse release anywhere in the rect activates it. Activation sets :active, fires :on_click, and schedules a :deactivate message to the owning process 200 ms later, which clears :active again. handle_custom_event/2 also accepts :activate, {:mouse, %{type: :press}}, :hover and :unhover.

Usage

button("Submit", on_click: fn -> :submit end, variant: :primary)

Summary

Functions

The registry tag for this widget.

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

Handles the button's out-of-band messages.

Activates the button on :enter or :" "; bubbles every other key.

Activates the button on mouse release, wherever in the rect it lands.

Builds the button state from props.

1 when opts[:compact] is true, otherwise 3.

Draws the button into rect.

Callback implementation for Drafter.Widget.unmount/1.

Folds fresh props into state.

Returns mount_props unchanged, so a re-render passes every option through to update/2.

Types

action()

@type action() ::
  {:pop, term()}
  | {:push, module(), map()}
  | {:replace, module(), map()}
  | {:app_callback, atom(), term()}

t()

@type t() :: %Drafter.Widget.Button{
  active: boolean(),
  app_module: module() | nil,
  button_type: variant(),
  classes: [atom()],
  compact: boolean(),
  disabled: boolean(),
  focused: boolean(),
  hovered: boolean(),
  on_click: (-> any()) | nil,
  style: map(),
  text: String.t()
}

variant()

@type variant() :: :default | :primary | :success | :warning | :error

Functions

component_tag()

@spec component_tag() :: :button

The registry tag for this widget.

iex> Drafter.Widget.Button.component_tag()
:button

focused(state)

from_component_opts(text, opts)

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

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

text is the positional argument. The variant is read from :variant, falling back to :type and then :default, and is emitted as :button_type. :class is normalised into :classes. :on_click is wrapped by Drafter.Widget.Callback.wrap_0/1, and is forced to nil when disabled: true.

iex> props = Drafter.Widget.Button.from_component_opts("Go", type: :success, on_click: :go)
iex> {props.text, props.button_type, props.disabled, props.compact, props.classes}
{"Go", :success, false, false, []}

iex> Drafter.Widget.Button.from_component_opts("Go", disabled: true, on_click: :go).on_click
nil

handle_custom_event(event, state)

@spec handle_custom_event(term(), t() | Drafter.Widget.props()) ::
  {:ok, t()} | {:ok, t(), [action()]} | {:bubble, t()}

Handles the button's out-of-band messages.

:activate and {:mouse, %{type: :press}} activate the button unless it is disabled. :deactivate clears :active, :hover sets :hovered and :unhover clears it, all returning {:ok, state}. Anything else returns {:bubble, state}, including :activate on a disabled button.

iex> b = Drafter.Widget.Button.mount(%{text: "Go"})
iex> {:ok, hovered} = Drafter.Widget.Button.handle_custom_event(:hover, b)
iex> hovered.hovered
true

iex> b = Drafter.Widget.Button.mount(%{text: "Go", disabled: true})
iex> Drafter.Widget.Button.handle_custom_event(:activate, b) |> elem(0)
:bubble

handle_event(event, state)

Callback implementation for Drafter.Widget.handle_event/2.

handle_key(key, state)

@spec handle_key(Drafter.Widget.key(), t() | Drafter.Widget.props()) ::
  {:ok, t()} | {:ok, t(), [action()]} | {:bubble, t()}

Activates the button on :enter or :" "; bubbles every other key.

A disabled button still consumes :enter and :" ", returning {:ok, state} without firing :on_click.

iex> b = Drafter.Widget.Button.mount(%{text: "Go"})
iex> {tag, ^b} = Drafter.Widget.Button.handle_key(:tab, b)
iex> tag
:bubble

iex> b = Drafter.Widget.Button.mount(%{text: "Go", disabled: true})
iex> {tag, unchanged} = Drafter.Widget.Button.handle_key(:enter, b)
iex> {tag, unchanged.active}
{:ok, false}

handle_mouse_up(x, y, state)

@spec handle_mouse_up(integer(), integer(), t() | Drafter.Widget.props()) ::
  {:ok, t()} | {:ok, t(), [action()]}

Activates the button on mouse release, wherever in the rect it lands.

Returns {:ok, state} unchanged when :disabled, otherwise {:ok, active_state, actions}. Accepts a raw props map as state.

mount(props)

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

Builds the button state from props.

A :variant other than :default is prepended to :classes, and :disabled prepends :disabled on top of that. :active and :hovered always start at false.

iex> b = Drafter.Widget.Button.mount(%{text: "Save", variant: :primary})
iex> {b.text, b.button_type, b.classes, b.disabled, b.compact}
{"Save", :primary, [:primary], false, false}

iex> b = Drafter.Widget.Button.mount(%{})
iex> {b.text, b.button_type, b.classes, b.active, b.hovered, b.focused}
{"", :default, [], false, false, false}

iex> Drafter.Widget.Button.mount(%{variant: :error, disabled: true}).classes
[:disabled, :error]

preferred_height(args, opts)

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

1 when opts[:compact] is true, otherwise 3.

iex> Drafter.Widget.Button.preferred_height("Go", [])
3

iex> Drafter.Widget.Button.preferred_height("Go", compact: true)
1

render(state, rect)

Draws the button into rect.

Accepts either a t/0 or a raw props map, which is mounted first. Produces one strip when :compact is set and three otherwise, then centres those rows vertically in rect.height, truncating from the bottom when the rect is shorter. A label wider than rect.width is cut, not wrapped.

unmount(state)

Callback implementation for Drafter.Widget.unmount/1.

update(props, state)

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

Folds fresh props into state.

Re-reads :text, :style, :focused, :on_click, :variant (or :button_type), :classes, :app_module, :disabled and :compact, then rebuilds the theme class list from the variant and disabled flag. :active and :hovered are left as they are.

iex> b = Drafter.Widget.Button.mount(%{text: "Save"})
iex> updated = Drafter.Widget.Button.update(%{text: "Saved", disabled: true}, b)
iex> {updated.text, updated.disabled, updated.classes}
{"Saved", true, [:disabled]}

update_props_from_mount(mount_props, existing_state, opts)

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

Returns mount_props unchanged, so a re-render passes every option through to update/2.