fix(editbar): restore caret to pad after toolbar-select change (#7589)#7598
fix(editbar): restore caret to pad after toolbar-select change (#7589)#7598JohnMcLear wants to merge 1 commit intoether:developfrom
Conversation
…#7589) After picking a value from a toolbar <select> (ep_headings style picker is the canonical case), keyboard focus was left on the nice-select wrapper rather than returned to the pad editor. Users had to click back into the pad before typing resumed. The ToolbarItem.bind() class already calls padeditor.ace.focus() at the end of triggerCommand, but that only runs for selects wired via data-key on the wrapping <li>. Plugin-provided selects (e.g. ep_headings2's #heading-selection inside <li id="headings">, no data-key) don't go through that path — they bind their own change handler and never return focus. Fix: add a delegated change handler on `#editbar select` that calls padeditor.ace.focus() after any toolbar select change. Deferred via setTimeout(0) so plugin change handlers (bound on the same event) complete their ace.callWithAce work before focus moves. Redundant but harmless for data-key-wired selects that are already refocused by triggerCommand. Added a Playwright regression test that simulates the nice-select option-click (val + change, which is what the wrapper dispatches internally) and verifies typing after the change lands in the pad. Skips when ep_headings2 isn't installed. Closes ether#7589.
Review Summary by QodoRestore caret to pad after toolbar select change
WalkthroughsDescription• Restore keyboard focus to pad editor after toolbar select changes • Add delegated change handler on #editbar select elements • Deferred focus restoration via setTimeout(0) for plugin handlers • New Playwright regression test for select focus behavior Diagramflowchart LR
A["Toolbar select change event"] --> B["Delegated handler on #editbar"]
B --> C["setTimeout defers focus call"]
C --> D["Plugin handlers complete first"]
D --> E["padeditor.ace.focus() restores caret"]
File Changes1. src/static/js/pad_editbar.ts
|
Code Review by Qodo
1. Flaky async focus test
|
| await hs.evaluate((el: HTMLSelectElement) => { | ||
| el.value = '0'; | ||
| el.dispatchEvent(new Event('change', {bubbles: true})); | ||
| }); | ||
|
|
||
| // After the change, keyboard input should go into the pad, not the | ||
| // toolbar. Write a marker and verify both chunks appear in the pad. | ||
| await page.keyboard.type('after'); | ||
| await page.waitForTimeout(200); | ||
| const bodyText = await padBody.innerText(); |
There was a problem hiding this comment.
1. Flaky async focus test 🐞 Bug ☼ Reliability
The focus restore in pad_editbar.ts is deferred via setTimeout(0), but the Playwright spec types immediately after dispatching the change event, so early keystrokes can be sent before focus returns to the editor. This can cause intermittent failures depending on event-loop timing and test runner load.
Agent Prompt
### Issue description
The test types immediately after triggering the select `change`, but the product code restores focus asynchronously via `setTimeout(0)`. This introduces a race where some keystrokes can be sent before focus is back on the Ace editor.
### Issue Context
The implementation in `pad_editbar.ts` defers the focus call specifically to run after other `change` handlers and after nice-select's own focus behavior.
### Fix Focus Areas
- src/tests/frontend-new/specs/select_focus_restore.spec.ts[23-38]
### Suggested change
After dispatching the `change` event, explicitly wait until the editor has focus before calling `page.keyboard.type('after')`.
Example approaches (pick one):
- `await expect(page.locator('iframe[name="ace_outer"]')).toBeFocused();`
- `await page.waitForFunction(() => (document.activeElement as HTMLElement | null)?.getAttribute?.('name') === 'ace_outer');`
Also replace `waitForTimeout(200)` with an `expect.poll(...)` / `await expect(padBody).toContainText(...)`-style wait to reduce flakiness.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
Summary
Closes #7589. After picking a value from a toolbar `` (ep_headings' style picker is the canonical case), keyboard focus was left on the nice-select wrapper rather than returned to the pad editor. Users had to click back into the pad before typing resumed. Root cause `ToolbarItem.bind()` in `pad_editbar.ts` calls `padeditor.ace.focus()` at the end of `triggerCommand` for its own wired selects — but only selects sitting inside an `` with `data-key`. Plugin-provided selects bypass this: `ep_headings2` wraps `#heading-selection` in `<li id="headings">` with no `data-key`. It binds its own `change` handler directly: `ace.callWithAce(...)` then nothing. Focus stays on the nice-select wrapper; next keystroke is lost. Fix Add a delegated `change` handler on `#editbar select` in `pad_editbar.ts` that calls `padeditor.ace.focus()` after any toolbar select change. Deferred via `setTimeout(0)` so plugin change handlers (bound on the same event) complete first. Redundant but harmless for data-key-wired selects that were already refocused by `triggerCommand`. Tests New Playwright spec `select_focus_restore.spec.ts` that simulates the nice-select option-click (the wrapper dispatches `val(x).trigger('change')` internally — see `static/js/vendors/nice-select.ts`) and asserts typing after the change lands in the pad. Skips if `ep_headings2` isn't installed. Test plan [x] Manual verification: pick a heading style in the toolbar, keep typing — caret stays in the pad. [x] `select_focus_restore.spec.ts` passes on Chromium and Firefox.