defmodule BitstylesPhoenix.Component.Button do
use BitstylesPhoenix.Component
alias BitstylesPhoenix.Bitstyles
@moduledoc """
The button component.
"""
@link_attributes [:href, :navigate, :patch]
import BitstylesPhoenix.Component.Icon
@doc """
Renders anchor or button elements that look like a button — using the `a-button` classes.
- `href`, `navigate`, `patch` - if there’s one of these, you’ll get an anchor element, otherwise a button element. (See Phoenix.Component.link/1)
- `color` - specifies the color of the button, e.g `secondary`, `transparent`. Leave empty for default color. Introduced in Bitstyles 5.0.0.
- `shape` - specifies the color of the button, e.g `square`, `small`. Leave empty for default color. Introduced in Bitstyles 5.0.0.
- `variant` - specifies which visual variant of button you want, from those available in the CSS classes e.g. `ui`, `danger`. Deprecated in favor of `color` and `shape` in Bitstyles 5.0.0.
- `class` - Extra classes to pass to the badge. See `BitstylesPhoenix.Helper.classnames/1` for usage.
- `icon` - An icon name as string or a tuple with `{icon_name, icon_opts}` which is passed to `BitstylesPhoenix.Component.Icon.ui_icon/1` as
attributes. Additionally it is possible to pass `after: true` to the icon_opts, to make the icon appear after the button label instead of in
front of it.
All other attributes you pass are forwarded to [Phoenix.Component.link/1](https://hexdocs.pm/phoenix_live_view/Phoenix.Component.html#link/1) or on the button tag, if one of those is rendered.
See the [bitstyles button docs](https://bitcrowd.github.io/bitstyles/?path=/docs/ui-buttons-buttons--page) for available button variants.
See `BitstylesPhoenix.Helper.Button.ui_button/2` for more examples and options.
"""
story(
"Default button",
'''
iex> assigns = %{}
...> render ~H"""
...> <.ui_button>
...> Publish
...> </.ui_button>
...> """
''',
'''
"""
<button type="button" class="a-button">
Publish
</button>
"""
'''
)
story(
"Default link",
'''
iex> assigns = %{}
...> render ~H"""
...> <.ui_button href="/">
...> Publish
...> </.ui_button>
...> """
''',
'''
"""
<a href="/" class="a-button">
Publish
</a>
"""
'''
)
story(
"Default disabled link renders disabled button instead",
'''
iex> assigns = %{}
...> render ~H"""
...> <.ui_button href="/" disabled>
...> Publish
...> </.ui_button>
...> """
''',
'''
"""
<button type="button" class="a-button" disabled="disabled">
Publish
</button>
"""
'''
)
story(
"Default submit button",
'''
iex> assigns = %{}
...> render ~H"""
...> <.ui_button type="submit">
...> Save
...> </.ui_button>
...> """
''',
'''
"""
<button type="submit" class="a-button">
Save
</button>
"""
'''
)
story(
"Default submit button with custom classes",
'''
iex> assigns = %{}
...> render ~H"""
...> <.ui_button type="submit" class="foo bar">
...> Save
...> </.ui_button>
...> """
''',
'''
"""
<button type="submit" class="a-button foo bar">
Save
</button>
"""
'''
)
story(
"Secondary button",
'''
iex> assigns = %{}
...> render ~H"""
...> <.ui_button type="submit" color={:secondary}>
...> Save
...> </.ui_button>
...> """
''',
'''
"""
<button type="submit" class="a-button a-button--secondary">
Save
</button>
"""
'''
)
story(
"Small button",
'''
iex> assigns = %{}
...> render ~H"""
...> <.ui_button type="submit" shape={:small}>
...> Save
...> </.ui_button>
...> """
''',
'''
"""
<button type="submit" class="a-button a-button--small">
Save
</button>
"""
'''
)
story(
"Dangerous button",
'''
iex> assigns = %{}
...> render ~H"""
...> <.ui_button type="submit" color={:danger}>
...> Save
...> </.ui_button>
...> """
''',
'''
"""
<button type="submit" class="a-button a-button--danger">
Save
</button>
"""
'''
)
story(
"Button with an icon",
'''
iex> assigns = %{}
...> render ~H"""
...> <.ui_button type="submit" icon="plus">
...> Add
...> </.ui_button>
...> """
''',
'''
"""
<button type="submit" class="a-button">
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" class="a-icon a-button__icon" focusable="false" height="16" width="16">
<use xlink:href="#icon-plus">
</use>
</svg>
<span class="a-button__label">
Add
</span>
</button>
"""
''',
extra_html: """
<svg xmlns="http://www.w3.org/2000/svg" hidden aria-hidden="true">
<symbol id="icon-plus" viewBox="0 0 100 100">
<path d="M54.57,87.43V54.57H87.43a4.57,4.57,0,0,0,0-9.14H54.57V12.57a4.57,4.57,0,1,0-9.14,0V45.43H12.57a4.57,4.57,0,0,0,0,9.14H45.43V87.43a4.57,4.57,0,0,0,9.14,0Z"/>
</symbol>
</svg>
"""
)
story(
"Button with an icon after",
'''
iex> assigns = %{}
...> render ~H"""
...> <.ui_button href="/" icon={{"plus", after: true}}>
...> Add
...> </.ui_button>
...> """
''',
'''
"""
<a href="/" class="a-button">
<span class="a-button__label">
Add
</span>
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" class="a-icon a-button__icon" focusable="false" height="16" width="16">
<use xlink:href="#icon-plus">
</use>
</svg>
</a>
"""
''',
extra_html: """
<svg xmlns="http://www.w3.org/2000/svg" hidden aria-hidden="true">
<symbol id="icon-plus" viewBox="0 0 100 100">
<path d="M54.57,87.43V54.57H87.43a4.57,4.57,0,0,0,0-9.14H54.57V12.57a4.57,4.57,0,1,0-9.14,0V45.43H12.57a4.57,4.57,0,0,0,0,9.14H45.43V87.43a4.57,4.57,0,0,0,9.14,0Z"/>
</symbol>
</svg>
"""
)
story(
"Pass along attributes to Phoenix helpers",
'''
iex> assigns = %{}
...> render ~H"""
...> <.ui_button href="/admin/admin_accounts/id" data-confirm="Are you sure?">
...> Add
...> </.ui_button>
...> """
''',
'''
"""
<a href="/admin/admin_accounts/id" class="a-button" data-confirm="Are you sure?">
Add
</a>
"""
'''
)
story(
"Button with LiveView patch",
'''
iex> assigns = %{}
...> render ~H"""
...> <.ui_button patch="/foo">
...> Add
...> </.ui_button>
...> """
''',
'''
"""
<a href="/foo" data-phx-link=\"patch\" data-phx-link-state=\"push\" class="a-button">
Add
</a>
"""
'''
)
def ui_button(assigns) do
extra = assigns_to_attributes(assigns, [:icon, :class, :color, :shape, :variant])
assigns =
if Bitstyles.Version.version() >= {5, 0, 0} && assigns[:variant] do
IO.warn("Attribute `variant` is deprecated in ui_button/1! Change to `color` and `shape`")
assigns
|> assign(:color, variant_to_color(assigns[:variant]))
|> assign(:shape, variant_to_shape(assigns[:variant]))
else
assigns
end
classes =
if Bitstyles.Version.version() >= {5, 0, 0} do
color_and_shape_classes(assigns[:color], assigns[:shape])
else
variant_classes(assigns[:variant])
end
class = classnames(["a-button"] ++ classes ++ [assigns[:class]])
assigns =
assigns
|> assign(:extra, extra)
|> assign(:class, class)
~H"""
<.button_tag class={@class} {@extra}>
<%= if assigns[:icon] do %>
<.icon_with_label icon={@icon}>
<%= render_slot(@inner_block) %>
</.icon_with_label>
<% else %>
<%= render_slot(@inner_block) %>
<% end %>
</.button_tag>
"""
end
defp button_tag(assigns) do
assigns =
if assigns[:to] do
IO.warn("Attribute `to` is deprecated in ui_button/1! Change to `href`")
assign(assigns, :href, assigns.to)
else
assigns
end
extra = assigns_to_attributes(assigns, [:type])
assigns =
assigns
|> assign(
:link,
!assigns[:disabled] && Enum.any?(@link_attributes, &Map.has_key?(assigns, &1))
)
|> assign(:extra, extra)
~H"""
<%= if @link do %>
<.link {@extra}>
<%= render_slot(@inner_block) %>
</.link>
<% else %>
<button type={assigns[:type] || "button"} {Keyword.drop(@extra, [:to, :href, :navigate, :patch])}>
<%= render_slot(@inner_block) %>
</button>
<% end %>
"""
end
defp variant_to_color("danger"), do: "danger"
defp variant_to_color("icon-reversed"), do: "transparent"
defp variant_to_color("icon"), do: "secondary"
defp variant_to_color("nav-large"), do: "transparent"
defp variant_to_color("nav"), do: "transparent"
defp variant_to_color("tab"), do: "tab"
defp variant_to_color("ui"), do: "secondary"
defp variant_to_color(_), do: ""
defp variant_to_shape("icon-reversed"), do: "square"
defp variant_to_shape("icon"), do: "square"
defp variant_to_shape("small"), do: "small"
defp variant_to_shape("tab"), do: "tab"
defp variant_to_shape(_), do: ""
defp color_and_shape_classes(color, shape) do
[color, shape]
|> Enum.filter(&(&1 not in ["", nil]))
|> Enum.map(&"a-button--#{&1}")
end
defp variant_classes(nil), do: []
defp variant_classes(variant) when is_binary(variant) or is_atom(variant),
do: variant_classes([variant])
defp variant_classes(variants) when is_list(variants),
do: Enum.map(variants, &"a-button--#{&1}")
defp icon_with_label(%{icon: icon} = assigns) when is_binary(icon) do
assigns
|> assign(:icon, {icon, []})
|> icon_with_label()
end
defp icon_with_label(%{icon: {icon, opts}} = assigns) do
{icon_after, opts} = Keyword.pop(opts, :after)
icon_opts = Keyword.merge(opts, name: icon, class: "a-button__icon")
assigns = assigns |> assign(:icon_opts, icon_opts) |> assign(:icon_after, icon_after)
~H"""
<%= unless @icon_after do %>
<.ui_icon {@icon_opts} />
<% end %>
<span class={classnames("a-button__label")}><%= render_slot(@inner_block) %></span>
<%= if @icon_after do %>
<.ui_icon {@icon_opts} />
<% end %>
"""
end
@doc """
An icon button with sr text and title. Accepts an icon name and a label.
The icon can be either provided as icon name string, or as tuple with `{name, icon_opts}` where the name is the
icon name and icon options that are passed as attributes to `BitstylesPhoenix.Component.Icon.ui_icon`.
## Attributes
- `icon` - An icon name as string or a tuple of {name, icon_opts} to be passed on.
- `label` - A screen reader label for the button.
- `reversed` - Icon reversed style (see examples)
- All other attributes are passed in as attributes to `BitstylesPhoenix.Component.Button.ui_button/1`.
"""
story(
"Icon button",
'''
iex> assigns = %{}
...> render ~H"""
...> <.ui_icon_button icon="plus" label="Add" href="#"/>
...> """
''',
[
"6.0.0": '''
"""
<a href="#" class="a-button a-button--square" title="Add">
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" class="a-icon" focusable="false" height="16" width="16">
<use xlink:href="#icon-plus">
</use>
</svg>
<span class="u-sr-only">
Add
</span>
</a>
"""
''',
"5.0.1": '''
"""
<a href="#" class="a-button a-button--square" title="Add">
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" class="a-icon" focusable="false" height="16" width="16">
<use xlink:href="#icon-plus">
</use>
</svg>
<span class="u-sr-only">
Add
</span>
</a>
"""
''',
"4.3.0": '''
"""
<a href="#" class="a-button a-button--icon" title="Add">
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" class="a-icon" focusable="false" height="16" width="16">
<use xlink:href="#icon-plus">
</use>
</svg>
<span class="u-sr-only">
Add
</span>
</a>
"""
'''
],
extra_html: """
<svg xmlns="http://www.w3.org/2000/svg" hidden aria-hidden="true">
<symbol id="icon-plus" viewBox="0 0 100 100">
<path d="M54.57,87.43V54.57H87.43a4.57,4.57,0,0,0,0-9.14H54.57V12.57a4.57,4.57,0,1,0-9.14,0V45.43H12.57a4.57,4.57,0,0,0,0,9.14H45.43V87.43a4.57,4.57,0,0,0,9.14,0Z"/>
</symbol>
</svg>
"""
)
story(
"Icon button with some options",
'''
iex> assigns = %{}
...> render ~H"""
...> <.ui_icon_button icon={{"bin", file: "assets/icons.svg", size: "xl"}} label="Delete" class="foo" />
...> """
''',
'''
"""
<button type="button" class="a-button a-button--square foo" title="Delete">
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" class="a-icon a-icon--xl" focusable="false" height="16" width="16">
<use xlink:href="assets/icons.svg#icon-bin">
</use>
</svg>
<span class="u-sr-only">
Delete
</span>
</button>
"""
'''
)
story(
"Icon button reversed",
'''
iex> assigns = %{}
...> render ~H"""
...> <.ui_icon_button icon="plus" label="Show" href="#" reversed />
...> """
''',
[
"6.0.0": '''
"""
<a href="#" class="a-button a-button--square" data-theme="dark" title="Show">
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" class="a-icon" focusable="false" height="16" width="16">
<use xlink:href="#icon-plus">
</use>
</svg>
<span class="u-sr-only">
Show
</span>
</a>
"""
''',
"5.0.1": '''
"""
<a href="#" class="a-button a-button--square" data-theme="dark" title="Show">
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" class="a-icon" focusable="false" height="16" width="16">
<use xlink:href="#icon-plus">
</use>
</svg>
<span class="u-sr-only">
Show
</span>
</a>
"""
''',
"4.3.0": '''
"""
<a href="#" class="a-button a-button--icon a-button--icon-reversed" title="Show">
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" class="a-icon" focusable="false" height="16" width="16">
<use xlink:href="#icon-plus">
</use>
</svg>
<span class="u-sr-only">
Show
</span>
</a>
"""
'''
],
extra_html: """
<svg xmlns="http://www.w3.org/2000/svg" hidden aria-hidden="true">
<symbol id="icon-plus" viewBox="0 0 100 100">
<path d="M54.57,87.43V54.57H87.43a4.57,4.57,0,0,0,0-9.14H54.57V12.57a4.57,4.57,0,1,0-9.14,0V45.43H12.57a4.57,4.57,0,0,0,0,9.14H45.43V87.43a4.57,4.57,0,0,0,9.14,0Z"/>
</symbol>
</svg>
"""
)
def ui_icon_button(assigns) do
extra = assigns_to_attributes(assigns, [:icon, :label, :reversed, :color, :title])
extra =
if Bitstyles.Version.version() >= {5, 0, 0} do
if assigns[:reversed] do
Keyword.put_new(extra, :"data-theme", "dark")
else
extra
end
else
if assigns[:reversed] do
Keyword.put_new(extra, :variant, ["icon", "icon-reversed"])
else
Keyword.put_new(extra, :variant, ["icon"])
end
end
{icon, icon_opts} =
case assigns.icon do
{icon, icon_opts} -> {icon, icon_opts}
icon -> {icon, []}
end
assigns =
assign(assigns,
extra: extra,
icon: icon,
icon_opts: icon_opts,
color: assigns[:color],
icon_after: assigns[:reversed]
)
~H"""
<.ui_button shape={:square} color={@color} title={assigns[:title] || @label} {@extra}>
<.ui_icon name={@icon} {@icon_opts}/>
<span class={classnames("u-sr-only")}><%= @label %></span>
</.ui_button>
"""
end
end