Skip to content

Commit 9b0d813

Browse files
committed
feat: Add Fullscreen API wrapping the browser Fullscreen API
Adds Component.requestFullscreen() that uses a wrapper approach to handle Vaadin theming and overlay components correctly, along with Page.requestFullscreen() for whole-page fullscreen, Page.exitFullscreen(), Page.isFullscreenEnabled(), and Page.addFullscreenChangeListener(). Fixes #21902
1 parent d16f71e commit 9b0d813

9 files changed

Lines changed: 430 additions & 0 deletions

File tree

flow-server/src/main/java/com/vaadin/flow/component/Component.java

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -885,6 +885,39 @@ public void scrollIntoView(ScrollOptions scrollOptions) {
885885
getElement().scrollIntoView(scrollOptions);
886886
}
887887

888+
/**
889+
* Requests that the browser display this component in fullscreen mode.
890+
* <p>
891+
* Because of how Vaadin theming and overlay components work, this method
892+
* does not call {@code requestFullscreen()} on the component's element
893+
* directly. Instead, it fullscreens the entire page
894+
* ({@code document.documentElement}), moves the component into a wrapper
895+
* element, and hides the rest of the view. When fullscreen is exited
896+
* (either programmatically or by the user pressing Escape), the component
897+
* is automatically restored to its original position in the DOM.
898+
* <p>
899+
* Note that browsers require transient user activation (e.g., a button
900+
* click) to enter fullscreen mode. Calling this method from a server push
901+
* or view constructor will likely not work.
902+
*
903+
* @see com.vaadin.flow.component.page.Page#requestFullscreen()
904+
* @see com.vaadin.flow.component.page.Page#exitFullscreen()
905+
* @see <a href=
906+
* "https://developer.mozilla.org/en-US/docs/Web/API/Fullscreen_API">MDN
907+
* Fullscreen API</a>
908+
*/
909+
public void requestFullscreen() {
910+
if (!isAttached()) {
911+
throw new IllegalStateException(
912+
"Component must be attached to the UI to request fullscreen");
913+
}
914+
UI ui = getUI().get();
915+
Element wrapperElement = ui.getInternals().getWrapperElement();
916+
ui.getPage().executeJs(
917+
"window.Vaadin.Flow.fullscreen.requestComponentFullscreen($0, $1)",
918+
getElement(), wrapperElement);
919+
}
920+
888921
/**
889922
* Traverses the component tree up and returns the first ancestor component
890923
* that matches the given type.

flow-server/src/main/java/com/vaadin/flow/component/UI.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@
108108
* @since 1.0
109109
*/
110110
@JsModule("@vaadin/common-frontend/ConnectionIndicator.js")
111+
@JsModule("./fullscreenConnector.js")
111112
public class UI extends Component
112113
implements PollNotifier, HasComponents, RouterLayout {
113114

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
* Copyright 2000-2026 Vaadin Ltd.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
5+
* use this file except in compliance with the License. You may obtain a copy of
6+
* the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations under
14+
* the License.
15+
*/
16+
package com.vaadin.flow.component.page;
17+
18+
import java.util.EventObject;
19+
20+
/**
21+
* Event that is fired when the fullscreen state of the page changes.
22+
*
23+
* @author Vaadin Ltd
24+
*
25+
* @see FullscreenChangeListener
26+
* @see Page#addFullscreenChangeListener(FullscreenChangeListener)
27+
*/
28+
public class FullscreenChangeEvent extends EventObject {
29+
30+
private final boolean fullscreen;
31+
32+
/**
33+
* Creates a new event.
34+
*
35+
* @param source
36+
* the page for which the fullscreen state has changed
37+
* @param fullscreen
38+
* {@code true} if the page is now in fullscreen mode,
39+
* {@code false} if fullscreen was exited
40+
*/
41+
public FullscreenChangeEvent(Page source, boolean fullscreen) {
42+
super(source);
43+
this.fullscreen = fullscreen;
44+
}
45+
46+
@Override
47+
public Page getSource() {
48+
return (Page) super.getSource();
49+
}
50+
51+
/**
52+
* Returns whether the page is currently in fullscreen mode.
53+
*
54+
* @return {@code true} if in fullscreen mode, {@code false} otherwise
55+
*/
56+
public boolean isFullscreen() {
57+
return fullscreen;
58+
}
59+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* Copyright 2000-2026 Vaadin Ltd.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
5+
* use this file except in compliance with the License. You may obtain a copy of
6+
* the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations under
14+
* the License.
15+
*/
16+
package com.vaadin.flow.component.page;
17+
18+
import java.io.Serializable;
19+
20+
/**
21+
* Listener that gets notified when the fullscreen state of the page changes.
22+
*
23+
* @author Vaadin Ltd
24+
*
25+
* @see Page#addFullscreenChangeListener(FullscreenChangeListener)
26+
*/
27+
@FunctionalInterface
28+
public interface FullscreenChangeListener extends Serializable {
29+
/**
30+
* Invoked when the fullscreen state changes.
31+
*
32+
* @param event
33+
* a fullscreen change event
34+
*/
35+
void fullscreenChanged(FullscreenChangeEvent event);
36+
}

flow-server/src/main/java/com/vaadin/flow/component/page/Page.java

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import java.util.Objects;
2525
import java.util.UUID;
2626

27+
import com.vaadin.flow.component.Component;
2728
import com.vaadin.flow.component.Direction;
2829
import com.vaadin.flow.component.UI;
2930
import com.vaadin.flow.component.dependency.JavaScript;
@@ -57,6 +58,8 @@ public class Page implements Serializable {
5758
private DomListenerRegistration resizeReceiver;
5859
private ArrayList<BrowserWindowResizeListener> resizeListeners;
5960
private ValueSignal<WindowSize> windowSizeSignal;
61+
private DomListenerRegistration fullscreenReceiver;
62+
private ArrayList<FullscreenChangeListener> fullscreenListeners;
6063

6164
/**
6265
* Creates a page instance for the given UI.
@@ -680,4 +683,104 @@ private Direction getDirectionByClientName(String directionClientName) {
680683
.equals(directionClientName))
681684
.findFirst().orElse(Direction.LEFT_TO_RIGHT);
682685
}
686+
687+
/**
688+
* Requests that the browser display the entire page in fullscreen mode.
689+
* <p>
690+
* This calls {@code document.documentElement.requestFullscreen()} which
691+
* fullscreens the entire page. Themes and overlay components (such as
692+
* Notification and ComboBox popups) work correctly in this mode.
693+
* <p>
694+
* Note that browsers require transient user activation (e.g., a button
695+
* click) to enter fullscreen mode. Calling this method from a server push
696+
* or view constructor will likely not work.
697+
*
698+
* @see Component#requestFullscreen()
699+
* @see #exitFullscreen()
700+
* @see <a href=
701+
* "https://developer.mozilla.org/en-US/docs/Web/API/Fullscreen_API">MDN
702+
* Fullscreen API</a>
703+
*/
704+
public void requestFullscreen() {
705+
executeJs("window.Vaadin.Flow.fullscreen.requestPageFullscreen()");
706+
}
707+
708+
/**
709+
* Exits fullscreen mode if the page is currently in fullscreen.
710+
* <p>
711+
* This calls {@code document.exitFullscreen()} on the browser. If a
712+
* component was previously fullscreened via
713+
* {@link Component#requestFullscreen()}, it will be automatically restored
714+
* to its original position.
715+
*
716+
* @see #requestFullscreen()
717+
* @see Component#requestFullscreen()
718+
*/
719+
public void exitFullscreen() {
720+
executeJs("document.exitFullscreen()");
721+
}
722+
723+
/**
724+
* Checks if the browser supports fullscreen mode.
725+
* <p>
726+
* Returns a {@link PendingJavaScriptResult} that resolves to a boolean. Use
727+
* it like:
728+
*
729+
* <pre>
730+
* page.isFullscreenEnabled().then(Boolean.class, enabled -&gt; {
731+
* if (enabled) {
732+
* page.requestFullscreen();
733+
* }
734+
* });
735+
* </pre>
736+
*
737+
* @return a pending result that resolves to {@code true} if fullscreen is
738+
* supported
739+
*/
740+
public PendingJavaScriptResult isFullscreenEnabled() {
741+
return executeJs("return document.fullscreenEnabled === true");
742+
}
743+
744+
/**
745+
* Adds a listener that is notified when the fullscreen state changes.
746+
* <p>
747+
* The listener is called when the page enters or exits fullscreen mode,
748+
* whether triggered programmatically or by the user (e.g., pressing
749+
* Escape).
750+
*
751+
* @param listener
752+
* the listener to add, not {@code null}
753+
* @return a registration object for removing the listener
754+
*
755+
* @see FullscreenChangeListener#fullscreenChanged(FullscreenChangeEvent)
756+
* @see Registration
757+
*/
758+
public Registration addFullscreenChangeListener(
759+
FullscreenChangeListener listener) {
760+
Objects.requireNonNull(listener);
761+
ensureFullscreenChangeListener();
762+
if (fullscreenListeners == null) {
763+
fullscreenListeners = new ArrayList<>(1);
764+
}
765+
fullscreenListeners.add(listener);
766+
return () -> fullscreenListeners.remove(listener);
767+
}
768+
769+
private void ensureFullscreenChangeListener() {
770+
if (fullscreenReceiver == null) {
771+
ui.getElement().executeJs(
772+
"window.Vaadin.Flow.fullscreen.setupFullscreenChangeListener(this)");
773+
fullscreenReceiver = ui.getElement()
774+
.addEventListener("flow-fullscreenchange", e -> {
775+
boolean fullscreen = e.getEventData()
776+
.get("event.fullscreen").asBoolean();
777+
if (fullscreenListeners != null) {
778+
var evt = new FullscreenChangeEvent(this,
779+
fullscreen);
780+
new ArrayList<>(fullscreenListeners)
781+
.forEach(l -> l.fullscreenChanged(evt));
782+
}
783+
}).addEventData("event.fullscreen").allowInert();
784+
}
785+
}
683786
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
window.Vaadin = window.Vaadin || {};
2+
window.Vaadin.Flow = window.Vaadin.Flow || {};
3+
window.Vaadin.Flow.fullscreen = {
4+
/**
5+
* Requests fullscreen for a specific component by moving it into the
6+
* wrapper element and hiding the rest of the view. Fullscreens
7+
* document.documentElement so that theming and overlays work correctly.
8+
*/
9+
requestComponentFullscreen: function (element, wrapper) {
10+
this._resetIfActive();
11+
if (document.fullscreenEnabled !== true) {
12+
return;
13+
}
14+
const placeholder = document.createComment('placeholder');
15+
const originalParent = element.parentNode;
16+
element.parentNode.insertBefore(placeholder, element);
17+
18+
wrapper.appendChild(element);
19+
wrapper.firstChild.style.display = 'none';
20+
document.documentElement.requestFullscreen();
21+
22+
this._reset = () => {
23+
originalParent.appendChild(element);
24+
placeholder.remove();
25+
wrapper.firstChild.style.display = '';
26+
document.documentElement.removeEventListener('fullscreenchange', this._onChange);
27+
delete this._onChange;
28+
delete this._reset;
29+
};
30+
31+
this._onChange = () => {
32+
if (!document.fullscreenElement) {
33+
this._reset();
34+
}
35+
};
36+
document.documentElement.addEventListener('fullscreenchange', this._onChange);
37+
},
38+
39+
/**
40+
* Requests fullscreen for the entire page
41+
* (document.documentElement).
42+
*/
43+
requestPageFullscreen: function () {
44+
this._resetIfActive();
45+
document.documentElement.requestFullscreen();
46+
},
47+
48+
/**
49+
* Sets up a listener on the document that re-dispatches
50+
* fullscreenchange events on the given UI element so the
51+
* server side can pick them up.
52+
*/
53+
setupFullscreenChangeListener: function (uiElement) {
54+
document.addEventListener('fullscreenchange', () => {
55+
const event = new Event('flow-fullscreenchange');
56+
event.fullscreen = document.fullscreenElement !== null;
57+
uiElement.dispatchEvent(event);
58+
});
59+
},
60+
61+
_resetIfActive: function () {
62+
if (this._reset) {
63+
this._reset();
64+
}
65+
}
66+
};

flow-server/src/test/java/com/vaadin/flow/component/ComponentTest.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2059,6 +2059,22 @@ public void cannotMoveComponentsToOtherUI() {
20592059
ex.getMessage());
20602060
}
20612061

2062+
@Test
2063+
public void requestFullscreen_attachedComponent_executesJs() {
2064+
EnabledDiv div = new EnabledDiv();
2065+
testUI.add(div);
2066+
div.requestFullscreen();
2067+
2068+
assertPendingJs(
2069+
"window.Vaadin.Flow.fullscreen.requestComponentFullscreen");
2070+
}
2071+
2072+
@Test
2073+
public void requestFullscreen_detachedComponent_throws() {
2074+
EnabledDiv div = new EnabledDiv();
2075+
assertThrows(IllegalStateException.class, div::requestFullscreen);
2076+
}
2077+
20622078
private void resetComponentTrackerProductionMode() throws Exception {
20632079
Field disabled = ComponentTracker.class.getDeclaredField("disabled");
20642080
disabled.setAccessible(true);
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* Copyright 2000-2026 Vaadin Ltd.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
5+
* use this file except in compliance with the License. You may obtain a copy of
6+
* the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations under
14+
* the License.
15+
*/
16+
package com.vaadin.flow.component.page;
17+
18+
import org.junit.jupiter.api.Test;
19+
20+
import com.vaadin.tests.util.MockUI;
21+
22+
import static org.junit.jupiter.api.Assertions.assertEquals;
23+
import static org.junit.jupiter.api.Assertions.assertFalse;
24+
import static org.junit.jupiter.api.Assertions.assertTrue;
25+
26+
class FullscreenChangeEventTest {
27+
28+
@Test
29+
public void eventReturnsConstructorValues() {
30+
Page page = new Page(new MockUI());
31+
32+
FullscreenChangeEvent enterEvent = new FullscreenChangeEvent(page,
33+
true);
34+
assertTrue(enterEvent.isFullscreen());
35+
assertEquals(page, enterEvent.getSource());
36+
37+
FullscreenChangeEvent exitEvent = new FullscreenChangeEvent(page,
38+
false);
39+
assertFalse(exitEvent.isFullscreen());
40+
}
41+
}

0 commit comments

Comments
 (0)