Compare commits

..

1 Commits

Author SHA1 Message Date
eb8fe7622a Fix autofocus on dynamically inserted forms, remove this.blur() handlers
form_controller.connect() now blurs the previously focused element and
explicitly focuses the [autofocus] element when a form is inserted into
the DOM (via Turbo Stream). Only runs when an [autofocus] element is
present, so closing forms and other stream updates are unaffected.

Remove all onclick='this.blur()' inline handlers from templates — they
were a workaround for the same autofocus problem, now solved properly
via the Stimulus lifecycle.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 14:16:34 +00:00
17 changed files with 33 additions and 43 deletions

View File

@@ -1,6 +1,14 @@
import { Controller } from "@hotwired/stimulus" import { Controller } from "@hotwired/stimulus"
export default class extends Controller { export default class extends Controller {
connect() {
const autofocusEl = this.element.querySelector('[autofocus]')
if (autofocusEl) {
document.activeElement?.blur()
autofocusEl.focus()
}
}
processKey(event) { processKey(event) {
switch (event.key) { switch (event.key) {
case "Escape": case "Escape":
@@ -22,30 +30,4 @@ export default class extends Controller {
event.preventDefault() event.preventDefault()
} }
} }
cancel(event) {
event.preventDefault()
const el = this.element
// Move focus to next open form if this one had focus
const focused = document.activeElement
if (!focused || focused === document.body || el.contains(focused)) {
const next = el.parentElement?.querySelector(`#${el.id} ~ tr:has([autofocus])`)
?? el.parentElement?.querySelector("tr:has([autofocus])")
next?.querySelector("[autofocus]")?.focus()
}
// Remove associated inner form element (tabular: <tr> wraps a separate <form>)
document.getElementById(el.dataset.form)?.remove()
// Re-enable trigger link
const enableId = el.dataset.link ?? el.dataset.cancelEnable
if (enableId) Turbo.StreamElement.prototype.enableElement(document.getElementById(enableId))
// Show hidden row (tabular forms) or no-items placeholder (create form)
document.getElementById(el.dataset.hiddenRow)?.removeAttribute("style")
document.getElementById(el.dataset.cancelShow)?.style.removeProperty("display")
el.remove()
}
} }

View File

@@ -19,7 +19,7 @@
<%= form.button %> <%= form.button %>
<%= image_link_to t(:cancel), "close-outline", measurements_path, <%= image_link_to t(:cancel), "close-outline", measurements_path,
class: 'dangerous', name: :cancel, class: 'dangerous', name: :cancel,
data: {action: 'click->form#cancel'} %> onclick: render_turbo_stream('edit_form_close', {row: row}) %>
</td> </td>
<% end %> <% end %>
<% end %> <% end %>

View File

@@ -0,0 +1,2 @@
<%= turbo_stream.close_form row %>
<%= turbo_stream.update :flashes %>

View File

@@ -26,7 +26,7 @@
<%= form.button %> <%= form.button %>
<%= image_link_to t(:cancel), "close-outline", measurements_path, <%= image_link_to t(:cancel), "close-outline", measurements_path,
class: 'dangerous', name: :cancel, class: 'dangerous', name: :cancel,
data: {action: 'click->form#cancel'} %> onclick: render_turbo_stream('edit_form_close', {row: row}) %>
</td> </td>
<% end %> <% end %>
</tbody> </tbody>

View File

@@ -1,8 +1,6 @@
<%= tabular_form_with model: Measurement.new, id: :measurement_form, <%= tabular_form_with model: Measurement.new, id: :measurement_form,
class: 'topside-area flex vertical center', class: 'topside-area flex vertical center',
html: {data: {controller: 'form', action: 'keydown->form#processKey', html: {data: {controller: 'form', action: 'keydown->form#processKey'}} do |form| %>
cancel_enable: 'new_measurement_link',
cancel_show: 'no_items'}} do |form| %>
<table class="items-table center"> <table class="items-table center">
<tbody id="readouts"> <tbody id="readouts">
@@ -36,6 +34,6 @@
<div class="flex reverse"> <div class="flex reverse">
<%= form.button id: :create_measurement_button, disabled: true -%> <%= form.button id: :create_measurement_button, disabled: true -%>
<%= image_link_to t(:cancel), "close-outline", measurements_path, name: :cancel, <%= image_link_to t(:cancel), "close-outline", measurements_path, name: :cancel,
class: 'dangerous', data: {action: 'click->form#cancel'} %> class: 'dangerous', onclick: render_turbo_stream('form_close') %>
</div> </div>
<% end %> <% end %>

View File

@@ -0,0 +1,4 @@
<%= turbo_stream.update :flashes %>
<%= turbo_stream.remove :measurement_form %>
<%= turbo_stream.show :no_items -%>
<%= turbo_stream.enable :new_measurement_link -%>

View File

@@ -4,7 +4,7 @@
<td> <td>
<% if current_user.at_least(:active) %> <% if current_user.at_least(:active) %>
<%= link_to readout.quantity, edit_measurement_path(readout), <%= link_to readout.quantity, edit_measurement_path(readout),
class: 'link', onclick: 'this.blur();', data: {turbo_stream: true} %> class: 'link', data: {turbo_stream: true} %>
<% else %> <% else %>
<%= readout.quantity %> <%= readout.quantity %>
<% end %> <% end %>

View File

@@ -20,7 +20,7 @@
<% if current_user.at_least(:active) %> <% if current_user.at_least(:active) %>
<%= link_to format("%.10g", readout.value), <%= link_to format("%.10g", readout.value),
edit_measurement_path(readout, view: :wide), edit_measurement_path(readout, view: :wide),
class: 'link', onclick: 'this.blur();', class: 'link',
data: {turbo_stream: true} %> data: {turbo_stream: true} %>
<% else %> <% else %>
<%= format("%.10g", readout.value) %> <%= format("%.10g", readout.value) %>

View File

@@ -2,7 +2,7 @@
<div class="rightside-area buttongrid" data-controller="measurements-view"> <div class="rightside-area buttongrid" data-controller="measurements-view">
<% if current_user.at_least(:active) %> <% if current_user.at_least(:active) %>
<%= image_link_to t('.new_measurement'), 'plus-outline', new_measurement_path, <%= image_link_to t('.new_measurement'), 'plus-outline', new_measurement_path,
id: :new_measurement_link, onclick: 'this.blur();', id: :new_measurement_link,
data: {turbo_stream: true} %> data: {turbo_stream: true} %>
<% end %> <% end %>
<%= image_button_tag '', 'view-rows', name: nil, type: 'button', <%= image_button_tag '', 'view-rows', name: nil, type: 'button',

View File

@@ -18,7 +18,7 @@
<td class="flex"> <td class="flex">
<%= form.button %> <%= form.button %>
<%= image_link_to t(:cancel), "close-outline", quantities_path, class: 'dangerous', <%= image_link_to t(:cancel), "close-outline", quantities_path, class: 'dangerous',
name: :cancel, data: {action: 'click->form#cancel'} %> name: :cancel, onclick: render_turbo_stream('form_close', {row: row}) %>
</td> </td>
<td></td> <td></td>
<% end %> <% end %>

View File

@@ -0,0 +1,2 @@
<%= turbo_stream.close_form row %>
<%= turbo_stream.update :flashes %>

View File

@@ -8,7 +8,7 @@
<td style="--depth:<%= quantity.depth %>"> <td style="--depth:<%= quantity.depth %>">
<%= link_to quantity, edit_quantity_path(quantity), class: 'link', <%= link_to quantity, edit_quantity_path(quantity), class: 'link',
onclick: 'this.blur();', data: {turbo_stream: true} %> data: {turbo_stream: true} %>
</td> </td>
<td><%= quantity.description %></td> <td><%= quantity.description %></td>
<td><%= quantity.default_unit&.symbol %></td> <td><%= quantity.default_unit&.symbol %></td>
@@ -16,7 +16,7 @@
<% if current_user.at_least(:active) %> <% if current_user.at_least(:active) %>
<td class="flex"> <td class="flex">
<%= image_link_to t('.new_subquantity'), 'plus-outline', new_quantity_path(quantity), <%= image_link_to t('.new_subquantity'), 'plus-outline', new_quantity_path(quantity),
id: dom_id(quantity, :new, :link), onclick: 'this.blur();', data: {turbo_stream: true} %> id: dom_id(quantity, :new, :link), data: {turbo_stream: true} %>
<%= image_button_to_if quantity.destroyable?, t('.destroy'), 'delete-outline', <%= image_button_to_if quantity.destroyable?, t('.destroy'), 'delete-outline',
quantity_path(quantity), method: :delete %> quantity_path(quantity), method: :delete %>

View File

@@ -1,7 +1,7 @@
<div class="rightside-area buttongrid"> <div class="rightside-area buttongrid">
<% if current_user.at_least(:active) %> <% if current_user.at_least(:active) %>
<%= image_link_to t('.new_quantity'), 'plus-outline', new_quantity_path, <%= image_link_to t('.new_quantity'), 'plus-outline', new_quantity_path,
id: dom_id(Quantity, :new, :link), onclick: 'this.blur();', id: dom_id(Quantity, :new, :link),
data: {turbo_stream: true} %> data: {turbo_stream: true} %>
<% end %> <% end %>
<%#= image_link_to t('.import_quantities'), 'download-outline', default_quantities_path, <%#= image_link_to t('.import_quantities'), 'download-outline', default_quantities_path,

View File

@@ -16,7 +16,7 @@
<td class="flex"> <td class="flex">
<%= form.button %> <%= form.button %>
<%= image_link_to t(:cancel), "close-outline", units_path, class: 'dangerous', <%= image_link_to t(:cancel), "close-outline", units_path, class: 'dangerous',
name: :cancel, data: {action: 'click->form#cancel'} %> name: :cancel, onclick: render_turbo_stream('form_close', {row: row}) %>
</td> </td>
<td></td> <td></td>
<% end %> <% end %>

View File

@@ -0,0 +1,2 @@
<%= turbo_stream.close_form row %>
<%= turbo_stream.update :flashes %>

View File

@@ -7,7 +7,7 @@
drag_drop_id_param_value: 'unit[base_id]'} do %> drag_drop_id_param_value: 'unit[base_id]'} do %>
<td style="--depth:<%= unit.base_id? ? 1 : 0 %>"> <td style="--depth:<%= unit.base_id? ? 1 : 0 %>">
<%= link_to unit, edit_unit_path(unit), class: 'link', onclick: 'this.blur();', <%= link_to unit, edit_unit_path(unit), class: 'link',
data: {turbo_stream: true} %> data: {turbo_stream: true} %>
</td> </td>
<td><%= unit.description %></td> <td><%= unit.description %></td>
@@ -17,7 +17,7 @@
<td class="flex"> <td class="flex">
<% unless unit.base_id? %> <% unless unit.base_id? %>
<%= image_link_to t('.new_subunit'), 'plus-outline', new_unit_path(unit), <%= image_link_to t('.new_subunit'), 'plus-outline', new_unit_path(unit),
id: dom_id(unit, :new, :link), onclick: 'this.blur();', data: {turbo_stream: true} %> id: dom_id(unit, :new, :link), data: {turbo_stream: true} %>
<% end %> <% end %>
<%= image_button_to_if unit.movable?, t('.destroy'), 'delete-outline', unit_path(unit), <%= image_button_to_if unit.movable?, t('.destroy'), 'delete-outline', unit_path(unit),

View File

@@ -1,7 +1,7 @@
<div class="rightside-area buttongrid"> <div class="rightside-area buttongrid">
<% if current_user.at_least(:active) %> <% if current_user.at_least(:active) %>
<%= image_link_to t('.new_unit'), 'plus-outline', new_unit_path, <%= image_link_to t('.new_unit'), 'plus-outline', new_unit_path,
id: dom_id(Unit, :new, :link), onclick: 'this.blur();', data: {turbo_stream: true} %> id: dom_id(Unit, :new, :link), data: {turbo_stream: true} %>
<% end %> <% end %>
<%= image_link_to t('.import_units'), 'download-outline', default_units_path, <%= image_link_to t('.import_units'), 'download-outline', default_units_path,
class: 'tools-area' %> class: 'tools-area' %>