Skip to main content

lib/skua/components/date.ex

defmodule Skua.Components.Date do
  @moduledoc """
  A date input — a hidden ISO `<input>` that carries the value plus a trigger
  that opens a keyboard-navigable calendar (W3C APG date-grid pattern).

      <.date_input field={@form[:due_on]} label="Due date" />
      <.date_input field={@form[:start]} min="2026-01-01" max="2026-12-31" />

  The hidden input holds `yyyy-mm-dd`, so `phx-change`/`phx-submit` and Ecto
  `:date` casting work unchanged. The calendar grid supports arrow keys
  (±1 day / ±1 week), Home/End (week), PageUp/PageDown (month), and
  Enter/Space to pick.
  """
  use Phoenix.Component

  alias Skua.Components.Form
  alias Skua.Field

  attr :field, Phoenix.HTML.FormField, default: nil
  attr :id, :string, default: nil
  attr :name, :string, default: nil
  attr :value, :any, default: nil, doc: "ISO yyyy-mm-dd"
  attr :min, :string, default: nil
  attr :max, :string, default: nil
  attr :today, :string, default: nil
  attr :label, :string, default: nil
  attr :hint, :string, default: nil
  attr :errors, :list, default: nil
  attr :required, :boolean, default: false
  attr :placeholder, :string, default: "Pick a date…"
  attr :time, :boolean, default: false, doc: "also pick a time (datetime value)"
  attr :time_format, :string, default: "12", values: ~w(12 24), doc: "12h AM/PM or 24h military"

  def date_input(assigns) do
    assigns =
      assigns
      |> assign_new(:time, fn -> false end)
      |> assign_new(:time_format, fn -> "12" end)
      |> Field.normalize()
      |> then(fn a -> assign(a, :id, a.id || "sk-date") end)

    assigns =
      assigns
      |> assign(:value, iso(assigns.value))
      |> assign(:describedby, describedby(assigns))

    ~H"""
    <div class="sk-field">
      <Form.label :if={@label} for={"#{@id}-trigger"} required={@required}>{@label}</Form.label>
      <div
        id={@id}
        class="sk-date"
        phx-hook="SkuaDate"
        data-value={@value}
        data-min={@min}
        data-max={@max}
        data-today={@today}
        data-placeholder={@placeholder}
        data-time={@time && ""}
        data-time-format={@time_format}
      >
        <input type="hidden" name={@name} value={@value} data-sk-date-value required={@required} />
        <button
          type="button"
          id={"#{@id}-trigger"}
          class={["sk-input sk-focusable sk-select-trigger", @errors != [] && "is-invalid"]}
          data-sk-trigger
          aria-haspopup="dialog"
          aria-expanded="false"
          aria-invalid={(@errors != [] && "true") || nil}
          aria-describedby={@describedby}
        >
          <span data-sk-value></span>
          <svg
            class="sk-affix sk-glyph"
            viewBox="0 0 24 24"
            style="margin-left:auto"
            aria-hidden="true"
          >
            <rect x="3" y="4" width="18" height="18" rx="2" /><path d="M16 2v4M8 2v4M3 10h18" />
          </svg>
        </button>
      </div>
      <span class="sk-msg">
        <Form.error :if={@errors != []} id={"#{@id}-error"} errors={@errors} />
        <span :if={@hint && @errors == []} id={"#{@id}-hint"} class="sk-help">{@hint}</span>
      </span>
    </div>
    """
  end

  @doc """
  A date **and time** picker — a time bar (hour / minute / AM·PM, or 24h
  military) sits above the calendar. The hidden value is an ISO datetime
  (`yyyy-mm-ddThh:mm`).

      <.datetime_input field={@form[:starts_at]} label="Starts" />
      <.datetime_input field={@form[:starts_at]} time_format="24" />
  """
  attr :field, Phoenix.HTML.FormField, default: nil
  attr :id, :string, default: nil
  attr :name, :string, default: nil
  attr :value, :any, default: nil, doc: "ISO yyyy-mm-ddThh:mm"
  attr :min, :string, default: nil
  attr :max, :string, default: nil
  attr :today, :string, default: nil
  attr :label, :string, default: nil
  attr :hint, :string, default: nil
  attr :errors, :list, default: nil
  attr :required, :boolean, default: false
  attr :placeholder, :string, default: "Pick a date & time…"
  attr :time_format, :string, default: "12", values: ~w(12 24)

  def datetime_input(assigns) do
    assigns
    |> assign(:time, true)
    |> assign_new(:placeholder, fn -> "Pick a date & time…" end)
    |> date_input()
  end

  defp iso(%Date{} = d), do: Date.to_iso8601(d)
  defp iso(%NaiveDateTime{} = d), do: NaiveDateTime.to_iso8601(d)
  defp iso(%DateTime{} = d), do: DateTime.to_iso8601(d)
  defp iso(value) when is_binary(value), do: value
  defp iso(_), do: nil

  defp describedby(%{errors: errors, id: id}) when errors != [], do: "#{id}-error"
  defp describedby(%{hint: hint, id: id}) when not is_nil(hint), do: "#{id}-hint"
  defp describedby(_), do: nil
end