Skip to content
Open
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
4 changes: 4 additions & 0 deletions flow-client/src/main/frontend/Flow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
} from '@vaadin/common-frontend';
import './Geolocation';
import { currentVisibility } from './PageVisibility';
import { isShareSupported } from './WebShare';

export interface FlowConfig {
imports?: () => Promise<any>;
Expand Down Expand Up @@ -563,6 +564,9 @@ export class Flow {
params['v-ga'] = await geolocation.queryAvailability();
}

/* Web Share API support */
params['v-ws'] = isShareSupported();

/* Stringify each value (they are parsed on the server side) */
const stringParams: Record<string, string> = {};
Object.keys(params).forEach((key) => {
Expand Down
45 changes: 45 additions & 0 deletions flow-client/src/main/frontend/WebShare.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* 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.
*/

/**
* Returns whether the current browser exposes the Web Share API
* (`navigator.share`). Used by the bootstrap path to seed the server-side
* support signal without waiting for a DOM event.
*/
export function isShareSupported(): boolean {
return typeof navigator.share === 'function';
}

const $wnd = window as any;
$wnd.Vaadin ??= {};
$wnd.Vaadin.Flow ??= {};
$wnd.Vaadin.Flow.share = {
isSupported: isShareSupported,

share(title: string | null, text: string | null, url: string | null): Promise<void> {
const data: ShareData = {};
if (title !== null) {
data.title = title;
}
if (text !== null) {
data.text = text;
}
if (url !== null) {
data.url = url;
}
return navigator.share(data);
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
import com.vaadin.flow.component.internal.ComponentMetaData.DependencyInfo;
import com.vaadin.flow.component.page.ExtendedClientDetails;
import com.vaadin.flow.component.page.Page;
import com.vaadin.flow.component.page.WebShareSupport;
import com.vaadin.flow.di.Instantiator;
import com.vaadin.flow.dom.Element;
import com.vaadin.flow.dom.ElementUtil;
Expand Down Expand Up @@ -247,6 +248,9 @@ public List<Object> getParameters() {
private final ValueSignal<GeolocationAvailability> geolocationAvailabilitySignal = new ValueSignal<>(
GeolocationAvailability.UNKNOWN);

private final ValueSignal<WebShareSupport> webShareSupportSignal = new ValueSignal<>(
WebShareSupport.UNKNOWN);

private GeolocationClient geolocationClient;

private Registration geolocationClientAvailabilityRegistration;
Expand Down Expand Up @@ -1480,6 +1484,28 @@ public void setGeolocationAvailability(
this.geolocationAvailabilitySignal.set(availability);
}

/**
* Returns the reactive signal holding the Web Share API support state for
* this UI. Starts as {@link WebShareSupport#UNKNOWN} before the first
* client bootstrap report, then transitions to the value the browser
* reports. Application code reads it via {@link Page#shareSupportSignal()}.
*
* @return the support signal
*/
public ValueSignal<WebShareSupport> getWebShareSupportSignal() {
return webShareSupportSignal;
}

/**
* Updates the Web Share support signal. For framework use only.
*
* @param support
* the new support state
*/
public void setWebShareSupport(WebShareSupport support) {
this.webShareSupportSignal.set(support);
}

/**
* Returns the geolocation client currently bound to this UI, or
* {@code null} if none has been installed yet. Framework-internal:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -445,8 +445,8 @@ void setColorScheme(ColorScheme.Value colorScheme) {
/**
* Parses browser details from the given JSON and updates the UI from them:
* stores the resulting {@link ExtendedClientDetails} on the UI's internals
* and seeds the page-visibility and geolocation-availability signals from
* the same payload.
* and seeds the page-visibility, geolocation-availability and
* web-share-support signals from the same payload.
* <p>
* For internal use only.
*
Expand Down Expand Up @@ -508,6 +508,12 @@ public static ExtendedClientDetails updateFromJson(UI ui, JsonNode json) {
// unknown value; leave the current availability alone
}
}
String ws = getStringElseNull.apply("v-ws");
if (ws != null) {
ui.getInternals().setWebShareSupport(
Boolean.parseBoolean(ws) ? WebShareSupport.SUPPORTED
: WebShareSupport.UNSUPPORTED);
}
return details;
}

Expand Down
76 changes: 76 additions & 0 deletions flow-server/src/main/java/com/vaadin/flow/component/page/Page.java
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ public class Page implements Serializable {
PageVisibility.UNKNOWN);
private final Signal<PageVisibility> pageVisibilityReadOnly = pageVisibilitySignal
.asReadonly();
private final ValueSignal<WebShareSupport> webShareSupportSignal;
private final Signal<WebShareSupport> webShareSupportReadOnly;

/**
* Creates a page instance for the given UI.
Expand All @@ -80,6 +82,8 @@ public Page(UI ui) {
.addEventListener("vaadin-page-visibility-change",
e -> setPageVisibility(e.getEventDetail(String.class)))
.addEventDetail().debounce(100).allowInert();
webShareSupportSignal = ui.getInternals().getWebShareSupportSignal();
webShareSupportReadOnly = webShareSupportSignal.asReadonly();
}

/**
Expand Down Expand Up @@ -748,6 +752,78 @@ public void fetchPageDirection(SerializableConsumer<Direction> callback) {
});
}

/**
* Returns a read-only signal that tracks whether the browser exposes the
* <a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Share_API">
* Web Share API</a> ({@code navigator.share}).
* <p>
* The signal value is {@link WebShareSupport#SUPPORTED} when the browser
* exposes {@code navigator.share} (mobile Chromium, mobile Safari, modern
* Edge, recent desktop Safari) and {@link WebShareSupport#UNSUPPORTED}
* otherwise (most desktop Firefox builds, older browsers). The initial
* value is {@link WebShareSupport#UNKNOWN}; it is replaced with a real
* value before any user code observes the signal.
* <p>
* The signal value is seeded from the initial client bootstrap, so user
* code always sees a real value. Subscribe with
* {@code Signal.effect(owner, ...)} to react to changes; call
* {@code shareSupportSignal().peek()} for a snapshot outside a reactive
* context, and {@code .get()} inside one.
* <p>
* Web Share support is established at page load and does not change during
* the session, so the signal effectively transitions {@code UNKNOWN} →
* {@code SUPPORTED}/{@code UNSUPPORTED} once and then remains stable.
*
* @return the read-only Web Share support signal
*/
public Signal<WebShareSupport> shareSupportSignal() {
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.

getShareSupportSignal() or perhaps only getShareSupport() as Sinal is the return type.

return webShareSupportReadOnly;
}

/**
* Invokes the browser's native share dialog using the <a href=
* "https://developer.mozilla.org/en-US/docs/Web/API/Navigator/share">Web
* Share API</a>. The dialog lets the user pick a target application
* (messaging, mail, social, AirDrop, …) to receive the supplied content.
* <p>
* The call is asynchronous: this method returns immediately and the browser
* shows the share sheet on the next event-loop turn. The returned
* {@link PendingJavaScriptResult} resolves when the share completes and
* rejects when the user cancels the sheet or the browser fails to deliver
* the content; rejections are logged at debug level by Flow itself, so
* callers do not need to attach an error handler unless they want to react
* in the UI.
* <p>
* Check {@link #shareSupportSignal()} before exposing a "share" control —
* calling this method when the value is {@link WebShareSupport#UNSUPPORTED}
* throws {@link UnsupportedOperationException}.
*
* @param title
* the title to share, may be {@code null}
* @param text
* the text to share, may be {@code null}
* @param url
* the URL to share, may be {@code null}
* @return a pending result that resolves when the share completes or
* rejects if the user cancels or sharing fails
* @throws UnsupportedOperationException
* if the browser does not support the Web Share API
*/
public PendingJavaScriptResult share(String title, String text,
String url) {
if (webShareSupportSignal.peek() == WebShareSupport.UNSUPPORTED) {
throw new UnsupportedOperationException(
"The browser does not support the Web Share API. "
+ "Check shareSupportSignal() before calling share().");
}
PendingJavaScriptResult result = executeJs(
"return window.Vaadin.Flow.share.share($0, $1, $2)", title,
text, url);
result.then(ok -> {
}, err -> LOGGER.debug("Web share failed: {}", err));
return result;
}

private Direction getDirectionByClientName(String directionClientName) {
return Arrays.stream(Direction.values())
.filter(direction -> direction.getClientName()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* 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.page;

/**
* Whether the browser exposes the Web Share API ({@code navigator.share}).
* <p>
* Held by {@link Page#shareSupportSignal()}. Reading the value does not show
* any browser dialog — it reports whether a subsequent
* {@link Page#share(String, String, String)} call would succeed in invoking the
* native share sheet, or whether it would fail because the API is missing in
* the current browser context.
* <p>
* Typical usage:
* <ul>
* <li>{@link #SUPPORTED} — show a "Share" button that triggers
* {@link Page#share(String, String, String)}.</li>
* <li>{@link #UNSUPPORTED} — fall back to a copy-link or social-network
* affordance; the native sheet is unavailable in this browser.</li>
* <li>{@link #UNKNOWN} — only seen in the brief window before the bootstrap
* handshake completes; treat the same as {@link #UNSUPPORTED} until a real
* value arrives.</li>
* </ul>
*/
public enum WebShareSupport {

/**
* No value has been reported by the browser yet. Used only as the initial
* value of the signal before the first client handshake delivers the real
* one. In normal request handling the signal is seeded before any user code
* (UI initialization, {@code UIInitListener}, component attach) runs, so
* this value is essentially never observed in practice; once a real value
* has arrived, the signal never returns to {@code UNKNOWN}.
*/
UNKNOWN,

/**
* The browser exposes {@code navigator.share}; calls to
* {@link Page#share(String, String, String)} will invoke the native share
* sheet.
*/
SUPPORTED,

/**
* The browser does not expose {@code navigator.share}; calls to
* {@link Page#share(String, String, String)} will throw
* {@link UnsupportedOperationException}. Some desktop browsers, in
* particular older Firefox versions, fall in this bucket.
*/
UNSUPPORTED
}
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,7 @@ Please submit an issue to https://github.com/vaadin/flow-components/issues/new/c
if(navigator.platform) {
params['v-np'] = navigator.platform;
}

/* Stringify each value (they are parsed on the server side) */
Object.keys(params).forEach(function(key) {
var value = params[key];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@
this.expression = expression;
firstParam = parameters[0];
count++;
return null;
return Mockito.mock(PendingJavaScriptResult.class);
}
}

Expand Down Expand Up @@ -471,6 +471,40 @@
assertEquals(ColorScheme.Value.DARK, page.getColorScheme());
}

@Test
void share_routesThroughClientShareModule() {
MockUI mockUI = new MockUI();
mockUI.getInternals().setWebShareSupport(WebShareSupport.SUPPORTED);

TestPage sharePage = new TestPage(mockUI);
sharePage.share("My Title", "Some text", "https://example.com");

assertEquals("return window.Vaadin.Flow.share.share($0, $1, $2)",
sharePage.expression);
assertEquals("My Title", sharePage.firstParam);
}

@Test
void share_throwsWhenSignalReportsUnsupported() {
MockUI mockUI = new MockUI();
mockUI.getInternals().setWebShareSupport(WebShareSupport.UNSUPPORTED);

Page sharePage = new Page(mockUI);
assertThrows(UnsupportedOperationException.class,
() -> sharePage.share("title", "text", "url"));
}

@Test
void shareSupportSignal_reflectsBootstrapValue() {
MockUI mockUI = new MockUI();
Page page = new Page(mockUI);

Check warning on line 500 in flow-server/src/test/java/com/vaadin/flow/component/page/PageTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Rename "page" which hides the field declared at line 80.

See more on https://sonarcloud.io/project/issues?id=vaadin_flow&issues=AZ4cTx2PVwbL6j_eOVrP&open=AZ4cTx2PVwbL6j_eOVrP&pullRequest=24325
assertEquals(WebShareSupport.UNKNOWN, page.shareSupportSignal().peek());

mockUI.getInternals().setWebShareSupport(WebShareSupport.SUPPORTED);
assertEquals(WebShareSupport.SUPPORTED,
page.shareSupportSignal().peek());
}

@Test
void setColorScheme_updatesGetColorScheme() {
MockUI mockUI = new MockUI();
Expand Down
Loading