Repository: woylie/doggo
Pull request #331 (2024-07-24 23:13:39)

refine icon
- **fix font size calculation**
- **improve icon and icon sprite components**



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 548cd0d..fbeda44 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,30 @@
 
 ## Unreleased
 
+### Added
+
+- Set up design tokens and CSS for demo application based on Barker. Styles for
+  all components will be added in the future.
+
+### Changed
+
+#### Stack component
+
+- Add styles to demo application.
+- Improve story and documentation.
+- Mark component as `stable`.
+
+#### Icon and icon sprite component
+
+- Add styles to demo application.
+- Rename `label_placement` attribute to `text_position`. Change type from
+  atom to string for consistency.
+- Use `before` and `after` as values for `text_position` instead of `left` and
+  `right` to better apply to right-to-left languages. Rename default classes
+  to `has-text-before` and `has-text-after` accordingly.
+- Make `text_position` classes configurable.
+- Add `right-to-left` variation group to icon story.
+
 ## [0.7.0] - 2024-07-24
 
 ### Changed
diff --git a/demo/assets/css/_extends.scss b/demo/assets/css/_extends.scss
index a5b8436..4feb72a 100644
--- a/demo/assets/css/_extends.scss
+++ b/demo/assets/css/_extends.scss
@@ -12,3 +12,13 @@
  */
 
 @use "functions" as f;
+
+%visually-hidden {
+  clip: rect(0 0 0 0);
+  clip-path: inset(50%);
+  height: 1px;
+  overflow: hidden;
+  position: absolute;
+  white-space: nowrap;
+  width: 1px;
+}
diff --git a/demo/assets/css/components/_index.scss b/demo/assets/css/components/_index.scss
index 0067e5d..11e1f03 100644
--- a/demo/assets/css/components/_index.scss
+++ b/demo/assets/css/components/_index.scss
@@ -16,3 +16,5 @@
  *       }
  *     }
  */
+
+@use "icon";
diff --git a/demo/assets/css/components/icon.scss b/demo/assets/css/components/icon.scss
new file mode 100644
index 0000000..cc7a8d9
--- /dev/null
+++ b/demo/assets/css/components/icon.scss
@@ -0,0 +1,60 @@
+@use "../functions" as f;
+
+.icon {
+  display: inline-flex;
+  align-items: baseline;
+  gap: f.spacer(1);
+
+  /* For LTR languages */
+  &.has-text-before {
+    > svg {
+      order: 1;
+    }
+
+    > span {
+      order: 0;
+    }
+  }
+
+  &.has-text-after {
+    > svg {
+      order: 0;
+    }
+
+    > span {
+      order: 1;
+    }
+  }
+
+  > svg {
+    width: 0.75em;
+    width: 1cap;
+    height: 0.75em;
+    height: 1cap;
+  }
+}
+
+/* For RTL languages */
+html[dir="rtl"] {
+  .icon {
+    &.has-text-before {
+      > svg {
+        order: 0;
+      }
+
+      > span {
+        order: 1;
+      }
+    }
+
+    &.has-text-after {
+      > svg {
+        order: 1;
+      }
+
+      > span {
+        order: 0;
+      }
+    }
+  }
+}
diff --git a/demo/assets/css/utilities/_index.scss b/demo/assets/css/utilities/_index.scss
index a201746..cd8ecba 100644
--- a/demo/assets/css/utilities/_index.scss
+++ b/demo/assets/css/utilities/_index.scss
@@ -13,3 +13,4 @@
 @use "colors";
 @use "spacing";
 @use "typography";
+@use "visibility";
diff --git a/demo/assets/css/utilities/_visibility.scss b/demo/assets/css/utilities/_visibility.scss
new file mode 100644
index 0000000..ebb5b95
--- /dev/null
+++ b/demo/assets/css/utilities/_visibility.scss
@@ -0,0 +1,5 @@
+@use "../extends";
+
+.is-visually-hidden {
+  @extend %visually-hidden;
+}
diff --git a/demo/assets/tokens/size/font.mjs b/demo/assets/tokens/size/font.mjs
index 4e83794..b9a503e 100644
--- a/demo/assets/tokens/size/font.mjs
+++ b/demo/assets/tokens/size/font.mjs
@@ -19,7 +19,7 @@ const addSmallerSizes = (sizes, scale, smallerSizes) => {
 
 const addLargerSizes = (sizes, scale, largerSizes) => {
   for (let i = 1; i <= largerSizes; i++) {
-    let rem = Math.pow(scale, -1 * i);
+    let rem = Math.pow(scale, 1 * i);
     let xCount = i - 1;
     let name = "x".repeat(xCount) + "l";
     sizes[name] = { $value: rem.toFixed(3) };
diff --git a/lib/doggo.ex b/lib/doggo.ex
index 09175c7..8108ac6 100644
--- a/lib/doggo.ex
+++ b/lib/doggo.ex
@@ -86,11 +86,6 @@ defmodule Doggo do
   def to_time(%NaiveDateTime{} = dt), do: NaiveDateTime.to_time(dt)
   def to_time(nil), do: nil
 
-  @doc false
-  def label_placement_class(:hidden), do: nil
-  def label_placement_class(:left), do: "has-text-left"
-  def label_placement_class(:right), do: "has-text-right"
-
   @doc false
   def normalize_value("date", %struct{} = value)
       when struct in [Date, NaiveDateTime, DateTime] do
diff --git a/lib/doggo/components/icon.ex b/lib/doggo/components/icon.ex
index 663a50c..b9ef2ab 100644
--- a/lib/doggo/components/icon.ex
+++ b/lib/doggo/components/icon.ex
@@ -27,7 +27,7 @@ defmodule Doggo.Components.Icon do
     To display the text visibly:
 
     ```heex
-    <.icon label="report bug" label_placement={:right}>
+    <.icon label="report bug" text_position="after">
       <Heroicons.bug_ant />
     </.icon>
     ```
@@ -45,13 +45,11 @@ defmodule Doggo.Components.Icon do
       type: :media,
       since: "0.6.0",
       maturity: :developing,
-      modifiers: [
-        size: [
-          values: ["small", "normal", "medium", "large"],
-          default: "normal"
-        ]
-      ],
+      modifiers: [],
       extra: [
+        text_position_after_class: "has-text-after",
+        text_position_before_class: "has-text-before",
+        text_position_hidden_class: nil,
         visually_hidden_class: "is-visually-hidden"
       ]
     ]
@@ -62,19 +60,18 @@ defmodule Doggo.Components.Icon do
     quote do
       slot :inner_block, doc: "Slot for the SVG element.", required: true
 
-      attr :label, :string,
+      attr :text, :string,
         default: nil,
         doc: """
-        Text that describes the icon. If `label_placement` is set to `:hidden`,
-        this text is set as `aria-label` attribute.
+        Text that describes the icon.
         """
 
-      attr :label_placement, :atom,
-        default: :hidden,
-        values: [:left, :right, :hidden],
+      attr :text_position, :string,
+        default: "hidden",
+        values: ["before", "after", "hidden"],
         doc: """
-        Position of the label relative to the icon. If set to `:hidden`, the
-        `label` text is used as `aria-label` attribute.
+        Position of the text relative to the icon. If set to `"hidden"`, the
+        `text` is visually hidden, but still accessible to screen readers.
         """
 
       attr :rest, :global, doc: "Any additional HTML attributes."
@@ -83,18 +80,35 @@ defmodule Doggo.Components.Icon do
 
   @impl true
   def init_block(_opts, extra) do
+    text_position_after_class =
+      Keyword.fetch!(extra, :text_position_after_class)
+
+    text_position_before_class =
+      Keyword.fetch!(extra, :text_position_before_class)
+
+    text_position_hidden_class =
+      Keyword.fetch!(extra, :text_position_hidden_class)
+
     visually_hidden_class = Keyword.fetch!(extra, :visually_hidden_class)
 
     quote do
+      text_position_class =
+        case var!(assigns).text_position do
+          "after" -> unquote(text_position_after_class)
+          "before" -> unquote(text_position_before_class)
+          "hidden" -> unquote(text_position_hidden_class)
+        end
+
+      text_class =
+        if var!(assigns).text_position == "hidden",
+          do: unquote(visually_hidden_class),
+          else: nil
+
       var!(assigns) =
         assigns
         |> var!()
-        |> Map.update!(
-          :class,
-          &(&1 ++
-              [Doggo.label_placement_class(var!(assigns).label_placement)])
-        )
-        |> assign(:visually_hidden_class, unquote(visually_hidden_class))
+        |> Map.update!(:class, &(&1 ++ [text_position_class]))
+        |> assign(:text_class, text_class)
     end
   end
 
@@ -103,11 +117,8 @@ defmodule Doggo.Components.Icon do
     ~H"""
     <span class={@class} {@rest}>
       <%= render_slot(@inner_block) %>
-      <span
-        :if={@label}
-        class={@label_placement == :hidden && @visually_hidden_class}
-      >
-        <%= @label %>
+      <span :if={@text} class={@text_class}>
+        <%= @text %>
       </span>
     </span>
     """
diff --git a/lib/doggo/components/icon_sprite.ex b/lib/doggo/components/icon_sprite.ex
index 1c55521..6dfc758 100644
--- a/lib/doggo/components/icon_sprite.ex
+++ b/lib/doggo/components/icon_sprite.ex
@@ -24,7 +24,7 @@ defmodule Doggo.Components.IconSprite do
     To display the text visibly:
 
     ```heex
-    <.icon name="arrow-left" label="Go back" label_placement={:right} />
+    <.icon name="arrow-left" label="Go back" text_position={:right} />
     ```
     """
   end
@@ -36,13 +36,11 @@ defmodule Doggo.Components.IconSprite do
       since: "0.6.0",
       maturity: :developing,
       base_class: "icon",
-      modifiers: [
-        size: [
-          values: ["small", "normal", "medium", "large"],
-          default: "normal"
-        ]
-      ],
+      modifiers: [],
       extra: [
+        text_position_after_class: "has-text-after",
+        text_position_before_class: "has-text-before",
+        text_position_hidden_class: nil,
         visually_hidden_class: "is-visually-hidden"
       ]
     ]
@@ -59,19 +57,18 @@ defmodule Doggo.Components.IconSprite do
         default: "/assets/icons/sprite.svg",
         doc: "The URL of the SVG sprite."
 
-      attr :label, :string,
+      attr :text, :string,
         default: nil,
         doc: """
-        Text that describes the icon. If `label_placement` is set to `:hidden`, this
-        text is set as `aria-label` attribute.
+        Text that describes the icon.
         """
 
-      attr :label_placement, :atom,
-        default: :hidden,
-        values: [:left, :right, :hidden],
+      attr :text_position, :string,
+        default: "hidden",
+        values: ["before", "after", "hidden"],
         doc: """
-        Position of the label relative to the icon. If set to `:hidden`, the
-        `label` text is used as `aria-label` attribute.
+        Position of the text relative to the icon. If set to `"hidden"`, the
+        `text` is visually hidden, but still accessible to screen readers.
         """
 
       attr :rest, :global, doc: "Any additional HTML attributes."
@@ -80,18 +77,35 @@ defmodule Doggo.Components.IconSprite do
 
   @impl true
   def init_block(_opts, extra) do
+    text_position_after_class =
+      Keyword.fetch!(extra, :text_position_after_class)
+
+    text_position_before_class =
+      Keyword.fetch!(extra, :text_position_before_class)
+
+    text_position_hidden_class =
+      Keyword.fetch!(extra, :text_position_hidden_class)
+
     visually_hidden_class = Keyword.fetch!(extra, :visually_hidden_class)
 
     quote do
+      text_position_class =
+        case var!(assigns).text_position do
+          "after" -> unquote(text_position_after_class)
+          "before" -> unquote(text_position_before_class)
+          "hidden" -> unquote(text_position_hidden_class)
+        end
+
+      text_class =
+        if var!(assigns).text_position == "hidden",
+          do: unquote(visually_hidden_class),
+          else: nil
+
       var!(assigns) =
         assigns
         |> var!()
-        |> Map.update!(
-          :class,
-          &(&1 ++
-              [Doggo.label_placement_class(var!(assigns).label_placement)])
-        )
-        |> assign(:visually_hidden_class, unquote(visually_hidden_class))
+        |> Map.update!(:class, &(&1 ++ [text_position_class]))
+        |> assign(:text_class, text_class)
     end
   end
 
@@ -100,11 +114,8 @@ defmodule Doggo.Components.IconSprite do
     ~H"""
     <span class={@class} {@rest}>
       <svg aria-hidden="true"><use href={"#{@sprite_url}##{@name}"} /></svg>
-      <span
-        :if={@label}
-        class={@label_placement == :hidden && @visually_hidden_class}
-      >
-        <%= @label %>
+      <span :if={@text} class={@text_class}>
+        <%= @text %>
       </span>
     </span>
     """
diff --git a/lib/doggo/storybook/icon.ex b/lib/doggo/storybook/icon.ex
index 5adc08a..f6fd264 100644
--- a/lib/doggo/storybook/icon.ex
+++ b/lib/doggo/storybook/icon.ex
@@ -11,30 +11,58 @@ defmodule Doggo.Storybook.Icon do
         slots: slots()
       },
       %VariationGroup{
-        id: :label,
+        id: :text_ltr,
+        description: "With text (ltr)",
         variations: [
           %Variation{
-            id: :left,
-            attributes: %{label: "info", label_placement: :left},
+            id: :after,
+            attributes: %{text: "text after icon", text_position: "after"},
             slots: slots()
           },
           %Variation{
-            id: :right,
-            attributes: %{label: "info", label_placement: :right},
+            id: :before,
+            attributes: %{text: "text before icon", text_position: "before"},
             slots: slots()
           },
           %Variation{
             id: :hidden,
-            attributes: %{label: "info", label_placement: :hidden},
+            attributes: %{text: "text hidden", text_position: "hidden"},
             slots: slots()
           }
         ]
+      },
+      %VariationGroup{
+        id: :text_rtl,
+        description: "With text (rtl)",
+        variations: [
+          %Variation{
+            id: :after,
+            attributes: %{text: "متن بعد از نماد", text_position: "after"},
+            slots: slots()
+          },
+          %Variation{
+            id: :before,
+            attributes: %{text: "متن قبل از نماد", text_position: "before"},
+            slots: slots()
+          },
+          %Variation{
+            id: :hidden,
+            attributes: %{text: "متن مخفی", text_position: "hidden"},
+            slots: slots()
+          }
+        ],
+        template: """
+        <div dir="rtl">
+          <.psb-variation />
+        </div>
+        """
       }
     ]
   end
 
   def modifier_variation_base(_id, _name, _value, _opts) do
     %{
+      attributes: %{},
       slots: slots()
     }
   end


Test patch:
diff --git a/test/doggo/components_test.exs b/test/doggo/components_test.exs
index 2747663..d578604 100644
--- a/test/doggo/components_test.exs
+++ b/test/doggo/components_test.exs
@@ -2333,66 +2333,55 @@ defmodule Doggo.ComponentsTest do
         """)
 
       span = find_one(html, "span:root")
-      assert attribute(span, "class") == "icon is-normal"
+      assert attribute(span, "class") == "icon"
       assert text(span) == "some-icon"
     end
 
-    test "with size" do
-      assigns = %{}
-
-      html =
-        parse_heex(~H"""
-        <TestComponents.icon size="small">some-icon</TestComponents.icon>
-        """)
-
-      assert attribute(html, "span:root", "class") == "icon is-small"
-    end
-
-    test "with label" do
+    test "with text" do
       assigns = %{}
 
       html =
         parse_heex(~H"""
-        <TestComponents.icon label="some-label">some-icon</TestComponents.icon>
+        <TestComponents.icon text="some-text">some-icon</TestComponents.icon>
         """)
 
       assert span = find_one(html, "span > span")
       assert attribute(span, "class") == "is-visually-hidden"
-      assert text(span) == "some-label"
+      assert text(span) == "some-text"
     end
 
-    test "with label left" do
+    test "with text before" do
       assigns = %{}
 
       html =
         parse_heex(~H"""
-        <TestComponents.icon label="some-label" label_placement={:left}>
+        <TestComponents.icon text="some-text" text_position="before">
           some-icon
         </TestComponents.icon>
         """)
 
       span = find_one(html, "span:root")
-      assert attribute(span, "class") == "icon is-normal has-text-left"
+      assert attribute(span, "class") == "icon has-text-before"
       assert span = find_one(html, "span > span")
       assert attribute(span, "class") == ""
-      assert text(span) == "some-label"
+      assert text(span) == "some-text"
     end
 
-    test "with label right" do
+    test "with text after" do
       assigns = %{}
 
       html =
         parse_heex(~H"""
-        <TestComponents.icon label="some-label" label_placement={:right}>
+        <TestComponents.icon text="some-text" text_position="after">
           some-icon
         </TestComponents.icon>
         """)
 
       span = find_one(html, "span:root")
-      assert attribute(span, "class") == "icon is-normal has-text-right"
+      assert attribute(span, "class") == "icon has-text-after"
       assert span = find_one(html, "span > span")
       assert attribute(span, "class") == ""
-      assert text(span) == "some-label"
+      assert text(span) == "some-text"
     end
 
     test "with global attribute" do
@@ -2417,7 +2406,7 @@ defmodule Doggo.ComponentsTest do
         """)
 
       span = find_one(html, "span:root")
-      assert attribute(span, "class") == "icon is-normal"
+      assert attribute(span, "class") == "icon"
       svg = find_one(span, "svg")
       assert attribute(svg, "aria-hidden") == "true"
       assert attribute(svg, "use", "href") == "/assets/icons/sprite.svg#edit"
@@ -2432,72 +2421,53 @@ defmodule Doggo.ComponentsTest do
         """)
 
       span = find_one(html, "span:root")
-      assert attribute(span, "class") == "icon is-normal"
+      assert attribute(span, "class") == "icon"
       svg = find_one(span, "svg")
       assert attribute(svg, "aria-hidden") == "true"
       assert attribute(svg, "use", "href") == "/images/icons.svg#edit"
     end
 
-    test "with size" do
-      assigns = %{}
-
-      html =
-        parse_heex(~H"""
-        <TestComponents.icon_sprite name="edit" size="small" />
-        """)
-
-      assert attribute(html, "span:root", "class") == "icon is-small"
-    end
-
     test "with label" do
       assigns = %{}
 
       html =
         parse_heex(~H"""
-        <TestComponents.icon_sprite name="edit" label="some-label" />
+        <TestComponents.icon_sprite name="edit" text="some-text" />
         """)
 
       assert span = find_one(html, "span > span")
       assert attribute(span, "class") == "is-visually-hidden"
-      assert text(span) == "some-label"
+      assert text(span) == "some-text"
     end
 
-    test "with label left" do
+    test "with text left" do
       assigns = %{}
 
       html =
         parse_heex(~H"""
-        <TestComponents.icon_sprite
-          name="edit"
-          label="some-label"
-          label_placement={:left}
-        />
+        <TestComponents.icon_sprite name="edit" text="some-text" text_position="before" />
         """)
 
       span = find_one(html, "span:root")
-      assert attribute(span, "class") == "icon is-normal has-text-left"
+      assert attribute(span, "class") == "icon has-text-before"
       assert span = find_one(html, "span > span")
       assert attribute(span, "class") == ""
-      assert text(span) == "some-label"
+      assert text(span) == "some-text"
     end
 
-    test "with label right" do
+    test "with text after" do
       assigns = %{}
 
       html =
         parse_heex(~H"""
-        <TestComponents.icon_sprite
-          name="edit"
-          label="some-label"
-          label_placement={:right}
-        />
+        <TestComponents.icon_sprite name="edit" text="some-text" text_position="after" />
         """)
 
       span = find_one(html, "span:root")
-      assert attribute(span, "class") == "icon is-normal has-text-right"
+      assert attribute(span, "class") == "icon has-text-after"
       assert span = find_one(html, "span > span")
       assert attribute(span, "class") == ""
-      assert text(span) == "some-label"
+      assert text(span) == "some-text"
     end
 
     test "with global attribute" do