Browser tab keyboard: open under the field being typed into; Enter hides it

The stage TV is portrait, and the keyboard opened at the bottom of the screen — a
reach to the floor to type into a search box at the top.

- browser-guest-preload.cjs reports the tapped field's position (and which field it
  is) with the tap, and again when focus moves to another field.
- browser-tab.js opens the keyboard directly under that field, centred on it and
  kept on screen; if there is no room under it, just above it. Focus moving to a
  different field (Tab, the next memorize blank) moves the keyboard with it.
  Re-tapping the same field leaves a dragged keyboard where it was.
- The keyboard's Enter submits to the page and then hides the keyboard, so search
  results are not covered. Tapping a field brings it back.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-21 21:17:16 -04:00
co-authored by Claude Opus 5.5
parent 94ec22d466
commit 9d63e00632
2 changed files with 57 additions and 12 deletions
+18 -2
View File
@@ -127,6 +127,19 @@ function restoreOsKeyboard(el) {
if (v === null) el.removeAttribute('inputmode'); else el.setAttribute('inputmode', v);
}
/*
* Where the field is, so the app can open its keyboard right under it: the stage TV is portrait, and a
* keyboard parked at the bottom of a 75" screen is a reach to the floor. `field` tells one field from
* another, so focus moving to a different one (Tab, the next memorize blank) can move the keyboard too.
*/
let fieldSeq = 0;
const fieldIds = new WeakMap();
function fieldInfo(el) {
if (!fieldIds.has(el)) fieldIds.set(el, ++fieldSeq);
const r = el.getBoundingClientRect();
return { field: fieldIds.get(el), rect: { x: Math.round(r.left), y: Math.round(r.top), w: Math.round(r.width), h: Math.round(r.height) } };
}
/*
* Only a TAP raises the keyboard. A page that focuses its own search box as it loads must not throw a
* keyboard over the stage while nobody is touching it; focus moving between fields (Tab, the next
@@ -137,10 +150,13 @@ window.addEventListener('pointerdown', (e) => {
const el = editableFrom(e.target);
if (!el) return;
quietOsKeyboard(el);
send('bibleapp:editable', { on: true, tap: true });
send('bibleapp:editable', Object.assign({ on: true, tap: true }, fieldInfo(el)));
}, true);
document.addEventListener('focusin', (e) => {
send('bibleapp:editable', { on: isEditable(e.target), tap: false });
const el = e.target;
if (!isEditable(el)) { send('bibleapp:editable', { on: false, tap: false }); return; }
// Measured a frame later: focusing can scroll the field into view first.
requestAnimationFrame(() => send('bibleapp:editable', Object.assign({ on: true, tap: false }, fieldInfo(el))));
}, true);
document.addEventListener('focusout', (e) => {
restoreOsKeyboard(e.target);