Skip to content

FocusScope fires blur on focused inputs during Dialog close, triggering form validation libraries #2630

@Valentin-Shyaka

Description

@Valentin-Shyaka

Description of the bug

When a Dialog containing a form is closed, FocusScope restores focus to the trigger element by
calling focus(previouslyFocusedElement). As a browser side-effect, calling .focus() on the
trigger fires a blur event on whichever input was last focused inside the dialog.

Libraries like vee-validate and FormKit validate on blur by default. This causes validation error
messages to flash on form fields as the dialog closes — even when the user never interacted with
those fields or intentionally dismissed the dialog.

Reproduction

validateOnBlur: true, which is the default)
2. Click into the input so it has focus
3. Press Escape or click the close button
4. Observe: the validation error appears on the input as the dialog closes

Root cause

In FocusScope.vue, the cleanupFn of the second watchEffect:

setTimeout(() => {                                                                                  
  if (!unmountEvent.defaultPrevented)
    focus(previouslyFocusedElement ?? document.body, { select: true })                              
  ...                                                                 
}, 0) 

Calling focus(trigger) causes the browser to synchronously fire blur on the currently-focused input.
There is no explicit .blur() call — this is standard browser behavior. reka-ui is not doing
anything wrong; the issue is that there is no signal to distinguish a system-initiated blur (focus
trap cleanup) from a user-initiated blur (tabbing away, clicking elsewhere).

The existing @unmount-auto-focus escape hatch does not help here because the blur fires
synchronously inside the setTimeout callback, after the event has already been dispatched.

Proposed solution

Add a data-focus-scope-unmounting attribute to the container for the duration of the focus
restoration. Consumers can check for this attribute in their blur handlers to skip validation:

// vee-validate custom integration
  function handleBlur(event: FocusEvent) {
    if ((event.target as Element).closest('[data-focus-scope-unmounting]')) return                    
    // ... run validation
  }    

The attribute is set before the setTimeout, so it is present when blur fires synchronously inside
it. It is removed after focus() returns, so it does not linger.

Workaround (until fixed upstream)

Disable blur-triggered validation on dialog forms:

// vee-validate                                                                                     
const { handleSubmit } = useForm({ validateOnBlur: false })

See linked PR for the minimal 4-line change.


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