Skip to content

Bug: Attachment callback is running before the pending UI update. They should run after any pending UI updates, just like how standalone $effects behave. #16395

@sreekar339339

Description

@sreekar339339

Describe the bug: Attachment callback is running before the pending UI updates which is an unexpected behaviour / bug. They should run after any pending UI updates, just like how standalone $effects behave.

https://svelte.dev/playground/195d38aeb8904649befaac64f0a856c4?version=5.36.2

How to reproduce?

  1. Play the TicTacToe game in the provided playground link WITH KEYBOARD ONLY until the game is finished (either winning or draw). Manage focus and navigate the focus on the cells using the Tab key and press the Enter key to make a move.
  2. After the game is over, navigate to the "Reset History" button by shifting the focus using the Tab key.
  3. Press Enter on the "Reset History" button.

The expected outcome is the focus should automatically shift from the "Reset History" button back to the first cell in the game. But that does not happen. The reason is the {@attach} attachment callback responsible for focus management is NOT behaving as expected.

  1. The captureFocus method on App class handles shifting of focus to the desired element referenced by this.nextEleToFocus state variable.
  2. This captureFocus method runs automatically after resetHistory method (the event handler of "Reset History" button) is executed, because captureFocus is run inside an implicit effect, since it is an {@attach} attachment on .container element. The state mutations caused by resetHistory resets all the state used in the component, which results in all cells being back to active, which were previously in disabled state.
  3. The problem is captureFocus method is executed before all the queued pending UI updates (caused by state mutations of the resetHistory method) are committed to the DOM. The DOM when captureFocus is invoked is in a stale/pre-mature state, where all the game cells are in disabled state i.e the history reset is not yet committed to the DOM, hence setting focus on the first cell does not work as expected.

When the same captureFocus function is used inside a standalone $effect instead of an attachment, this bug is not seen. The standalone $effect waits before all the pending UI updates caused by state mutations from resetHistory method are committed to the DOM, before running the effect callback. The focus is correctly shifted to the first cell after pressing the Enter key on "Reset History" button.

As a quickfix, Im using the tick() function to manually wait for pending UI updates inside the captureFocus method (when used as an attachment) before firing the focus() method on the desired element. This quickfix is not needed for the standalone effect approach.

             class App {
                 ...
                 resetHistory = () => {
			this.history.reset()
			this.setFocusTo(this.selector.cell(0))
		}
                 ...
		captureFocus = async (container) => {
			if (!this.nextEleToFocus) return
			 // await tick() // tick should not be needed but used as a quickfix.
			container.querySelector(this.nextEleToFocus.selector).focus()
		}
                ...
            }

                	const app = new App()
	        // let container <- bind this to the '.container' element, uncomment these lines, and remove the {@attach} code on '.container' element to test the $standalone effect behaviour.
	        // $effect(() => app.captureFocus(container));
                ...

Reproduction

https://svelte.dev/playground/195d38aeb8904649befaac64f0a856c4?version=5.36.2

System Info

Playground env version=5.36.2

Severity

annoyance

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions