From 702e107972edb682b49bff6c9f3a6ccf26d26073 Mon Sep 17 00:00:00 2001 From: screentinker Date: Fri, 14 Aug 2026 08:59:31 -0500 Subject: [PATCH] Directory search: don't let the platform keyboard cover our own (#275) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 , 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. --- server/routes/widgets.js | 8 ++++++++ server/test/directory-search.test.js | 18 ++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/server/routes/widgets.js b/server/routes/widgets.js index 0e09157..4489377 100644 --- a/server/routes/widgets.js +++ b/server/routes/widgets.js @@ -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 + , 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){ diff --git a/server/test/directory-search.test.js b/server/test/directory-search.test.js index d8bf97c..fbe2a13 100644 --- a/server/test/directory-search.test.js +++ b/server/test/directory-search.test.js @@ -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 , 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');