Skip to content

a declarative way of using keyboard shortcuts + callbacks in leptos applications

License

Notifications You must be signed in to change notification settings

gaucho-labs/leptos-hotkeys

Repository files navigation

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

Leptos-hotkeys creates and manages keyboard shortcuts. It provides macros and functions that simplify the definition of keybindings, since the management of event lifecycle associated with keyboard interactions has been done for you!

Live example

Curious to see how it works? See the demo.

To get started, follow the Quick Start section.

Features

Note

This crate has three types of hotkeys: global, scoped, and focus-trapped.

The use_hotkeys! Macro

Use this macro to declare global and scoped hotkeys. This macro has js idioms while preserving Leptos standards. More about the macro.

Global Hotkeys

This example creates two global hotkeys: W and F.

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!(("keyw") => move |_| {
        logging::log!("w has been pressed");
        set_count.update(|c| *c += 1);
    });

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

    view! { <p>Num Respects: {count}</p> }
}

Tip

How do I write certain keys? See Key Grammar.

Note

The * symbol is reserved for the global scope_.

The W hotkey omitted the scope parameter, implicitly making it global.

Scoped Hotkeys

Scopes provide context behind hotkeys. This context can be chained to a component, a state, or logic.

This example shows an inner and outer scope and hotkeys that toggle scopes.

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!(("keyi", "outer") => move |_| {
        disable_scope.call("outer".to_string());
        enable_scope.call("inner".to_string());
    });

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

    view! {
        <div id="outer">
            // outer logic residing...
            <div id="inner">
            // inner logic
            </div>
        </div>
    }
}

Note

Scopes are case-insensitive. That means my_scope and mY_sCoPe are considered the same scope.

Focus trapped Hotkeys (the use_hotkeys_ref! macro)

This example embeds a hotkey to a <p> tag. This hotkey will fire iff the element is focused and the scope is correct.

use leptos_hotkeys::use_hotkeys_ref;

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

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

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

Quick Start

Installation

cargo add leptos_hotkeys

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. provide_hotkeys_context() returns a HotkeyContext. To manage hotkeys, you can pull necessary signals out of HotkeysContext.

use leptos_hotkeys::{provide_hotkeys_context, HotkeysContext, scopes};

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

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

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

Note

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

Keybinding Grammar

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

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

"KeyG+KeyR,MetaLeft+KeyO,ControlLeft+keyK"

Keys are case-agnostic and whitspace-agnostic. For a hotkey with multiple keys, use the , as a delimiter in a sequence of keys.

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::{provide_hotkeys_context, scopes};

#[component]
pub fn App() -> impl IntoView {
    let main_ref = create_node_ref::<html::Main>();
    provide_hotkeys_context(main_ref, false, scopes!("scope_a", "settings_scope"));

    view! {
        <Router>
            <main _ref=main_ref>
                <Routes>
                    // ... routes
                </Routes>
            </main>
        </Router>
    }
}

The debug feature flag

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

Just simply:

leptos_hotkeys = { path = "0.2.1", features = ["debug"] }

Contributors

Álvaro Mondéjar
Álvaro Mondéjar

💻
Robert Junkins
Robert Junkins

💻
LeoniePhiline
LeoniePhiline

📖
Gábor Szabó
Gábor Szabó

📖
Phillip Baird
Phillip Baird

🐛 💻
zakstucke
zakstucke

🐛 💻
Ryangguk Kim
Ryangguk Kim

💻
Max Bergmark
Max Bergmark

💻
Pavlo Myroniuk
Pavlo Myroniuk

💻