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/0button label. Default"". Supplied positionally through thebutton/2element:on_click- atom event name or zero-arity function called when the button is activated. Defaultnil:variant- visual style atom::default(default),:primary,:success,:warning,:error. Any value other than:defaultis also prepended to the theme classes.:typeis accepted as an alias by the element, and:button_typeis accepted directly bymount/1:disabled-boolean/0. Defaultfalse. A disabled button consumes interaction without firing:on_clickand gains the:disabledtheme class; the element also drops:on_clickentirely:compact-boolean/0. Defaultfalse. Renders the label row only, without the highlight and shadow rows:style-map/0of style overrides applied on top of theme defaults. Default%{}:class- theme class atom or list of them, reachingmount/1as:classes. Default[]:focused-boolean/0initial focus flag. Defaultfalse:app_module- module supplying a per-app theme, passed by the renderer as:__app_module__. Defaultnil
: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.
Callback implementation for Drafter.Widget.handle_event/2.
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
@type variant() :: :default | :primary | :success | :warning | :error
Functions
@spec component_tag() :: :button
The registry tag for this widget.
iex> Drafter.Widget.Button.component_tag()
:button
@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
@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
Callback implementation for Drafter.Widget.handle_event/2.
@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}
@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.
@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]
@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
@spec render(t() | Drafter.Widget.props(), Drafter.Widget.rect()) :: [ Drafter.Draw.Strip.t() ]
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.
Callback implementation for Drafter.Widget.unmount/1.
@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]}
@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.