Skip to content

Commit

Permalink
Fullscreen control element (#4165)
Browse files Browse the repository at this point in the history
I recently created a component to make use Quasars's AppFullscreen
plugin and thought it were also helpful for other users, so I documented
it and added it to the NiceGUI docu.

It enables the user to switch to fullscreen programmatically which
especially on a smartphone or tablet context greatly improves the user
experience.


![image](https://github.com/user-attachments/assets/b55be2a6-785c-4a46-8740-30811a3626da)

---------

Co-authored-by: Falko Schindler <[email protected]>
  • Loading branch information
Alyxion and falkoschindler authored Jan 24, 2025
1 parent 1fb2d12 commit ce015a4
Show file tree
Hide file tree
Showing 7 changed files with 206 additions and 0 deletions.
32 changes: 32 additions & 0 deletions nicegui/elements/fullscreen.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
export default {
props: {
requireEscapeHold: Boolean,
},
mounted() {
document.addEventListener("fullscreenchange", this.handleFullscreenChange);
document.addEventListener("mozfullscreenchange", this.handleFullscreenChange);
document.addEventListener("webkitfullscreenchange", this.handleFullscreenChange);
document.addEventListener("msfullscreenchange", this.handleFullscreenChange);
},
unmounted() {
document.removeEventListener("fullscreenchange", this.handleFullscreenChange);
document.removeEventListener("mozfullscreenchange", this.handleFullscreenChange);
document.removeEventListener("webkitfullscreenchange", this.handleFullscreenChange);
document.removeEventListener("msfullscreenchange", this.handleFullscreenChange);
},
methods: {
handleFullscreenChange() {
this.$emit("update:model-value", Quasar.AppFullscreen.isActive);
},
enter() {
Quasar.AppFullscreen.request().then(() => {
if (this.requireEscapeHold && navigator.keyboard && typeof navigator.keyboard.lock === "function") {
navigator.keyboard.lock(["Escape"]);
}
});
},
exit() {
Quasar.AppFullscreen.exit();
},
},
};
59 changes: 59 additions & 0 deletions nicegui/elements/fullscreen.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
from typing import Optional

from ..events import Handler, ValueChangeEventArguments
from .mixins.value_element import ValueElement


class Fullscreen(ValueElement, component='fullscreen.js'):
LOOPBACK = None

def __init__(self, *,
require_escape_hold: bool = False,
on_value_change: Optional[Handler[ValueChangeEventArguments]] = None) -> None:
"""Fullscreen control element
This element is based on Quasar's `AppFullscreen <https://quasar.dev/quasar-plugins/app-fullscreen>`_ plugin
and provides a way to enter, exit and toggle the fullscreen mode.
Important notes:
* Due to security reasons, the fullscreen mode can only be entered from a previous user interaction such as a button click.
* The long-press escape requirement only works in some browsers like Google Chrome or Microsoft Edge.
*Added in version 2.11.0*
:param require_escape_hold: whether the user needs to long-press the escape key to exit fullscreen mode
:param on_value_change: callback which is invoked when the fullscreen state changes
"""
super().__init__(value=False, on_value_change=on_value_change)
self._props['requireEscapeHold'] = require_escape_hold

@property
def require_escape_hold(self) -> bool:
"""Whether the user needs to long-press of the escape key to exit fullscreen mode.
This feature is only supported in some browsers like Google Chrome or Microsoft Edge.
In unsupported browsers, this setting has no effect.
"""
return self._props['requireEscapeHold']

@require_escape_hold.setter
def require_escape_hold(self, value: bool) -> None:
self._props['requireEscapeHold'] = value
self.update()

def enter(self) -> None:
"""Enter fullscreen mode."""
self.value = True

def exit(self) -> None:
"""Exit fullscreen mode."""
self.value = False

def toggle(self) -> None:
"""Toggle fullscreen mode."""
self.value = not self.value

def _handle_value_change(self, value: bool) -> None:
super()._handle_value_change(value)
self.run_method('enter' if value else 'exit')
2 changes: 2 additions & 0 deletions nicegui/ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
'element',
'expansion',
'footer',
'fullscreen',
'grid',
'header',
'highchart',
Expand Down Expand Up @@ -154,6 +155,7 @@
from .elements.echart import EChart as echart
from .elements.editor import Editor as editor
from .elements.expansion import Expansion as expansion
from .elements.fullscreen import Fullscreen as fullscreen
from .elements.grid import Grid as grid
from .elements.highchart import highchart
from .elements.html import Html as html
Expand Down
68 changes: 68 additions & 0 deletions tests/test_fullscreen.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
from unittest.mock import patch

import pytest

from nicegui import ui
from nicegui.testing import Screen


@pytest.mark.parametrize('require_escape_hold', [True, False])
def test_fullscreen_creation(screen: Screen, require_escape_hold: bool):
fullscreen = ui.fullscreen(require_escape_hold=require_escape_hold)
assert not fullscreen.value
assert fullscreen.require_escape_hold == require_escape_hold

screen.open('/')


def test_fullscreen_methods(screen: Screen):
values = []

fullscreen = ui.fullscreen(on_value_change=lambda e: values.append(e.value))

screen.open('/')

with patch.object(fullscreen, 'run_method') as mock_run:
fullscreen.enter()
mock_run.assert_called_once_with('enter')
mock_run.reset_mock()

fullscreen.exit()
mock_run.assert_called_once_with('exit')
mock_run.reset_mock()

fullscreen.toggle()
mock_run.assert_called_once_with('enter')
mock_run.reset_mock()

fullscreen.value = False
mock_run.assert_called_once_with('exit')
mock_run.reset_mock()

fullscreen.value = True
mock_run.assert_called_once_with('enter')
mock_run.reset_mock()

assert values == [True, False, True, False, True]


def test_fullscreen_button_click(screen: Screen):
"""Test that clicking a button to enter fullscreen creates the correct JavaScript call.
Note: We cannot test actual fullscreen behavior as it requires user interaction,
but we can verify the JavaScript method is called correctly.
"""
values = []

fullscreen = ui.fullscreen(on_value_change=lambda e: values.append(e.value))
ui.button('Enter Fullscreen', on_click=fullscreen.enter)
ui.button('Exit Fullscreen', on_click=fullscreen.exit)

screen.open('/')
screen.click('Enter Fullscreen')
screen.wait(0.5)
assert values == [True]

screen.click('Exit Fullscreen')
screen.wait(0.5)
assert values == [True, False]
42 changes: 42 additions & 0 deletions website/documentation/content/fullscreen_documentation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
from nicegui import ui

from . import doc


@doc.demo(ui.fullscreen)
def main_demo() -> None:
fullscreen = ui.fullscreen()

ui.button('Enter Fullscreen', on_click=fullscreen.enter)
ui.button('Exit Fullscreen', on_click=fullscreen.exit)
ui.button('Toggle Fullscreen', on_click=fullscreen.toggle)


@doc.demo('Requiring long-press to exit', '''
You can require users to long-press the escape key to exit fullscreen mode.
This is useful to prevent accidental exits, for example when working on forms or editing data.
Note that this feature only works in some browsers like Google Chrome or Microsoft Edge.
''')
def long_press_demo():
fullscreen = ui.fullscreen()
ui.switch('Require escape hold').bind_value_to(fullscreen, 'require_escape_hold')
ui.button('Toggle Fullscreen', on_click=fullscreen.toggle)


@doc.demo('Tracking fullscreen state', '''
You can track when the fullscreen state changes.
Note that due to security reasons, fullscreen mode can only be entered from a previous user interaction
such as a button click.
''')
def state_demo():
fullscreen = ui.fullscreen(
on_value_change=lambda e: ui.notify('Enter' if e.value else 'Exit')
)
ui.button('Toggle Fullscreen', on_click=fullscreen.toggle)
ui.label().bind_text_from(fullscreen, 'state',
lambda state: 'Fullscreen' if state else '')


doc.reference(ui.fullscreen)
1 change: 1 addition & 0 deletions website/documentation/content/overview.py
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,7 @@ def map_of_nicegui():
- `ui.context`: get the current UI context including the `client` and `request` objects
- [`ui.dark_mode`](/documentation/dark_mode): get and set the dark mode on a page
- [`ui.download`](/documentation/download): download a file to the client
- [`ui.fullscreen`](/documentation/fullscreen): enter, exit and toggle fullscreen mode
- [`ui.keyboard`](/documentation/keyboard): define keyboard event handlers
- [`ui.navigate`](/documentation/navigate): let the browser navigate to another location
- [`ui.notify`](/documentation/notification): show a notification
Expand Down
2 changes: 2 additions & 0 deletions website/documentation/content/section_page_layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
dialog_documentation,
doc,
expansion_documentation,
fullscreen_documentation,
grid_documentation,
list_documentation,
menu_documentation,
Expand Down Expand Up @@ -55,6 +56,7 @@ def auto_context_demo():
doc.intro(row_documentation)
doc.intro(grid_documentation)
doc.intro(list_documentation)
doc.intro(fullscreen_documentation)


@doc.demo('Clear Containers', '''
Expand Down

0 comments on commit ce015a4

Please sign in to comment.