Directory search: don't let the platform keyboard cover our own (#275)

The directory-search widget draws its own on-screen keyboard, on by default, sized
and themed to the panel. On Android it was never visible: the page autofocuses a
real <input>, which is the signal to raise the system IME, and that lands over the
bottom of the screen — exactly where our keyboard is.

So a directory panel showed Google's keyboard instead of the one the widget ships:
split across a 1920x1080 screen, with mic, GIF, emoji, clipboard and a settings key
that opens Google's own UI on a kiosk. On the panel that turned this up, the system
keyboard WAS voice input — the only enabled IME was Google's voice IME, so touching
the search box opened a microphone, on a wall-mounted tenant directory.

When we draw a keyboard, the input now carries inputmode="none", so the platform
leaves its keyboard down. The buttons write input.value directly, so nothing about
typing changes. Browsers that don't know inputmode ignore it, which is the right
fallback — a desktop preview behaves exactly as before.

Gated on the flag, not applied to the markup: with show_onscreen_keyboard off there
is nothing to cover, and the platform keyboard is the only way left to type.

Verified by removing the line and watching the new guard fail. 1669/1669 pass.
This commit is contained in:
screentinker 2026-08-14 08:59:31 -05:00 committed by GitHub
parent 04a2ad99d1
commit 702e107972
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 26 additions and 0 deletions

View file

@ -1206,6 +1206,14 @@ function renderDirectorySearch(c) {
// ----- on-screen keyboard (drives the same filter path as typing) -----
if (cfg.show_onscreen_keyboard) {
/* Tell the platform not to raise ITS keyboard for this field. We autofocus a real
<input>, which on Android is the signal to throw the system IME over the bottom of
the screen - directly on top of the keyboard we draw below, so a directory panel
showed Google's keyboard (mic, GIF and emoji keys included) and never showed its
own. The buttons write input.value directly, so suppressing the platform keyboard
costs nothing here. Ignored by browsers that don't know inputmode, which is the
right fallback: a desktop preview keeps behaving exactly as before. */
input.setAttribute('inputmode', 'none');
var kb = document.getElementById('keyboard');
function press(ch) { input.value += ch; try { input.focus(); } catch(e){} onInput(); }
['1234567890','qwertyuiop','asdfghjkl','zxcvbnm'].forEach(function(r){

View file

@ -59,6 +59,24 @@ test('show_onscreen_keyboard flag is carried into the page config', async () =>
assert.ok(html.includes('"show_onscreen_keyboard":false'), 'keyboard flag inlined (page hides the keyboard when false)');
});
test('the built-in keyboard suppresses the platform one', async () => {
// The page autofocuses a real <input>, which on Android raises the system IME over the
// bottom of the screen - covering the keyboard this widget draws itself. A panel showed
// Gboard, complete with a mic key, and never showed its own keyboard.
seed('search_kb_on', 'directory-search', { source_widget_id: 'board1', show_onscreen_keyboard: true });
const { html } = await fetchRender('search_kb_on');
assert.ok(/setAttribute\(\s*'inputmode'\s*,\s*'none'\s*\)/.test(html),
'page tells the platform not to raise its own keyboard');
// Only when we are drawing one. With the built-in keyboard off there is nothing to cover,
// and the platform keyboard is the only way left to type.
seed('search_kb_off2', 'directory-search', { source_widget_id: 'board1', show_onscreen_keyboard: false });
const off = await fetchRender('search_kb_off2');
assert.ok(off.html.includes('"show_onscreen_keyboard":false'), 'flag inlined as false');
assert.ok(!/inputmode="none"/.test(off.html),
'the input is not statically marked inputmode=none - suppression is gated on the flag at runtime');
});
test('missing source -> friendly fallback page, not a 500', async () => {
seed('search_missing', 'directory-search', { source_widget_id: 'does-not-exist' });
const { status, html } = await fetchRender('search_missing');