Skip to content
Draft
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
Expand Up @@ -165,6 +165,30 @@ public static <T extends Component & ClickNotifier<?>> 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:
*
* <pre>{@code
* LitRenderer.<Customer> of(
* "<span>${item.email}</span><button @click=${copy}>Copy</button>")
* .withProperty("email", Customer::email).withClientAction("copy",
* Clipboard.write().text(ClientValue.itemProperty("email")));
* }</pre>
*
* 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
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
* <p>
* 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.
*
* <pre>{@code
* LitRenderer.<Customer> of(
* "<span>${item.email}</span><button @click=${copy}>Copy</button>")
* .withProperty("email", Customer::email).withClientAction("copy",
* Clipboard.write().text(ClientValue.itemProperty("email")));
* }</pre>
*/
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<String> 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<String> value,
SerializableConsumer<@Nullable String> onCopied,
SerializableConsumer<Error> onError) {
Objects.requireNonNull(value, "value must not be null");
return ClientActions.of(new WriteToClipboardAction(value.getInput(),
null, onCopied, onError));
}
}
Original file line number Diff line number Diff line change
@@ -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.
* <p>
* 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 <em>what</em>
* 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:
*
* <pre>{@code
* grid.addColumn(LitRenderer.<Customer> of(
* "<span>${item.email}</span><button @click=${copy}>Copy</button>")
* .withProperty("email", Customer::email).withClientAction("copy",
* Clipboard.write().text(ClientValue.itemProperty("email"))));
* }</pre>
*
* 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.
* <p>
* 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.
* <p>
* Called by the component or renderer that accepts the action, once per
* place it is rendered into. The returned {@link Registration} detaches the
* binding.
* <p>
* 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);
}
Original file line number Diff line number Diff line change
@@ -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.
* <p>
* 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.
* <p>
* 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);
}
Original file line number Diff line number Diff line change
@@ -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.
* <p>
* 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.
*
* <pre>{@code
* Clipboard.write().text(ClientValue.itemProperty("email"));
* }</pre>
*
* @param <T>
* the type of the value produced
*/
public final class ClientValue<T> implements Serializable {

private final Action.Input<T> input;

private ClientValue(Action.Input<T> 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<String> 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<String> of(String value) {
Objects.requireNonNull(value, "value must not be null");
return new ClientValue<>(new LiteralInput<>(value));
}

/**
* The input backing this value.
* <p>
* For internal use only. May be renamed or removed in a future release.
*
* @return the input, never {@code null}
*/
public Action.Input<T> getInput() {
return input;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
* <p>
* 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}
Expand Down Expand Up @@ -121,10 +127,10 @@ public abstract static class Input<T> 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.
*
* <p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Loading
Loading