When a computed prop updates faster than the DOM
What happens when a component that performs animations that run slower than your favorite Javascript Framework performs updates to the application state faster than the DOM can parse the state of the page? The answer is that you get into strange situations with components that do not respond to the HTML DOM API.
Picture this scenario while working on an interaction where the idea was for a user to open a side drawer using a button, disable the button, and perform an action in that drawer. Once the user finishes their tasks in the drawer, have the drawer close and finally set focus again on the button that opened the drawer in case they need to return to the previous action.
In theory, the solution was simple: the drawer component should emit an event when the drawer is closed and, from there, call the focus()
method of the HTML DOM API in the desired element so it gains focus once the event is emitted and done, right?
Well, this happened instead:
My reaction was the following:
So, after being left speechless, I had to get to the flame graphs to get to the bottom of this:
This problem has two parts. The first is that the callback when the drawer is dismissed happens after the focus() method is called, which means that the button is in a non-focusable state. The second part is that the callback is called before the component is destroyed, which changes the order in which the functions are called. I can see two ways around this problem.
- Update the component to use the
onUnmounted
lifecycle hook in Vue to change the order when the functions are called, leaving thefocus
order where it should be. - Use a MutationObserver as the potential reach that a change like that could have across the codebase due to the current expectations of the component's behavior.
MutationObserver solution
This solution is simple: add a MutationObserver to the DOM element as part of a ref and have it check when the button is disabled. That element will change between enabled and disabled states once a user enters and exits the drawer component. e.g.
mounted() {
this.drawerButton = this.$refs.drawerButton.$el;
this.observer = new MutationObserver(() => {
if(!this.drawerButton.disabled) {
this.drawerButton.focus();
}
});
if (this.drawerButton) {
this.observer.observe(this.drawerButton, { attributes: true });
}
},
Now, we get a focused element after the Drawer closes.
This has a couple of limitations/observations, however:
- You are now responsible for managing when the button is focused, which depends on the design of the page; you might run into problems when working with larger forms
- If you need to work with more than one element showing this behavior, you might need to redesign your component to utilize Vue's component lifecycle better.