From 621c88992375e14509495bde09aa666c3a63f356 Mon Sep 17 00:00:00 2001
From: "totally-not-ai[bot]"
<290682512+totally-not-ai[bot]@users.noreply.github.com>
Date: Wed, 16 Sep 2026 06:25:57 +0000
Subject: [PATCH] feat: identify focus and blur invocations with a command
object
Focusable.focus() and blur() schedule their JavaScript through the new
Element.executeJs(JsCommand), so the pending invocation carries a typed
FocusCommand or BlurCommand. A driver of the client side that cannot run
JavaScript recognizes the invocation by the type of its command instead
of by matching the text of the generated expression, which is the
framework's script wrapped by executeJs.
The expression and the parameters sent to a browser are unchanged.
Part of https://github.com/vaadin/flow/issues/25734
---
.../vaadin/flow/component/BlurCommand.java | 48 ++++++++
.../vaadin/flow/component/FocusCommand.java | 105 ++++++++++++++++++
.../com/vaadin/flow/component/Focusable.java | 44 +-------
.../flow/component/internal/UIInternals.java | 31 ++++++
.../java/com/vaadin/flow/dom/Element.java | 43 ++++++-
.../java/com/vaadin/flow/dom/JsCommand.java | 82 ++++++++++++++
.../vaadin/flow/component/FocusableTest.java | 70 ++++++++++++
.../java/com/vaadin/flow/dom/ElementTest.java | 35 ++++++
8 files changed, 410 insertions(+), 48 deletions(-)
create mode 100644 flow-server/src/main/java/com/vaadin/flow/component/BlurCommand.java
create mode 100644 flow-server/src/main/java/com/vaadin/flow/component/FocusCommand.java
create mode 100644 flow-server/src/main/java/com/vaadin/flow/dom/JsCommand.java
diff --git a/flow-server/src/main/java/com/vaadin/flow/component/BlurCommand.java b/flow-server/src/main/java/com/vaadin/flow/component/BlurCommand.java
new file mode 100644
index 00000000000..bda5e81b2aa
--- /dev/null
+++ b/flow-server/src/main/java/com/vaadin/flow/component/BlurCommand.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2000-2026 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.flow.component;
+
+import com.vaadin.flow.dom.JsCommand;
+
+/**
+ * The command that {@link Focusable#blur()} schedules: remove focus from the
+ * element the invocation is scheduled on.
+ *
+ * The blur is server-initiated, which the generated script marks for the client
+ * so that the resulting {@link BlurNotifier.BlurEvent} reports
+ * {@code isFromClient() == false}. A driver that acts on this command instead
+ * of running the script is responsible for the same.
+ *
+ * @see FocusCommand
+ */
+public record BlurCommand() implements JsCommand {
+
+ private static final String BLUR_SCRIPT = """
+ setTimeout(() => {
+ try {
+ this._nextBlurIsFromClient = false;
+ this.blur();
+ } finally {
+ this._nextBlurIsFromClient = true;
+ }
+ }, 0)
+ """;
+
+ @Override
+ public String getExpression() {
+ return BLUR_SCRIPT;
+ }
+}
diff --git a/flow-server/src/main/java/com/vaadin/flow/component/FocusCommand.java b/flow-server/src/main/java/com/vaadin/flow/component/FocusCommand.java
new file mode 100644
index 00000000000..13125560946
--- /dev/null
+++ b/flow-server/src/main/java/com/vaadin/flow/component/FocusCommand.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2000-2026 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.flow.component;
+
+import java.util.List;
+
+import org.jspecify.annotations.Nullable;
+import tools.jackson.databind.node.ObjectNode;
+
+import com.vaadin.flow.dom.Element;
+import com.vaadin.flow.dom.JsCommand;
+
+/**
+ * The command that {@link Focusable#focus(FocusOption...)} schedules: focus the
+ * element the invocation is scheduled on, with the given options.
+ *
+ * The focus is server-initiated, which the generated script marks for the
+ * client so that the resulting {@link FocusNotifier.FocusEvent} reports
+ * {@code isFromClient() == false}. A driver that acts on this command instead
+ * of running the script is responsible for the same.
+ *
+ * @param options
+ * the options passed to {@link Focusable#focus(FocusOption...)}, in
+ * the order they were given; only the last {@link FocusOption} of
+ * each kind reaches the browser
+ * @see BlurCommand
+ */
+public record FocusCommand(List options) implements JsCommand {
+
+ private static final String FOCUS_SCRIPT = """
+ setTimeout(() => {
+ try {
+ this._nextFocusIsFromClient = false;
+ this.focus();
+ } finally {
+ this._nextFocusIsFromClient = true;
+ }
+ }, 0)
+ """;
+
+ private static final String FOCUS_WITH_OPTIONS_SCRIPT = """
+ setTimeout(() => {
+ try {
+ this._nextFocusIsFromClient = false;
+ this.focus($0);
+ } finally {
+ this._nextFocusIsFromClient = true;
+ }
+ }, 0)
+ """;
+
+ /**
+ * Creates a focus command with the given options.
+ *
+ * @param options
+ * the focus options, not null and with no
+ * null elements
+ */
+ public FocusCommand {
+ options = List.copyOf(options);
+ }
+
+ /**
+ * Creates a focus command with the given options.
+ *
+ * @param options
+ * zero or more focus options, with no null elements
+ */
+ public FocusCommand(FocusOption... options) {
+ this(List.of(options));
+ }
+
+ @Override
+ public String getExpression() {
+ return optionsJson() == null ? FOCUS_SCRIPT : FOCUS_WITH_OPTIONS_SCRIPT;
+ }
+
+ @Override
+ public List