Round 4 of adversarial review, both reproduced live:
1. removeButton's mousedown-only wiring (round 3's own fix) dropped keyboard
activation entirely -- Tab-to-focus, Enter/Space did nothing, silently.
onClick is wired back alongside the mousedown handler; a real mouse click
still fires both, but the removal it triggers is idempotent (see below),
so the second call is a safe no-op.
2. Deleting a row that ITSELF still has an uncommitted edit could corrupt a
DIFFERENT, already-saved row. removeButton's own synchronous redraw
detaches whatever field still has a pending edit; a browser fires `blur`
on a detached-while-focused element even with preventDefault() called on
the mousedown, and that reentrant commit -- firing INSIDE the very
setVariables() call still removing a row -- was still indexed against
the row's OLD numeric position in an array that had already shrunk by
one underneath it.
Both share one cause round 3's fix narrowed but didn't remove: every commit
and removal in this editor addressed a row by the numeric index a render
closed over, and a synchronous redraw can invalidate that index before a
still-in-flight commit reads it. Fixed by addressing rows by the row OBJECT
itself, found by reference in the CURRENT array at the moment a commit or
removal actually fires -- a row no longer there (already removed, whether by
the user's own click or a reentrant one) is a no-op instead of a slot number
that now means something else. Confirmed with two new Playwright tests:
editing then removing the SAME row leaves the neighbour untouched, and
Enter/Space on a focused ✕ removes it.