lona-web-org / lona

Write responsive web apps in full python
MIT License
527 stars 27 forks source link

Capture global keypress event to dismiss modal #288

Open mwx23 opened 1 year ago

mwx23 commented 1 year ago

Lately I've been using tailwindcss and daisyui for my UI css. There's some nice css behaviours those libs have to handle visibility etc that seems to get swallowed by lona. This issue isn't about that (I'll post a minimal example later), as a work-around I'd just like to capture key events. Specifically if a modal is displayed and esc is pressed, toggle close the modal.

My general question is it possible to capture a keypress event and have it sent to the backend?

I've tried handle_input_event_root on a LonaView and handle_input_event on a Widget but that doesn't seem to work unless a TextInput is involved and focused.

I can write a frontend widget to do this (I think) but is there a more lona way to achieve this?

If I were to generalise the issue even more, is there a way to subscribe to frontend DOM events from the backend? This might be out of scope, but I saw in other issues you talking about v2 so I'm hopeful :)

fscherf commented 1 year ago

Hi @mwx23,

Lately I've been using tailwindcss and daisyui for my UI css

Nice! I didn't know daisyui, but it looks pretty cool and versatile. If we can resolve your issues we could make package for lona like https://github.com/lona-web-org/lona-bootstrap-5

I've tried handle_input_event_root on a LonaView and handle_input_event on a Widget but that doesn't seem to work unless a TextInput is involved and focused.

If I were to generalise the issue even more, is there a way to subscribe to frontend DOM events from the backend?

Actually, that is exactly how events are implemented. Every node has a list of events it can fire (Node.events). This list is empty by default, but some nodes from the standard library like lona.html.Button come pre-configured with for click events. You can add any event type from lona.html to any node (currently implemented: CLICK, FOCUS, CHANGE, BLUR).

from lona.html import CLICK, FOCUS, CHANGE, BLUR
from lona.html import Div

my_div = Div(events=[CLICK])

The information that my_div can be clicked gets send to JavaScript client with the actual HTML. When the client renders my_div a JavaScript event handler gets setup that produces a high-level event that can be send back over the websocket, so Lona can call my_div.handle_click().

That means: With this approach, you can only handle events that are supported by both the server and the client, and are configured correctly.

My general question is it possible to capture a keypress event and have it sent to the backend? [...] I can write a frontend widget to do this (I think) but is there a more lona way to achieve this?

Currently, that is the way to go. Lona frontend widgets can send custom events. That means you can create a Modal widget, with a specialized frontend widget that captures global keypresses and sends event to the server to close the modal. That's what i currently do in bootstrap code aswell.

There are ideas to extend the event API to make code like this work:

from lona.html import KEY_ALT, KEY_T
from lona.html import Div

my_div = Div(events=[KEY_ALT + KEY_T)  # simple key binding

but the problem with first implementation was that most elements have to set tabindex to be able to emit key events, and then the key events only work when the right container is focused. The next idea was to let the view describe the key bindings globally like this:

from lona.html import KEY_ALT, KEY_T
from lona import LonaView

class MyLonaView(LonaView):
    KEY_BINDINGS = {
        KEY_ALT + KEY_T: self.handle_alt_t,
    }

but until now the need was not big enough to implement something like this. Also i don't have a good idea yet how to connect global key event (like in implementation 2) to a very specific node like a daisyui modal.

fscherf commented 1 year ago

Hi @mwx23,

I did some prototyping came up with this API:

from lona.html import HTML, Div, H1, KeyboardShortcuts
from lona import LonaView

from my_code import Modal

class MyLonaView(LonaView):
    def handle_alt_enter(self, input_event):
        print('ALT + Enter was pressed')

    def handle_arrow_up(self, input_event):
        print('Arrow Up was pressed')

    def dismiss_modal(self, input_event):
        with self.html.lock:
            if self.modal.visible:
                self.modal.dismiss()

    def handle_request(self, request):
        self.shortcuts = KeyboardShortcuts({
            'ALT+ENTER': self.handle_alt_enter,
            'ARROW-UP': self.handle_arrow_up,
            'ESC': self.dismiss_modal,
        })

        self.modal = Modal()

        return HTML(
            H1('Hello World'),
            self.modal,
            self.shortcuts,
        )

The idea is to add a new component that serves a JavaScript widget that interprets the config given to the KeyboardShortcuts class (the name is not final), and sends custom input events which get mapped to the configured callbacks on the server.

@SmithChart, @maratori, @grigolet, @laundmo

maratori commented 1 year ago

Looks nice 👍

SmithChart commented 1 year ago

I really like the idea of handlers for global shortcuts in the frontend.

How would this integrate with a higher-level integration like lona-somethingcss that would probably like to handle some key presses and a view that would like to handle some others?

fscherf commented 1 year ago

@SmithChart: That's a good question, and I am not sure yet. Currently, Lona implements event capturing (handling events from the outer-most node first) only very basic, and I plan on changing this. Every node should have a capture_input_event(). With that feature in place, use-cases like this should be feasible.