diff --git a/lib/live_select/component.ex b/lib/live_select/component.ex index 7eccb3b..c08e452 100644 --- a/lib/live_select/component.ex +++ b/lib/live_select/component.ex @@ -149,6 +149,14 @@ defmodule LiveSelect.Component do |> assign_new(opt, fn -> default end) end) |> update(:options, &normalize_options/1) + |> then(fn socket -> + if Map.has_key?(assigns, :options) && + socket.assigns.active_option >= length(socket.assigns.options) do + assign(socket, active_option: -1) + else + socket + end + end) |> assign(:text_input_field, String.to_atom("#{socket.assigns.field.field}_text_input")) socket = @@ -312,9 +320,18 @@ defmodule LiveSelect.Component do @impl true def handle_event("option_click", %{"idx" => idx}, socket) do - socket = assign(socket, :active_option, String.to_integer(idx)) + idx = String.to_integer(idx) - {:noreply, maybe_select(socket)} + socket = + if idx >= 0 && idx < length(socket.assigns.options) do + socket + |> assign(:active_option, idx) + |> maybe_select() + else + socket + end + + {:noreply, socket} end @impl true diff --git a/test/live_select_test.exs b/test/live_select_test.exs index e8fee8d..ab55024 100644 --- a/test/live_select_test.exs +++ b/test/live_select_test.exs @@ -1155,4 +1155,62 @@ defmodule LiveSelectTest do assert_selected_static(live, "B") end + + test "resets active_option when options shrink via send_update", %{conn: conn} do + stub_options([%{label: "A", value: 1}, %{label: "B", value: 2}, %{label: "C", value: 3}]) + + {:ok, live, _html} = live(conn, "/") + + type(live, "ABC") + + navigate(live, 3, :down) + + send_update(live, options: [%{label: "X", value: 10}, %{label: "Y", value: 20}]) + + keydown(live, "Enter") + + refute_selected(live) + end + + test "does not crash when pressing Enter with out-of-bounds active_option", %{conn: conn} do + stub_options([%{label: "A", value: 1}, %{label: "B", value: 2}, %{label: "C", value: 3}]) + + {:ok, live, _html} = live(conn, "/") + + type(live, "ABC") + + navigate(live, 5, :down) + + send_update(live, options: [%{label: "X", value: 10}, %{label: "Y", value: 20}]) + + keydown(live, "Enter") + + refute_selected(live) + end + + test "ignores out-of-bounds idx in option_click event", %{conn: conn} do + stub_options([%{label: "A", value: 1}, %{label: "B", value: 2}, %{label: "C", value: 3}]) + + {:ok, live, _html} = live(conn, "/") + + type(live, "ABC") + + element(live, selectors()[:container]) + |> render_hook("option_click", %{idx: "999"}) + + refute_selected(live) + end + + test "ignores negative idx in option_click event", %{conn: conn} do + stub_options([%{label: "A", value: 1}, %{label: "B", value: 2}, %{label: "C", value: 3}]) + + {:ok, live, _html} = live(conn, "/") + + type(live, "ABC") + + element(live, selectors()[:container]) + |> render_hook("option_click", %{idx: "-1"}) + + refute_selected(live) + end end