June 27, 2023
We are building neeto which is a collection of software. Keyboard shortcuts are a vital feature in any product that aims to improve user experience. This blog will discuss how we standardized keyboard shortcuts across all Neeto products.
Before we delve into how we've standardized keyboard shortcuts, it's important to understand why we felt the need to do so. With multiple products under the Neeto ecosystem, each product team was implementing keyboard shortcuts functionality in their way. This led to several problems:
Cross-platform browser problems
Some products use react-hotkeys
and some use react-hotkeys-hook
for
keyboard shortcuts. And some create their custom hooks to handle keyboard
shortcuts. However, the issue with all three approaches is that developers may
miss adding alternative hotkeys for operative systems which is different from
their development machine.
Furthermore, OS-based key translation behavior is inconsistent. For instance,
one product may use the combination option + s
for Mac and alt + s
for
Windows, while another product may use option + s
for Mac and ctrl + s
for
Windows. Such inconsistencies degrade the user experience when users switch
between operating systems.
Difference in UI for listing all shortcuts
Each product displayed shortcuts differently. Some used react-hotkeys
built-in feature, while others created separate pages, modals, or tooltips to
show hotkeys. We wanted consistent behavior across all products in neeto.
Difficulty finding and modifying registered hotkeys in the codebase
There was no standard convention on how/where to keep hotkey data. Some products have created a file to store all the hotkeys & related info, and some have their conventions. When a developer is transferred from one product to another, they find it hard to adapt to the new conventions over there. We wanted consistency in code structuring as well for all products in neeto.
To fix all these problems, we decided to bring in some code conventions and extract the common code to an npm package so that it can be reused in all products consistently.
We introduced a custom hook to run a specified function when the associated hotkey is fired. We also built a side pane component to display the list of all shortcuts.
ShortcutsPane is a React component that displays all the keyboard shortcuts in
the product. It belongs to neeto-molecules
package to enable code reuse. Users
can open/close the shortcuts pane by clicking on the Keyboard shortcuts icon in
the sidebar or pressing a hotkey shift+/
.
This is how the pane looks:
This is how the component is integrated into the products:
<KeyboardShortcuts.Pane productShortcuts={SHORTCUTS}>
We include KeyboardShortcuts.Pane
in the Main
component, which, as per neeto
standards, is the topmost parent component. Here SHORTCUTS
constant contains
the product-specific custom shortcuts. We have set a convention to define it in
a file named constants/keyboardShortcuts.js
. productShortcuts
prop is
optional and it can be omitted if a product doesn't have any custom shortcuts.
Given below is the sample structure of the keyboardShorcuts.js
file.
export const CATEGORY_NAMES = {
messenger: "MESSENGER",
settings: "SETTINGS",
};
export const SHORTCUTS = {
[CATEGORY_NAMES.messenger]: {
addNewLine: {
sequence: "shift+return",
description: "Add new line",
},
sendMessage: {
sequence: "return",
description: "Send message",
},
addEmoji: {
sequence: "command+option+e",
description: "Add emoji",
},
closeConversation: {
sequence: "command+option+y",
description: "Close conversation",
},
},
[CATEGORY_NAMES.settings]: {
addPlaceholder: {
sequence: "command+option+t",
description: "Add placeholder",
},
},
};
Because of this consistency the developers moving from one Neeto product to another Neeto product would find it easy to identify and make changes to product shortcuts.
When SHORTCUTS
is passed into KeyboardShortcuts.Pane
, it will be merged with
the common keyboard shortcuts list. The common list will contain shortcuts like
open/close pane, close modals, submit form, etc that apply to all products.
Developers have to pass the hotkey
for MacOs and the component will take care
of identifying the user's platform using platform.js
and render appropriate
platform-specific keys. For example, option
will be converted to alt
for
Windows users.
We wanted a hook that combined the features of both react-hotkeys
and
react-hotkeys-hook
. We wanted all the features of react-hotkeys-hook
like
their hook style & easy to use API and the sequential
mode of react-hotkeys
package. So we went with building our own hook named useHotKeys
. We added the
hook to the package @bigbinary/neeto-commons-frontend
. All our reusable hooks,
common utility functions, configs, etc reside in this package. Developers will
pass a hotkey, a handler function and an optional configurations object to the
hook.
useHotKeys
is built using the popular
mousetrap.js package. mousetrap.js
is a tiny
library that helps handle keyboard shortcuts in an application.
Because of the below features of useHotKeys
, we can handle most of the
shortcut management cases for any application.
return
.command
is converted to
ctrl
on Windows, and the user will only pass the hotkey for macOS.enabled
config which can be used to enable/disable the hotkey.
By default, all hotkeys will be enabled.default
: On default mode hotkeys won't be fired if the current focus is on
input fields.scoped
: The mode scoped is used to restrict a hotkeys handler to a
specific DOM element eg: forms.global
: global is similar to the default mode. The only difference is that
it will fire even if the user is focused on an input field.The following is an example of using the default mode. Here, the handler
handleCloseConversation
will be invoked whenever the user presses the close
conversation hotkey:
useHotKeys(MESSENGER_SHORTCUTS.closeConversation.sequence, () =>
setCoversationModalOpen(true)
);
We define constants like MESSENGER_SHORTCUTS
in the
constants/keyboardShortcuts.js
file to make it easier to access sequences and
pass them to the useHotKeys
hook. It would look something like this:
export const MESSENGER_SHORTCUTS = SHORTCUTS[CATEGORY_NAMES.messenger];
The following is an example of using the scoped mode. When the mode is scoped,
useHotKeys
will return a React ref that we can attach to the desired element.
The handler function will only be invoked when the user presses the hotkey and
is focused inside the form, e.g, the input element:
const formRef = useHotKeys(
MESSENGER_SHORTCUTS.toggleModal.sequence,
() => setIsModalOpen(isOpen => !isOpen),
{ mode: "scoped" }
);
return (
<div>
<form ref={formRef}>
<input type="text" name="username" />
</form>
</div>
);
While rolling out these components to products, we noticed that it would be nice
if some pages were styled differently when the pane is open. So, we added a
hook, usePaneState
, that will tell whether the pane is open or not. Pages can
use this hook to know the current status of the pane & apply styles accordingly.
Try out any of the neeto products and see the keyboard shortcuts in action for yourself.
If this blog was helpful, check out our full blog archive.