From d99d02a4dbb6f77f51c05c8b68f9b624c51b35ce Mon Sep 17 00:00:00 2001 From: Steffen Deusch Date: Wed, 30 Oct 2024 19:46:19 +0100 Subject: [PATCH] restore focus after patching cloned tree (#3476) The simplified morphdom call used to patch cloned trees did not restore focus to the previously focused element. This commit applies the same restoreFocus logic used in the normal morphdom call. Fixes #3448. --- assets/js/phoenix_live_view/dom_patch.js | 7 ++- test/e2e/support/issues/issue_3448.ex | 65 ++++++++++++++++++++++++ test/e2e/test_helper.exs | 1 + test/e2e/tests/issues/3448.spec.js | 17 +++++++ 4 files changed, 88 insertions(+), 2 deletions(-) create mode 100644 test/e2e/support/issues/issue_3448.ex create mode 100644 test/e2e/tests/issues/3448.spec.js diff --git a/assets/js/phoenix_live_view/dom_patch.js b/assets/js/phoenix_live_view/dom_patch.js index 18c6a6a396..fe93a977a6 100644 --- a/assets/js/phoenix_live_view/dom_patch.js +++ b/assets/js/phoenix_live_view/dom_patch.js @@ -28,7 +28,8 @@ import morphdom from "morphdom" export default class DOMPatch { static patchWithClonedTree(container, clonedTree, liveSocket){ - let activeElement = liveSocket.getActiveElement() + let focused = liveSocket.getActiveElement() + let {selectionStart, selectionEnd} = focused && DOM.hasSelectionRange(focused) ? focused : {} let phxUpdate = liveSocket.binding(PHX_UPDATE) morphdom(container, clonedTree, { @@ -38,12 +39,14 @@ export default class DOMPatch { // we cannot morph locked children if(!container.isSameNode(fromEl) && fromEl.hasAttribute(PHX_REF_LOCK)){ return false } if(DOM.isIgnored(fromEl, phxUpdate)){ return false } - if(activeElement && activeElement.isSameNode(fromEl) && DOM.isFormInput(fromEl)){ + if(focused && focused.isSameNode(fromEl) && DOM.isFormInput(fromEl)){ DOM.mergeFocusedInput(fromEl, toEl) return false } } }) + + liveSocket.silenceEvents(() => DOM.restoreFocus(focused, selectionStart, selectionEnd)) } constructor(view, container, id, html, streams, targetCID){ diff --git a/test/e2e/support/issues/issue_3448.ex b/test/e2e/support/issues/issue_3448.ex new file mode 100644 index 0000000000..2ddb878169 --- /dev/null +++ b/test/e2e/support/issues/issue_3448.ex @@ -0,0 +1,65 @@ +defmodule Phoenix.LiveViewTest.E2E.Issue3448Live do + # https://github.com/phoenixframework/phoenix_live_view/issues/3448 + + use Phoenix.LiveView + + alias Phoenix.LiveView.JS + + def mount(_params, _session, socket) do + form = to_form(%{"a" => []}) + + {:ok, assign_new(socket, :form, fn -> form end)} + end + + def render(assigns) do + ~H""" + <.form for={@form} id="my_form" phx-change="validate" class="flex flex-col gap-2"> + <.my_component> + <:left_content :for={value <- @form[:a].value || []}> +
<%= value %>
+ + + +
+ "[]"} + value="settings" + checked={"settings" in (@form[:a].value || [])} + phx-click={JS.dispatch("input") |> JS.focus(to: "#search")} + /> + + "[]"} + value="content" + checked={"content" in (@form[:a].value || [])} + phx-click={JS.dispatch("input") |> JS.focus(to: "#search")} + /> +
+ + """ + end + + def handle_event("validate", params, socket) do + {:noreply, assign(socket, form: to_form(params))} + end + + def handle_event("search", _params, socket) do + {:noreply, socket} + end + + slot :left_content + + defp my_component(assigns) do + ~H""" +
+
+ <%= render_slot(left_content) %> +
+ + +
+ """ + end +end diff --git a/test/e2e/test_helper.exs b/test/e2e/test_helper.exs index 44a2c9b0e9..a3bd917693 100644 --- a/test/e2e/test_helper.exs +++ b/test/e2e/test_helper.exs @@ -148,6 +148,7 @@ defmodule Phoenix.LiveViewTest.E2E.Router do live "/3194", Issue3194Live live "/3194/other", Issue3194Live.OtherLive live "/3378", Issue3378.HomeLive + live "/3448", Issue3448Live end end diff --git a/test/e2e/tests/issues/3448.spec.js b/test/e2e/tests/issues/3448.spec.js new file mode 100644 index 0000000000..ce2b2707e9 --- /dev/null +++ b/test/e2e/tests/issues/3448.spec.js @@ -0,0 +1,17 @@ +const { test, expect } = require("../../test-fixtures"); +const { syncLV } = require("../../utils"); + +// https://github.com/phoenixframework/phoenix_live_view/issues/3448 +test("focus is handled correctly when patching locked form", async ({ page }) => { + await page.goto("/issues/3448"); + await syncLV(page); + + await page.evaluate(() => window.liveSocket.enableLatencySim(500)); + + await page.locator("input[type=checkbox]").first().check(); + await expect(page.locator("input#search")).toBeFocused(); + await syncLV(page); + + // after the patch is applied, the input should still be focused + await expect(page.locator("input#search")).toBeFocused(); +});