10

Keyboard shortcuts are a bit tricky to manage in web applications.

consider a Widget component.

I want to be able to focus certain elements, and run functions on this component, based on keyboard shorcuts.

class Widget extends React.Component {
  componentDidMount() {
    this.setBindings()
  },
  componentWillUnmount() {
    this.removeBindings();
  }
} 

setBindings and removeBindings, would use a library like mousetrap to bind specific keyboard shortcuts

Now, there's two problems with the above solution:

  1. It makes keyboard shortcut behavior unpredictable
    • Consider the case where two Widgets mount, one would override the other
  2. Widget becomes tightly coupled with the shortcuts -- now if someone doesn't want to use shortcuts, they have to have some sort of flag on Widget. This destroy's the 'granularity' of the code -- ideally a user should be able to use Widget, then WidgetWithShortcuts, or something like this

Another potential solution, is to pass an instance

const widgetShortcuts = (widgetInstance) => {
  return {
   'ctrl i': () => widgetInstance.focusInput(),
  }
}

The problem with the second solution is:

  1. widgetInstance will have to expose a lot of publicly accessible methods, like focusSomeThing, or invokeProp, etc

  2. if Widget wants to have some tooltip, that shows the keyboard shortcuts at certain places, the info about the keyboard shorcuts will be duplicated in different places. It will become possible to change the shortcuts in one place, and forget to do so in another places

Is there a best practice, or some ideas on how keyboard shortcuts can be implemented with solutions to the problems above?

Stepan Parunashvili
  • 2,627
  • 5
  • 30
  • 51

2 Answers2

5

React-hotkeys is not a method, but a tool that sounds like it would solve the problem you're facing.

Some things to note:

  • You use it by wrapping your component in a custom <HotKeys> component that has the hotkeys attached to it.
  • Methods aren't made public, but are instead assigned to hotkeys through a keymap & handler
  • Hotkeys added to child-components will take priority over parent component hotkeys

One issue I'm facing with this plugin is that there doesn't seem to be a way to keep hotkeys from firing when typing in an input field, but other than that it seems to work pretty great.

Chance Smith
  • 1,211
  • 1
  • 15
  • 32
Zach
  • 871
  • 1
  • 11
  • 21
  • The problem with this library is that you have to manually focus on the component that HotKeys wraps or the shortcuts won't work. Unless I'm doing something wrong? – bert Oct 18 '18 at 14:49
  • I believe if you want to have global hotkeys, you would just wrap the entire app in the hotkey component, and then any other component within the app would inherit those hotkeys. The only time you would need to wrap a smaller component in a hotkey component is if you wanted certain global hotkeys to be overwritten while said component is focused. – Zach Nov 14 '18 at 20:54
  • Unfortunately that’s not the case. If you wrap the whole app in it you still have to focus the window. – bert Nov 14 '18 at 20:55
1

I think your best bet is setup your keyboard shortcut listener once at the top level and pass down info to components who may or may not care that a shortcut happened. This solves problem 1 where you may bind listeners more than once, and this also precludes the need to expose any component functions.

class ShortcutProvider extends Component {
   state = { shortcut: null }

   componentDidMount() {
     // shortcut library listener
     onShortcut(shortcut => this.setState({ shortcut })
   }

   render() {
     <App shortcut={this.state.shortcut} />
   }
}

Then your widget can react (or not react) to the prop changes:

class Widget extends Component {
  ...

  componentWillReceiveProps(nextProps) {
    if (this.state.shouldReactToShortcut) {
      if (nextProps.shortcut === 'ctrl i') {
        // do something
      }
    }
  }

  ...
}

If you're passing the shortcut prop down many components it may be worth it to put the shortcut state into context.

azium
  • 20,056
  • 7
  • 57
  • 79