Explorar o código

Fullscreen control element (#4165)

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 <falko@zauberzeug.com>
Michael Ikemann hai 3 meses
pai
achega
ce015a49b4

+ 32 - 0
nicegui/elements/fullscreen.js

@@ -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 - 0
nicegui/elements/fullscreen.py

@@ -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 - 0
nicegui/ui.py

@@ -39,6 +39,7 @@ __all__ = [
     'element',
     'expansion',
     'footer',
+    'fullscreen',
     'grid',
     'header',
     'highchart',
@@ -154,6 +155,7 @@ from .elements.dialog import Dialog as dialog
 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

+ 68 - 0
tests/test_fullscreen.py

@@ -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 - 0
website/documentation/content/fullscreen_documentation.py

@@ -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 - 0
website/documentation/content/overview.py

@@ -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

+ 2 - 0
website/documentation/content/section_page_layout.py

@@ -8,6 +8,7 @@ from . import (
     dialog_documentation,
     doc,
     expansion_documentation,
+    fullscreen_documentation,
     grid_documentation,
     list_documentation,
     menu_documentation,
@@ -55,6 +56,7 @@ doc.intro(column_documentation)
 doc.intro(row_documentation)
 doc.intro(grid_documentation)
 doc.intro(list_documentation)
+doc.intro(fullscreen_documentation)
 
 
 @doc.demo('Clear Containers', '''