fix(voice): move focus with the voice panel that chat asks for

Asking from the chat composer to open Voice Settings or Voice Preview
closed chat around the focused input, leaving focus on a hidden control.
Focus held in chat now moves to the requested view's tab, as the panel's
own tabs move it; focus anywhere else stays put.
This commit is contained in:
Sameh Khamis committed 2026-11-03 16:04:54 -08:00
1 parent 66fb4b6aca
commit 6ce9498d18
2 files changed
+62 -3

No files matched your search

+46 -2
View File
@@ -512,7 +512,13 @@ test('the voice inactivity timeout never ends a typed chat session', async (t) =
async function directActions(
t,
{ storage = null, delayExit = false, exposePanel = true, typed = false } = {},
{
storage = null,
delayExit = false,
exposePanel = true,
typed = false,
view = false,
} = {},
) {
let f, releaseExit;
const state = { active: true, exits: 0, visual: [] };
@@ -544,7 +550,7 @@ async function directActions(
readVoicePanelOperation: () =>
exposePanel ? f?.commands.captureVoicePanelOperation() : undefined,
});
f = mounted(t, { native: true, runner, storage });
f = mounted(t, { native: true, runner, storage, view });
// A typed chat session starts from chat's first send instead.
if (typed) {
const adapter = f.instances[0];
@@ -671,6 +677,44 @@ for (const minimized of [false, true])
assert.equal(adapter.stops, 0);
});
for (const [panelId, target] of [
['voice-settings', 'gev-voice-settings-tab'],
['voice-preview', 'gev-voice-preview-tab'],
])
test(`${panelId} asked for from the chat composer takes focus with it`, async (t) => {
const f = await directActions(t, { typed: true, view: true });
await f.settle();
$('#gev-chat-button').click();
await f.commands.session.start({ textOnly: true });
await f.settle();
$('#gev-chat-input').focus();
assert.equal(active(), 'gev-chat-input');
const result = await f.run('set_panel_open', { panelId, open: true });
assert.equal(result.ok, true);
await f.settle();
assert.equal($('#gev-chat-panel').hidden, true);
assert.equal(active(), target);
assert.equal(f.ui.visible('voicePanel'), true);
});
test('a voice panel asked for from chat leaves unrelated focus alone', async (t) => {
const f = await directActions(t, { typed: true, view: true });
await f.settle();
$('#gev-chat-button').click();
await f.commands.session.start({ textOnly: true });
await f.settle();
const elsewhere = globalThis.document.body.appendChild(
globalThis.document.createElement('button'),
);
elsewhere.id = 'elsewhere';
t.after(() => elsewhere.remove());
elsewhere.focus();
await f.run('set_panel_open', { panelId: 'voice-settings', open: true });
await f.settle();
assert.equal($('#gev-chat-panel').hidden, true);
assert.equal(active(), 'elsewhere');
});
test('canonical exit reveals requested voice UI; newer manual choice or Stop defeats pending reveal', async (t) => {
const f = await directActions(t, { delayExit: true });
const pending = f.run('control_cockpit', {
+16 -1
View File
@@ -160,13 +160,28 @@ export function createVoiceCommands({
// An explicit, successful show hands the frame from chat to the voice
// panel, which chat's open state hides. Closing chat keeps its typed
// session, running turn and messages; reopening chat restores them.
// Focus held in chat (its composer, or chat Settings' Back) would be
// left on a hidden control, so it moves to the panel's view, as the
// panel's own tabs do; focus anywhere else stays where it is.
controls.captureVoicePanelOperation = () => {
const operation = panelMount.captureOperation();
return {
...operation,
show(view, open = true) {
const focused = globalThis.document?.activeElement;
const inChat =
Boolean(focused) &&
(ui.element('chatPanel')?.contains?.(focused) ||
focused === ui.element('chatBack'));
const result = operation.show(view, open);
if (open && result.ok) chat?.close();
if (!open || !result.ok) return result;
chat?.close();
if (inChat)
void ui.focus(
result.view === 'settings'
? ['settingsTab', 'voiceBack', 'settingsClose']
: ['previewTab', 'settingsTab', 'settingsClose'],
);
return result;
},
};