diff --git a/lib/doggo.ex b/lib/doggo.ex index f7320978..ee6b66d6 100644 --- a/lib/doggo.ex +++ b/lib/doggo.ex @@ -3332,8 +3332,8 @@ defmodule Doggo do ```heex Favorite Dog - <.radio_group labelled_by="dog-rg-label"> + <.radio_group labelledby="dog-rg-label"> ``` You should ensure that either the `label` or the `labelledby` attribute is @@ -3424,8 +3424,10 @@ defmodule Doggo do -

Favorite Dog

- + With labelledby: + +

Favorite Dog

+ """ end @@ -4311,6 +4313,199 @@ defmodule Doggo do """ end + @doc """ + Renders a hierarchical list as a tree. + + A good use case for this component is a folder structure. For navigation and + other menus, a regular nested list should be preferred. + + > #### Not ready {: .warning} + > + > Doggo does not ship with JavaScript yet. The necessary JavaScript to + > expand and collapse nodes, to select items, and to navigate the tree will + > be added in a later release. + + ## Example + + ```heex + + + Breeds + <:items> + Golden Retriever + Labrador Retriever + + + + Characteristics + <:items> + Playful + Loyal + + + + ``` + + ## CSS + + To target the wrapper, use an attribute selector: + + ```css + [role="tree"] {} + ``` + """ + + @doc type: :form + + attr :label, :string, + default: nil, + doc: """ + A accessibility label for the truee. Set as `aria-label` attribute. + + You should ensure that either the `label` or the `labelledby` attribute is + set. + + Do not repeat the word `tree` in the label, since it is already announced + by screen readers. + """ + + attr :labelledby, :string, + default: nil, + doc: """ + The DOM ID of an element that labels this tree. + + Example: + + ```html +

Dogs

+ <.tree labelledby="dog-tree-label"> + ``` + + You should ensure that either the `label` or the `labelledby` attribute is + set. + """ + + 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, + doc: """ + Slot for the root nodes of the tree. Use the `tree_item/1` component as + direct children. + """ + + 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: + + + + With labelledby: + +

Favorite Dog

+ + """ + end + + ~H""" +
    + <%= render_slot(@inner_block) %> +
+ """ + end + + @doc """ + Renders a tree item within a `tree/1`. + + This component can be used as a direct child of `tree/1` or within the `items` + slot of this component. + + > #### Not ready {: .warning} + > + > Doggo does not ship with JavaScript yet. The necessary JavaScript to + > expand and collapse nodes, to select items, and to navigate the tree will + > be added in a later release. + + ## Example + + ```heex + + + Breeds + <:items> + Golden Retriever + Labrador Retriever + + + + Characteristics + <:items> + Playful + Loyal + + + + ``` + + Icons can be added before the label: + + + Breeds + <:items> + Golden Retriever + Labrador Retriever + + + """ + + slot :items, + doc: """ + Slot for children of this item. Place one or more additional `tree_item/1` + components within this slot, or omit if this is a leaf node. + """ + + slot :inner_block, + required: true, + doc: """ + Slot for the item label. + """ + + def tree_item(assigns) do + ~H""" +
  • + <%= render_slot(@inner_block) %> +
      + <%= render_slot(@items) %> +
    +
  • + """ + end + ## Helpers defp humanize(atom) when is_atom(atom) do diff --git a/priv/storybook/components/tree.story.exs b/priv/storybook/components/tree.story.exs new file mode 100644 index 00000000..29167f11 --- /dev/null +++ b/priv/storybook/components/tree.story.exs @@ -0,0 +1,127 @@ +defmodule Storybook.Components.Tree do + use PhoenixStorybook.Story, :component + + def function, do: &Doggo.tree/1 + + def folder_svg do + """ + + + + """ + end + + def paw_svg do + """ + + + + + + + """ + end + + def like_svg do + """ + + """ + end + + def variations do + [ + %Variation{ + id: :default, + attributes: %{ + label: "Dogs" + }, + slots: [ + """ + + Breeds + <:items> + Golden Retriever + Labrador Retriever + + + + Characteristics + <:items> + Playful + Loyal + + + """ + ] + }, + %Variation{ + id: :with_icons, + attributes: %{ + label: "Dogs" + }, + slots: [ + """ + + #{folder_svg()} Breeds + <:items> + + #{paw_svg()} Golden Retriever + + + #{paw_svg()} Labrador Retriever + + + + + #{folder_svg()} Characteristics + <:items> + + #{paw_svg()} Playful + + + #{paw_svg()} Loyal + + + + """ + ] + } + ] + end +end diff --git a/test/doggo_test.exs b/test/doggo_test.exs index 6ca59351..5ff0a18f 100644 --- a/test/doggo_test.exs +++ b/test/doggo_test.exs @@ -5006,6 +5006,149 @@ defmodule DoggoTest do end end + describe "tree/1" do + test "with label" do + assigns = %{} + + html = + parse_heex(~H""" + + items + + """) + + assert attribute(html, "ul:root", "role") == "tree" + assert attribute(html, ":root", "aria-label") == "Dogs" + assert text(html, ":root") == "items" + end + + test "with labelledby" do + assigns = %{} + + html = + parse_heex(~H""" + + """) + + assert attribute(html, ":root", "aria-labelledby") == "dog-tree-label" + end + + test "raises if both label and labelledby are set" do + error = + assert_raise RuntimeError, fn -> + assigns = %{} + + parse_heex(~H""" + + """) + end + + assert error.message =~ "invalid label attributes" + end + + test "raises if neither label nor labelledby are set" do + error = + assert_raise RuntimeError, fn -> + assigns = %{} + + parse_heex(~H""" + + """) + end + + assert error.message =~ "invalid label attributes" + end + + test "with additional class as string" do + assigns = %{} + + html = + parse_heex(~H""" + + """) + + assert attribute(html, ":root", "class") == "is-nice" + end + + test "with additional classes as list" do + assigns = %{} + + html = + parse_heex(~H""" + + """) + + assert attribute(html, ":root", "class") == "is-nice is-small" + end + + test "with global attribute" do + assigns = %{} + + html = + parse_heex(~H""" + + """) + + assert attribute(html, ":root", "data-test") == "hi" + end + end + + describe "tree_item/1" do + test "without children" do + assigns = %{} + + html = + parse_heex(~H""" + + Breeds + + """) + + assert attribute(html, "li:root", "role") == "treeitem" + assert attribute(html, ":root", "aria-expanded") == nil + assert attribute(html, ":root", "aria-selected") == "false" + assert text(html, ":root > span") == "Breeds" + assert Floki.find(html, ":root ul") == [] + end + + test "with children" do + assigns = %{} + + html = + parse_heex(~H""" + + Breeds + <:items> + Golden Retriever + Labrador Retriever + + + """) + + assert attribute(html, "li:root", "role") == "treeitem" + assert attribute(html, ":root", "aria-expanded") == "false" + assert attribute(html, ":root", "aria-selected") == "false" + assert text(html, ":root > span") == "Breeds" + + assert ul = find_one(html, "li:root > ul") + assert attribute(ul, "role") == "group" + + assert li = find_one(ul, "li:first-child") + assert attribute(li, "role") == "treeitem" + assert attribute(li, ":root", "aria-expanded") == nil + assert attribute(li, ":root", "aria-selected") == "false" + assert text(li, ":root > span") == "Golden Retriever" + assert Floki.find(li, ":root ul") == [] + + assert li = find_one(ul, "li:last-child") + assert attribute(li, "role") == "treeitem" + assert attribute(li, ":root", "aria-expanded") == nil + assert attribute(li, ":root", "aria-selected") == "false" + assert text(li, ":root > span") == "Labrador Retriever" + assert Floki.find(li, ":root ul") == [] + end + end + describe "modifier_classes/1" do test "returns a map of modifier classes" do assert %{variants: [variant | _]} = Doggo.modifier_classes()