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.
The need for standardization
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.
Components and hooks created for standardization
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
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:
1<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.
1export const CATEGORY_NAMES = { 2 messenger: "MESSENGER", 3 settings: "SETTINGS", 4}; 5 6export const SHORTCUTS = { 7 [CATEGORY_NAMES.messenger]: { 8 addNewLine: { 9 sequence: "shift+return", 10 description: "Add new line", 11 }, 12 sendMessage: { 13 sequence: "return", 14 description: "Send message", 15 }, 16 addEmoji: { 17 sequence: "command+option+e", 18 description: "Add emoji", 19 }, 20 closeConversation: { 21 sequence: "command+option+y", 22 description: "Close conversation", 23 }, 24 }, 25 [CATEGORY_NAMES.settings]: { 26 addPlaceholder: { 27 sequence: "command+option+t", 28 description: "Add placeholder", 29 }, 30 }, 31};
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.
useHotKeys hook
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.
- It supports sequential hotkeys. For example "press s and then r".
- It can bind simultaneous hotkeys. For example "press s and at the same time press r".
- It can bind single hotkeys eg: return.
- It auto converts a hotkey based on platform eg: command is converted to ctrl on Windows, and the user will only pass the hotkey for macOS.
- It supports enabled config which can be used to enable/disable the hotkey. By default, all hotkeys will be enabled.
- It supports multiple modes of operations as explained below:
- 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:
1useHotKeys(MESSENGER_SHORTCUTS.closeConversation.sequence, () => 2 setCoversationModalOpen(true) 3);
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:
1export 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:
1const formRef = useHotKeys( 2 MESSENGER_SHORTCUTS.toggleModal.sequence, 3 () => setIsModalOpen(isOpen => !isOpen), 4 { mode: "scoped" } 5); 6 7return ( 8 <div> 9 <form ref={formRef}> 10 <input type="text" name="username" /> 11 </form> 12 </div> 13);
usePaneState hook
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.