lib/phoenix/ui/components/avatar_group.ex

defmodule Phoenix.UI.Components.AvatarGroup do
  @moduledoc """
  Provides avatar_group component.
  """
  import Phoenix.UI.Components.Avatar, only: [avatar: 1]

  use Phoenix.UI, :component

  attr(:border, :boolean, default: true)
  attr(:color, :string, default: "slate", values: Theme.colors())
  attr(:element, :string, default: "div")
  attr(:max, :integer, default: 5)
  attr(:rest, :global)
  attr(:size, :string, default: "md", values: ["xs", "sm", "md", "lg", "xl"])
  attr(:spacing, :string, default: "md", values: ["xs", "sm", "md", "lg", "xl"])
  attr(:variant, :string, default: "circular", values: ["circular", "rounded"])

  slot(:avatar)

  @doc """
  Renders avatar_group component.

  ## Examples

      ```
      <.avatar_group>
        <:avatar src={@src1}/>
        <:avatar src={@src2}/>
        <:avatar src={@src3}/>
        ...
      </.avatar_group>
      ```

  """
  @spec avatar_group(Socket.assigns()) :: Rendered.t()
  def avatar_group(assigns) do
    spacing = spacing_mapping(assigns[:spacing])

    assigns =
      assigns
      |> assign_class(~w(avatar-group inline-flex flex-row-reverse items-center pl-#{spacing}))
      |> calc_total()
      |> calc_extra()
      |> normalize_avatars()

    ~H"""
    <.dynamic_tag class={@class} name={@element} {@rest}>
      <%= if (@total - @max) > 0 do %>
        <.avatar
          border={@border}
          color={@color}
          extend_class={"avatar-group-avatar -ml-#{spacing_mapping(@spacing)}"}
          size={@size}
          variant={@variant}
        >
          +<%= @total - @max %>
        </.avatar>
      <% end %>
      <%= for avatar <- @avatar do %>
        <.avatar {avatar} />
      <% end %>
    </.dynamic_tag>
    """
  end

  defp calc_total(%{avatar: avatars} = assigns) do
    assign_new(assigns, :total, fn -> length(avatars) end)
  end

  defp calc_extra(%{max: max, total: total} = assigns) do
    assign_new(assigns, :extra, fn -> total - max end)
  end

  ### Normalize Avatars ##########################

  defp normalize_avatars(%{avatar: [_ | _] = avatars, max: max, total: total} = assigns) do
    take_count = if max >= total, do: total, else: max
    spacing = spacing_mapping(assigns[:spacing])

    avatar =
      avatars
      |> Enum.take(take_count)
      |> Enum.map(fn avatar ->
        extend_class = build_class(~w(
          avatar-group-avatar -ml-#{spacing}
          #{Map.get(avatar, :extend_class)}
        ))

        avatar
        |> Map.put_new(:border, assigns[:border])
        |> Map.put_new(:color, assigns[:color])
        |> Map.put_new(:size, assigns[:size])
        |> Map.put_new(:variant, assigns[:variant])
        |> Map.put(:extend_class, extend_class)
        |> apply_avatar_inner_block()
      end)
      |> Enum.reverse()

    assign(assigns, :avatar, avatar)
  end

  defp spacing_mapping("xs"), do: "2.5"
  defp spacing_mapping("sm"), do: "2"
  defp spacing_mapping("md"), do: "1.5"
  defp spacing_mapping("lg"), do: "1"
  defp spacing_mapping("xl"), do: "0.5"

  defp apply_avatar_inner_block(%{inner_block: nil} = avatar) do
    Map.put(avatar, :inner_block, [])
  end

  defp apply_avatar_inner_block(avatar), do: avatar
end