Refactor Shortcuts architecture
What does this MR do and why?
This is an attempt to resolve #392845 (closed). Specifically, this tries to solve the following problems:
- Get rid of this error-prone and weird page shortcuts setup logic.
- Prevent accidental duplicated execution of
Shortcuts
subclasses.
In broad strokes, the approach goes like this:
- Make
Shortcuts
a singleton, by instantiating it at the top level of~/behaviors/shortcuts
(via dynamic import). - Stop (and prevent) subclassing of
Shortcuts
to avoid duplicated calls of its constructor (which currently require to workarounds). - Delegate instantiation of other shortcuts classes (like
ShortcutsNavigation
) to theShortcuts
instance itself, which ensures no duplicates are created.- These duplicates happen easily because of how our webpack page bundle loading works. It loads not just the current page's entry point, but also all ancestor entry points. This means, for instance,
~/pages/projects/show/index.js
and~/pages/projects/foo/show/index.js
and~/pages/projects/foo/bar/show/index.js
all load when you load theprojects:bar:show:index
. So, if all three instantiateShortcutsNavigation
, then it's instantiated three times. - As a concrete example, on
master
, visit a blob page (e.g., http://gdk.test:3000/gitlab-org/gitlab-test/-/blob/master/MAINTENANCE.md). TheShortcuts
constructor is called 3 times,ShortcutsNavigation
twice, andShortcutsBlob
once. In this MR, each only gets called once. See #392845 (comment 1349996104) for other examples.
- These duplicates happen easily because of how our webpack page bundle loading works. It loads not just the current page's entry point, but also all ancestor entry points. This means, for instance,
Notes about an earlier iteration
An earlier iteration also did this:
- Make this
Shortcuts
singleton the way to set up/tear down keyboard shortcuts. This is because:- It removes some boilerplate, like calling
keysFor
on a command object before passing it toMousetrap.bind/unbind
. - It creates a facade around
Mousetrap
. This makes it easier for us to swapMousetrap
out in future for something else. Some issues with Mousetrap include:- It is not actively maintained; the most recent commit was over three years ago.
- It always exposes a global
Mousetrap
variable, even with bundlers like webpack. - It has doesn't properly clean up removed handlers (though this is minor).
- This allows us to smooth over some more awkward parts of its API for now (e.g.,
stopCallback
).
- It removes some boilerplate, like calling
I decided to remove that, as it:
- seemed like too broad a change;
- didn't feel like an elegant solution;
- was a leaky abstraction, as some element-local
Mousetrap
instances are still useful to have, e.g.:- https://gitlab.com/gitlab-org/gitlab/-/blob/444a75bde3ac94610a8c1b2fd1da712e05b7e555/app/assets/javascripts/vue_shared/components/form/input_copy_toggle_visibility.vue#L102-103
- https://gitlab.com/gitlab-org/gitlab/-/blob/444a75bde3ac94610a8c1b2fd1da712e05b7e555/app/assets/javascripts/authentication/two_factor_auth/components/recovery_codes.vue#L62-64
Screenshots or screen recordings
n/a
How to set up and validate locally
Visit any page with registered keyboard shortcuts, and verify they still work. Ideally, check pages which set up different Shortcuts*
classes, like:
Shortcuts
ShortcutsBlob
ShortcutsEpic
ShortcutsFindFile
ShortcutsIssuable
ShortcutsNavigation
ShortcutsNetwork
ShortcutsTestCase
ShortcutsWiki
The shortcuts help modal (accessed with ?) details most of these.
Alternatively/in addition, you could run the out-of-tree test suite I wrote to make working on this easier.
MR acceptance checklist
This checklist encourages us to confirm any changes have been analyzed to reduce risks in quality, performance, reliability, security, and maintainability.
-
I have evaluated the MR acceptance checklist for this MR.
Related to #392845 (closed)
Edited by Mark Florian