diff --git a/flow-server/src/main/java/com/vaadin/flow/component/clipboard/Clipboard.java b/flow-server/src/main/java/com/vaadin/flow/component/clipboard/Clipboard.java index 2936cea9505..debf1f763f9 100644 --- a/flow-server/src/main/java/com/vaadin/flow/component/clipboard/Clipboard.java +++ b/flow-server/src/main/java/com/vaadin/flow/component/clipboard/Clipboard.java @@ -165,6 +165,30 @@ public static > ClipboardBinding onClick( return new ClipboardBinding(new ClickTrigger(component)); } + /** + * Starts a clipboard write that is not bound to any component — the entry + * point for copy affordances rendered on the client, where there is no + * server-side component to bind to. Chain the payload onto the returned + * {@link ClipboardWrite} and hand the resulting + * {@link com.vaadin.flow.component.trigger.ClientAction} to whatever + * renders the affordance: + * + *
{@code
+     * LitRenderer. of(
+     *         "${item.email}")
+     *         .withProperty("email", Customer::email).withClientAction("copy",
+     *                 Clipboard.write().text(ClientValue.itemProperty("email")));
+     * }
+ * + * The action still runs inside the browser.s own event handler, so the user + * gesture is valid, exactly as with {@link #onClick(Component)}. + * + * @return a fluent surface for declaring what to copy + */ + public static ClipboardWrite write() { + return new ClipboardWrite(); + } + /** * Registers a listener for browser {@code paste} events on the given * component. The listener is invoked on the UI thread once per paste diff --git a/flow-server/src/main/java/com/vaadin/flow/component/clipboard/ClipboardWrite.java b/flow-server/src/main/java/com/vaadin/flow/component/clipboard/ClipboardWrite.java new file mode 100644 index 00000000000..dc22590b9f7 --- /dev/null +++ b/flow-server/src/main/java/com/vaadin/flow/component/clipboard/ClipboardWrite.java @@ -0,0 +1,88 @@ +/* + * 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.clipboard; + +import java.io.Serializable; +import java.util.Objects; + +import org.jspecify.annotations.Nullable; + +import com.vaadin.flow.component.trigger.ClientAction; +import com.vaadin.flow.component.trigger.ClientValue; +import com.vaadin.flow.component.trigger.internal.ClientActions; +import com.vaadin.flow.component.trigger.internal.PromiseAction.Error; +import com.vaadin.flow.component.trigger.internal.WriteToClipboardAction; +import com.vaadin.flow.function.SerializableConsumer; + +/** + * Fluent surface returned from {@link Clipboard#write()}, used to declare what + * a clipboard write copies when something on the client fires it. + *

+ * Unlike {@link Clipboard#onClick(com.vaadin.flow.component.Component)}, the + * write declared here is not bound to a component: it produces a + * {@link ClientAction} that whatever renders the affordance fires — typically a + * renderer that draws the same affordance for every item it renders. + * + *

{@code
+ * LitRenderer. of(
+ *         "${item.email}")
+ *         .withProperty("email", Customer::email).withClientAction("copy",
+ *                 Clipboard.write().text(ClientValue.itemProperty("email")));
+ * }
+ */ +public final class ClipboardWrite implements Serializable { + + ClipboardWrite() { + // Created by Clipboard.write() + } + + /** + * Copies the given value to the clipboard as {@code text/plain}. The value + * is read on the client at the moment the action fires, which is what lets + * one action serve every item a renderer draws. + * + * @param value + * the value to copy, not {@code null} + * @return an unbound action, to be handed to whatever fires it + */ + public ClientAction text(ClientValue value) { + Objects.requireNonNull(value, "value must not be null"); + return ClientActions + .of(new WriteToClipboardAction(value.getInput(), null)); + } + + /** + * Like {@link #text(ClientValue)} but reports the outcome back to the + * server. + * + * @param value + * the value to copy, not {@code null} + * @param onCopied + * UI-thread callback receiving the copied string, not + * {@code null} + * @param onError + * UI-thread callback receiving the browser's error, not + * {@code null} + * @return an unbound action, to be handed to whatever fires it + */ + public ClientAction text(ClientValue value, + SerializableConsumer<@Nullable String> onCopied, + SerializableConsumer onError) { + Objects.requireNonNull(value, "value must not be null"); + return ClientActions.of(new WriteToClipboardAction(value.getInput(), + null, onCopied, onError)); + } +} diff --git a/flow-server/src/main/java/com/vaadin/flow/component/trigger/ClientAction.java b/flow-server/src/main/java/com/vaadin/flow/component/trigger/ClientAction.java new file mode 100644 index 00000000000..1eb6d66a44e --- /dev/null +++ b/flow-server/src/main/java/com/vaadin/flow/component/trigger/ClientAction.java @@ -0,0 +1,70 @@ +/* + * 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.trigger; + +import java.io.Serializable; + +import com.vaadin.flow.dom.Element; +import com.vaadin.flow.shared.Registration; + +/** + * A browser-side action — copy to the clipboard, share, enter fullscreen, start + * a download — that is not yet bound to anything that fires it. + *

+ * Ordinary trigger bindings name the component that fires them + * ({@code Clipboard.onClick(button)}), which requires a server-side component + * per binding. A {@code ClientAction} instead describes only what + * should happen, so it can be handed to something that renders its own elements + * on the client — a {@code LitRenderer} template, for example — and fired from + * there once per rendered element while still costing one binding: + * + *

{@code
+ * grid.addColumn(LitRenderer. of(
+ *         "${item.email}")
+ *         .withProperty("email", Customer::email).withClientAction("copy",
+ *                 Clipboard.write().text(ClientValue.itemProperty("email"))));
+ * }
+ * + * The action runs inside the browser's own event handler, so the user gesture + * is still valid — the whole reason clipboard, share and fullscreen calls + * cannot be made from a server-side listener. + *

+ * Instances are created by the feature facades ({@code Clipboard.write()}, + * {@code WebShare.share(…)}, …), not by application code. + * + * @see ClientValue + */ +public interface ClientAction extends Serializable { + + /** + * Binds this action to {@code host} and hands the rendered client-side + * function to {@code sink}, which decides when it runs. + *

+ * Called by the component or renderer that accepts the action, once per + * place it is rendered into. The returned {@link Registration} detaches the + * binding. + *

+ * For internal use only. May be renamed or removed in a future release. + * + * @param host + * the element whose lifecycle the binding belongs to, not + * {@code null} + * @param sink + * receives the rendered action function, not {@code null} + * @return a registration that detaches the binding, never {@code null} + */ + Registration bindTo(Element host, ClientActionSink sink); +} diff --git a/flow-server/src/main/java/com/vaadin/flow/component/trigger/ClientActionSink.java b/flow-server/src/main/java/com/vaadin/flow/component/trigger/ClientActionSink.java new file mode 100644 index 00000000000..0a76b52ca47 --- /dev/null +++ b/flow-server/src/main/java/com/vaadin/flow/component/trigger/ClientActionSink.java @@ -0,0 +1,49 @@ +/* + * 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.trigger; + +import java.io.Serializable; + +import com.vaadin.flow.dom.JsFunction; +import com.vaadin.flow.shared.Registration; + +/** + * Receives the client-side function a {@link ClientAction} renders to, and is + * responsible for getting it invoked in the browser. + *

+ * Implemented by whatever accepts client actions — typically a renderer that + * passes the function into its own client-side template so a {@code @click} + * binding can call it. The function takes {@code (event, context)}: the client + * event that fired it, and an object describing what it fired for. A row + * renderer supplies {@code {item, index, key}} for the row the event came from; + * that context is what {@link ClientValue#itemProperty(String)} reads. + *

+ * For internal use only. May be renamed or removed in a future release. + */ +@FunctionalInterface +public interface ClientActionSink extends Serializable { + + /** + * Takes the rendered action function and arranges for it to be called on + * the client. + * + * @param action + * the action function, to be invoked as + * {@code action(event, context)}, not {@code null} + * @return a registration that undoes the installation, never {@code null} + */ + Registration install(JsFunction action); +} diff --git a/flow-server/src/main/java/com/vaadin/flow/component/trigger/ClientValue.java b/flow-server/src/main/java/com/vaadin/flow/component/trigger/ClientValue.java new file mode 100644 index 00000000000..6a560454c95 --- /dev/null +++ b/flow-server/src/main/java/com/vaadin/flow/component/trigger/ClientValue.java @@ -0,0 +1,87 @@ +/* + * 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.trigger; + +import java.io.Serializable; +import java.util.Objects; + +import com.vaadin.flow.component.trigger.internal.Action; +import com.vaadin.flow.component.trigger.internal.ContextInput; +import com.vaadin.flow.component.trigger.internal.LiteralInput; + +/** + * A value a {@link ClientAction} reads on the client when it runs, rather than + * one captured on the server when it is bound. + *

+ * This is what makes a single binding usable for many rendered elements: the + * copy button in every grid row is one action, and + * {@link #itemProperty(String)} resolves the row it was clicked in. + * + *

{@code
+ * Clipboard.write().text(ClientValue.itemProperty("email"));
+ * }
+ * + * @param + * the type of the value produced + */ +public final class ClientValue implements Serializable { + + private final Action.Input input; + + private ClientValue(Action.Input input) { + this.input = input; + } + + /** + * A property of the item the action fired for, read from the renderer's + * client-side item data. The property must be one the renderer sends to the + * client — for a {@code LitRenderer}, one declared with + * {@code withProperty}. + * + * @param propertyName + * the item property to read, not {@code null} + * @return a value resolving to that property of the item the action fired + * for + */ + public static ClientValue itemProperty(String propertyName) { + Objects.requireNonNull(propertyName, "propertyName must not be null"); + return new ClientValue<>( + new ContextInput<>("item", propertyName, String.class)); + } + + /** + * A fixed value, the same for every element the action is rendered into. + * + * @param value + * the value, not {@code null} + * @return a value resolving to {@code value} + */ + public static ClientValue of(String value) { + Objects.requireNonNull(value, "value must not be null"); + return new ClientValue<>(new LiteralInput<>(value)); + } + + /** + * The input backing this value. + *

+ * For internal use only. May be renamed or removed in a future release. + * + * @return the input, never {@code null} + */ + public Action.Input getInput() { + return input; + } +} diff --git a/flow-server/src/main/java/com/vaadin/flow/component/trigger/internal/Action.java b/flow-server/src/main/java/com/vaadin/flow/component/trigger/internal/Action.java index 76096a91952..789fcae6433 100644 --- a/flow-server/src/main/java/com/vaadin/flow/component/trigger/internal/Action.java +++ b/flow-server/src/main/java/com/vaadin/flow/component/trigger/internal/Action.java @@ -57,13 +57,19 @@ public abstract class Action implements Serializable { /** * Builds the {@link JsFunction} that runs this action when the surrounding - * trigger fires. The returned function takes one runtime argument named - * {@code event} (declared by the framework when it composes the trigger - * handler); subclasses do not declare argument names themselves. + * trigger fires. The returned function takes two runtime arguments: {@code + * event}, the client event that fired the trigger, and {@code context}, the + * trigger context — an object describing what the trigger fired for, or + * {@code undefined} for triggers that have no such notion. A + * {@link SinkTrigger} rendered into a client-side row renderer, for + * example, supplies {@code {item, index, key}} for the row the event came + * from, which is what lets one action serve a whole column. *

* The body is one statement. To embed a value produced on the client, * capture an {@link Input}'s {@link Input#toJs(Trigger) JsFunction} as a - * capture and invoke it inside the body as {@code $N(event)}. + * capture and invoke it inside the body as {@code $N(event, context)} — + * always forwarding both arguments, so inputs that read the context work in + * every action. * * @param trigger * the surrounding trigger this render is for, not {@code null} @@ -121,10 +127,10 @@ public abstract static class Input implements Serializable { /** * Builds the {@link JsFunction} that yields this input's value when - * called. The function may take {@code event} as a runtime argument - * (declared by the subclass via - * {@link JsFunction#withArguments(String...)}); inputs that don't need - * {@code event} simply omit the declaration and ignore the argument the + * called. The function may take {@code event} and {@code context} as + * runtime arguments (declared by the subclass via + * {@link JsFunction#withArguments(String...)}); inputs that need + * neither simply omit the declaration and ignore the arguments the * caller passes. * *

diff --git a/flow-server/src/main/java/com/vaadin/flow/component/trigger/internal/CallbackAction.java b/flow-server/src/main/java/com/vaadin/flow/component/trigger/internal/CallbackAction.java index 11f8fbe3e05..6dae83ab3be 100644 --- a/flow-server/src/main/java/com/vaadin/flow/component/trigger/internal/CallbackAction.java +++ b/flow-server/src/main/java/com/vaadin/flow/component/trigger/internal/CallbackAction.java @@ -141,8 +141,9 @@ protected final JsFunction toJs(Trigger trigger) { // $0 = the return channel; $1 = the source input's JsFunction. // Invoking the source with `event` produces its value, which is // forwarded straight into the channel call. - return JsFunction.of("$0($1(event));", channel, source.toJs(trigger)) - .withArguments("event"); + return JsFunction + .of("$0($1(event, context));", channel, source.toJs(trigger)) + .withArguments("event", "context"); } private ReturnChannelRegistration channelFor(StateNode hostNode) { diff --git a/flow-server/src/main/java/com/vaadin/flow/component/trigger/internal/ClientActions.java b/flow-server/src/main/java/com/vaadin/flow/component/trigger/internal/ClientActions.java new file mode 100644 index 00000000000..4f170fced13 --- /dev/null +++ b/flow-server/src/main/java/com/vaadin/flow/component/trigger/internal/ClientActions.java @@ -0,0 +1,60 @@ +/* + * 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.trigger.internal; + +import java.io.Serializable; +import java.util.Objects; + +import com.vaadin.flow.component.trigger.ClientAction; +import com.vaadin.flow.component.trigger.ClientActionSink; +import com.vaadin.flow.dom.Element; +import com.vaadin.flow.shared.Registration; + +/** + * Bridges the public {@link ClientAction} handle to the internal {@link Action} + * it carries, so feature facades (clipboard, fullscreen, share, …) can hand an + * unbound action to whatever renders it without exposing the trigger internals. + *

+ * For internal use only. May be renamed or removed in a future release. + */ +public final class ClientActions implements Serializable { + + private ClientActions() { + // utility class + } + + /** + * Wraps an internal action as a public {@link ClientAction} handle. Binding + * the handle creates a {@link SinkTrigger} on the host element and wires + * the action to it. + * + * @param action + * the action to run when whatever renders the handle fires it, + * not {@code null} + * @return an unbound client action handle + */ + public static ClientAction of(Action action) { + Objects.requireNonNull(action, "action must not be null"); + return new ClientAction() { + @Override + public Registration bindTo(Element host, ClientActionSink sink) { + SinkTrigger trigger = new SinkTrigger(host, sink); + trigger.triggers(action); + return trigger::remove; + } + }; + } +} diff --git a/flow-server/src/main/java/com/vaadin/flow/component/trigger/internal/ContextInput.java b/flow-server/src/main/java/com/vaadin/flow/component/trigger/internal/ContextInput.java new file mode 100644 index 00000000000..75172e50afa --- /dev/null +++ b/flow-server/src/main/java/com/vaadin/flow/component/trigger/internal/ContextInput.java @@ -0,0 +1,76 @@ +/* + * 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.trigger.internal; + +import java.util.Objects; + +import com.vaadin.flow.dom.JsFunction; + +/** + * Reads a value out of the trigger context — the object a trigger supplies to + * describe what it fired for. A {@link SinkTrigger} rendered into a row + * renderer supplies {@code {item, index, key}} for the row the event came from, + * so {@code new ContextInput<>("item", "email", String.class)} yields that + * row's {@code email} property at fire time. + *

+ * This is what lets one action serve a whole column: the value is not captured + * on the server per row, it is read from the row's own client-side data when + * the action runs. + *

+ * For internal use only. May be renamed or removed in a future release. + * + * @param + * the runtime type of the value produced + */ +public class ContextInput extends Action.Input { + + private final String contextProperty; + private final String valueProperty; + + /** + * Creates an input reading {@code context[contextProperty][valueProperty]} + * at fire time. + * + * @param contextProperty + * the context member to read from (e.g. {@code "item"}), not + * {@code null} + * @param valueProperty + * the property to read off that member, not {@code null} + * @param valueType + * runtime type of the produced value, not {@code null} + */ + public ContextInput(String contextProperty, String valueProperty, + Class valueType) { + this.contextProperty = Objects.requireNonNull(contextProperty); + this.valueProperty = Objects.requireNonNull(valueProperty); + Objects.requireNonNull(valueType); + } + + @Override + public JsFunction toJs(Trigger trigger) { + if (!trigger.suppliesContext()) { + throw new IllegalArgumentException("Input reads the trigger " + + "context, but a " + trigger.getClass().getSimpleName() + + " supplies none. Use a value source that does not depend " + + "on the context, or bind the action to something that " + + "renders repeating elements."); + } + // Both names are captures, so nothing is concatenated into the body. + return JsFunction + .of("return context[$0][$1]", contextProperty, valueProperty) + .withArguments("event", "context"); + } +} diff --git a/flow-server/src/main/java/com/vaadin/flow/component/trigger/internal/DownloadAction.java b/flow-server/src/main/java/com/vaadin/flow/component/trigger/internal/DownloadAction.java index 55db36d72b0..a15ab26db2d 100644 --- a/flow-server/src/main/java/com/vaadin/flow/component/trigger/internal/DownloadAction.java +++ b/flow-server/src/main/java/com/vaadin/flow/component/trigger/internal/DownloadAction.java @@ -167,13 +167,15 @@ protected JsFunction toJs(Trigger trigger) { // emitted JS minimal. Both URL and filename are JsFunctions invoked // with event so any handler-scoped inputs read live state. if (filenameInput == null) { - return JsFunction.of("window.Vaadin.Flow.download.start($0(event))", - urlInput.toJs(trigger)).withArguments("event"); + return JsFunction + .of("window.Vaadin.Flow.download.start($0(event, context))", + urlInput.toJs(trigger)) + .withArguments("event", "context"); } - return JsFunction - .of("window.Vaadin.Flow.download.start($0(event), $1(event))", - urlInput.toJs(trigger), filenameInput.toJs(trigger)) - .withArguments("event"); + return JsFunction.of( + "window.Vaadin.Flow.download.start($0(event, context), $1(event, context))", + urlInput.toJs(trigger), filenameInput.toJs(trigger)) + .withArguments("event", "context"); } private static LiteralInput literal(String value, String name) { diff --git a/flow-server/src/main/java/com/vaadin/flow/component/trigger/internal/OpenInNewTabAction.java b/flow-server/src/main/java/com/vaadin/flow/component/trigger/internal/OpenInNewTabAction.java index a18479ef7fd..646d1ecbcc2 100644 --- a/flow-server/src/main/java/com/vaadin/flow/component/trigger/internal/OpenInNewTabAction.java +++ b/flow-server/src/main/java/com/vaadin/flow/component/trigger/internal/OpenInNewTabAction.java @@ -196,10 +196,12 @@ protected JsFunction toJs(Trigger trigger) { // resolved URL is a javascript: URL; see its Javadoc for the regex. // $0 = URL input's JsFunction; $1 = features input's JsFunction — // both invoked with the firing event so handler-scoped inputs work. - return JsFunction.of("((u) => " + JAVASCRIPT_SCHEME_JS_GUARD - + " || window.open(u, \"_blank\", $1(event)))" + "($0(event))", + return JsFunction.of( + "((u) => " + JAVASCRIPT_SCHEME_JS_GUARD + + " || window.open(u, \"_blank\", $1(event, context)))" + + "($0(event, context))", urlInput.toJs(trigger), featuresInput.toJs(trigger)) - .withArguments("event"); + .withArguments("event", "context"); } private static LiteralInput urlLiteral(String url) { diff --git a/flow-server/src/main/java/com/vaadin/flow/component/trigger/internal/PromiseAction.java b/flow-server/src/main/java/com/vaadin/flow/component/trigger/internal/PromiseAction.java index aac29adc908..b66ea124803 100644 --- a/flow-server/src/main/java/com/vaadin/flow/component/trigger/internal/PromiseAction.java +++ b/flow-server/src/main/java/com/vaadin/flow/component/trigger/internal/PromiseAction.java @@ -143,8 +143,8 @@ protected PromiseAction(Class payloadType, * *

{@code
      * return JsFunction.of(
-     *         "return ((v) => navigator.clipboard.writeText(v).then(() => v))($0(event))",
-     *         textInput.toJs(trigger)).withArguments("event");
+     *         "return ((v) => navigator.clipboard.writeText(v).then(() => v))($0(event, context))",
+     *         textInput.toJs(trigger)).withArguments("event", "context");
      * }
* * @param trigger @@ -172,12 +172,12 @@ protected final JsFunction toJs(Trigger trigger) { } ReturnChannelRegistration channel = channelFor( trigger.getHost().getNode()); - // $0(observer)($1(event)(promise), $2(channel)) — invoke the inner + // $0(observer)($1(event, context)(promise), $2(channel)) — invoke the + // inner // function to get a Promise, then hand it plus the return channel to // the shared observer JsFunction which subscribes to .then/.catch. - return JsFunction - .of("$0($1(event), $2)", OBSERVE_PROMISE, inner, channel) - .withArguments("event"); + return JsFunction.of("$0($1(event, context), $2)", OBSERVE_PROMISE, + inner, channel).withArguments("event", "context"); } private ReturnChannelRegistration channelFor(StateNode hostNode) { diff --git a/flow-server/src/main/java/com/vaadin/flow/component/trigger/internal/SetPropertyAction.java b/flow-server/src/main/java/com/vaadin/flow/component/trigger/internal/SetPropertyAction.java index efb9af6ea74..b8ca055abb4 100644 --- a/flow-server/src/main/java/com/vaadin/flow/component/trigger/internal/SetPropertyAction.java +++ b/flow-server/src/main/java/com/vaadin/flow/component/trigger/internal/SetPropertyAction.java @@ -120,7 +120,8 @@ protected JsFunction toJs(Trigger trigger) { // $0 = target element (captured), $1 = property name (string // capture, Jackson-quoted on the client), $2 = source JsFunction // (invoked with event so handler-scoped inputs work). - return JsFunction.of("$0[$1] = $2(event)", target, propertyName, - source.toJs(trigger)).withArguments("event"); + return JsFunction.of("$0[$1] = $2(event, context)", target, + propertyName, source.toJs(trigger)) + .withArguments("event", "context"); } } diff --git a/flow-server/src/main/java/com/vaadin/flow/component/trigger/internal/ShareAction.java b/flow-server/src/main/java/com/vaadin/flow/component/trigger/internal/ShareAction.java index e59e41b9fbb..bbbcb9c1fa6 100644 --- a/flow-server/src/main/java/com/vaadin/flow/component/trigger/internal/ShareAction.java +++ b/flow-server/src/main/java/com/vaadin/flow/component/trigger/internal/ShareAction.java @@ -130,7 +130,8 @@ private static void validate(Action.@Nullable Input title, @Override protected JsFunction toPromiseJs(Trigger trigger) { - // navigator.share({title:$0(event), ...}) with only the slots that were + // navigator.share({title:$0(event, context), ...}) with only the slots + // that were // set; each slot's value is produced on the client by invoking the // input's JsFunction with the trigger event. validate() already ensures // at least one slot is present, so the object is never empty (the Web @@ -143,7 +144,7 @@ protected JsFunction toPromiseJs(Trigger trigger) { appendSlot(expression, args, "url", urlInput, trigger); expression.append("})"); return JsFunction.of(expression.toString(), args.toArray()) - .withArguments("event"); + .withArguments("event", "context"); } private static void appendSlot(StringBuilder expression, @@ -156,7 +157,7 @@ private static void appendSlot(StringBuilder expression, expression.append(','); } expression.append(key).append(":$").append(args.size()) - .append("(event)"); + .append("(event, context)"); args.add(input.toJs(trigger)); } } diff --git a/flow-server/src/main/java/com/vaadin/flow/component/trigger/internal/SinkTrigger.java b/flow-server/src/main/java/com/vaadin/flow/component/trigger/internal/SinkTrigger.java new file mode 100644 index 00000000000..8cba8788285 --- /dev/null +++ b/flow-server/src/main/java/com/vaadin/flow/component/trigger/internal/SinkTrigger.java @@ -0,0 +1,69 @@ +/* + * 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.trigger.internal; + +import java.util.Objects; + +import com.vaadin.flow.component.trigger.ClientActionSink; +import com.vaadin.flow.dom.Element; +import com.vaadin.flow.dom.JsFunction; +import com.vaadin.flow.shared.Registration; + +/** + * A trigger that installs no client-side listener of its own: it hands the + * rendered action function to a {@link ClientActionSink} and lets the sink + * decide when it runs. + *

+ * Used where the thing that fires is rendered on the client and has no + * server-side component to listen on — a {@code LitRenderer} template binding + * {@code @click=${copy}}, for example. The sink passes the function into the + * renderer, the template calls it from its own event binding, and the action + * therefore runs inside the browser's event handler with the user gesture still + * valid, exactly as it does for a {@link DomEventTrigger}. + *

+ * For internal use only. May be renamed or removed in a future release. + */ +public class SinkTrigger extends Trigger { + + private final ClientActionSink sink; + + /** + * Creates a trigger that hands its rendered actions to {@code sink}. + * + * @param host + * the element whose lifecycle the trigger belongs to (the + * renderer's container), not {@code null} + * @param sink + * receives the rendered action function, not {@code null} + */ + public SinkTrigger(Element host, ClientActionSink sink) { + super(host); + this.sink = Objects.requireNonNull(sink, "sink must not be null"); + } + + @Override + public boolean suppliesContext() { + // The renderer knows which item each rendered element belongs to and + // passes it to the action as the context argument. + return true; + } + + @Override + protected Registration install(JsFunction action) { + return Objects.requireNonNull(sink.install(action), + "ClientActionSink.install must return a Registration"); + } +} diff --git a/flow-server/src/main/java/com/vaadin/flow/component/trigger/internal/Trigger.java b/flow-server/src/main/java/com/vaadin/flow/component/trigger/internal/Trigger.java index 66ca78a25c5..bfdf6f9dcac 100644 --- a/flow-server/src/main/java/com/vaadin/flow/component/trigger/internal/Trigger.java +++ b/flow-server/src/main/java/com/vaadin/flow/component/trigger/internal/Trigger.java @@ -87,6 +87,21 @@ protected Trigger(Component host) { verifyArmedBeforeClientResponse(); } + /** + * Creates a new trigger bound to the given element. + *

+ * Used by triggers whose host has no component of its own — for example a + * renderer whose container element is all the server side has of the rows + * it renders. + * + * @param host + * the element the trigger fires on, not {@code null} + */ + protected Trigger(Element host) { + this.host = Objects.requireNonNull(host); + verifyArmedBeforeClientResponse(); + } + /** * Schedules a check, run once the host is attached and just before the * client response is built, that fails if no action was committed to this @@ -124,6 +139,23 @@ private void verifyArmedBeforeClientResponse() { })); } + /** + * Whether this trigger supplies a context object to the actions it fires, + * describing what the trigger fired for. + *

+ * {@code false} for the plain event triggers, whose handler has nothing to + * say beyond the event itself; {@code true} for triggers rendered into + * something that repeats, such as a row renderer. Inputs that read the + * context ({@link ContextInput}) reject a trigger that supplies none, so + * the mistake surfaces on the server instead of as an undefined value in + * the browser. + * + * @return whether a context is supplied, {@code false} by default + */ + public boolean suppliesContext() { + return false; + } + /** * The host element this trigger fires on. * diff --git a/flow-server/src/main/java/com/vaadin/flow/component/trigger/internal/WriteToClipboardAction.java b/flow-server/src/main/java/com/vaadin/flow/component/trigger/internal/WriteToClipboardAction.java index 799e632cd82..b5b4f651fd5 100644 --- a/flow-server/src/main/java/com/vaadin/flow/component/trigger/internal/WriteToClipboardAction.java +++ b/flow-server/src/main/java/com/vaadin/flow/component/trigger/internal/WriteToClipboardAction.java @@ -242,7 +242,7 @@ protected JsFunction toPromiseJs(Trigger trigger) { JsFunction image = imageInput != null ? imageInput.toJs(trigger) : NULL_INPUT_FN; return JsFunction.of( - "return window.Vaadin.Flow.clipboard.writePayload($0(event), $1(event), $2(event))", - text, html, image).withArguments("event"); + "return window.Vaadin.Flow.clipboard.writePayload($0(event, context), $1(event, context), $2(event, context))", + text, html, image).withArguments("event", "context"); } } diff --git a/flow-server/src/main/java/com/vaadin/flow/component/trigger/package-info.java b/flow-server/src/main/java/com/vaadin/flow/component/trigger/package-info.java new file mode 100644 index 00000000000..fd41188c798 --- /dev/null +++ b/flow-server/src/main/java/com/vaadin/flow/component/trigger/package-info.java @@ -0,0 +1,27 @@ +/* + * 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. + */ +/** + * Actions that run in the browser when something fires them, and the values + * they read when they do. + *

+ * Most applications never touch this package: they bind an action to a + * component through a feature facade ({@code Clipboard.onClick(button)}, + * {@code Fullscreen.onClick(button)}). The types here are for the case where + * the thing that fires the action is rendered on the client and has no + * server-side component of its own — a {@code LitRenderer} row template, for + * example. See {@link com.vaadin.flow.component.trigger.ClientAction}. + */ +package com.vaadin.flow.component.trigger; diff --git a/flow-server/src/test/java/com/vaadin/flow/component/fullscreen/FullscreenTest.java b/flow-server/src/test/java/com/vaadin/flow/component/fullscreen/FullscreenTest.java index 359b143e78f..2a838a0fd0c 100644 --- a/flow-server/src/test/java/com/vaadin/flow/component/fullscreen/FullscreenTest.java +++ b/flow-server/src/test/java/com/vaadin/flow/component/fullscreen/FullscreenTest.java @@ -101,7 +101,7 @@ void enter_withCallbacks_wrapsWithObserver() { }); JsFunction action = actionOf(singleInstallFn(ui)); - assertEquals("$0($1(event), $2)", action.getBody()); + assertEquals("$0($1(event, context), $2)", action.getBody()); JsFunction inner = (JsFunction) action.getCaptures().get(1); assertEquals( @@ -137,7 +137,7 @@ void enterComponent_withCallbacks_wrapsWithObserver() { }); JsFunction action = actionOf(singleInstallFn(ui)); - assertEquals("$0($1(event), $2)", action.getBody()); + assertEquals("$0($1(event, context), $2)", action.getBody()); JsFunction inner = (JsFunction) action.getCaptures().get(1); assertEquals( diff --git a/flow-server/src/test/java/com/vaadin/flow/component/trigger/ClientActionTest.java b/flow-server/src/test/java/com/vaadin/flow/component/trigger/ClientActionTest.java new file mode 100644 index 00000000000..e7f866f9973 --- /dev/null +++ b/flow-server/src/test/java/com/vaadin/flow/component/trigger/ClientActionTest.java @@ -0,0 +1,153 @@ +/* + * 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.trigger; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.UI; +import com.vaadin.flow.component.clipboard.Clipboard; +import com.vaadin.flow.component.trigger.internal.ClickTrigger; +import com.vaadin.flow.component.trigger.internal.WriteToClipboardAction; +import com.vaadin.flow.dom.Element; +import com.vaadin.flow.dom.JsFunction; +import com.vaadin.flow.internal.nodefeature.ReturnChannelRegistration; +import com.vaadin.flow.shared.Registration; +import com.vaadin.tests.util.MockUI; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Verifies the client-action seam: an action bound to a sink instead of to a + * component, so a renderer can fire it once per element it renders on the + * client while the server holds a single binding. + */ +class ClientActionTest { + + private static class Div extends Component { + Div() { + super(new Element("div")); + } + } + + /** + * Stands in for the renderer: keeps the rendered action functions so they + * can be handed to the client-side template. + */ + private static class RecordingSink implements ClientActionSink { + + private final List installed = new ArrayList<>(); + + @Override + public Registration install(JsFunction action) { + installed.add(action); + return () -> installed.remove(action); + } + } + + @Test + void itemProperty_actionReadsValueFromTheRowItFiredFor() { + UI ui = new MockUI(); + Div container = new Div(); + ui.add(container); + RecordingSink sink = new RecordingSink(); + + Clipboard.write().text(ClientValue.itemProperty("email")) + .bindTo(container.getElement(), sink); + + // One binding for the whole renderer, whatever the row count. + assertEquals(1, sink.installed.size()); + JsFunction action = sink.installed.get(0); + + // The renderer calls the function from its own event binding and + // supplies the row it fired for as the context argument. + assertEquals(List.of("event", "context"), action.getArgumentNames()); + assertEquals( + "return window.Vaadin.Flow.clipboard.writePayload($0(event, context), $1(event, context), $2(event, context))", + action.getBody()); + + JsFunction text = (JsFunction) action.getCaptures().get(0); + assertEquals(List.of("event", "context"), text.getArgumentNames()); + assertEquals("return context[$0][$1]", text.getBody()); + assertEquals(List.of("item", "email"), text.getCaptures()); + } + + @Test + void unarmedCheckPasses_andRegistrationDetachesTheBinding() { + UI ui = new MockUI(); + Div container = new Div(); + ui.add(container); + RecordingSink sink = new RecordingSink(); + + Registration registration = Clipboard.write() + .text(ClientValue.itemProperty("email")) + .bindTo(container.getElement(), sink); + + // The action is committed as part of binding, so the trigger is armed + // and the before-response check does not fail. + ui.getInternals().getStateTree().runExecutionsBeforeClientResponse(); + + registration.remove(); + assertTrue(sink.installed.isEmpty(), + "removing the registration must detach the installed function"); + } + + @Test + void observedWrite_reportsOutcomeThroughTheContainerNode() { + UI ui = new MockUI(); + Div container = new Div(); + ui.add(container); + RecordingSink sink = new RecordingSink(); + List copied = new ArrayList<>(); + + Clipboard.write() + .text(ClientValue.itemProperty("email"), copied::add, + error -> copied.add("error: " + error.name())) + .bindTo(container.getElement(), sink); + + // The outcome comes back over a return channel on the renderer's own + // node — the row identity is not part of it, only the copied value. + JsFunction action = sink.installed.get(0); + Object channel = action.getCaptures().stream() + .filter(ReturnChannelRegistration.class::isInstance).findFirst() + .orElseThrow(); + assertInstanceOf(ReturnChannelRegistration.class, channel); + assertFalse(action.getBody().contains("context["), + "the outcome wrapper itself carries no context"); + } + + @Test + void contextValue_onATriggerWithoutContext_failsOnTheServer() { + // A context-dependent value bound to a plain click trigger would + // evaluate to undefined in the browser; it is rejected here instead. + UI ui = new MockUI(); + Div button = new Div(); + ui.add(button); + + assertThrows(IllegalArgumentException.class, + () -> new ClickTrigger(button) + .triggers(new WriteToClipboardAction( + ClientValue.itemProperty("email").getInput(), + null))); + } +} diff --git a/flow-server/src/test/java/com/vaadin/flow/component/trigger/CrossPackageExtensionTest.java b/flow-server/src/test/java/com/vaadin/flow/component/trigger/CrossPackageExtensionTest.java index eaf3ea35ed6..25c2ceef7dc 100644 --- a/flow-server/src/test/java/com/vaadin/flow/component/trigger/CrossPackageExtensionTest.java +++ b/flow-server/src/test/java/com/vaadin/flow/component/trigger/CrossPackageExtensionTest.java @@ -63,8 +63,9 @@ private static class LogAction extends Action { @Override protected JsFunction toJs(Trigger trigger) { return JsFunction - .of("console.log($0(event))", message.toJs(trigger)) - .withArguments("event"); + .of("console.log($0(event, context))", + message.toJs(trigger)) + .withArguments("event", "context"); } JsFunction render(Trigger trigger) { @@ -93,7 +94,7 @@ void customAction_consumesInputItDidNotCreate() { JsFunction rendered = action.render(click); - assertEquals("console.log($0(event))", rendered.getBody()); + assertEquals("console.log($0(event, context))", rendered.getBody()); JsFunction source = (JsFunction) rendered.getCaptures().get(0); assertEquals("return event[$0]", source.getBody()); assertEquals("screenX", source.getCaptures().get(0)); diff --git a/flow-server/src/test/java/com/vaadin/flow/component/trigger/internal/CallbackActionTest.java b/flow-server/src/test/java/com/vaadin/flow/component/trigger/internal/CallbackActionTest.java index cb60283d80b..7a555dde032 100644 --- a/flow-server/src/test/java/com/vaadin/flow/component/trigger/internal/CallbackActionTest.java +++ b/flow-server/src/test/java/com/vaadin/flow/component/trigger/internal/CallbackActionTest.java @@ -71,7 +71,7 @@ void handlerBody_callsChannelWithSourceExpression() { JsFunction action = actionOf(singleInstallFn(ui)); // $0 = the return channel; $1 = the source input's JsFunction. The // body forwards the source-fn's value straight into the channel call. - assertEquals("$0($1(event));", action.getBody()); + assertEquals("$0($1(event, context));", action.getBody()); assertEquals(2, action.getCaptures().size()); assertTrue( action.getCaptures() diff --git a/flow-server/src/test/java/com/vaadin/flow/component/trigger/internal/DomEventTriggerTest.java b/flow-server/src/test/java/com/vaadin/flow/component/trigger/internal/DomEventTriggerTest.java index eb0d89a06a1..7ef06c85288 100644 --- a/flow-server/src/test/java/com/vaadin/flow/component/trigger/internal/DomEventTriggerTest.java +++ b/flow-server/src/test/java/com/vaadin/flow/component/trigger/internal/DomEventTriggerTest.java @@ -54,11 +54,12 @@ void setProperty_emitsAddEventListenerOnAttach() { // The install $0 is the action's JsFunction directly — no intermediate // composed handler layer. The action is also the DOM event listener. JsFunction action = actionOf(install); - assertEquals(List.of("event"), action.getArgumentNames()); + assertEquals(List.of("event", "context"), action.getArgumentNames()); // SetPropertyAction body shape; target captured as $0, property name - // string capture at $1, source JsFunction invoked as $2(event). - assertEquals("$0[$1] = $2(event)", action.getBody()); + // string capture at $1, source JsFunction invoked as $2(event, + // context). + assertEquals("$0[$1] = $2(event, context)", action.getBody()); assertSame(field.getElement(), action.getCaptures().get(0)); assertEquals("value", action.getCaptures().get(1)); } diff --git a/flow-server/src/test/java/com/vaadin/flow/component/trigger/internal/DownloadActionTest.java b/flow-server/src/test/java/com/vaadin/flow/component/trigger/internal/DownloadActionTest.java index d22a3260463..0610eb26850 100644 --- a/flow-server/src/test/java/com/vaadin/flow/component/trigger/internal/DownloadActionTest.java +++ b/flow-server/src/test/java/com/vaadin/flow/component/trigger/internal/DownloadActionTest.java @@ -44,7 +44,7 @@ void urlString_actionCallsStartWithOneArgument() { ui.getInternals().getStateTree().runExecutionsBeforeClientResponse(); JsFunction action = actionOf(singleInstallFn(ui)); - assertEquals("window.Vaadin.Flow.download.start($0(event))", + assertEquals("window.Vaadin.Flow.download.start($0(event, context))", action.getBody()); assertLiteralInputValue(action, 0, "/api/report.pdf"); } @@ -64,7 +64,8 @@ void urlStringWithFilename_actionCallsStartWithTwoArguments() { // verbatim into the input function's capture — Jackson handles the // escaping on the wire, no hand-quoting in the JS. JsFunction action = actionOf(singleInstallFn(ui)); - assertEquals("window.Vaadin.Flow.download.start($0(event), $1(event))", + assertEquals( + "window.Vaadin.Flow.download.start($0(event, context), $1(event, context))", action.getBody()); assertLiteralInputValue(action, 0, "/api/report.pdf"); assertLiteralInputValue(action, 1, "Q1 \"report\".pdf"); diff --git a/flow-server/src/test/java/com/vaadin/flow/component/trigger/internal/OpenInNewTabActionTest.java b/flow-server/src/test/java/com/vaadin/flow/component/trigger/internal/OpenInNewTabActionTest.java index 46c658a6f63..1c3d32dc7c7 100644 --- a/flow-server/src/test/java/com/vaadin/flow/component/trigger/internal/OpenInNewTabActionTest.java +++ b/flow-server/src/test/java/com/vaadin/flow/component/trigger/internal/OpenInNewTabActionTest.java @@ -30,7 +30,7 @@ class OpenInNewTabActionTest { private static final String EXPECTED_BODY = "((u) =>" + " /^[\\x00-\\x20]*javascript:/i.test(String(u))" - + " || window.open(u, \"_blank\", $1(event)))($0(event))"; + + " || window.open(u, \"_blank\", $1(event, context)))($0(event, context))"; @Test void urlString_emitsWindowOpenWithBlankTargetAndDefaultFeatures() { diff --git a/flow-server/src/test/java/com/vaadin/flow/component/trigger/internal/PromiseActionTest.java b/flow-server/src/test/java/com/vaadin/flow/component/trigger/internal/PromiseActionTest.java index f864d7bb881..b01da899851 100644 --- a/flow-server/src/test/java/com/vaadin/flow/component/trigger/internal/PromiseActionTest.java +++ b/flow-server/src/test/java/com/vaadin/flow/component/trigger/internal/PromiseActionTest.java @@ -78,7 +78,7 @@ void withCallbacks_actionFnWrapsInnerWithObserverAndChannel() { // $1 is the inner JsFunction returning the promise, $2 is the return // channel. JsFunction action = actionOf(singleInstallFn(ui)); - assertEquals("$0($1(event), $2)", action.getBody()); + assertEquals("$0($1(event, context), $2)", action.getBody()); List<@Nullable Object> captures = action.getCaptures(); assertEquals(3, captures.size(), diff --git a/flow-server/src/test/java/com/vaadin/flow/component/trigger/internal/ReadFromClipboardActionTest.java b/flow-server/src/test/java/com/vaadin/flow/component/trigger/internal/ReadFromClipboardActionTest.java index a8366ef35f2..3ade3a81bd4 100644 --- a/flow-server/src/test/java/com/vaadin/flow/component/trigger/internal/ReadFromClipboardActionTest.java +++ b/flow-server/src/test/java/com/vaadin/flow/component/trigger/internal/ReadFromClipboardActionTest.java @@ -54,7 +54,7 @@ void actionFnWrapsClipboardReadPromiseWithObserverAndChannel() { // Action wraps the inner promise function with OBSERVE_PROMISE + // return channel. The inner just invokes the Clipboard.ts helper. JsFunction action = actionOf(singleInstallFn(ui)); - assertEquals("$0($1(event), $2)", action.getBody()); + assertEquals("$0($1(event, context), $2)", action.getBody()); JsFunction inner = (JsFunction) action.getCaptures().get(1); assertEquals("return window.Vaadin.Flow.clipboard.readPayload()", diff --git a/flow-server/src/test/java/com/vaadin/flow/component/trigger/internal/RequestFullscreenActionTest.java b/flow-server/src/test/java/com/vaadin/flow/component/trigger/internal/RequestFullscreenActionTest.java index aa5ad0a7a0c..d9febd2c6eb 100644 --- a/flow-server/src/test/java/com/vaadin/flow/component/trigger/internal/RequestFullscreenActionTest.java +++ b/flow-server/src/test/java/com/vaadin/flow/component/trigger/internal/RequestFullscreenActionTest.java @@ -74,7 +74,7 @@ void pageMode_withCallbacks_wrapsRequestPageFullscreenWithObserver() { // With callbacks: outer function is the observer wrapper; the inner // function is captured at $1 and contains the actual fullscreen call. JsFunction action = actionOf(singleInstallFn(ui)); - assertEquals("$0($1(event), $2)", action.getBody()); + assertEquals("$0($1(event, context), $2)", action.getBody()); JsFunction inner = (JsFunction) action.getCaptures().get(1); assertEquals( @@ -117,7 +117,7 @@ void componentMode_withCallbacks_wrapsRequestComponentFullscreenWithObserver() { ui.getInternals().getStateTree().runExecutionsBeforeClientResponse(); JsFunction action = actionOf(singleInstallFn(ui)); - assertEquals("$0($1(event), $2)", action.getBody()); + assertEquals("$0($1(event, context), $2)", action.getBody()); JsFunction inner = (JsFunction) action.getCaptures().get(1); assertEquals( diff --git a/flow-server/src/test/java/com/vaadin/flow/component/trigger/internal/ShareActionTest.java b/flow-server/src/test/java/com/vaadin/flow/component/trigger/internal/ShareActionTest.java index ced4f71b175..8fd0b691b49 100644 --- a/flow-server/src/test/java/com/vaadin/flow/component/trigger/internal/ShareActionTest.java +++ b/flow-server/src/test/java/com/vaadin/flow/component/trigger/internal/ShareActionTest.java @@ -45,7 +45,7 @@ void fireAndForget_allSlots_emitsNavigatorShareWithEachInputFunction() { // stringified into the body. JsFunction action = actionOf(singleInstallFn(ui)); assertEquals( - "return navigator.share({title:$0(event),text:$1(event),url:$2(event)})", + "return navigator.share({title:$0(event, context),text:$1(event, context),url:$2(event, context)})", action.getBody()); assertEquals("Hi", ((JsFunction) action.getCaptures().get(0)) .getCaptures().get(0)); @@ -68,7 +68,7 @@ void fireAndForget_onlyUrl_emitsObjectWithSingleField() { ui.getInternals().getStateTree().runExecutionsBeforeClientResponse(); JsFunction action = actionOf(singleInstallFn(ui)); - assertEquals("return navigator.share({url:$0(event)})", + assertEquals("return navigator.share({url:$0(event, context)})", action.getBody()); assertEquals("https://vaadin.com", ((JsFunction) action.getCaptures().get(0)).getCaptures() @@ -92,10 +92,10 @@ void withCallbacks_wrapsInnerNavigatorSharePromiseWithObserver() { // with OBSERVE_PROMISE + the return channel; the inner $1 still calls // navigator.share. JsFunction action = actionOf(singleInstallFn(ui)); - assertEquals("$0($1(event), $2)", action.getBody()); + assertEquals("$0($1(event, context), $2)", action.getBody()); JsFunction inner = (JsFunction) action.getCaptures().get(1); - assertEquals("return navigator.share({title:$0(event)})", + assertEquals("return navigator.share({title:$0(event, context)})", inner.getBody()); } diff --git a/flow-server/src/test/java/com/vaadin/flow/component/trigger/internal/WriteToClipboardActionTest.java b/flow-server/src/test/java/com/vaadin/flow/component/trigger/internal/WriteToClipboardActionTest.java index b8e01668054..535525ad829 100644 --- a/flow-server/src/test/java/com/vaadin/flow/component/trigger/internal/WriteToClipboardActionTest.java +++ b/flow-server/src/test/java/com/vaadin/flow/component/trigger/internal/WriteToClipboardActionTest.java @@ -38,7 +38,7 @@ class WriteToClipboardActionTest { - private static final String HELPER_BODY = "return window.Vaadin.Flow.clipboard.writePayload($0(event), $1(event), $2(event))"; + private static final String HELPER_BODY = "return window.Vaadin.Flow.clipboard.writePayload($0(event, context), $1(event, context), $2(event, context))"; @Test void fireAndForget_textOnly_callsHelperWithHtmlAndImageSlotsReturningNull() { @@ -200,7 +200,7 @@ void withCallbacks_actionFnWrapsInnerWithObserverAndChannel() { // calls writePayload — the action class itself does no string // assembly beyond the static body constant. JsFunction action = actionOf(singleInstallFn(ui)); - assertEquals("$0($1(event), $2)", action.getBody()); + assertEquals("$0($1(event, context), $2)", action.getBody()); JsFunction inner = (JsFunction) action.getCaptures().get(1); assertEquals(HELPER_BODY, inner.getBody()); @@ -223,7 +223,7 @@ void withCallbacks_imageOnly_wrapsInnerWithObserverAndChannel() { // The dedicated image observed constructor produces the same outer // shape as the text/html one — only the inner $2 (image) slot differs. JsFunction action = actionOf(singleInstallFn(ui)); - assertEquals("$0($1(event), $2)", action.getBody()); + assertEquals("$0($1(event, context), $2)", action.getBody()); JsFunction inner = (JsFunction) action.getCaptures().get(1); assertEquals(HELPER_BODY, inner.getBody()); diff --git a/flow-server/src/test/java/com/vaadin/flow/component/webshare/WebShareTest.java b/flow-server/src/test/java/com/vaadin/flow/component/webshare/WebShareTest.java index 88df914f8ab..fe4059b47dd 100644 --- a/flow-server/src/test/java/com/vaadin/flow/component/webshare/WebShareTest.java +++ b/flow-server/src/test/java/com/vaadin/flow/component/webshare/WebShareTest.java @@ -80,7 +80,7 @@ void share_literalSlots_emitsNavigatorShareWithAllFields() { // event; the literals are captured inside those nested functions. JsFunction handler = handlerFn(ui); assertEquals( - "return navigator.share({title:$0(event),text:$1(event),url:$2(event)})", + "return navigator.share({title:$0(event, context),text:$1(event, context),url:$2(event, context)})", handler.getBody()); assertEquals("Hi", ((JsFunction) handler.getCaptures().get(0)) .getCaptures().get(0)); @@ -104,7 +104,7 @@ void share_titleFromHasValue_emitsPropertyInputForValue() { // PropertyInput renders as its own JsFunction (return $0[$1]) with the // property name captured at $1. JsFunction handler = handlerFn(ui); - assertEquals("return navigator.share({title:$0(event)})", + assertEquals("return navigator.share({title:$0(event, context)})", handler.getBody()); JsFunction titleInput = (JsFunction) handler.getCaptures().get(0); assertEquals("return $0[$1]", titleInput.getBody());