#leptos #hotkeys #keyboard-shortcuts

leptos_hotkeys

A library that declaratively pairs keybindings with callbacks for Leptos applications

7 releases

0.2.0-alpha.1 Mar 21, 2024
0.1.5 Feb 27, 2024

#80 in WebAssembly

Download history 5/week @ 2024-02-04 66/week @ 2024-02-11 126/week @ 2024-02-18 215/week @ 2024-02-25 31/week @ 2024-03-03 8/week @ 2024-03-10 118/week @ 2024-03-17 5/week @ 2024-03-24 23/week @ 2024-03-31 24/week @ 2024-04-07 9/week @ 2024-04-14

72 downloads per month

MIT license

41KB
600 lines

leptos-hotkeys

All Contributors

Declaratively create and pair keybindings with callbacks for Leptos applications.

Crates.io discord

A person playing a burning piano at a sandy beach under a cloudy sky

[!NOTE] This library is ready for use. If you're curious about updates read the CHANGELOG.

Live example

Curious to see how it works? See the demo and its source code!

To get started, follow the Quick Start section. It's worth the read!

Features

use_hotkeys! Macro

For simplicity and ease, use the use_hotkeys! macro to declare global and scoped hotkeys. We brought some js idioms while maintaining the leptos look. Learn more about the macro.

If you prefer writing out your callbacks the leptos way, we also have non-macro hotkeys. Learn more about trad hotkeys.

Global Hotkeys

This example creates two global hotkeys: W and S.

[!TIP] For more information about how to write your keybindings, check out Key Grammar.

[!NOTE] The * symbol is reserved for the global scope_

use leptos_hotkeys::use_hotkeys;

#[component]
pub fn SomeComponent() -> impl IntoView {
    let (count, set_count) = create_signal(0);

    // creating a global scope for the W key
    use_hotkeys!(("w") => move |_| {
        logging::log!("w has been pressed");
        set_count.update(|c| *c += 1);
    });

    // this is also a global scope for the S key!
    use_hotkeys!(("s", "*") => move |_| {
        logging::log!("s has been pressed");
        set_count.update(|c| *c -= 1);
    });

    view! {
        <p>Current count: {count}</p>
    }
}

Scoped Hotkeys

This example shows an inner and outer scope and hotkeys that switch between the scopes.

[!TIP] Assign hotkeys specific to individual sections without collisions using scopes. Use functions in HotkeysContext for scope management. For more information about how to write your keybindings, check out Key Grammar.

[!NOTE] Scopes are case-insensitive. That means my_scope and mY_sCoPe are considered the same scope.

use leptos_hotkeys::{use_hotkeys, use_hotkeys_context, HotkeysContext};

#[component]
pub fn SomeComponent() -> impl IntoView {

    let HotkeysContext { enable_scope, disable_scope, .. } = use_hotkeys_context();

    // switch into the inner scope
    use_hotkeys!(("i", "outer") => move |_| {
        disable_scope("outer");
        enable_scope("inner");
    });

    // switch into the outer scope
    use_hotkeys!(("o", "inner") => move |_| {
        disable_scope("inner");
        enable_scope("outer");
    });

    view! {
        <div id="outer">
            //...some outer scope html...
            <div id="inner">
            //...some inner scope html...
            </div>
            //...some outer scope html....
        </div>
    }
}

Focus trapped Hotkeys

[!TIP] Embed a hotkey with an html element and the hotkey will only fire if the element is focused and the scope is enabled.

use leptos_hotkeys::use_hotkeys_ref;

#[component]
pub fn SomeComponent() -> impl IntoView {

    let p_ref = use_hotkeys_ref!(("K", "*") => move |_| {
        // some logic
    });

    view! {
        <p
            tabIndex=-1
            _ref=p_ref
        >
            p tag with node ref
        </p>
    }
}

Quick Start

Installation

cargo add leptos_hotkeys

[!NOTE] leptos-hotkeys supports both client-side rendered and server-side rendered applications.

For client side rendered:

leptos_hotkeys = "0.2.0-alpha.1"

For server side rendered:

leptos_hotkeys = { version = "0.2.0-alpha.1", features = ["ssr"] }

For client side and server side rendered:

leptos_hotkeys = "0.2.0-alpha.1"

[features]
ssr = ["leptos_hotkeys/ssr"]

We also offer other feature flags that enhance developer experience, see features.

provide_hotkeys_context()

Call provide_hotkeys_context() in the App() component. This will provide the HotkeysContext for the current reactive node and all of its descendents. This function takes three parameters, the node_ref, a flag to disable blur events and a list of initially_active_scopes.

[!NOTE] provide_hotkeys_context() returns a HotkeysContext. See HotkeysContext.

use leptos_hotkeys::{provide_hotkeys_context, scopes};

#[component]
pub fn App() -> impl IntoView {
    provide_meta_context();

    let main_ref = create_node_ref::<html::Main>();
    provide_hotkeys_context(main_ref, false, scopes!());

    view! {
        <HotkeysProvider>
            <Router>
                <main _ref=main_ref>  // <-- attach main ref here!
                    <Routes>
                        <Route path="/" view=HomePage/>
                        <Route path="/:else" view=ErrorPage/>
                    </Routes>
                </main>
            </Router>
        </HotkeysProvider>
    }
}

Initialize scopes

If you're using scopes, you can initialize with a specific scope.

use leptos_hotkeys::{scopes, HotkeysProvider};

view! {
    <HotkeysProvider
        initially_active_scopes=scopes!("some_scope_id")
    >
        <Router>
            //... routes
        </Router>
    </HotkeysProvider>
}

Keybinding Grammar

leptos_hotkeys matches key values from KeyboardEvent's key property. For reference, here's a list of all key values for keyboard events.

You can bind multiple hotkeys to a callback. For example:

"G+R,meta+O,control+k"

The above example creates three hotkeys: G+R, Meta+O, and Ctrl+K. The + symbol is used to create a combo hotkey. A combo hotkey is a keybinding requiring more than one key press.

[!NOTE] Keys are case-agnostic and whitespace-agnostic. You use the , as a delimiter in a sequence of multiple hotkeys.

Macro API

We wanted to strip the verbosity that comes with str and String type handling. We kept leptos best practices in mind, keeping the move |_| idiom in our macro.

use_hotkeys!()

Here is a general look at the macro:

use leptos_hotkeys::use_hotkeys;

use_hotkeys!(("keys", "scope") => move |_| {
    // callback logic here
});

For global hotkeys, you can omit the second parameter as it will implicitly add the global scope.

use_hotkeys!(("key") => move |_| {
    // callback logic here
});

use_hotkeys_ref!()

This macro is used when you want to focus trap with a specific html element.

use leptos_hotkeys::use_hotkeys_ref;

#[component]
pub fn SomeComponent() -> impl IntoView {
    let some_ref = use_hotkeys_ref!(("key", "scope") => move |_| {
        // callback logic here
    });

    view! {
        <div tabIndex=-1 _ref=some_ref>
        </div>
    }
}

scopes!()

Maybe you want to initialize a certain scope upon load, that's where the prop initially_active_scopes comes into play. Instead of having to create a vec!["scope_name".to_string()], use the scopes!() macro.

use leptos_hotkeys::{scopes, HotkeysProvider};

view! {
    <HotkeysProvider
        initially_active_scopes=scopes!("scope_a", "settings_scope");
    >
        // pages here...
    </HotkeysProvider>
}

Feature Flags

debug

We want to improve developer experience by introducing the debug flag which adds logging to your console in CSR. It logs the current pressed key values, hotkeys fires, and scopes toggling.

Just simply:

leptos_hotkeys = { path = "0.2.0-alpha.1", features = ["debug"] }

API

<HotkeysProvider />

Prop Name Type Default Value Description
allow_blur_event bool false Determines if the component should reset pressed_keys when a blur event occurs on the window. This is useful for resetting the state when the user navigates away from the window.
initially_active_scopes HashSet<String> scopes!("*") (Global State) Specifies the set of scopes that are active when the component mounts. Useful for initializing the component with a predefined set of active hotkey scopes.

HotkeysContext

Field Name Type Description
pressed_keys RwSignal<HashSet<String>> A reactive signal tracking the set of keys currently pressed by the user.
active_ref_target RwSignal<Option<EventTarget>> A reactive signal holding the currently active event target, useful for focusing events.
set_ref_target Callback<Option<EventTarget>> A method to update the currently active event target.
active_scopes RwSignal<HashSet<String>> A reactive signal tracking the set of currently active scopes, allowing for scoped hotkey management.
enable_scope Callback<String> A method to activate a given hotkey scope.
disable_scope Callback<String> A method to deactivate a given hotkey scope.
toggle_scope Callback<String> A method to toggle the activation state of a given hotkey scope.

Basic Types

Keyboard Modifiers

Field Name Type Description
alt bool Indicates if the Alt key modifier is active (true) or not (false).
ctrl bool Indicates if the Control (Ctrl) key modifier is active (true) or not (false).
meta bool Indicates if the Meta (Command on macOS, Windows key on Windows) key modifier is active (true) or not (false).
shift bool Indicates if the Shift key modifier is active (true) or not (false).

Hotkey

Field Name Type Description
modifiers KeyboardModifiers The set of key modifiers (Alt, Ctrl, Meta, Shift) associated with the hotkey.
keys Vec<String> The list of keys that, along with any modifiers, define the hotkey.
description String A human-readable description of what the hotkey does. Intended for future use with scopes.

Trad Hotkeys

If the macro isn't to your liking, we offer three hotkeys: global, scoped, and focus trapped.

Global: use_hotkeys_scoped() where scope = *

use leptos_hotkeys::{use_hotkeys_scoped};

#[component]
fn Component() -> impl IntoView {
    let (count, set_count) = create_signal(0);

    use_hotkeys_scoped(
        "F", // the F key
        Callback::new(move |_| {
            set_count.update(|count| { *count += 1 })
        }),
        vec!["*"]
    );

    view! {
        <p>
        Press 'F' to pay respect.
        {count} times
        </p>
    }
}

Scoped - use_hotkeys_scoped

use leptos_hotkeys::{
    use_hotkeys_scoped, use_hotkeys_context, HotkeysContext
};

#[component]
fn Component() -> impl IntoView {
    let hotkeys_context: HotkeysContext = use_hotkeys_context();

    let toggle = hotkeys_context.toggle_scope;
    let enable = hotkeys_context.enable_scope;
    let disable = hotkeys_context.disable_scope;

    use_hotkeys_scoped(
        "arrowup",
        Callback::new(move |_| {
            // move character up
        }),
        vec!["game_scope"]
    );

    use_hotkeys_scoped(
        "arrowdown",
        Callback::new(move |_| {
            // move character down
        }),
        vec!["game_scope"]
    );

    view! {
        <button
        // activates the 'game_scope' scope
        on:click=move |_| enable("game_scope")
        >
            Start game
        </button>

        <button
        // toggles the 'game_scope' from enabled to disabled
        on:click=move |_| toggle("game_scope")
        >
            Pause game
        </button>


        <button
            // disables the 'game_scope' scope
            on:click=move |_| disable("game_scope")
        >
            End game
        </button>
    }
}

Focus trapped - use_hotkeys_ref()

use leptos_hotkeys::use_hotkeys_ref;

#[component]
fn Component() -> impl IntoView {
    let node_ref = use_hotkeys_ref("l", Callback::new(move |_| {
        // some logic here
    }));

    view! {
        <body>
            <div _ref=node_ref>
            // when this div is focused, the "l" hotkey will fire
            </div>
        </body>
    }
}

Contributors

Álvaro Mondéjar
Álvaro Mondéjar

💻
Robert Junkins
Robert Junkins

💻
LeoniePhiline
LeoniePhiline

📖

Dependencies

~20–33MB
~522K SLoC