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
1 change: 1 addition & 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 './WakeLock';

export interface FlowConfig {
imports?: () => Promise<any>;
Expand Down
110 changes: 110 additions & 0 deletions flow-client/src/main/frontend/WakeLock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/*
* 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.
*/

type VaadinWakeLockState = 'ACTIVE' | 'RELEASED';

// Whether the server-side has asked us to hold the lock. The browser releases
// the lock whenever the tab is hidden; this flag is what lets the
// visibilitychange handler re-acquire silently when the tab returns.
let wanted = false;
let sentinel: WakeLockSentinel | null = null;
let visibilityListenerInstalled = false;

function dispatch(element: HTMLElement, state: VaadinWakeLockState): void {
element.dispatchEvent(new CustomEvent('vaadin-wake-lock-change', { detail: state }));
}

async function acquire(element: HTMLElement): Promise<void> {
if (sentinel) {
return;
}
if (!('wakeLock' in navigator)) {
return;
}
try {
const next = await (navigator as any).wakeLock.request('screen');
// The user (or the browser) may have released the lock or the tab may have
// been hidden again while the request was in flight.
if (!wanted || document.visibilityState !== 'visible') {
try {
await next.release();
} catch (_e) {
// Ignore; releasing an already-released sentinel throws on some
// browsers and there is nothing meaningful to do here.
}
return;
}
sentinel = next;
next.addEventListener('release', () => {
sentinel = null;
dispatch(element, 'RELEASED');
});
dispatch(element, 'ACTIVE');
} catch (_e) {
// The browser refused (insecure context, feature unavailable, low battery,
// user revoked, …). The signal stays in 'RELEASED'; no need to surface
// the error — applications observe activeSignal() to know the truth.
}
}

function installVisibilityListener(element: HTMLElement): void {
if (visibilityListenerInstalled) {
return;
}
visibilityListenerInstalled = true;
document.addEventListener('visibilitychange', () => {
if (wanted && !sentinel && document.visibilityState === 'visible') {
acquire(element);
}
});
}

const $wnd = window as any;
$wnd.Vaadin ??= {};
$wnd.Vaadin.Flow ??= {};
$wnd.Vaadin.Flow.wakeLock = {
request(element: HTMLElement): Promise<void> {
wanted = true;
installVisibilityListener(element);
if (document.visibilityState !== 'visible') {
// The browser will not grant a lock while the page is hidden; the
// visibilitychange listener will pick it up on the next 'visible'.
return Promise.resolve();
}
return acquire(element);
},

async release(element: HTMLElement): Promise<void> {
wanted = false;
if (!sentinel) {
return;
}
const current = sentinel;
sentinel = null;
try {
await current.release();
} catch (_e) {
// Ignore; the 'release' event listener installed in acquire() also
// dispatches RELEASED, so the state still reaches the server even when
// the explicit release() call rejects.
}
dispatch(element, 'RELEASED');
}
};

// Empty export to ensure TypeScript emits this as an ES module, which is
// required for Vite to load it via import.
export {};
12 changes: 12 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 @@ -59,6 +59,7 @@ public class Page implements Serializable {

private final UI ui;
private final History history;
private final WakeLock wakeLock;
private DomListenerRegistration resizeReceiver;
private ArrayList<BrowserWindowResizeListener> resizeListeners;
private ValueSignal<WindowSize> windowSizeSignal;
Expand All @@ -76,6 +77,7 @@ public class Page implements Serializable {
public Page(UI ui) {
this.ui = ui;
history = new History(ui);
wakeLock = new WakeLock(ui);
ui.getElement()
.addEventListener("vaadin-page-visibility-change",
e -> setPageVisibility(e.getEventDetail(String.class)))
Expand Down Expand Up @@ -392,6 +394,16 @@ public History getHistory() {
return history;
}

/**
* Returns the {@link WakeLock} facade for this page. Use it to keep the
* screen from dimming or locking while a view is active.
*
* @return the wake lock facade, never {@code null}
*/
public WakeLock getWakeLock() {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Other browser feature APIs have their own static methods instead of being accessed throug Page.
Should we make this consistent?

return wakeLock;
}

/**
* Reloads the page in the browser.
*/
Expand Down
157 changes: 157 additions & 0 deletions flow-server/src/main/java/com/vaadin/flow/component/page/WakeLock.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
/*
* 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;

import java.io.Serializable;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.vaadin.flow.component.UI;
import com.vaadin.flow.signals.Signal;
import com.vaadin.flow.signals.local.ValueSignal;

/**
* Browser Screen Wake Lock API for Flow applications. Keeps the screen from
* dimming or locking while the lock is held — useful for dashboards, kiosks,
* presentations, and recipe / workout screens.
* <p>
* Reach the per-UI instance through {@link Page#getWakeLock()}.
*
* <p>
* <b>Example:</b>
*
* <pre>
* WakeLock wakeLock = UI.getCurrent().getPage().getWakeLock();
* wakeLock.request();
*
* Signal.effect(this, () -&gt; {
* statusLabel.setText(
* wakeLock.activeSignal().get() ? "Screen will stay on" : "Idle");
* });
* </pre>
*
* <p>
* <b>Lifecycle.</b> {@link #request()} fires the browser request asynchronously
* and {@link #activeSignal()} flips to {@code true} once the browser confirms.
* The browser releases the lock automatically when the tab is hidden; the
* client transparently re-acquires it when the tab becomes visible again, so a
* single {@link #request()} is enough for the lifetime of the view. Call
* {@link #release()} to stop re-acquiring and drop the current lock.
*
* <p>
* <b>Reliability caveats.</b>
* <ul>
* <li>Safari only ships the Screen Wake Lock API from version 16.4.</li>
* <li>The browser requires a secure context (HTTPS or {@code localhost}); on an
* insecure origin the call fails silently and {@link #activeSignal()} stays
* {@code false}.</li>
* <li>Some browsers release the lock on low battery or when the device enters
* power-saving mode; {@link #activeSignal()} reflects this immediately.</li>
* </ul>
*
* @see Page#getWakeLock()
*/
public final class WakeLock implements Serializable {

private static final Logger LOGGER = LoggerFactory
.getLogger(WakeLock.class);

static final String STATE_CHANGE_EVENT = "vaadin-wake-lock-change";

private final UI ui;
private final ValueSignal<Boolean> activeSignal = new ValueSignal<>(
Boolean.FALSE);
private final Signal<Boolean> activeSignalReadOnly = activeSignal
.asReadonly();

WakeLock(UI ui) {
this.ui = ui;
ui.getElement()
.addEventListener(STATE_CHANGE_EVENT,
e -> setActive(e.getEventDetail(String.class)))
.addEventDetail().allowInert();
}

/**
* Asks the browser to acquire a screen wake lock and keep re-acquiring it
* across tab visibility changes.
* <p>
* The call is asynchronous and fire-and-forget: by the time this method
* returns the browser has not necessarily granted the lock yet. Observe
* {@link #activeSignal()} to react to the actual state. Calling
* {@code request()} when a lock is already held is a no-op.
*/
public void request() {
ui.getElement().executeJs("window.Vaadin.Flow.wakeLock.request(this)")
.then(ignored -> {
}, err -> LOGGER
.debug("Client-side wakeLock.request failed: {}", err));
}

/**
* Releases the screen wake lock and stops re-acquiring it on subsequent
* visibility changes. Idempotent — calling it when no lock is held is a
* no-op.
*/
public void release() {
ui.getElement().executeJs("window.Vaadin.Flow.wakeLock.release(this)")
.then(ignored -> {
}, err -> LOGGER
.debug("Client-side wakeLock.release failed: {}", err));
}

/**
* Returns a read-only signal that reflects whether the browser is currently
* holding the wake lock on behalf of this UI.
* <p>
* Starts as {@code false}, flips to {@code true} when the browser confirms
* a request, and flips back to {@code false} whenever the browser releases
* the lock — explicitly through {@link #release()}, automatically when the
* tab becomes hidden, or because the browser dropped it (power saving, low
* battery). When the tab is shown again the client re-requests the lock if
* {@link #release()} has not been called, so the signal flips back to
* {@code true} shortly after.
* <p>
* Use {@code Signal.effect(owner, ...)} to react to changes and
* {@code .peek()} for a snapshot outside a reactive context.
*
* @return the read-only active-state signal
*/
public Signal<Boolean> activeSignal() {
return activeSignalReadOnly;
}

/**
* Updates the signal from a raw state value reported by the client. Unknown
* values are logged at debug level so a forward-compatible client value
* does not silently disappear.
*
* @param value
* the raw value, or {@code null}
*/
void setActive(String value) {
if (value == null) {
return;
}
switch (value) {
case "ACTIVE" -> activeSignal.set(Boolean.TRUE);
case "RELEASED" -> activeSignal.set(Boolean.FALSE);
default ->
LOGGER.debug("Unknown wake lock state from client: {}", value);
}
}
}
Loading
Loading