+ class(@style, :selected_option, @selected_option_class)
+
+ @max_selectable > 0 && length(@selection) >= @max_selectable ->
+ class(@style, :unavailable_option, @unavailable_option_class)
+
+ true ->
+ class(@style, :available_option, @available_option_class)
+ end
}>
then(
diff --git a/styling.md b/styling.md
index c20dbb1..8d40436 100644
--- a/styling.md
+++ b/styling.md
@@ -38,13 +38,14 @@ Here's a visual representation of the elements:
![styled elements](https://raw.githubusercontent.com/maxmarcon/live_select/main/priv/static/images/styled_elements.png)
-In `tags` mode there are 4 additional stylable elements:
+In `tags` and `quick_tags` mode there are 6 additional stylable elements:
-7. The **tag** showing the selected options
-8. The **tags_container** that contains the tags
-9. The **selected_option**. This is an option in the dropdown that has already been selected. It's still visible, but can't be selected again
-10. The **available_option**. This is an option in the dropdown that has not been selected and is available for selection
-11. The **clear buttons** to remove the tags
+7. **tag** showing the selected options
+8. **tags_container** that contains the tags
+9. **selected_option**. This is an option in the dropdown that has already been selected. It's still visible, but can't be selected again
+11. **available_option**. This is an option in the dropdown that has not been selected and is available for selection
+10. **unavailable_option**. This is an option in the dropdown that has not been selected but is not available for selection. This happens when there is a specified maximum number of selectable elements and that number has been reached
+12. **clear buttons** to remove the tags
![styled elements_tags](https://raw.githubusercontent.com/maxmarcon/live_select/main/priv/static/images/styled_elements_tags.png)
@@ -66,11 +67,12 @@ The following table shows the default styles for each element and the options yo
| *container* | dropdown dropdown-open | h-full relative text-black | container_class | container_extra_class |
| *dropdown* | bg-base-200 dropdown-content menu menu-compact p-1 rounded-box shadow w-full z-[1] | absolute bg-gray-100 inset-x-0 rounded-md shadow top-full z-50 | dropdown_class | dropdown_extra_class |
| *option* | | px-4 py-1 rounded | option_class | option_extra_class |
-| *selected_option* | disabled | text-gray-400 | selected_option_class | |
+| *selected_option* | cursor-pointer font-bold | cursor-pointer font-bold hover:bg-gray-400 rounded | selected_option_class | |
| *tag* | badge badge-primary p-1.5 text-sm | bg-blue-400 flex p-1 rounded-lg text-sm | tag_class | tag_extra_class |
| *tags_container* | flex flex-wrap gap-1 p-1 | flex flex-wrap gap-1 p-1 | tags_container_class | tags_container_extra_class |
| *text_input* | input input-bordered pr-6 w-full | disabled:bg-gray-100 disabled:placeholder:text-gray-400 disabled:text-gray-400 pr-6 rounded-md w-full | text_input_class | text_input_extra_class |
| *text_input_selected* | input-primary | border-gray-600 text-gray-600 | text_input_selected_class | |
+| *unavailable_option* | disabled | text-gray-400 | unavailable_option_class | |
For example, if you want to remove rounded borders from the options, have the active option use white text on a red background,
and use green as a background color for tags instead of blue, render [live_select/1](`LiveSelect.live_select/1`)
diff --git a/test/live_select/component_test.exs b/test/live_select/component_test.exs
index 463e7d4..1bb72a0 100644
--- a/test/live_select/component_test.exs
+++ b/test/live_select/component_test.exs
@@ -492,6 +492,7 @@ defmodule LiveSelect.ComponentTest do
Keyword.values(
Keyword.drop(override_class_option(), [
:available_option,
+ :unavailable_option,
:selected_option
])
),
@@ -539,7 +540,7 @@ defmodule LiveSelect.ComponentTest do
]
end
- for style <- [:daisyui, :tailwind, :none, nil] do
+ for style <- [nil] do
@style style
describe "when style = #{@style || "default"}" do
@@ -564,7 +565,8 @@ defmodule LiveSelect.ComponentTest do
)
assert Floki.attribute(component, selectors()[@element], "class") == [
- get_in(expected_class(), [@style || default_style(), @element]) || ""
+ (get_in(expected_class(), [@style || default_style(), @element]) || [])
+ |> Enum.join(" ")
]
end
@@ -584,9 +586,8 @@ defmodule LiveSelect.ComponentTest do
if(@style, do: [style: @style], else: []) ++ [{option, "foo"}]
)
- assert Floki.attribute(component, selectors()[@element], "class") == [
- "foo"
- ]
+ assert Floki.attribute(component, selectors()[@element], "class") ==
+ ~W(foo)
end
test "#{@element} class can be overridden with #{override_class_option()[@element]} by passing a list",
@@ -604,9 +605,8 @@ defmodule LiveSelect.ComponentTest do
if(@style, do: [style: @style], else: []) ++ [{option, ["foo", nil, "goo"]}]
)
- assert Floki.attribute(component, selectors()[@element], "class") == [
- "foo goo"
- ]
+ assert Floki.attribute(component, selectors()[@element], "class") ==
+ ~W(foo goo)
end
end
@@ -627,9 +627,9 @@ defmodule LiveSelect.ComponentTest do
)
assert Floki.attribute(component, selectors()[@element], "class") == [
- ((get_in(expected_class(), [@style || default_style(), @element]) || "") <>
- " foo")
- |> String.trim()
+ ((get_in(expected_class(), [@style || default_style(), @element]) || []) ++
+ ~W(foo))
+ |> Enum.join(" ")
]
end
@@ -649,9 +649,9 @@ defmodule LiveSelect.ComponentTest do
)
assert Floki.attribute(component, selectors()[@element], "class") == [
- ((get_in(expected_class(), [@style || default_style(), @element]) || "") <>
- " foo goo")
- |> String.trim()
+ ((get_in(expected_class(), [@style || default_style(), @element]) || []) ++
+ ~W(foo goo))
+ |> Enum.join(" ")
]
end
@@ -662,10 +662,10 @@ defmodule LiveSelect.ComponentTest do
base_classes = get_in(expected_class(), [@style || default_style(), @element])
if base_classes do
- class_to_remove = String.split(base_classes) |> List.first()
+ class_to_remove = base_classes |> List.first()
expected_classes =
- String.split(base_classes)
+ base_classes
|> Enum.drop(1)
|> Enum.join(" ")
@@ -701,12 +701,12 @@ defmodule LiveSelect.ComponentTest do
)
expected_class =
- (get_in(expected_class(), [@style || default_style(), :text_input]) || "") <>
- " " <>
- (get_in(expected_class(), [@style || default_style(), :text_input_selected]) || "")
+ ((get_in(expected_class(), [@style || default_style(), :text_input]) || []) ++
+ (get_in(expected_class(), [@style || default_style(), :text_input_selected]) || []))
+ |> Enum.join(" ")
assert Floki.attribute(component, selectors()[:text_input], "class") == [
- String.trim(expected_class)
+ expected_class
]
end
@@ -724,96 +724,15 @@ defmodule LiveSelect.ComponentTest do
)
expected_class =
- (get_in(expected_class(), [@style || default_style(), :text_input]) || "") <>
- " foo"
+ ((get_in(expected_class(), [@style || default_style(), :text_input]) || []) ++
+ ~W(foo))
+ |> Enum.join(" ")
assert Floki.attribute(component, selectors()[:text_input], "class") == [
String.trim(expected_class)
]
end
- test "class for selected option is set", %{form: form} do
- component =
- render_component(
- &LiveSelect.live_select/1,
- [
- mode: :tags,
- field: form[:city_search],
- options: ["A", "B", "C"],
- value: "B"
- ] ++
- if(@style, do: [style: @style], else: [])
- )
-
- assert_selected_option_class(
- component,
- 2,
- get_in(expected_class(), [@style || default_style(), :selected_option]) || ""
- )
- end
-
- test "class for selected option can be overridden", %{form: form} do
- component =
- render_component(
- &LiveSelect.live_select/1,
- [
- mode: :tags,
- field: form[:city_search],
- options: ["A", "B", "C"],
- value: "B",
- selected_option_class: "foo"
- ] ++
- if(@style, do: [style: @style], else: [])
- )
-
- assert_selected_option_class(
- component,
- 2,
- "foo"
- )
- end
-
- test "class for available option is set", %{form: form} do
- component =
- render_component(
- &LiveSelect.live_select/1,
- [
- mode: :tags,
- field: form[:city_search],
- options: ["A", "B", "C"],
- value: "B"
- ] ++
- if(@style, do: [style: @style], else: [])
- )
-
- assert_available_option_class(
- component,
- 2,
- get_in(expected_class(), [@style || default_style(), :available_option]) || ""
- )
- end
-
- test "class for available option can be overridden", %{form: form} do
- component =
- render_component(
- &LiveSelect.live_select/1,
- [
- mode: :tags,
- field: form[:city_search],
- options: ["A", "B", "C"],
- value: "B",
- available_option_class: "foo"
- ] ++
- if(@style, do: [style: @style], else: [])
- )
-
- assert_available_option_class(
- component,
- 2,
- "foo"
- )
- end
-
test "class for clear button can be overridden", %{form: form} do
component =
render_component(
@@ -829,7 +748,7 @@ defmodule LiveSelect.ComponentTest do
if(@style, do: [style: @style], else: [])
)
- assert Floki.attribute(component, selectors()[:clear_button], "class") == ["foo"]
+ assert Floki.attribute(component, selectors()[:clear_button], "class") == ~W(foo)
end
if @style != :none do
@@ -849,8 +768,9 @@ defmodule LiveSelect.ComponentTest do
)
assert Floki.attribute(component, selectors()[:clear_button], "class") == [
- ((get_in(expected_class(), [@style || default_style(), :clear_button]) || "") <>
- " foo")
+ ((get_in(expected_class(), [@style || default_style(), :clear_button]) || []) ++
+ ~W(foo))
+ |> Enum.join(" ")
|> String.trim()
]
end
@@ -877,7 +797,8 @@ defmodule LiveSelect.ComponentTest do
)
assert Floki.attribute(component, selectors()[@element], "class") == [
- get_in(expected_class(), [@style || default_style(), @element]) || ""
+ (get_in(expected_class(), [@style || default_style(), @element]) || [])
+ |> Enum.join(" ")
]
end
@@ -899,9 +820,7 @@ defmodule LiveSelect.ComponentTest do
if(@style, do: [style: @style], else: []) ++ [{option, "foo"}]
)
- assert Floki.attribute(component, selectors()[@element], "class") == [
- "foo"
- ]
+ assert Floki.attribute(component, selectors()[@element], "class") == ~w(foo)
end
end
@@ -924,8 +843,9 @@ defmodule LiveSelect.ComponentTest do
)
assert Floki.attribute(component, selectors()[@element], "class") == [
- ((get_in(expected_class(), [@style || default_style(), @element]) || "") <>
- " foo")
+ ((get_in(expected_class(), [@style || default_style(), @element]) || []) ++
+ ~W(foo))
+ |> Enum.join(" ")
|> String.trim()
]
end
@@ -937,10 +857,10 @@ defmodule LiveSelect.ComponentTest do
base_classes = get_in(expected_class(), [@style || default_style(), @element])
if base_classes do
- class_to_remove = String.split(base_classes) |> List.first()
+ class_to_remove = base_classes |> List.first()
expected_classes =
- String.split(base_classes)
+ base_classes
|> Enum.drop(1)
|> Enum.join(" ")
diff --git a/test/live_select_test.exs b/test/live_select_test.exs
index 9f8ae89..ffb5558 100644
--- a/test/live_select_test.exs
+++ b/test/live_select_test.exs
@@ -848,6 +848,115 @@ defmodule LiveSelectTest do
"foo"
)
end
+
+ test "class for selected option is set", %{conn: conn} do
+ {:ok, live, _html} = live(conn, "/?style=#{@style}")
+
+ stub_options(["A", "B", "C"])
+
+ type(live, "ABC")
+
+ select_nth_option(live, 2)
+
+ type(live, "ABC")
+
+ assert_selected_option_class(
+ live,
+ 2,
+ get_in(expected_class(), [@style || default_style(), :selected_option]) || []
+ )
+ end
+
+ test "class for selected option can be overridden", %{conn: conn} do
+ {:ok, live, _html} = live(conn, "/?style=#{@style}&selected_option_class=foo")
+
+ stub_options(["A", "B", "C"])
+
+ type(live, "ABC")
+
+ select_nth_option(live, 2)
+
+ type(live, "ABC")
+
+ assert_selected_option_class(
+ live,
+ 2,
+ ~W(foo)
+ )
+ end
+
+ test "class for available option is set", %{conn: conn} do
+ {:ok, live, _html} = live(conn, "/?style=#{@style}")
+
+ stub_options(["A", "B", "C"])
+
+ type(live, "ABC")
+
+ select_nth_option(live, 2)
+
+ type(live, "ABC")
+
+ assert_available_option_class(
+ live,
+ 2,
+ get_in(expected_class(), [@style || default_style(), :available_option]) || []
+ )
+ end
+
+ test "class for available option can be overridden", %{conn: conn} do
+ {:ok, live, _html} = live(conn, "/?style=#{@style}&available_option_class=foo")
+
+ stub_options(["A", "B", "C"])
+
+ type(live, "ABC")
+
+ select_nth_option(live, 2)
+
+ type(live, "ABC")
+
+ assert_available_option_class(
+ live,
+ 2,
+ ~W(foo)
+ )
+ end
+
+ test "class for unavailable option is set", %{conn: conn} do
+ {:ok, live, _html} = live(conn, "/?style=#{@style}&mode=tags&max_selectable=1")
+
+ stub_options(["A", "B", "C"])
+
+ type(live, "ABC")
+
+ select_nth_option(live, 2)
+
+ type(live, "ABC")
+
+ assert_unavailable_option_class(
+ live,
+ 2,
+ get_in(expected_class(), [@style || default_style(), :unavailable_option]) || []
+ )
+ end
+
+ test "class for unavailable option can be overridden", %{conn: conn} do
+ {:ok, live, _html} =
+ live(conn, "/?style=#{@style}&mode=tags&max_selectable=1&unavailable_option_class=foo")
+
+ stub_options(["A", "B", "C"])
+
+ type(live, "ABC")
+
+ select_nth_option(live, 2)
+
+ type(live, "ABC")
+
+ assert_unavailable_option_class(
+ live,
+ 2,
+ ~W(foo)
+ )
+ end
end
end
end
diff --git a/test/support/helpers.ex b/test/support/helpers.ex
index 9c4bcda..a89a3a4 100644
--- a/test/support/helpers.ex
+++ b/test/support/helpers.ex
@@ -8,39 +8,43 @@ defmodule LiveSelect.TestHelpers do
@expected_class [
daisyui: [
- active_option: ~S(active),
- available_option: ~S(cursor-pointer),
- clear_button: ~S(hidden cursor-pointer),
- clear_tag_button: ~S(cursor-pointer),
- container: ~S(dropdown dropdown-open),
+ active_option: ~W(active),
+ available_option: ~W(cursor-pointer),
+ unavailable_option: ~W(disabled),
+ clear_button: ~W(hidden cursor-pointer),
+ clear_tag_button: ~W(cursor-pointer),
+ container: ~W(dropdown dropdown-open),
dropdown:
- ~S(dropdown-content z-[1] menu menu-compact shadow rounded-box bg-base-200 p-1 w-full),
- selected_option: ~S(disabled),
- tag: ~S(p-1.5 text-sm badge badge-primary),
- tags_container: ~S(flex flex-wrap gap-1 p-1),
- text_input: ~S(input input-bordered w-full pr-6),
- text_input_selected: ~S(input-primary)
+ ~W(dropdown-content z-[1] menu menu-compact shadow rounded-box bg-base-200 p-1 w-full),
+ option: nil,
+ selected_option: ~W(cursor-pointer font-bold),
+ text_input: ~W(input input-bordered w-full pr-6),
+ text_input_selected: ~W(input-primary),
+ tags_container: ~W(flex flex-wrap gap-1 p-1),
+ tag: ~W(p-1.5 text-sm badge badge-primary)
],
tailwind: [
- active_option: ~S(text-white bg-gray-600),
- available_option: ~S(cursor-pointer hover:bg-gray-400 rounded),
- clear_button: ~S(hidden cursor-pointer),
- clear_tag_button: ~S(cursor-pointer),
- container: ~S(h-full text-black relative),
- dropdown: ~S(absolute rounded-md shadow z-50 bg-gray-100 inset-x-0 top-full),
- option: ~S(rounded px-4 py-1),
- selected_option: ~S(text-gray-400),
- tag: ~S(p-1 text-sm rounded-lg bg-blue-400 flex),
- tags_container: ~S(flex flex-wrap gap-1 p-1),
+ active_option: ~W(text-white bg-gray-600),
+ available_option: ~W(cursor-pointer hover:bg-gray-400 rounded),
+ unavailable_option: ~W(text-gray-400),
+ clear_button: ~W(hidden cursor-pointer),
+ clear_tag_button: ~W(cursor-pointer),
+ container: ~W(h-full text-black relative),
+ dropdown: ~W(absolute rounded-md shadow z-50 bg-gray-100 inset-x-0 top-full),
+ option: ~W(rounded px-4 py-1),
+ selected_option: ~W(cursor-pointer font-bold hover:bg-gray-400 rounded),
text_input:
- ~S(rounded-md w-full disabled:bg-gray-100 disabled:placeholder:text-gray-400 disabled:text-gray-400 pr-6),
- text_input_selected: ~S(border-gray-600 text-gray-600)
+ ~W(rounded-md w-full disabled:bg-gray-100 disabled:placeholder:text-gray-400 disabled:text-gray-400 pr-6),
+ text_input_selected: ~W(border-gray-600 text-gray-600),
+ tags_container: ~W(flex flex-wrap gap-1 p-1),
+ tag: ~W(p-1 text-sm rounded-lg bg-blue-400 flex)
]
]
def expected_class(), do: @expected_class
@override_class_option [
available_option: :available_option_class,
+ unavailable_option: :unavailable_option_class,
clear_button: :clear_button_class,
clear_tag_button: :clear_tag_button_class,
container: :container_class,
@@ -340,46 +344,81 @@ defmodule LiveSelect.TestHelpers do
})
end
- def assert_selected_option_class(_live, _selected_pos, ""), do: true
+ def assert_selected_option_class(_html, _selected_pos, []), do: true
- def assert_selected_option_class(html, selected_pos, selected_class) when is_binary(html) do
+ def assert_selected_option_class(html, selected_pos, selected_class)
+ when is_binary(html) and is_list(selected_class) do
element_classes =
html
- |> Floki.attribute("ul[name=live-select-dropdown] > li", "class")
+ |> Floki.attribute("ul > li", "class")
|> Enum.map(&String.trim/1)
# ensure we're checking both selected and unselected elements
- assert length(element_classes) > selected_pos || selected_pos > 1
+ assert length(element_classes) > selected_pos
+ selected_class = Enum.join(selected_class, " ")
for {element_class, idx} <- Enum.with_index(element_classes, 1) do
if idx == selected_pos do
- assert String.contains?(element_class, selected_class)
+ assert element_class == selected_class
else
- refute String.contains?(element_class, selected_class)
+ assert element_class != selected_class
end
end
end
- def assert_available_option_class(_live, _selected_pos, ""), do: true
+ def assert_selected_option_class(live, selected_pos, selected_class),
+ do: assert_selected_option_class(render(live), selected_pos, selected_class)
- def assert_available_option_class(html, selected_pos, available_class) when is_binary(html) do
+ def assert_available_option_class(_html, _selected_pos, []), do: true
+
+ def assert_available_option_class(html, selected_pos, available_class)
+ when is_binary(html) and is_list(available_class) do
element_classes =
html
- |> Floki.attribute("ul[name=live-select-dropdown] > li", "class")
+ |> Floki.attribute("ul > li", "class")
|> Enum.map(&String.trim/1)
# ensure we're checking both selected and unselected elements
- assert length(element_classes) > selected_pos || selected_pos > 1
+ assert length(element_classes) > selected_pos
+ available_class = Enum.join(available_class, " ")
for {element_class, idx} <- Enum.with_index(element_classes, 1) do
if idx == selected_pos do
- refute String.contains?(element_class, available_class)
+ assert element_class != available_class
else
- assert String.contains?(element_class, available_class)
+ assert element_class == available_class
end
end
end
+ def assert_available_option_class(live, selected_pos, available_class),
+ do: assert_available_option_class(render(live), selected_pos, available_class)
+
+ def assert_unavailable_option_class(_html, _selected_pos, []), do: true
+
+ def assert_unavailable_option_class(html, selected_pos, unavailable_class)
+ when is_binary(html) and is_list(unavailable_class) do
+ element_classes =
+ html
+ |> Floki.attribute("ul > li", "class")
+ |> Enum.map(&String.trim/1)
+
+ # ensure we're checking both selected and unselected elements
+ assert length(element_classes) > selected_pos
+ unavailable_class = Enum.join(unavailable_class, " ")
+
+ for {element_class, idx} <- Enum.with_index(element_classes, 1) do
+ if idx == selected_pos do
+ assert element_class != unavailable_class
+ else
+ assert element_class == unavailable_class
+ end
+ end
+ end
+
+ def assert_unavailable_option_class(live, selected_pos, unavailable_class),
+ do: assert_unavailable_option_class(render(live), selected_pos, unavailable_class)
+
def assert_clear(live, input_event \\ true) do
assert_clear_static(live)