Skip to content

feat: PromiseAction + CopyTextToClipboardAction + RequestFullscreenAction#24394

Open
Artur- wants to merge 6 commits into
mainfrom
feature/trigger-step2
Open

feat: PromiseAction + CopyTextToClipboardAction + RequestFullscreenAction#24394
Artur- wants to merge 6 commits into
mainfrom
feature/trigger-step2

Conversation

@Artur-
Copy link
Copy Markdown
Member

@Artur- Artur- commented May 21, 2026

Adds a PromiseAction base class in the trigger framework and two concrete subclasses — CopyTextToClipboardAction and RequestFullscreenAction — on top of the executeJs-based trigger framework introduced in step 1.

PromiseAction is the generic primitive: many gesture-bound browser APIs are asynchronous (clipboard, fullscreen, file picker, share, payments, …) and follow the same shape — call the API, then handle the resolved value or the rejection. Subclasses override appendPromiseExpression(JsBuilder, StringBuilder) to emit the promise-yielding JS; the base wires the outcome reporting.

Two construction modes, mirroring the Geolocation API:

  • No-arg super() — fire-and-forget; the rendered JS is just the promise expression and the server never sees the outcome.
  • super(onSuccess, onError)onSuccess runs after the promise resolves; onError receives the browser's error message after the promise rejects. Both run on the UI thread; both are required (pass () -> {} or err -> {} to opt out of one, matching the Geolocation pattern).

The with-outcome mode lazily registers one
ReturnChannelRegistration per trigger host node and appends .then(()=>$N(true,null)).catch(e=>$N(false, msg)) to the promise expression, so the client invokes the channel after the promise resolves or rejects. JsBuilder.capture(Object) is the new package-private hook that lets actions allocate a $N placeholder for non-Element captures (the return channel here, which materialises client-side as a callable JS function).

Concrete subclasses:

  • CopyTextToClipboardAction — supplies navigator.clipboard.writeText(textExpr) as the promise expression. Takes an Action.Input<String> for the text. Named verb-first so parallels like CopyImageToClipboardAction and PasteFromClipboardAction read naturally.
  • RequestFullscreenAction — supplies <target>.requestFullscreen() as the promise expression. Takes a target Component whose root element will be fullscreened.

Both ITs (TriggerCopyTextToClipboardView/IT,
TriggerRequestFullscreenView/IT) shim the underlying browser API to resolving/rejecting promises and assert the server-side status div updates accordingly — avoiding dependence on browser clipboard / fullscreen permissions in CI.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 21, 2026

Test Results

 1 418 files  + 5   1 418 suites  +5   1h 22m 16s ⏱️ +32s
 9 997 tests +15   9 928 ✅ +15  69 💤 ±0  0 ❌ ±0 
10 472 runs  +15  10 401 ✅ +15  71 💤 ±0  0 ❌ ±0 

Results for commit de39bdb. ± Comparison against base commit c998fd5.

♻️ This comment has been updated with latest results.

…tion

Adds a `PromiseAction` base class in the trigger framework and two
concrete subclasses — `CopyTextToClipboardAction` and
`RequestFullscreenAction` — on top of the executeJs-based trigger
framework introduced in step 1.

`PromiseAction` is the generic primitive: many gesture-bound browser
APIs are asynchronous (clipboard, fullscreen, file picker, share,
payments, …) and follow the same shape — call the API, then handle the
resolved value or the rejection. Subclasses override
`appendPromiseExpression(JsBuilder, StringBuilder)` to emit the
promise-yielding JS; the base wires the outcome reporting.

Two construction modes, mirroring the Geolocation API:

- No-arg `super()` — fire-and-forget; the rendered JS is just the
  promise expression and the server never sees the outcome.
- `super(onSuccess, onError)` — `onSuccess` runs after the promise
  resolves; `onError` receives the browser's error message after the
  promise rejects. Both run on the UI thread; both are required (pass
  `() -> {}` or `err -> {}` to opt out of one, matching the
  Geolocation pattern).

The with-outcome mode lazily registers one
`ReturnChannelRegistration` per trigger host node and appends
`.then(()=>$N(true,null)).catch(e=>$N(false, msg))` to the promise
expression, so the client invokes the channel after the promise
resolves or rejects. `JsBuilder.capture(Object)` is the new
package-private hook that lets actions allocate a `$N` placeholder for
non-Element captures (the return channel here, which materialises
client-side as a callable JS function).

Concrete subclasses:

- `CopyTextToClipboardAction` — supplies
  `navigator.clipboard.writeText(textExpr)` as the promise expression.
  Takes an `Action.Input<String>` for the text. Named verb-first so
  parallels like `CopyImageToClipboardAction` and
  `PasteFromClipboardAction` read naturally.
- `RequestFullscreenAction` — supplies `<target>.requestFullscreen()`
  as the promise expression. Takes a target `Component` whose root
  element will be fullscreened.

Both ITs (`TriggerCopyTextToClipboardView`/`IT`,
`TriggerRequestFullscreenView`/`IT`) shim the underlying browser API
to resolving/rejecting promises and assert the server-side status div
updates accordingly — avoiding dependence on browser clipboard /
fullscreen permissions in CI.
@mshabarov mshabarov self-requested a review May 21, 2026 10:47
@Artur- Artur- force-pushed the feature/trigger-step2 branch 2 times, most recently from 1dc9334 to db61e7a Compare May 21, 2026 10:55
Artur- added 5 commits May 21, 2026 14:17
`PromiseAction.appendStatement` used to assemble the `.then`/`.catch`
glue by chained `StringBuilder.append` calls, with the channel
receiving positional `(boolean, message)` args decoded by index in
`dispatch`. Two smells in one place: structural JS hidden in string
concatenation, and an implicit positional wire contract.

Replace both:

- A static `JsFunction OBSERVE_PROMISE` carries the wrapper body once,
  with named runtime arguments `(promise, channel)`. Subclasses are
  unchanged; `appendStatement` now allocates two captures (observer +
  channel), then composes one call: `$observe($promiseExpr, $channel)`.
  No more `.append(".then(()=>")…` chains; the handler body is one
  line, the JS literal lives in one place.

- Introduce a private `record Outcome(boolean ok, @nullable String
  error)` as the wire shape. The OBSERVE_PROMISE body pushes
  `{ok: true}` / `{ok: false, error: …}`; `dispatch` reads it via
  `JacksonUtils.readValue` rather than positional `args.get(0)` /
  `args.get(1)` decoding. Renaming or adding a field can no longer
  silently desynchronize the two sides.

Test assertions follow: the handler body is now `$0(<promise>, $1);`
across `PromiseActionTest`, `CopyTextToClipboardActionTest`, and
`RequestFullscreenActionTest`; the channel-invocation tests pass an
`Outcome`-shaped `ObjectNode` instead of positional booleans/strings.

`OBSERVE_PROMISE` is a `static final` shared instance — `JsFunction`
is `Serializable` and immutable, and each `addJsInitializer`
registration still gets its own capture entry, so there's no
cross-UI state and no leak. 19/19 trigger-internal tests pass.
`LiteralInput` was package-private and accepted `@Nullable T`. Both
were carry-overs from its only original user, `SetPropertyAction`'s
null-clearing convenience constructor, but neither survives as
LiteralInput becomes a building block for other actions:

- Public so callers can wire a fixed value through any action that
  takes an `Action.Input<? extends T>` — e.g. copying a constant
  string with `new CopyTextToClipboardAction(new LiteralInput<>("…"))`.

- Non-null because `null` as a literal payload almost never matches
  a sensible browser API. `writeText(null)` writes the string
  `"null"` to the clipboard; `target.requestFullscreen.call(null)`
  is meaningless. Forbidding it at the `LiteralInput` constructor
  catches the foot-gun at compile time (via NullAway) wherever the
  type permits, and at runtime via `Objects.requireNonNull`
  otherwise.

`SetPropertyAction`'s null-accepting convenience constructor stays —
clearing a property by assigning `null` is a real use case. It now
routes the null branch through a private `NULL_LITERAL` singleton
that emits the JS literal `null`, instead of leaking nullability
into `LiteralInput`.

Adds a `fireAndForget_literalInput_encodesValueAsJsonLiteral` test
in `CopyTextToClipboardActionTest` that copies `hello "world"\n` and
asserts the quotes and newline are JSON-escaped — proves callers
can't break out of the JS string by passing values with quotes or
newlines.
The view now has two buttons — `#copy` ("Copy input value") wired to a
PropertyInput on the input field, and `#copy-static` ("Copy static
text") wired to a LiteralInput carrying `hello "world"\n`. The literal
deliberately includes a quote and a newline so the IT verifies the
value the browser ends up with is exactly what Java passed in, i.e.
the JSON encoding done at build time round-trips without losing or
mangling characters.

Button labels updated to describe what each button copies.
@sonarqubecloud
Copy link
Copy Markdown

Copy link
Copy Markdown
Contributor

@mshabarov mshabarov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

API and implementation looks good to me overall - clear purposes for added classes, nice classes hierarchy. I left several comments with some of them not being 100% changes requests, so ready to merge even as is with a meaningful elaborations of why it's implemented this way.

Comment on lines +46 to +47
* Two construction modes, mirroring the
* {@link com.vaadin.flow.component.geolocation.Geolocation Geolocation} API:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Referring to geolocation API here is probably redundant. I assume it added by Claude because it had a geolocation knowledge in a context.

.withArguments("promise", "channel");

private final @Nullable SerializableRunnable onSuccess;
private final @Nullable SerializableConsumer<String> onError;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error handler can give also an error name as an argument, so it can be possible to discriminate various types of errors. Also a value, but not 100% if anyone would need it.

error: e && e.message ? e.message : String(e)}));""")
.withArguments("promise", "channel");

private final @Nullable SerializableRunnable onSuccess;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just an out loud thinking: is it ever a case when a copied value is needed in the onSuccess (or even in orError callback)? I think it's usually not, nothing wants to show a copied value, but the fact that it's copied.


@Override
protected final void appendStatement(JsBuilder builder, StringBuilder out) {
if (onSuccess == null) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this also check onError callback availability? I guess this is for fire-and-forget case, so I'd expect both callbacks to absent. Or just only the success?

Comment on lines +35 to +39
* Note that fullscreening an arbitrary component element does not interact well
* with Vaadin theming or overlay components — those expect the fullscreen
* element to be {@code document.documentElement}. This action is intentionally
* low-level; the higher-level {@code Component.requestFullscreen()} facade
* handles the wrapping needed for full Vaadin compatibility.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does this mean, are we just repeating by this that this class is purely internal and Component will provide a proper API?

StringBuilder out);

@Override
protected final void appendStatement(JsBuilder builder, StringBuilder out) {
Copy link
Copy Markdown
Contributor

@mshabarov mshabarov May 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure, maybe it's fine for internal class, but are we intentionally want to keep this away from overrides?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants