live_toast icon indicating copy to clipboard operation
live_toast copied to clipboard

Better custom component support

Open bamorim opened this issue 1 year ago • 0 comments

This is a WIP, but mainly I was trying to have a completely custom component for all flashes/toasts.

I realized that the DX was weird because I had to pass the component each time. Worse than that, the X icon was still being rendered even with a custom component.

So I'm trying to improve this by:

  • Allowing component to be passed straight to LiveToast.toast_group function
  • Use that by default if the update received in the LiveComponent doesn't include a component
  • Not rendering the close button when custom component is passed (breaking change)
  • Passing phx-* attributes to be used for closing the toast down to the component, so one can place the close button/action wherever they want.
  • Setting a more minimalist classes on the wrapping div when component is passed (breaking change).

This is still missing a lot of important details. I also was failing to run the tests because some Jason issue that I'll look into after.

But with this setup I was able to customize the toast completely with the following toast component:

defmodule PursonalWeb.CoreComponents.Toast do
  use Phoenix.Component

  import Heroicons.LiveView
  import Turboprop.Variants

  @variant_config [
    slots: [
      base: [
        "group",
        "pointer-events-auto",
        "relative",
        "flex",
        "w-full",
        "items-center",
        "justify-between",
        "space-x-4",
        "overflow-hidden",
        "rounded-md",
        "p-6",
        "pr-8",
        "shadow-lg",
        "transition-all",
        "mt-4"
      ],
      cross: [
        "absolute",
        "right-2",
        "top-2",
        "rounded-md",
        "p-1",
        "opacity-0",
        "transition-opacity",
        "focus:opacity-100",
        "focus:outline-none",
        "focus:ring-2",
        "group-hover:opacity-100"
      ]
    ],
    variants: [
      variant: [
        default: [
          base: "bg-background text-foreground border",
          cross: "text-foreground/50 hover:text-foreground"
        ],
        destructive: [
          base: "bg-destructive text-destructive-foreground",
          cross: "text-destructive-foreground/50 hover:text-destructive-foreground"
        ]
      ]
    ]
  ]

  attr :id, :string, doc: "the optional id of flash container"
  attr :title, :string, default: nil
  attr :body, :string, default: ""
  attr :kind, :atom, values: [:info, :error], doc: "used for styling and flash lookup"
  attr :action, :any, doc: "Optinal action component"
  attr :close_args, :any, default: []

  def toast(assigns) do
    assigns =
      assigns
      |> assign(:base_variant_class, toast_variant(assigns, :base))
      |> assign(:cross_variant_class, toast_variant(assigns, :cross))

    ~H"""
    <div role="status" aria-live="off" aria-atomic="true" tabindex="0" class={@base_variant_class}>
      <div class="grid gap-1">
        <div :if={@title} class="text-sm font-semibold" data-part="title"><%= @title %></div>
        <div class="text-sm opacity-90"><%= @body %></div>
      </div>
      <button type="button" class={@cross_variant_class} {@close_args}>
        <.icon name="x-mark" type="solid" class="h-5 w-5" />
      </button>
    </div>
    """
  end

  defp toast_variant(assigns, slot) do
    variant(@variant_config, slot: slot, variant: variant_name(assigns))
  end

  defp variant_name(%{kind: :error}), do: :destructive
  defp variant_name(_), do: :default
end

bamorim avatar Jul 30 '24 01:07 bamorim