From 6c3bd7e4a0413d15ac516cb4bac84726612766ab Mon Sep 17 00:00:00 2001 From: Lucy Macartney Date: Thu, 16 Apr 2026 14:25:59 +0100 Subject: [PATCH 1/4] Default autocomplete="off" in shared Phoenix input components Add autocomplete as an explicit attr with default "off" on input/1, input_element/1, and textarea_element/1 in new_inputs.ex instead of relying on static HTML attributes that can't be overridden. Add Keyword.put_new(:autocomplete, "off") to password_field/1 and text_field/1 in form.ex. Remove now-redundant per-field autocomplete="off" from sandbox components since the component default handles it. Add ExUnit tests verifying the default and override behaviour for both component systems. Closes #1533 --- lib/lightning_web/components/new_inputs.ex | 16 +++- lib/lightning_web/live/components/form.ex | 10 ++- .../live/sandbox_live/components.ex | 1 - .../live/sandbox_live/form_component.ex | 1 - test/lightning_web/components/form_test.exs | 87 +++++++++++++++++++ .../components/new_inputs_test.exs | 58 +++++++++++++ 6 files changed, 166 insertions(+), 7 deletions(-) create mode 100644 test/lightning_web/components/form_test.exs diff --git a/lib/lightning_web/components/new_inputs.ex b/lib/lightning_web/components/new_inputs.ex index f54fc8b0c3d..a6bb15bd910 100644 --- a/lib/lightning_web/components/new_inputs.ex +++ b/lib/lightning_web/components/new_inputs.ex @@ -400,9 +400,11 @@ defmodule LightningWeb.Components.NewInputs do attr :placeholder, :string, default: "" + attr :autocomplete, :string, default: "off" + attr :rest, :global, include: - ~w(accept autocomplete capture cols disabled form list max maxlength min minlength + ~w(accept capture cols disabled form list max maxlength min minlength multiple pattern placeholder readonly required rows size step) attr :class, :string, default: "" @@ -616,6 +618,7 @@ defmodule LightningWeb.Components.NewInputs do id={@id} name={@name} class={@class} + autocomplete={@autocomplete} value={@value} placeholder={@placeholder} {@rest} @@ -635,6 +638,7 @@ defmodule LightningWeb.Components.NewInputs do id={@id} name={@name} class={["rounded-md w-full font-mono bg-slate-800 text-slate-100", @class]} + autocomplete={@autocomplete} value={@value} placeholder={@placeholder} errors={@errors} @@ -660,6 +664,7 @@ defmodule LightningWeb.Components.NewInputs do
assigns_to_attributes() + opts: + assigns.rest + |> assigns_to_attributes() + |> Keyword.put_new(:autocomplete, "off") ) |> assign_new(:label, fn -> nil end) |> assign_new(:hint, fn -> nil end) @@ -269,7 +272,10 @@ defmodule LightningWeb.Components.Form do label_classes: label_classes, error_classes: error_classes, input_classes: input_classes, - opts: assigns.rest |> assigns_to_attributes() + opts: + assigns.rest + |> assigns_to_attributes() + |> Keyword.put_new(:autocomplete, "off") ) ~H""" diff --git a/lib/lightning_web/live/sandbox_live/components.ex b/lib/lightning_web/live/sandbox_live/components.ex index f6e20e2fe61..f2b2728b878 100644 --- a/lib/lightning_web/live/sandbox_live/components.ex +++ b/lib/lightning_web/live/sandbox_live/components.ex @@ -167,7 +167,6 @@ defmodule LightningWeb.SandboxLive.Components do field={@confirm_form[:name]} label="Sandbox name" placeholder={@sandbox.name} - autocomplete="off" required /> <.errors field={@confirm_form[:name]} /> diff --git a/lib/lightning_web/live/sandbox_live/form_component.ex b/lib/lightning_web/live/sandbox_live/form_component.ex index ce40b72407e..c8a590c8962 100644 --- a/lib/lightning_web/live/sandbox_live/form_component.ex +++ b/lib/lightning_web/live/sandbox_live/form_component.ex @@ -241,7 +241,6 @@ defmodule LightningWeb.SandboxLive.FormComponent do field={f[:raw_name]} label="Name" required - autocomplete="off" placeholder="My Sandbox" phx-debounce="300" /> diff --git a/test/lightning_web/components/form_test.exs b/test/lightning_web/components/form_test.exs new file mode 100644 index 00000000000..af50724fa45 --- /dev/null +++ b/test/lightning_web/components/form_test.exs @@ -0,0 +1,87 @@ +defmodule LightningWeb.Components.FormTest do + @moduledoc false + use LightningWeb.ConnCase, async: true + + import Phoenix.LiveViewTest + + alias LightningWeb.Components.Form + + # A minimal embedded schema to produce real Ecto changesets for testing. + defmodule Item do + use Ecto.Schema + + embedded_schema do + field :name, :string + field :secret, :string + end + + def changeset(item \\ %__MODULE__{}, params) do + Ecto.Changeset.cast(item, params, [:name, :secret]) + end + end + + defp form_for_item do + Item.changeset(%{"name" => "test", "secret" => ""}) + |> Phoenix.Component.to_form() + end + + # ---- password_field/1 autocomplete ---------------------------------------- + + describe "password_field/1 autocomplete" do + test "renders autocomplete='off' by default" do + form = form_for_item() + + html = + render_component(&Form.password_field/1, %{ + form: form, + id: :secret + }) + + assert html =~ ~s(autocomplete="off") + end + + test "allows explicit autocomplete override" do + form = form_for_item() + + html = + render_component(&Form.password_field/1, %{ + form: form, + id: :secret, + autocomplete: "current-password" + }) + + assert html =~ ~s(autocomplete="current-password") + refute html =~ ~s(autocomplete="off") + end + end + + # ---- text_field/1 autocomplete -------------------------------------------- + + describe "text_field/1 autocomplete" do + test "renders autocomplete='off' by default" do + form = form_for_item() + + html = + render_component(&Form.text_field/1, %{ + form: form, + field: :name + }) + + assert html =~ ~s(autocomplete="off") + end + + test "allows explicit autocomplete override" do + form = form_for_item() + + html = + render_component(&Form.text_field/1, %{ + form: form, + field: :name, + autocomplete: "email" + }) + + assert html =~ ~s(autocomplete="email") + refute html =~ ~s(autocomplete="off") + end + end +end diff --git a/test/lightning_web/components/new_inputs_test.exs b/test/lightning_web/components/new_inputs_test.exs index fd7c0758c47..1ac35907aa3 100644 --- a/test/lightning_web/components/new_inputs_test.exs +++ b/test/lightning_web/components/new_inputs_test.exs @@ -111,6 +111,64 @@ defmodule LightningWeb.Components.NewInputsTest do end end + # ---- autocomplete defaults -------------------------------------------------- + + describe "autocomplete defaults" do + test "input/1 renders autocomplete='off' by default" do + form = used_field_form() + + html = + render_component(&NewInputs.input/1, %{ + field: form[:name], + type: "text" + }) + + assert html =~ ~s(autocomplete="off") + end + + test "input/1 allows explicit autocomplete override" do + form = used_field_form() + + html = + render_component(&NewInputs.input/1, %{ + field: form[:name], + type: "text", + autocomplete: "email" + }) + + # The override value is rendered via {@rest} after the static default. + # Browsers use the last attribute value, so autocomplete="email" wins. + assert html =~ ~s(autocomplete="email") + end + + test "input/1 password type renders autocomplete='off' by default" do + form = used_field_form() + + html = + render_component(&NewInputs.input/1, %{ + field: form[:name], + type: "password" + }) + + assert html =~ ~s(autocomplete="off") + end + + test "input/1 password type allows explicit autocomplete override" do + form = used_field_form() + + html = + render_component(&NewInputs.input/1, %{ + field: form[:name], + type: "password", + autocomplete: "current-password" + }) + + # The override value is rendered via {@rest} after the static default. + # Browsers use the last attribute value, so autocomplete="current-password" wins. + assert html =~ ~s(autocomplete="current-password") + end + end + # ---- old_error/1 (CoreComponents) ----------------------------------------- describe "old_error/1 used_input? gating" do From 931a8e7dbc4f4ccb0ce3a588cb47645f3db6cda9 Mon Sep 17 00:00:00 2001 From: Lucy Macartney Date: Thu, 16 Apr 2026 14:28:28 +0100 Subject: [PATCH 2/4] Add semantic autocomplete to auth forms and fix raw inputs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add explicit autocomplete values to authentication forms so browser autofill works correctly: email, current-password, new-password, given-name, family-name, one-time-code on login, registration, password reset, profile, TOTP/MFA, re-authenticate, first setup, admin user, and book demo forms. Add autocomplete="off" to raw tags that bypass shared components (webhook URL, token display, webhook auth password/API key inputs). Use autocomplete="new-password" on non-auth password fields (credentials, OAuth client ID/secret, webhook auth, Kafka) to prevent Chrome from autofilling saved passwords — Chrome ignores "off" on type="password" fields but respects "new-password". --- .../user_registration_html/new.html.heex | 10 +++++++++- .../user_reset_password_html/edit.html.heex | 8 +++++++- .../controllers/user_session_html/new.html.heex | 8 +++++++- .../controllers/user_totp_html/new.html.heex | 2 +- lib/lightning_web/live/book_demo_banner.ex | 16 ++++++++++++++-- .../json_schema_body_component.ex | 1 + .../oauth_client_form_component.ex | 2 ++ .../live/first_setup_live/superuser.html.heex | 8 +++++++- .../live/job_live/kafka_setup_component.ex | 2 +- .../live/profile_live/form_component.html.heex | 6 ++++++ .../live/profile_live/mfa_component.html.heex | 2 +- .../live/re_authenticate_live/new.html.heex | 3 ++- .../live/tokens_live/index.html.heex | 1 + .../live/user_live/form_component.html.heex | 2 +- .../live/workflow_live/components.ex | 1 + .../webhook_auth_method_form_component.ex | 16 ++++++++++++++-- 16 files changed, 75 insertions(+), 13 deletions(-) diff --git a/lib/lightning_web/controllers/user_registration_html/new.html.heex b/lib/lightning_web/controllers/user_registration_html/new.html.heex index 629a3920884..46591056c66 100644 --- a/lib/lightning_web/controllers/user_registration_html/new.html.heex +++ b/lib/lightning_web/controllers/user_registration_html/new.html.heex @@ -32,10 +32,16 @@ field={f[:first_name]} label="First Name" required={true} + autocomplete="given-name" />
- <.input field={f[:last_name]} required={true} label="Last Name" /> + <.input + field={f[:last_name]} + required={true} + label="Last Name" + autocomplete="family-name" + />
<.input @@ -43,6 +49,7 @@ type="email" label="Email" required={true} + autocomplete="email" />
@@ -51,6 +58,7 @@ type="password" label="Password" required={true} + autocomplete="new-password" />
diff --git a/lib/lightning_web/controllers/user_reset_password_html/edit.html.heex b/lib/lightning_web/controllers/user_reset_password_html/edit.html.heex index 9a766d1d33b..a9a92e5538e 100644 --- a/lib/lightning_web/controllers/user_reset_password_html/edit.html.heex +++ b/lib/lightning_web/controllers/user_reset_password_html/edit.html.heex @@ -27,7 +27,12 @@
<.label for={:password}>New password - <.input type="password" field={f[:password]} required /> + <.input + type="password" + field={f[:password]} + required + autocomplete="new-password" + />
@@ -38,6 +43,7 @@ type="password" field={f[:password_confirmation]} required + autocomplete="new-password" />
diff --git a/lib/lightning_web/controllers/user_session_html/new.html.heex b/lib/lightning_web/controllers/user_session_html/new.html.heex index 8a945fc56f7..8e2949b9f8c 100644 --- a/lib/lightning_web/controllers/user_session_html/new.html.heex +++ b/lib/lightning_web/controllers/user_session_html/new.html.heex @@ -33,10 +33,16 @@ field={f[:email]} required={true} label="Email" + autocomplete="email" />
- <.input type="password" field={f[:password]} label="Password" /> + <.input + type="password" + field={f[:password]} + label="Password" + autocomplete="current-password" + />
<.check_box form={f} field={:remember_me}>
diff --git a/lib/lightning_web/controllers/user_totp_html/new.html.heex b/lib/lightning_web/controllers/user_totp_html/new.html.heex index 5821ff3f5fe..a852e526b23 100644 --- a/lib/lightning_web/controllers/user_totp_html/new.html.heex +++ b/lib/lightning_web/controllers/user_totp_html/new.html.heex @@ -30,7 +30,7 @@ type="text" field={f[:code]} required="true" - autocomplete="off" + autocomplete="one-time-code" inputmode="numeric" placeholder="XXXXXX" class="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" diff --git a/lib/lightning_web/live/book_demo_banner.ex b/lib/lightning_web/live/book_demo_banner.ex index 6440ad46efa..2448ad34c58 100644 --- a/lib/lightning_web/live/book_demo_banner.ex +++ b/lib/lightning_web/live/book_demo_banner.ex @@ -157,8 +157,20 @@ defmodule LightningWeb.BookDemoBanner do phx-submit="schedule-call" >
- <.input type="text" field={f[:name]} label="Name" required={true} /> - <.input type="text" field={f[:email]} label="Email" required={true} /> + <.input + type="text" + field={f[:name]} + label="Name" + required={true} + autocomplete="name" + /> + <.input + type="text" + field={f[:email]} + label="Email" + required={true} + autocomplete="email" + /> <.input type="textarea" field={f[:message]} diff --git a/lib/lightning_web/live/credential_live/json_schema_body_component.ex b/lib/lightning_web/live/credential_live/json_schema_body_component.ex index 1689873580d..42ed39858c7 100644 --- a/lib/lightning_web/live/credential_live/json_schema_body_component.ex +++ b/lib/lightning_web/live/credential_live/json_schema_body_component.ex @@ -78,6 +78,7 @@ defmodule LightningWeb.CredentialLive.JsonSchemaBodyComponent do field={@form_field} label={@title} required={@required} + autocomplete={if @type == "password", do: "new-password", else: "off"} checked={@type == "checkbox" and @form_field.value == true} />
diff --git a/lib/lightning_web/live/credential_live/oauth_client_form_component.ex b/lib/lightning_web/live/credential_live/oauth_client_form_component.ex index bc948abc4d1..e4b318fb824 100644 --- a/lib/lightning_web/live/credential_live/oauth_client_form_component.ex +++ b/lib/lightning_web/live/credential_live/oauth_client_form_component.ex @@ -422,6 +422,7 @@ defmodule LightningWeb.CredentialLive.OauthClientFormComponent do field={f[:client_id]} label="Client ID" required="true" + autocomplete="new-password" /> @@ -433,6 +434,7 @@ defmodule LightningWeb.CredentialLive.OauthClientFormComponent do field={f[:client_secret]} label="Client Secret" required="true" + autocomplete="new-password" /> diff --git a/lib/lightning_web/live/first_setup_live/superuser.html.heex b/lib/lightning_web/live/first_setup_live/superuser.html.heex index 09799be5ed2..ae37d082e8d 100644 --- a/lib/lightning_web/live/first_setup_live/superuser.html.heex +++ b/lib/lightning_web/live/first_setup_live/superuser.html.heex @@ -35,7 +35,12 @@
- <.input type="password" field={f[:password]} label="Password" /> + <.input + type="password" + field={f[:password]} + label="Password" + autocomplete="new-password" + />
@@ -45,6 +50,7 @@ type="password" field={f[:password_confirmation]} label="Password confirmation" + autocomplete="new-password" /> diff --git a/lib/lightning_web/live/job_live/kafka_setup_component.ex b/lib/lightning_web/live/job_live/kafka_setup_component.ex index ef49daf7db4..cb2c065210d 100644 --- a/lib/lightning_web/live/job_live/kafka_setup_component.ex +++ b/lib/lightning_web/live/job_live/kafka_setup_component.ex @@ -69,7 +69,6 @@ defmodule LightningWeb.JobLive.KafkaSetupComponent do type="text" field={kafka_config[:username]} label="Username" - autocomplete="off" disabled={@disabled} /> @@ -81,6 +80,7 @@ defmodule LightningWeb.JobLive.KafkaSetupComponent do label="Password" disabled={@disabled} value={password} + autocomplete="new-password" /> diff --git a/lib/lightning_web/live/profile_live/form_component.html.heex b/lib/lightning_web/live/profile_live/form_component.html.heex index 7e82d8bf32e..5ada64c6dca 100644 --- a/lib/lightning_web/live/profile_live/form_component.html.heex +++ b/lib/lightning_web/live/profile_live/form_component.html.heex @@ -17,6 +17,7 @@ field={f[:first_name]} label="First name" required="true" + autocomplete="given-name" />
@@ -25,6 +26,7 @@ field={f[:last_name]} label="Last name" required="true" + autocomplete="family-name" />
@@ -80,6 +82,7 @@ label="Enter password to confirm" required="true" phx-debounce="blur" + autocomplete="current-password" />
@@ -113,6 +116,7 @@ field={f[:password]} label="New password" required="true" + autocomplete="new-password" />
@@ -121,6 +125,7 @@ field={f[:password_confirmation]} label="Confirm new password" required="true" + autocomplete="new-password" />
@@ -129,6 +134,7 @@ field={f[:current_password]} label="Current password" required="true" + autocomplete="current-password" />
diff --git a/lib/lightning_web/live/profile_live/mfa_component.html.heex b/lib/lightning_web/live/profile_live/mfa_component.html.heex index 82eaf208823..209b8a60add 100644 --- a/lib/lightning_web/live/profile_live/mfa_component.html.heex +++ b/lib/lightning_web/live/profile_live/mfa_component.html.heex @@ -160,7 +160,7 @@ <.input type="text" field={f[:code]} - autocomplete="off" + autocomplete="one-time-code" placeholder="XXXXXX" label="Verify the code from the app" /> diff --git a/lib/lightning_web/live/re_authenticate_live/new.html.heex b/lib/lightning_web/live/re_authenticate_live/new.html.heex index 2d5f8fce1e4..0f0c02c52de 100644 --- a/lib/lightning_web/live/re_authenticate_live/new.html.heex +++ b/lib/lightning_web/live/re_authenticate_live/new.html.heex @@ -61,6 +61,7 @@ type="password" field={f[:password]} required="true" + autocomplete="current-password" class="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" /> <% else %> @@ -68,7 +69,7 @@ type="text" field={f[:code]} required="true" - autocomplete="off" + autocomplete="one-time-code" inputmode="numeric" placeholder="XXXXXX" class="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" diff --git a/lib/lightning_web/live/tokens_live/index.html.heex b/lib/lightning_web/live/tokens_live/index.html.heex index 0d3d20c0ef5..b0c0e135b57 100644 --- a/lib/lightning_web/live/tokens_live/index.html.heex +++ b/lib/lightning_web/live/tokens_live/index.html.heex @@ -43,6 +43,7 @@ value={@new_token} class="my-auto w-full border-0 bg-inherit disabled:opacity-70 mr-1" disabled + autocomplete="off" />