Repository: woylie/doggo
Pull request #319 (2024-07-23 02:12:04)

field macros
- **turn field group into macro**
- **turn field description into macro**
- **turn field errors into macro**
- **turn label into macro**



Patch:
diff --git a/demo/lib/demo_web/components/core_components.ex b/demo/lib/demo_web/components/core_components.ex
index 09f189a..b643076 100644
--- a/demo/lib/demo_web/components/core_components.ex
+++ b/demo/lib/demo_web/components/core_components.ex
@@ -28,10 +28,14 @@ defmodule DemoWeb.CoreComponents do
   drawer()
   fab()
   fallback()
+  field_description_builder()
+  field_errors_builder()
+  field_group_builder()
   frame_builder()
   icon()
   icon_sprite()
   image()
+  label_builder()
   menu()
   menu_bar()
   menu_button()
diff --git a/demo/storybook/form/field_description.story.exs b/demo/storybook/form/field_description.story.exs
new file mode 100644
index 0000000..69cebc5
--- /dev/null
+++ b/demo/storybook/form/field_description.story.exs
@@ -0,0 +1,4 @@
+defmodule Storybook.Components.FieldDescription do
+  use PhoenixStorybook.Story, :component
+  use Doggo.Storybook, module: DemoWeb.CoreComponents, name: :field_description
+end
diff --git a/demo/storybook/form/field_errors.story.exs b/demo/storybook/form/field_errors.story.exs
new file mode 100644
index 0000000..a66bde5
--- /dev/null
+++ b/demo/storybook/form/field_errors.story.exs
@@ -0,0 +1,4 @@
+defmodule Storybook.Components.FieldErrors do
+  use PhoenixStorybook.Story, :component
+  use Doggo.Storybook, module: DemoWeb.CoreComponents, name: :field_errors
+end
diff --git a/demo/storybook/form/field_group.story.exs b/demo/storybook/form/field_group.story.exs
new file mode 100644
index 0000000..304502b
--- /dev/null
+++ b/demo/storybook/form/field_group.story.exs
@@ -0,0 +1,4 @@
+defmodule Storybook.Components.FieldGroup do
+  use PhoenixStorybook.Story, :component
+  use Doggo.Storybook, module: DemoWeb.CoreComponents, name: :field_group
+end
diff --git a/demo/storybook/form/label.story.exs b/demo/storybook/form/label.story.exs
new file mode 100644
index 0000000..aa3d3ee
--- /dev/null
+++ b/demo/storybook/form/label.story.exs
@@ -0,0 +1,4 @@
+defmodule Storybook.Components.Label do
+  use PhoenixStorybook.Story, :component
+  use Doggo.Storybook, module: DemoWeb.CoreComponents, name: :label
+end
diff --git a/lib/doggo.ex b/lib/doggo.ex
index 636428d..37eb161 100644
--- a/lib/doggo.ex
+++ b/lib/doggo.ex
@@ -806,7 +806,8 @@ defmodule Doggo do
     """
   end
 
-  defp field_errors_id(id) when is_binary(id), do: "#{id}_errors"
+  @doc false
+  def field_errors_id(id) when is_binary(id), do: "#{id}_errors"
 
   @doc """
   Renders the description of an input.
@@ -833,7 +834,8 @@ defmodule Doggo do
     """
   end
 
-  defp field_description_id(id) when is_binary(id), do: "#{id}_description"
+  @doc false
+  def field_description_id(id) when is_binary(id), do: "#{id}_description"
 
   defp translate_error({msg, opts}, nil) do
     Enum.reduce(opts, msg, fn {key, value}, acc ->
@@ -859,53 +861,6 @@ defmodule Doggo do
     end
   end
 
-  @doc """
-  Use the field group component to visually group multiple inputs in a form.
-
-  This component is intended for styling purposes and does not provide semantic
-  grouping. For semantic grouping of related form elements, use the `<fieldset>`
-  and `<legend>` HTML elements instead.
-
-  ## Examples
-
-  Visual grouping of inputs:
-
-  ```heex
-  <Doggo.field_group>
-    <Doggo.input field={@form[:given_name]} label="Given name" />
-    <Doggo.input field={@form[:family_name]} label="Family name"/>
-  </Doggo.field_group>
-  ```
-
-  Semantic grouping (for reference):
-
-  ```heex
-  <fieldset>
-    <legend>Personal Information</legend>
-    <Doggo.input field={@form[:given_name]} label="Given name" />
-    <Doggo.input field={@form[:family_name]} label="Family name"/>
-  </fieldset>
-  ```
-  """
-  @doc type: :form
-  @doc since: "0.3.0"
-
-  attr :class, :any,
-    default: [],
-    doc: "Additional CSS classes. Can be a string or a list of strings."
-
-  attr :rest, :global, doc: "Any additional HTML attributes."
-
-  slot :inner_block, required: true
-
-  def field_group(assigns) do
-    ~H"""
-    <div class={["field-group" | List.wrap(@class)]} {@rest}>
-      <%= render_slot(@inner_block) %>
-    </div>
-    """
-  end
-
   ## Helpers
 
   @doc false
diff --git a/lib/doggo/components.ex b/lib/doggo/components.ex
index 06c4ae4..e48c711 100644
--- a/lib/doggo/components.ex
+++ b/lib/doggo/components.ex
@@ -43,10 +43,14 @@ defmodule Doggo.Components do
         drawer()
         fab()
         fallback()
+        field_description_builder()
+        field_errors_builder()
+        field_group_builder()
         frame_builder()
         icon()
         icon_sprite()
         image()
+        label_builder()
         menu()
         menu_bar()
         menu_button()
@@ -2405,6 +2409,134 @@ defmodule Doggo.Components do
       end
   )
 
+  component(
+    :field_description_builder,
+    name: :field_description,
+    base_class: "field-description",
+    modifiers: [],
+    doc: """
+    Renders the description of an input.
+    """,
+    usage: """
+    ```heex
+    <.field_description for="name">
+      max. 100 characters
+    </.field_description>
+    ```
+    """,
+    type: :form,
+    since: "0.6.0",
+    maturity: :developing,
+    attrs_and_slots:
+      quote do
+        attr :for, :string, required: true, doc: "The ID of the input."
+        attr :rest, :global, doc: "Any additional HTML attributes."
+
+        slot :inner_block, required: true
+      end,
+    heex:
+      quote do
+        %{for: for} = var!(assigns)
+
+        var!(assigns) =
+          assign(var!(assigns), :id, Doggo.field_description_id(for))
+
+        ~H"""
+        <div id={@id} class={@class}>
+          <%= render_slot(@inner_block) %>
+        </div>
+        """
+      end
+  )
+
+  component(
+    :field_errors_builder,
+    name: :field_errors,
+    base_class: "field-errors",
+    modifiers: [],
+    doc: """
+    Renders the errors for an input.
+    """,
+    usage: """
+    ```heex
+    <.field_errors for="name" errors={["too many characters"]} />
+    ```
+    """,
+    type: :form,
+    since: "0.6.0",
+    maturity: :developing,
+    attrs_and_slots:
+      quote do
+        attr :for, :string, required: true, doc: "The ID of the input."
+        attr :errors, :list, required: true, doc: "A list of errors as strings."
+        attr :rest, :global, doc: "Any additional HTML attributes."
+      end,
+    heex:
+      quote do
+        %{for: for} = var!(assigns)
+
+        var!(assigns) =
+          assign(var!(assigns), :id, Doggo.field_errors_id(for))
+
+        ~H"""
+        <ul :if={@errors != []} id={@id} class={@class} {@rest}>
+          <li :for={error <- @errors}><%= error %></li>
+        </ul>
+        """
+      end
+  )
+
+  component(
+    :field_group_builder,
+    name: :field_group,
+    base_class: "field-group",
+    modifiers: [],
+    doc: """
+    Use the field group component to visually group multiple inputs in a form.
+
+    This component is intended for styling purposes and does not provide semantic
+    grouping. For semantic grouping of related form elements, use the `<fieldset>`
+    and `<legend>` HTML elements instead.
+    """,
+    usage: """
+    Visual grouping of inputs:
+
+    ```heex
+    <.field_group>
+      <.input field={@form[:given_name]} label="Given name" />
+      <.input field={@form[:family_name]} label="Family name"/>
+    </.field_group>
+    ```
+
+    Semantic grouping (for reference):
+
+    ```heex
+    <fieldset>
+      <legend>Personal Information</legend>
+      <.input field={@form[:given_name]} label="Given name" />
+      <.input field={@form[:family_name]} label="Family name"/>
+    </fieldset>
+    ```
+    """,
+    type: :form,
+    since: "0.6.0",
+    maturity: :developing,
+    attrs_and_slots:
+      quote do
+        attr :rest, :global, doc: "Any additional HTML attributes."
+
+        slot :inner_block, required: true
+      end,
+    heex:
+      quote do
+        ~H"""
+        <div class={@class} {@rest}>
+          <%= render_slot(@inner_block) %>
+        </div>
+        """
+      end
+  )
+
   component(
     :frame_builder,
     name: :frame,
@@ -2755,6 +2887,105 @@ defmodule Doggo.Components do
       end
   )
 
+  component(
+    :label_builder,
+    name: :label,
+    base_class: "label",
+    modifiers: [],
+    extra: [
+      visually_hidden_class: "is-visually-hidden"
+    ],
+    doc: """
+    Renders the label for an input.
+    """,
+    usage: """
+    ```heex
+    <.label for="name" required>
+      Name
+    </.label>
+    ```
+    """,
+    type: :form,
+    since: "0.6.0",
+    maturity: :developing,
+    attrs_and_slots:
+      quote do
+        attr :for, :string, default: nil, doc: "The ID of the input."
+
+        attr :required, :boolean,
+          default: false,
+          doc: "If set to `true`, a 'required' mark is rendered."
+
+        attr :required_text, :any,
+          default: "*",
+          doc: """
+          Sets the presentational text or symbol to mark an input as required.
+          """
+
+        attr :required_title, :any,
+          default: "required",
+          doc: """
+          Sets the `title` attribute of the required mark.
+          """
+
+        attr :visually_hidden, :boolean,
+          default: false,
+          doc: """
+          Adds an "is-visually-hidden" class to the `<label>`.
+          """
+
+        attr :rest, :global, doc: "Any additional HTML attributes."
+
+        slot :inner_block, required: true
+      end,
+    heex: fn extra ->
+      visually_hidden_class = Keyword.fetch!(extra, :visually_hidden_class)
+
+      quote do
+        %{class: class, visually_hidden: visually_hidden} = var!(assigns)
+
+        class =
+          if visually_hidden,
+            do: class ++ [unquote(visually_hidden_class)],
+            else: class
+
+        var!(assigns) = assign(var!(assigns), :class, class)
+
+        ~H"""
+        <label
+          for={@for}
+          class={[@visually_hidden && "is-visually-hidden" | @class]}
+          {@rest}
+        >
+          <%= render_slot(@inner_block) %>
+          <Doggo.Components.required_mark
+            :if={@required}
+            title={@required_title}
+            text={@required_text}
+          />
+        </label>
+        """
+      end
+    end
+  )
+
+  # inputs are announced as required by screen readers if the `required`
+  # attribute is set. This makes this mark purely visual. `aria-hidden="true"`
+  # is added so that screen readers don't announce redundant information. The
+  # title attribute has poor accessibility characteristics, but since this is
+  # purely presentational, this is acceptable.
+  # It is good practice to add a sentence explaining that fields marked with an
+  # asterisk (*) are required to the form.
+  # Alternatively, the word `required` might be used instead of an asterisk.
+  @doc false
+  def required_mark(assigns) do
+    ~H"""
+    <span :if={@text} class="label-required" aria-hidden="true" title={@title}>
+      <%= @text %>
+    </span>
+    """
+  end
+
   component(
     :menu,
     modifiers: [],
diff --git a/lib/doggo/storybook/field_description_builder.ex b/lib/doggo/storybook/field_description_builder.ex
new file mode 100644
index 0000000..ba505d5
--- /dev/null
+++ b/lib/doggo/storybook/field_description_builder.ex
@@ -0,0 +1,31 @@
+defmodule Doggo.Storybook.FieldDescriptionBuilder do
+  @moduledoc false
+  alias PhoenixStorybook.Stories.Variation
+
+  def dependent_components, do: [:input]
+
+  def variations(opts) do
+    [
+      %Variation{
+        id: :default,
+        attributes: %{for: "user_name"},
+        slots: slots(opts)
+      }
+    ]
+  end
+
+  def modifier_variation_base(_id, _name, _value, opts) do
+    %{
+      attributes: %{for: "user_name"},
+      slots: slots(opts)
+    }
+  end
+
+  defp slots(_opts) do
+    [
+      """
+      only lowercase letters and numbers, max. 30 characters
+      """
+    ]
+  end
+end
diff --git a/lib/doggo/storybook/field_errors_builder.ex b/lib/doggo/storybook/field_errors_builder.ex
new file mode 100644
index 0000000..b0baf69
--- /dev/null
+++ b/lib/doggo/storybook/field_errors_builder.ex
@@ -0,0 +1,34 @@
+defmodule Doggo.Storybook.FieldErrorsBuilder do
+  @moduledoc false
+  alias PhoenixStorybook.Stories.Variation
+
+  def dependent_components, do: [:input]
+
+  def variations(_opts) do
+    [
+      %Variation{
+        id: :default,
+        attributes: %{
+          for: "user_name",
+          errors: errors()
+        }
+      }
+    ]
+  end
+
+  def modifier_variation_base(_id, _name, _value, _opts) do
+    %{
+      attributes: %{
+        for: "user_name",
+        errors: errors()
+      }
+    }
+  end
+
+  defp errors do
+    [
+      "should be at most 30 characters",
+      "should only contain ASCII characters"
+    ]
+  end
+end
diff --git a/lib/doggo/storybook/field_group_builder.ex b/lib/doggo/storybook/field_group_builder.ex
new file mode 100644
index 0000000..19a0568
--- /dev/null
+++ b/lib/doggo/storybook/field_group_builder.ex
@@ -0,0 +1,50 @@
+defmodule Doggo.Storybook.FieldGroupBuilder do
+  @moduledoc false
+  alias PhoenixStorybook.Stories.Variation
+
+  def dependent_components, do: [:input]
+
+  def template do
+    """
+    <Phoenix.Component.form for={%{}} as={:story} :let={f}>
+      <.psb-variation />
+    </Phoenix.Component.form>
+    """
+  end
+
+  def variations(opts) do
+    [
+      %Variation{
+        id: :default,
+        slots: slots(opts)
+      }
+    ]
+  end
+
+  def modifier_variation_base(_id, _name, _value, opts) do
+    %{
+      attributes: %{},
+      slots: slots(opts)
+    }
+  end
+
+  defp slots(opts) do
+    dependent_components = opts[:dependent_components]
+    input_fun = dependent_components[:input]
+
+    if input_fun do
+      [
+        """
+        <.#{input_fun} type="text" field={f[:given_name]} label="Given name" />
+        <.#{input_fun} type="text" field={f[:family_name]} label="Family name" />
+        """
+      ]
+    else
+      [
+        """
+        <p>Please compile the <code>input</code> component for a complete preview.</p>
+        """
+      ]
+    end
+  end
+end
diff --git a/priv/storybook/form/label.story.exs b/lib/doggo/storybook/label_builder.ex
similarity index 52%
rename from priv/storybook/form/label.story.exs
rename to lib/doggo/storybook/label_builder.ex
index d27290d..c725f31 100644
--- a/priv/storybook/form/label.story.exs
+++ b/lib/doggo/storybook/label_builder.ex
@@ -1,9 +1,10 @@
-defmodule Storybook.Components.Label do
-  use PhoenixStorybook.Story, :component
+defmodule Doggo.Storybook.LabelBuilder do
+  @moduledoc false
+  alias PhoenixStorybook.Stories.Variation
 
-  def function, do: &Doggo.label/1
+  def dependent_components, do: [:input]
 
-  def variations do
+  def variations(_opts) do
     [
       %Variation{
         id: :default,
@@ -21,4 +22,11 @@ defmodule Storybook.Components.Label do
       }
     ]
   end
+
+  def modifier_variation_base(id, _name, _value, _opts) do
+    %{
+      attributes: %{id: id},
+      slots: ["E-mail"]
+    }
+  end
 end
diff --git a/priv/storybook/form/field_description.story.exs b/priv/storybook/form/field_description.story.exs
deleted file mode 100644
index b7d4020..0000000
--- a/priv/storybook/form/field_description.story.exs
+++ /dev/null
@@ -1,19 +0,0 @@
-defmodule Storybook.Components.FieldDescription do
-  use PhoenixStorybook.Story, :component
-
-  def function, do: &Doggo.field_description/1
-
-  def variations do
-    [
-      %Variation{
-        id: :default,
-        attributes: %{for: "user_name"},
-        slots: [
-          """
-          only lowercase letters and numbers, max. 30 characters
-          """
-        ]
-      }
-    ]
-  end
-end
diff --git a/priv/storybook/form/field_errors.story.exs b/priv/storybook/form/field_errors.story.exs
deleted file mode 100644
index 90226bd..0000000
--- a/priv/storybook/form/field_errors.story.exs
+++ /dev/null
@@ -1,20 +0,0 @@
-defmodule Storybook.Components.FieldErrors do
-  use PhoenixStorybook.Story, :component
-
-  def function, do: &Doggo.field_errors/1
-
-  def variations do
-    [
-      %Variation{
-        id: :default,
-        attributes: %{
-          for: "user_name",
-          errors: [
-            "should be at most 30 characters",
-            "should only contain ASCII characters"
-          ]
-        }
-      }
-    ]
-  end
-end
diff --git a/priv/storybook/form/field_group.story.exs b/priv/storybook/form/field_group.story.exs
deleted file mode 100644
index a287fb2..0000000
--- a/priv/storybook/form/field_group.story.exs
+++ /dev/null
@@ -1,27 +0,0 @@
-defmodule Storybook.Components.FieldGroup do
-  use PhoenixStorybook.Story, :component
-
-  def function, do: &Doggo.field_group/1
-
-  def template do
-    """
-    <Phoenix.Component.form for={%{}} as={:story} :let={f}>
-      <.psb-variation />
-    </Phoenix.Component.form>
-    """
-  end
-
-  def variations do
-    [
-      %Variation{
-        id: :default,
-        slots: [
-          """
-          <Doggo.input type="text" field={f[:given_name]} label="Given name" />
-          <Doggo.input type="text" field={f[:family_name]} label="Family name" />
-          """
-        ]
-      }
-    ]
-  end
-end


Test patch:
diff --git a/test/doggo/components_test.exs b/test/doggo/components_test.exs
index b2f0eea..fef4e63 100644
--- a/test/doggo/components_test.exs
+++ b/test/doggo/components_test.exs
@@ -38,10 +38,14 @@ defmodule Doggo.ComponentsTest do
     drawer()
     fab()
     fallback()
+    field_description_builder()
+    field_errors_builder()
+    field_group_builder()
     frame_builder()
     icon()
     icon_sprite()
     image()
+    label_builder()
     menu()
     menu_bar()
     menu_button()
@@ -2082,6 +2086,76 @@ defmodule Doggo.ComponentsTest do
     end
   end
 
+  describe "field_description/1" do
+    test "default" do
+      assigns = %{}
+
+      html =
+        parse_heex(~H"""
+        <TestComponents.field_description for="some-input">
+          text
+        </TestComponents.field_description>
+        """)
+
+      div = find_one(html, "div:root")
+      assert attribute(div, "class") == "field-description"
+      assert attribute(div, "id") == "some-input_description"
+    end
+  end
+
+  describe "field_errors/1" do
+    test "without errors" do
+      assigns = %{}
+
+      html =
+        parse_heex(~H"""
+        <TestComponents.field_errors for="some-input" errors={[]} />
+        """)
+
+      assert html == []
+    end
+
+    test "with errors" do
+      assigns = %{}
+
+      html =
+        parse_heex(~H"""
+        <TestComponents.field_errors for="some-input" errors={["some error"]} />
+        """)
+
+      ul = find_one(html, "ul:root")
+      assert attribute(ul, "class") == "field-errors"
+      assert attribute(ul, "id") == "some-input_errors"
+      assert text(html, "ul > li") == "some error"
+    end
+  end
+
+  describe "field_group/1" do
+    test "default" do
+      assigns = %{}
+
+      html =
+        parse_heex(~H"""
+        <TestComponents.field_group>fields</TestComponents.field_group>
+        """)
+
+      div = find_one(html, "div")
+      assert attribute(div, "class") == "field-group"
+      assert text(div) == "fields"
+    end
+
+    test "with global attribute" do
+      assigns = %{}
+
+      html =
+        parse_heex(~H"""
+        <TestComponents.field_group data-what="ever">fields</TestComponents.field_group>
+        """)
+
+      assert attribute(html, "div", "data-what") == "ever"
+    end
+  end
+
   describe "frame/1" do
     test "default" do
       assigns = %{}
@@ -2562,6 +2636,63 @@ defmodule Doggo.ComponentsTest do
     end
   end
 
+  describe "label/1" do
+    test "default" do
+      assigns = %{}
+
+      html =
+        parse_heex(~H"""
+        <TestComponents.label for="some-input">text</TestComponents.label>
+        """)
+
+      label = find_one(html, "label")
+      assert attribute(label, "class") == "label"
+      assert attribute(label, "for") == "some-input"
+      assert text(label) == "text"
+      assert Floki.find(html, ".label-required") == []
+    end
+
+    test "with required mark" do
+      assigns = %{}
+
+      html =
+        parse_heex(~H"""
+        <TestComponents.label required>text</TestComponents.label>
+        """)
+
+      mark = find_one(html, "label > span.label-required")
+      assert attribute(mark, "title") == "required"
+
+      # inputs with `required` attribute are already announced as required
+      assert attribute(mark, "aria-hidden") == "true"
+    end
+
+    test "with required mark and custom text" do
+      assigns = %{}
+
+      html =
+        parse_heex(~H"""
+        <TestComponents.label required_title="necessary" required>
+          text
+        </TestComponents.label>
+        """)
+
+      assert attribute(html, "label > span.label-required", "title") ==
+               "necessary"
+    end
+
+    test "with global attribute" do
+      assigns = %{}
+
+      html =
+        parse_heex(~H"""
+        <TestComponents.label data-test="hello">text</TestComponents.label>
+        """)
+
+      assert attribute(html, ":root", "data-test") == "hello"
+    end
+  end
+
   describe "menu/1" do
     test "default" do
       assigns = %{}
diff --git a/test/doggo/storybook_test.exs b/test/doggo/storybook_test.exs
index a10b3c9..3b822a4 100644
--- a/test/doggo/storybook_test.exs
+++ b/test/doggo/storybook_test.exs
@@ -41,9 +41,24 @@ defmodule Doggo.StorybookTest do
     drawer(modifiers: [variant: [values: [nil, "yes"], default: nil]])
     fab(modifiers: [variant: [values: [nil, "yes"], default: nil]])
     fallback(modifiers: [variant: [values: [nil, "yes"], default: nil]])
+
+    field_description_builder(
+      modifiers: [variant: [values: [nil, "yes"], default: nil]]
+    )
+
+    field_errors_builder(
+      modifiers: [variant: [values: [nil, "yes"], default: nil]]
+    )
+
+    field_group_builder(
+      modifiers: [variant: [values: [nil, "yes"], default: nil]]
+    )
+
     frame_builder()
     icon(modifiers: [variant: [values: [nil, "yes"], default: nil]])
     image(modifiers: [variant: [values: [nil, "yes"], default: nil]])
+
+    label_builder(modifiers: [variant: [values: [nil, "yes"], default: nil]])
     menu(modifiers: [variant: [values: [nil, "yes"], default: nil]])
     menu_bar(modifiers: [variant: [values: [nil, "yes"], default: nil]])
     menu_button(modifiers: [variant: [values: [nil, "yes"], default: nil]])
diff --git a/test/doggo_test.exs b/test/doggo_test.exs
index 2a18272..2478bac 100644
--- a/test/doggo_test.exs
+++ b/test/doggo_test.exs
@@ -4,98 +4,6 @@ defmodule DoggoTest do
 
   import Doggo.TestHelpers
 
-  describe "field_description/1" do
-    test "default" do
-      assigns = %{}
-
-      html =
-        parse_heex(~H"""
-        <Doggo.field_description for="some-input">text</Doggo.field_description>
-        """)
-
-      div = find_one(html, "div:root")
-      assert attribute(div, "class") == "field-description"
-      assert attribute(div, "id") == "some-input_description"
-    end
-  end
-
-  describe "field_errors/1" do
-    test "without errors" do
-      assigns = %{}
-
-      html =
-        parse_heex(~H"""
-        <Doggo.field_errors for="some-input" errors={[]} />
-        """)
-
-      assert html == []
-    end
-
-    test "with errors" do
-      assigns = %{}
-
-      html =
-        parse_heex(~H"""
-        <Doggo.field_errors for="some-input" errors={["some error"]} />
-        """)
-
-      ul = find_one(html, "ul:root")
-      assert attribute(ul, "class") == "field-errors"
-      assert attribute(ul, "id") == "some-input_errors"
-      assert text(html, "ul > li") == "some error"
-    end
-  end
-
-  describe "field_group/1" do
-    test "default" do
-      assigns = %{}
-
-      html =
-        parse_heex(~H"""
-        <Doggo.field_group>fields</Doggo.field_group>
-        """)
-
-      div = find_one(html, "div")
-      assert attribute(div, "class") == "field-group"
-      assert text(div) == "fields"
-    end
-
-    test "with additional class as string" do
-      assigns = %{}
-
-      html =
-        parse_heex(~H"""
-        <Doggo.field_group class="is-narrow">fields</Doggo.field_group>
-        """)
-
-      assert attribute(html, "div", "class") == "field-group is-narrow"
-    end
-
-    test "with additional classes as list" do
-      assigns = %{}
-
-      html =
-        parse_heex(~H"""
-        <Doggo.field_group class={["is-narrow", "is-crisp"]}>
-          fields
-        </Doggo.field_group>
-        """)
-
-      assert attribute(html, "div", "class") == "field-group is-narrow is-crisp"
-    end
-
-    test "with global attribute" do
-      assigns = %{}
-
-      html =
-        parse_heex(~H"""
-        <Doggo.field_group data-what="ever">fields</Doggo.field_group>
-        """)
-
-      assert attribute(html, "div", "data-what") == "ever"
-    end
-  end
-
   describe "input/1" do
     test "with text input" do
       assigns = %{form: to_form(%{})}
@@ -574,87 +482,6 @@ defmodule DoggoTest do
     end
   end
 
-  describe "label/1" do
-    test "default" do
-      assigns = %{}
-
-      html =
-        parse_heex(~H"""
-        <Doggo.label for="some-input">text</Doggo.label>
-        """)
-
-      label = find_one(html, "label")
-      assert attribute(label, "class") == ""
-      assert attribute(label, "for") == "some-input"
-      assert text(label) == "text"
-      assert Floki.find(html, ".label-required") == []
-    end
-
-    test "with required mark" do
-      assigns = %{}
-
-      html =
-        parse_heex(~H"""
-        <Doggo.label required>text</Doggo.label>
-        """)
-
-      mark = find_one(html, "label > span.label-required")
-      assert attribute(mark, "title") == "required"
-
-      # inputs with `required` attribute are already announced as required
-      assert attribute(mark, "aria-hidden") == "true"
-    end
-
-    test "with required mark and custom text" do
-      assigns = %{}
-
-      html =
-        parse_heex(~H"""
-        <Doggo.label required_title="necessary" required>text</Doggo.label>
-        """)
-
-      assert attribute(html, "label > span.label-required", "title") ==
-               "necessary"
-    end
-
-    test "with additional class as string" do
-      assigns = %{}
-
-      html =
-        parse_heex(~H"""
-        <Doggo.label class="is-crooked" visually_hidden>text</Doggo.label>
-        """)
-
-      assert attribute(html, ":root", "class") ==
-               "is-visually-hidden is-crooked"
-    end
-
-    test "with additional classes as list" do
-      assigns = %{}
-
-      html =
-        parse_heex(~H"""
-        <Doggo.label class={["is-crooked", "is-groovy"]} visually_hidden>
-          text
-        </Doggo.label>
-        """)
-
-      assert attribute(html, ":root", "class") ==
-               "is-visually-hidden is-crooked is-groovy"
-    end
-
-    test "with global attribute" do
-      assigns = %{}
-
-      html =
-        parse_heex(~H"""
-        <Doggo.label data-test="hello">text</Doggo.label>
-        """)
-
-      assert attribute(html, ":root", "data-test") == "hello"
-    end
-  end
-
   defmodule TestComponents do
     @moduledoc """
     Generates components for tests.