lib/bitstyles_phoenix/component/icon.ex

defmodule BitstylesPhoenix.Component.Icon do
  use BitstylesPhoenix.Component
  import BitstylesPhoenix.Component.UseSVG

  @moduledoc """
  An SVG icon system, that expects the icons to be present on the page, rendered as SVG `<symbol>`s.
  """

  @doc ~S"""
  Renders an icon element.

  This uses `BitstylesPhoenix.Component.UseSVG` to render an icon either inlined in the page or
  referenced in an external SVG file. Icons are assumed to have an id prefixed with `icon-` followed
  by the name of the icon, which is used to reference the icon.

  ## Attributes

  - `name` *(required)* - The name of the icon. Assumes icons are prefixed with `icon-`.
  - `size` - Specify the icon size to use. Available sizes are specified in CSS, and default to `s`, `m`, `l`, `xl`. If you do not specify a size, the icon will fit into a `1em` square.
  - `file` - To be set if icons should be loaded from an external resource (see `BitstylesPhoenix.Component.UseSVG.ui_svg/1`).
    This can also be configured to a default `icon_file`, see `BitstylesPhoenix` for config options. With the configuration present, inline icons can still be rendered with `file={nil}`.
  - `class` - Extra classes to pass to the svg. See `BitstylesPhoenix.Helper.classnames/1` for usage.

  See the [bitstyles icon docs](https://bitcrowd.github.io/bitstyles/?path=/docs/atoms-icon) for examples of icon usage, and available icons in the [bitstyles icon set](https://bitcrowd.github.io/bitstyles/?path=/docs/ui-data-icons).
  """

  story(
    "An icon (from inline svg)",
    '''
        iex> assigns = %{}
        ...> render ~H"""
        ...> <.ui_icon name="inline-arrow"/>
        ...> """
    ''',
    '''
        """
        <svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" class="a-icon" focusable="false" height="16" width="16">
          <use xlink:href="#icon-inline-arrow">
          </use>
        </svg>
        """
    ''',
    extra_html: """
    <svg xmlns="http://www.w3.org/2000/svg" hidden aria-hidden="true">
      <symbol id="icon-inline-arrow" viewBox="0 0 100 100">
        <path d="M32.83,97.22a6.07,6.07,0,1,1-8.59-8.58L58.59,54.29a6.07,6.07,0,0,0,0-8.58L24.24,11.36a6.07,6.07,0,1,1,8.59-8.58L75.76,45.71a6.07,6.07,0,0,1,0,8.58Z" fill-rule="evenodd" />
      </symbol>
    </svg>
    """
  )

  story(
    "An icon with a size",
    '''
        iex> assigns = %{}
        ...> render ~H"""
        ...> <.ui_icon name="hamburger" file="/assets/icons.svg" size="xl"/>
        ...> """
    ''',
    '''
        """
        <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-hamburger">
          </use>
        </svg>
        """
    '''
  )

  story(
    "An icon with extra options",
    '''
        iex> assigns = %{}
        ...> render ~H"""
        ...> <.ui_icon name="bin" file="/assets/icons.svg" class="foo bar"/>
        ...> """
    ''',
    '''
        """
        <svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" class="a-icon foo bar" focusable="false" height="16" width="16">
          <use xlink:href="/assets/icons.svg#icon-bin">
          </use>
        </svg>
        """
    '''
  )

  def ui_icon(assigns) do
    icon = "icon-#{assigns.name}"

    class =
      classnames([
        "a-icon",
        {"a-icon--#{assigns[:size]}", assigns[:size] != nil},
        assigns[:class]
      ])

    extra =
      assigns
      |> assigns_to_attributes([:class, :name, :size])
      |> put_defaults

    assigns = assign(assigns, extra: extra, class: class, icon: icon)

    ~H"""
    <.ui_svg use={@icon} class={@class} aria-hidden="true" focusable="false" {@extra} />
    """
  end

  @default_size 16
  defp put_defaults(opts) do
    opts
    |> Keyword.put_new(:width, @default_size)
    |> Keyword.put_new(:height, @default_size)
    |> put_icon_file(Application.get_env(:bitstyles_phoenix, :icon_file, :inline))
  end

  defp put_icon_file(opts, :inline), do: opts

  defp put_icon_file(opts, file) when is_binary(file) do
    Keyword.put_new(opts, :file, file)
  end

  defp put_icon_file(opts, {module, function, arguments}) do
    file = apply(module, function, arguments)
    put_icon_file(opts, file)
  end

  defp put_icon_file(opts, {module, function}) do
    file = apply(module, function)
    put_icon_file(opts, file)
  end
end