Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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.
* <p>
* 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;
}
}
105 changes: 105 additions & 0 deletions flow-server/src/main/java/com/vaadin/flow/component/FocusCommand.java
Original file line number Diff line number Diff line change
@@ -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.
* <p>
* 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<FocusOption> 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 <code>null</code> and with no
* <code>null</code> elements
*/
public FocusCommand {
options = List.copyOf(options);
}

/**
* Creates a focus command with the given options.
*
* @param options
* zero or more focus options, with no <code>null</code> 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<Object> getParameters() {
ObjectNode json = optionsJson();
return json == null ? List.of() : List.of(json);
}

/**
* The options as the browser receives them, or <code>null</code> when every
* option is at its default and {@link Element#focus()} is called without
* arguments.
*/
private @Nullable ObjectNode optionsJson() {
return FocusOption.buildOptions(options.toArray(new FocusOption[0]));
}
}
44 changes: 2 additions & 42 deletions flow-server/src/main/java/com/vaadin/flow/component/Focusable.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,6 @@
*/
package com.vaadin.flow.component;

import tools.jackson.databind.node.ObjectNode;

import com.vaadin.flow.dom.Element;

/**
* Represents a component that can gain and lose focus.
*
Expand Down Expand Up @@ -134,34 +130,7 @@ default int getTabIndex() {
* @since 25.0
*/
default void focus(FocusOption... options) {
Element element = getElement();
ObjectNode json = FocusOption.buildOptions(options);

if (json == null) {
// No options, call focus() without arguments
element.executeJs("""
setTimeout(() => {
try {
this._nextFocusIsFromClient = false;
this.focus();
} finally {
this._nextFocusIsFromClient = true;
}
}, 0)
""");
} else {
// Call focus with options object passed as parameter
element.executeJs("""
setTimeout(() => {
try {
this._nextFocusIsFromClient = false;
this.focus($0);
} finally {
this._nextFocusIsFromClient = true;
}
}, 0)
""", json);
}
getElement().executeJs(new FocusCommand(options));
}

// for binary compatibility with the previous Vaadin versions
Expand Down Expand Up @@ -190,16 +159,7 @@ default void focus() {
* at MDN</a>
*/
default void blur() {
getElement().executeJs("""
setTimeout(() => {
try {
this._nextBlurIsFromClient = false;
this.blur();
} finally {
this._nextBlurIsFromClient = true;
}
}, 0)
""");
getElement().executeJs(new BlurCommand());
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
import com.vaadin.flow.di.Instantiator;
import com.vaadin.flow.dom.Element;
import com.vaadin.flow.dom.ElementUtil;
import com.vaadin.flow.dom.JsCommand;
import com.vaadin.flow.dom.impl.BasicElementStateProvider;
import com.vaadin.flow.function.DeploymentConfiguration;
import com.vaadin.flow.internal.ActiveStyleSheetTracker;
Expand Down Expand Up @@ -127,6 +128,7 @@ public class UIInternals implements Serializable {
public static class JavaScriptInvocation implements Serializable {
private final String expression;
private final List<Object> parameters = new ArrayList<>();
private final @Nullable JsCommand command;

/**
* Creates a new invocation.
Expand All @@ -138,6 +140,23 @@ public static class JavaScriptInvocation implements Serializable {
* @since 25.0
*/
public JavaScriptInvocation(String expression, Object... parameters) {
this((JsCommand) null, expression, parameters);
}

/**
* Creates a new invocation for the given command, whose expression and
* parameters the caller has already resolved.
*
* @param command
* the command that this invocation performs, or
* <code>null</code> if the invocation is plain JavaScript
* @param expression
* the expression to invoke
* @param parameters
* a list of parameters to use when invoking the script
*/
public JavaScriptInvocation(@Nullable JsCommand command,
String expression, Object... parameters) {
/*
* To ensure attached elements are actually attached, the parameters
* won't be serialized until the phase the UIDL message is created.
Expand All @@ -151,6 +170,7 @@ public JavaScriptInvocation(String expression, Object... parameters) {

this.expression = expression;
Collections.addAll(this.parameters, parameters);
this.command = command;
}

/**
Expand All @@ -170,6 +190,17 @@ public String getExpression() {
public List<Object> getParameters() {
return Collections.unmodifiableList(parameters);
}

/**
* Gets the command that this invocation performs, for a caller that
* acts on the invocation instead of running its JavaScript.
*
* @return the command, or <code>null</code> if the invocation is plain
* JavaScript with no command describing it
*/
public @Nullable JsCommand getCommand() {
return command;
}
}

/**
Expand Down
43 changes: 37 additions & 6 deletions flow-server/src/main/java/com/vaadin/flow/dom/Element.java
Original file line number Diff line number Diff line change
Expand Up @@ -1850,8 +1850,8 @@ public PendingJavaScriptResult callJsFunction(String functionName,
System.arraycopy(arguments, 0, jsParameters, 1, arguments.length);
}

return scheduleJavaScriptInvocation("return $0." + functionName + "("
+ paramPlaceholderString + ")", jsParameters);
return scheduleJavaScriptInvocation(null, "return $0." + functionName
+ "(" + paramPlaceholderString + ")", jsParameters);
}

/**
Expand Down Expand Up @@ -1924,6 +1924,36 @@ public PendingJavaScriptResult callJsFunction(String functionName,
*/
public PendingJavaScriptResult executeJs(String expression,
Object... parameters) {
return scheduleExecuteJs(null, expression, parameters);
}

/**
* Asynchronously runs the JavaScript of the given command in the browser in
* the context of this element, exactly as
* {@link #executeJs(String, Object...)} runs the command's
* {@link JsCommand#getExpression() expression} with its
* {@link JsCommand#getParameters() parameters}.
* <p>
* What the command adds is server-side: it stays with the invocation in the
* pending JavaScript queue of the UI, so that a driver of the client side
* that can not run JavaScript can recognize the invocation by the type of
* its command instead of by the text of the generated expression. See
* {@link JsCommand}.
*
* @param command
* the command to run, not <code>null</code>
* @return a pending result that can be used to get a value returned from
* the expression
*/
public PendingJavaScriptResult executeJs(JsCommand command) {
Objects.requireNonNull(command, "Command cannot be null");
return scheduleExecuteJs(command, command.getExpression(),
command.getParameters().toArray());
}

private PendingJavaScriptResult scheduleExecuteJs(
@Nullable JsCommand command, String expression,
Object[] parameters) {

// Add "this" as the last parameter
Object[] wrappedParameters;
Expand All @@ -1939,7 +1969,7 @@ public PendingJavaScriptResult executeJs(String expression,
String wrappedExpression = "return (async function() { " + expression
+ "}).apply($" + parameters.length + ")";

return scheduleJavaScriptInvocation(wrappedExpression,
return scheduleJavaScriptInvocation(command, wrappedExpression,
wrappedParameters);
}

Expand Down Expand Up @@ -2005,11 +2035,12 @@ public Registration addJsInitializer(String expression,
}

private PendingJavaScriptResult scheduleJavaScriptInvocation(
String expression, Object[] parameters) {
@Nullable JsCommand command, String expression,
Object[] parameters) {
StateNode node = getNode();

JavaScriptInvocation invocation = new JavaScriptInvocation(expression,
parameters);
JavaScriptInvocation invocation = new JavaScriptInvocation(command,
expression, parameters);

PendingJavaScriptInvocation pending = new PendingJavaScriptInvocation(
node, invocation);
Expand Down
Loading
Loading