javascriptfocussvelte

Inconsistent focus loss in sortable list


I’ve been playing around with keyboard navigation trying to build a list of items that can be sorted manually. The steps to use it are the following:

  1. Press Tab until the list is focused.
  2. Press Arrow Up and Arrow Down to navigate through the items.
  3. Press Space to lift an item, then the Arrow Up and Arrow Down to relocate it, and Space again to drop it.

The thing is that I’m getting different behaviors depending on where I drop the item. If I drop it one position down, everything behaves as expected, but if I drop it one position up or two or more down, the focus is lost. I’ve been trying to debug it for days now and I still cannot tell if this is something I ignore about Svelte’s reactivity system or simply related to focus handling in the browser. Either way, it really confuses me that I’m getting inconsistent outcomes.

You can test it out in this REPL: https://svelte.dev/repl/2fbb34a7283e417ea995d674946a991a?version=4.2.12

It’s worth mentioning that I am aware that each browser manages focus in slight different ways (specially when it comes to losing focus), and that this is only happening in Chromium browsers, not in Firefox or Safari where I am getting consistent results (have tested it in Windows as well as in MacOS).

If I remove the key from the #each loop (going from {#each items as item, index (item.id)} to {#each items as item, index}), it all starts behaving consistently in Chromium browsers too, but this is not a good practice, shouldn’t be necessary and it’s causing me different kinds of issues as you might imagine.


Solution

  • Thanks to the James’s comment pointing to a particular issue in the Svelte repo I was able to find a workaround for my problem.

    Seems that Svelte (at least in v4) does not retain focus when elements are moved (even when a proper key is assigned to them), so it is necessary to manually keep focus on the corresponding element after an update takes place.

    Basically, we can re-focus the active element every time an update occurs:

    import { beforeUpdate, afterUpdate } from 'svelte'  
        
    let activeElement: HTMLElement;
    
    beforeUpdate(() => {
        activeElement = document?.activeElement as HTMLElement;
    });
    
    afterUpdate(() => {
        if (activeElement) activeElement.focus();
    });
    

    Here’s a new REPL that includes the changes mentioned above: solution REPL.