Repository: woylie/doggo
Pull request #207 (2024-01-13 16:19:00)

add InvalidLabelError


Patch:
diff --git a/lib/doggo.ex b/lib/doggo.ex
index 05f7ae6..01a51a3 100644
--- a/lib/doggo.ex
+++ b/lib/doggo.ex
@@ -981,28 +981,7 @@ defmodule Doggo do
   end
 
   def carousel(assigns) do
-    label = assigns[:label]
-    labelledby = assigns[:labelledby]
-
-    if (label && labelledby) || !(label || labelledby) do
-      raise """
-      invalid label attributes for carousel
-
-      Doggo.carousel requires either 'label' or 'labelledby' set for
-      accessibility, but not both.
-
-      ## Examples
-
-          With label:
-
-          <Doggo.carousel label="Our Dogs" ... />
-
-          With labelledby:
-
-          <h3 id="dog-carousel-label">Our Dogs</h3>
-          <Doggo.carousel labelledby="dog-carousel-label" ... />
-      """
-    end
+    ensure_label!(assigns, "Doggo.carousel", "Our Dogs")
 
     ~H"""
     <section
@@ -3914,28 +3893,7 @@ defmodule Doggo do
   attr :rest, :global, doc: "Any additional HTML attributes."
 
   def radio_group(assigns) do
-    label = assigns[:label]
-    labelledby = assigns[:labelledby]
-
-    if (label && labelledby) || !(label || labelledby) do
-      raise """
-      invalid label attributes for radio_group
-
-      Doggo.radio_group requires either 'label' or 'labelledby' set for
-      accessibility, but not both.
-
-      ## Examples
-
-          With label:
-
-          <Doggo.radio_group label="Favorite Dog" ... />
-
-          With labelledby:
-
-          <h3 id="favorite-dog-label">Favorite Dog</h3>
-          <Doggo.radio_group labelledby="favorite-dog-label" ... />
-      """
-    end
+    ensure_label!(assigns, "Doggo.radio_group", "Favorite Dog")
 
     ~H"""
     <div
@@ -4133,28 +4091,7 @@ defmodule Doggo do
   slot :secondary, required: true
 
   def split_pane(assigns) do
-    label = assigns[:label]
-    labelledby = assigns[:labelledby]
-
-    if (label && labelledby) || !(label || labelledby) do
-      raise """
-      invalid label attributes for split_pane
-
-      Doggo.split_pane requires either 'label' or 'labelledby' set for
-      accessibility, but not both.
-
-      ## Examples
-
-          With label:
-
-          <Doggo.split_pane label="Favorite Dog" ... />
-
-          With labelledby:
-
-          <h3 id="favorite-dog-label">Favorite Dog</h3>
-          <Doggo.split_pane labelledby="favorite-dog-label" ... />
-      """
-    end
+    ensure_label!(assigns, "Doggo.split_pane", "Sidebar")
 
     ~H"""
     <div id={@id} class="split-pane" data-orientation={@orientation}>
@@ -4655,28 +4592,7 @@ defmodule Doggo do
   end
 
   def tabs(assigns) do
-    label = assigns[:label]
-    labelledby = assigns[:labelledby]
-
-    if (label && labelledby) || !(label || labelledby) do
-      raise """
-      invalid label attributes for toolbar
-
-      Doggo.toolbar requires either 'label' or 'labelledby' set for
-      accessibility, but not both.
-
-      ## Examples
-
-          With label:
-
-          <Doggo.tabs label="Dog Facts"></Doggo.tabs>
-
-          With labelledby:
-
-          <h3 id="dog-facts-label">Dog Facts</h3>
-          <Doggo.tabs labelledby="dog-facts-label"></Doggo.tabs>
-      """
-    end
+    ensure_label!(assigns, "Doggo.tabs", "Dog Facts")
 
     ~H"""
     <div id={@id} class={["tabs" | List.wrap(@class)]} {@rest}>
@@ -5096,28 +5012,7 @@ defmodule Doggo do
     """
 
   def toolbar(assigns) do
-    label = assigns[:label]
-    labelledby = assigns[:labelledby]
-
-    if (label && labelledby) || !(label || labelledby) do
-      raise """
-      invalid label attributes for toolbar
-
-      Doggo.toolbar requires either 'label' or 'labelledby' set for
-      accessibility, but not both.
-
-      ## Examples
-
-          With label:
-
-          <Doggo.toolbar label="Actions for the dog"></Doggo.toolbar>
-
-          With labelledby:
-
-          <h3 id="dog-toolbar-label">Actions for the dog</h3>
-          <Doggo.toolbar labelledby="dog-toolbar-label"></Doggo.toolbar>
-      """
-    end
+    ensure_label!(assigns, "Doggo.toolbar", "Dog profile actions")
 
     ~H"""
     <div
@@ -5223,28 +5118,7 @@ defmodule Doggo do
     """
 
   def tree(assigns) do
-    label = assigns[:label]
-    labelledby = assigns[:labelledby]
-
-    if (label && labelledby) || !(label || labelledby) do
-      raise """
-      invalid label attributes for tree
-
-      Doggo.tree requires either 'label' or 'labelledby' set for accessibility,
-      but not both.
-
-      ## Examples
-
-          With label:
-
-          <Doggo.tree label="Dogs" ... />
-
-          With labelledby:
-
-          <h3 id="dog-tree-label">Favorite Dog</h3>
-          <Doggo.tree labelledby="dog-tree-label" ... />
-      """
-    end
+    ensure_label!(assigns, "Doggo.tree", "Dog Breeds")
 
     ~H"""
     <ul
@@ -5426,4 +5300,18 @@ defmodule Doggo do
       variants: Enum.map(variants(), &variant_class/1)
     }
   end
+
+  defp ensure_label!(%{label: s, labelledby: nil}, _, _) when is_binary(s) do
+    :ok
+  end
+
+  defp ensure_label!(%{label: nil, labelledby: s}, _, _) when is_binary(s) do
+    :ok
+  end
+
+  defp ensure_label!(_, component, example_label) do
+    raise Doggo.InvalidLabelError,
+      component: component,
+      example_label: example_label
+  end
 end
diff --git a/lib/doggo/errors.ex b/lib/doggo/errors.ex
new file mode 100644
index 0000000..1973140
--- /dev/null
+++ b/lib/doggo/errors.ex
@@ -0,0 +1,34 @@
+defmodule Doggo.InvalidLabelError do
+  @moduledoc false
+  defexception [:component, :message]
+
+  def exception(opts) do
+    component = Keyword.fetch!(opts, :component)
+    example_label = Keyword.fetch!(opts, :example_label)
+
+    msg = """
+    Invalid label attributes for carousel
+
+    #{component} requires exactly one of 'label' or 'labelledby' set for
+    accessibility. These attributes are essential for screen readers and other
+    assistive technologies to provide a textual alternative for interactive
+    elements.
+
+    - If neither is set, the component is not properly accessible.
+    - If both are set, it may cause confusion for assistive technologies.
+
+    ## Examples
+
+    With 'label' attribute (when there is no visual label):
+
+        <#{component} label="#{example_label}" />
+
+    With 'labelledby' attribute (to reference an existing visible label):
+
+        <h3 id="example-label">#{example_label}</h3>
+        <#{component} labelledby="example-label" />
+    """
+
+    %__MODULE__{message: msg, component: component}
+  end
+end


Test patch:
diff --git a/test/doggo_test.exs b/test/doggo_test.exs
index 1c5f3dc..3d7731d 100644
--- a/test/doggo_test.exs
+++ b/test/doggo_test.exs
@@ -1562,37 +1562,31 @@ defmodule DoggoTest do
     end
 
     test "raises if both label and labelledby are set" do
-      error =
-        assert_raise RuntimeError, fn ->
-          assigns = %{}
+      assert_raise Doggo.InvalidLabelError, fn ->
+        assigns = %{}
 
-          parse_heex(~H"""
-          <Doggo.carousel
-            id="dog-carousel"
-            label="Dog Carousel"
-            labelledby="dog-carousel-label"
-          >
-            <:item label="1 of 1"></:item>
-          </Doggo.carousel>
-          """)
-        end
-
-      assert error.message =~ "invalid label attributes"
+        parse_heex(~H"""
+        <Doggo.carousel
+          id="dog-carousel"
+          label="Dog Carousel"
+          labelledby="dog-carousel-label"
+        >
+          <:item label="1 of 1"></:item>
+        </Doggo.carousel>
+        """)
+      end
     end
 
     test "raises if neither label nor labelledby are set" do
-      error =
-        assert_raise RuntimeError, fn ->
-          assigns = %{}
-
-          parse_heex(~H"""
-          <Doggo.carousel id="dog-carousel">
-            <:item label="1 of 1"></:item>
-          </Doggo.carousel>
-          """)
-        end
+      assert_raise Doggo.InvalidLabelError, fn ->
+        assigns = %{}
 
-      assert error.message =~ "invalid label attributes"
+        parse_heex(~H"""
+        <Doggo.carousel id="dog-carousel">
+          <:item label="1 of 1"></:item>
+        </Doggo.carousel>
+        """)
+      end
     end
 
     test "with role descriptions" do
@@ -4149,39 +4143,33 @@ defmodule DoggoTest do
     end
 
     test "raises if both label and labelledby are set" do
-      error =
-        assert_raise RuntimeError, fn ->
-          assigns = %{}
+      assert_raise Doggo.InvalidLabelError, fn ->
+        assigns = %{}
 
-          parse_heex(~H"""
-          <Doggo.radio_group
-            id="favorite_dog_rg"
-            name="favorite_dog"
-            label="Favorite Dog"
-            labelledby="rg-label"
-            options={[{"Labrador Retriever", "labrador"}]}
-          />
-          """)
-        end
-
-      assert error.message =~ "invalid label attributes"
+        parse_heex(~H"""
+        <Doggo.radio_group
+          id="favorite_dog_rg"
+          name="favorite_dog"
+          label="Favorite Dog"
+          labelledby="rg-label"
+          options={[{"Labrador Retriever", "labrador"}]}
+        />
+        """)
+      end
     end
 
     test "raises if neither label nor labelledby are set" do
-      error =
-        assert_raise RuntimeError, fn ->
-          assigns = %{}
-
-          parse_heex(~H"""
-          <Doggo.radio_group
-            id="favorite_dog_rg"
-            name="favorite_dog"
-            options={[{"Labrador Retriever", "labrador"}]}
-          />
-          """)
-        end
+      assert_raise Doggo.InvalidLabelError, fn ->
+        assigns = %{}
 
-      assert error.message =~ "invalid label attributes"
+        parse_heex(~H"""
+        <Doggo.radio_group
+          id="favorite_dog_rg"
+          name="favorite_dog"
+          options={[{"Labrador Retriever", "labrador"}]}
+        />
+        """)
+      end
     end
 
     test "with additional class as string" do
@@ -4370,45 +4358,39 @@ defmodule DoggoTest do
     end
 
     test "raises if both label and labelledby are set" do
-      error =
-        assert_raise RuntimeError, fn ->
-          assigns = %{}
-
-          parse_heex(~H"""
-          <Doggo.split_pane
-            id="sidebar-splitter"
-            label="Sidebar"
-            labelledby="sidebar-heading"
-            orientation="horizontal"
-            default_size={200}
-          >
-            <:primary>One</:primary>
-            <:secondary>Two</:secondary>
-          </Doggo.split_pane>
-          """)
-        end
+      assert_raise Doggo.InvalidLabelError, fn ->
+        assigns = %{}
 
-      assert error.message =~ "invalid label attributes"
+        parse_heex(~H"""
+        <Doggo.split_pane
+          id="sidebar-splitter"
+          label="Sidebar"
+          labelledby="sidebar-heading"
+          orientation="horizontal"
+          default_size={200}
+        >
+          <:primary>One</:primary>
+          <:secondary>Two</:secondary>
+        </Doggo.split_pane>
+        """)
+      end
     end
 
     test "raises if neither label nor labelledby are set" do
-      error =
-        assert_raise RuntimeError, fn ->
-          assigns = %{}
-
-          parse_heex(~H"""
-          <Doggo.split_pane
-            id="sidebar-splitter"
-            orientation="horizontal"
-            default_size={200}
-          >
-            <:primary>One</:primary>
-            <:secondary>Two</:secondary>
-          </Doggo.split_pane>
-          """)
-        end
+      assert_raise Doggo.InvalidLabelError, fn ->
+        assigns = %{}
 
-      assert error.message =~ "invalid label attributes"
+        parse_heex(~H"""
+        <Doggo.split_pane
+          id="sidebar-splitter"
+          orientation="horizontal"
+          default_size={200}
+        >
+          <:primary>One</:primary>
+          <:secondary>Two</:secondary>
+        </Doggo.split_pane>
+        """)
+      end
     end
   end
 
@@ -4890,35 +4872,29 @@ defmodule DoggoTest do
     end
 
     test "raises if both label and labelledby are set" do
-      error =
-        assert_raise RuntimeError, fn ->
-          assigns = %{}
-
-          parse_heex(~H"""
-          <Doggo.tabs id="my-tabs" label="My Tabs" labelledby="my-tabs-title">
-            <:panel label="Panel 1">some text</:panel>
-            <:panel label="Panel 2">some other text</:panel>
-          </Doggo.tabs>
-          """)
-        end
+      assert_raise Doggo.InvalidLabelError, fn ->
+        assigns = %{}
 
-      assert error.message =~ "invalid label attributes"
+        parse_heex(~H"""
+        <Doggo.tabs id="my-tabs" label="My Tabs" labelledby="my-tabs-title">
+          <:panel label="Panel 1">some text</:panel>
+          <:panel label="Panel 2">some other text</:panel>
+        </Doggo.tabs>
+        """)
+      end
     end
 
     test "raises if neither label nor labelledby are set" do
-      error =
-        assert_raise RuntimeError, fn ->
-          assigns = %{}
+      assert_raise Doggo.InvalidLabelError, fn ->
+        assigns = %{}
 
-          parse_heex(~H"""
-          <Doggo.tabs id="my-tabs">
-            <:panel label="Panel 1">some text</:panel>
-            <:panel label="Panel 2">some other text</:panel>
-          </Doggo.tabs>
-          """)
-        end
-
-      assert error.message =~ "invalid label attributes"
+        parse_heex(~H"""
+        <Doggo.tabs id="my-tabs">
+          <:panel label="Panel 1">some text</:panel>
+          <:panel label="Panel 2">some other text</:panel>
+        </Doggo.tabs>
+        """)
+      end
     end
 
     test "with additional class as string" do
@@ -5636,30 +5612,24 @@ defmodule DoggoTest do
     end
 
     test "raises if both label and labelledby are set" do
-      error =
-        assert_raise RuntimeError, fn ->
-          assigns = %{}
-
-          parse_heex(~H"""
-          <Doggo.toolbar label="Dog actions" labelledby="dog-toolbar-label">
-          </Doggo.toolbar>
-          """)
-        end
+      assert_raise Doggo.InvalidLabelError, fn ->
+        assigns = %{}
 
-      assert error.message =~ "invalid label attributes"
+        parse_heex(~H"""
+        <Doggo.toolbar label="Dog actions" labelledby="dog-toolbar-label">
+        </Doggo.toolbar>
+        """)
+      end
     end
 
     test "raises if neither label nor labelledby are set" do
-      error =
-        assert_raise RuntimeError, fn ->
-          assigns = %{}
-
-          parse_heex(~H"""
-          <Doggo.toolbar></Doggo.toolbar>
-          """)
-        end
+      assert_raise Doggo.InvalidLabelError, fn ->
+        assigns = %{}
 
-      assert error.message =~ "invalid label attributes"
+        parse_heex(~H"""
+        <Doggo.toolbar></Doggo.toolbar>
+        """)
+      end
     end
 
     test "with global attribute" do
@@ -5702,29 +5672,23 @@ defmodule DoggoTest do
     end
 
     test "raises if both label and labelledby are set" do
-      error =
-        assert_raise RuntimeError, fn ->
-          assigns = %{}
-
-          parse_heex(~H"""
-          <Doggo.tree label="Dogs" labelledby="dog-tree-label"></Doggo.tree>
-          """)
-        end
+      assert_raise Doggo.InvalidLabelError, fn ->
+        assigns = %{}
 
-      assert error.message =~ "invalid label attributes"
+        parse_heex(~H"""
+        <Doggo.tree label="Dogs" labelledby="dog-tree-label"></Doggo.tree>
+        """)
+      end
     end
 
     test "raises if neither label nor labelledby are set" do
-      error =
-        assert_raise RuntimeError, fn ->
-          assigns = %{}
-
-          parse_heex(~H"""
-          <Doggo.tree></Doggo.tree>
-          """)
-        end
+      assert_raise Doggo.InvalidLabelError, fn ->
+        assigns = %{}
 
-      assert error.message =~ "invalid label attributes"
+        parse_heex(~H"""
+        <Doggo.tree></Doggo.tree>
+        """)
+      end
     end
 
     test "with additional class as string" do