---
title: Item add modal missing auto-focus and Esc handling
status: resolved
area: ui
phase_origin: 11-uat
created: 2026-03-20
updated: 2026-03-20
---

# Item Add Modal: Missing Auto-Focus and Esc Handling

## Current Focus

hypothesis: CONFIRMED — Two combined bugs with distinct root causes, both fixed.
test: cargo build succeeds, cargo test passes (all 13 tests green).
expecting: Human verification that search box auto-focuses on modal open, and Esc closes modal from both TextInput focus states.
next_action: Await human verification

## Symptoms

expected: Modal should auto-focus the search TextInput on open. Esc should close the modal first; only after modal is closed should Esc reach the global handler.
actual: Search box requires manual click to focus. Esc fires the global FocusScope handler (changing filters and navigating to home screen) instead of closing the modal.
errors: No error messages — behavioral bug.
reproduction: Open item add modal from any card, observe no focus. Press Esc, observe modal stays open but discovery mode changes.
started: Since modal was implemented (Phase 9).

## Eliminated

- hypothesis: modal-focus FocusScope init focus call works correctly
  evidence: init fires once at component construction time when visible=false. No re-trigger on visibility toggle. Focus calls during invisible state have no effect when modal later becomes visible.
  timestamp: 2026-03-20

- hypothesis: modal-focus FocusScope intercepts Esc from TextInput via event bubbling
  evidence: modal-focus is a sibling Rectangle of the inner modal panel, not an ancestor. Key events only bubble through the parent chain. search-input's parent chain is: TextInput -> Rectangle -> Rectangle (inner panel) -> Rectangle (backdrop) -- modal-focus is never in that chain.
  timestamp: 2026-03-20

- hypothesis: public function at component root can reference IDs defined inside if-blocks
  evidence: Slint compiler error "Cannot access id 'search-input'" when function body referenced the conditional ID. IDs inside if-blocks are scoped to their block.
  timestamp: 2026-03-20

- hypothesis: changed root.prop-name syntax works inside a child element to watch a root property
  evidence: Slint compiler error "Syntax error: expected '=>'" at the dot in root.search-focus-trigger. The changed keyword only accepts unqualified property names belonging to the current element.
  timestamp: 2026-03-20

## Evidence

- timestamp: 2026-03-20
  checked: lookup-modal.slint element hierarchy
  found: modal-focus FocusScope is a sibling of the inner panel Rectangle (both children of the backdrop Rectangle). search-input is a descendant of the inner panel, not of modal-focus.
  implication: Key events from search-input cannot reach modal-focus via bubbling.

- timestamp: 2026-03-20
  checked: init timing vs visible property
  found: LookupModal is instantiated in dashboard.slint DOM with visible=false at dashboard load. init fires once at construction. When lookup-modal-open becomes true later, no init re-fires.
  implication: Both init focus calls (modal-focus and search-input) fire while invisible and have no lasting effect.

- timestamp: 2026-03-20
  checked: if-block element lifecycle in Slint
  found: Elements inside if-blocks are created/destroyed when condition changes. Create form block is first created when user clicks "Create New" (modal visible) -- init fires usefully. Search block exists at construction (modal invisible) -- init is useless for initial focus.
  implication: Create form init is fine as-is. Search init is not reliable for initial open focus.

- timestamp: 2026-03-20
  checked: Slint scope rules for IDs inside if-blocks and changed syntax
  found: IDs inside if-blocks are not accessible from outside the block. changed keyword inside an element only accepts unqualified property names of that element. Workaround: mirror root property locally with two-way binding, then watch the local copy.
  implication: Focus trigger pattern implemented as: private property <int> search-focus-trigger at root + local mirror + changed local-focus-trigger inside the if-block.

- timestamp: 2026-03-20
  checked: cargo build and cargo test after fix
  found: Build succeeds. All 13 tests pass.
  implication: Fix is structurally correct and introduces no regressions.

## Resolution

root_cause: Two combined issues: (1) init handlers for focus calls fire at component construction when the modal is invisible -- they never re-fire when the modal becomes visible. (2) The modal-focus FocusScope is structurally unreachable from TextInput key events because it is a sibling element, not an ancestor of search-input in the element tree. The result: Esc always reaches global-keys (which holds permanent focus), and no TextInput is ever auto-focused on modal open.

fix: |
  lookup-modal.slint:
  - Added private property <int> search-focus-trigger and public function focus-lookup-search() at component root (increments the counter). Does not reference any conditional IDs.
  - Inside the search if-block: added property <int> local-focus-trigger <=> root.search-focus-trigger mirror + changed local-focus-trigger => { search-input.focus(); }. Fires each time Rust calls focus-lookup-search() with modal already visible.
  - Removed broken init => { search-input.focus(); } from search block (fired at construction when invisible).
  - Kept init => { create-name-input.focus(); } in create form block (fires when show-create-form becomes true, modal already visible -- works correctly).
  - Added key-pressed Escape handlers on all three TextInputs (search-input, create-name-input, shopify URL TextInput) calling root.close-requested(). Ensures Esc closes the modal regardless of which TextInput has focus.
  - Removed ineffective init => { modal-focus.focus(); } from the backdrop Rectangle.

  dashboard.slint:
  - Added lookup-modal-inst := ID to the LookupModal instance.
  - Added public function focus-lookup-search() that forwards to lookup-modal-inst.focus-lookup-search().

  main.rs:
  - After w.set_lookup_modal_open(true) in on_card_add_item, added w.invoke_focus_lookup_search().

verification: Build succeeds. All 13 tests pass. Awaiting runtime verification.
files_changed: [crates/app/ui/lookup-modal.slint, crates/app/ui/dashboard.slint, crates/app/src/main.rs]
