Skip to content

Commit

Permalink
restore focus after patching cloned tree (phoenixframework#3476)
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 phoenixframework#3448.
  • Loading branch information
SteffenDE authored Oct 30, 2024
1 parent dc52a5a commit d99d02a
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 d99d02a

Please sign in to comment.