Skip to content

Commit

Permalink
restore focus after patching cloned tree
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
SteffenDE committed Oct 20, 2024
1 parent 280d381 commit 8fceed6
Show file tree
Hide file tree
Showing 4 changed files with 88 additions and 2 deletions.
7 changes: 5 additions & 2 deletions assets/js/phoenix_live_view/dom_patch.js
Original file line number Diff line number Diff line change
Expand Up @@ -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, {
Expand All @@ -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){
Expand Down
65 changes: 65 additions & 0 deletions test/e2e/support/issues/issue_3448.ex
Original file line number Diff line number Diff line change
@@ -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 || []}>
<div><%= value %></div>
</:left_content>
</.my_component>
<div class="flex gap-2">
<input
type="checkbox"
name={@form[:a].name <> "[]"}
value="settings"
checked={"settings" in (@form[:a].value || [])}
phx-click={JS.dispatch("input") |> JS.focus(to: "#search")}
/>
<input
type="checkbox"
name={@form[:a].name <> "[]"}
value="content"
checked={"content" in (@form[:a].value || [])}
phx-click={JS.dispatch("input") |> JS.focus(to: "#search")}
/>
</div>
</.form>
"""
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"""
<div>
<div :for={left_content <- @left_content}>
<%= render_slot(left_content) %>
</div>
<input id="search" type="search" name="value" phx-change="search" />
</div>
"""
end
end
1 change: 1 addition & 0 deletions test/e2e/test_helper.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
17 changes: 17 additions & 0 deletions test/e2e/tests/issues/3448.spec.js
Original file line number Diff line number Diff line change
@@ -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();
});

0 comments on commit 8fceed6

Please sign in to comment.