From 7fded5a8df1a1f6d0b455267e1f7838a3fae3e03 Mon Sep 17 00:00:00 2001 From: Matthew Reichhoff Date: Sun, 14 Jun 2026 12:24:58 -0400 Subject: [PATCH 1/2] Fix blur event <> resize interaction Android soft keyboard, sigh. --- public/js/modules/main.js | 9 ++----- public/js/modules/search.js | 51 +++++++++++++++++++++++++++---------- 2 files changed, 40 insertions(+), 20 deletions(-) diff --git a/public/js/modules/main.js b/public/js/modules/main.js index 9aa2d36f..fb88516f 100644 --- a/public/js/modules/main.js +++ b/public/js/modules/main.js @@ -101,16 +101,11 @@ Promise.all( search(hanziBox.value); switchToState(stateKeys.main); - // we're about to force a blur, which should hide the soft keyboard on android or ios - // but in some cases, the keyboard hiding triggers a resize, so you get an annoying graph re-render. - // This in very rare cases could cause cause a skipped re-layout on window size change - // but that should be rare. - document.dispatchEvent(new Event('skip-graph-resize')); hanziBox.blur(); }); - // similar to the blur logic above, the soft keyboard will show. Skip the next resize event. - // Same edge case with possible skipped 'real' resizes, but that should be very rare. + // The soft keyboard can trigger a resize when it opens. Skip the next resize event to + // avoid relayouting the graph just because the search input gained focus. hanziBox.addEventListener('focus', function () { document.dispatchEvent(new Event('skip-graph-resize')); }); diff --git a/public/js/modules/search.js b/public/js/modules/search.js index 597b8102..4e17ec1f 100644 --- a/public/js/modules/search.js +++ b/public/js/modules/search.js @@ -244,13 +244,20 @@ function sendDataToWorker() { }); } -let skipBlur = false; +let lastPointerDown = null; -function clearIfOutsideSearchControl(event) { - if (!searchControl.contains(event.target) && !hanziBox.contains(event.target)) { +function isInSearchUi(target) { + return target && ( + searchControl.contains(target) || + searchSuggestionsContainer.contains(target) || + hanziBox.contains(target) + ); +} + +function handleDocumentPointerDown(event) { + lastPointerDown = { target: event.target, time: Date.now() }; + if (!isInSearchUi(event.target)) { clearSuggestions(); - } else { - document.addEventListener('mousedown', clearIfOutsideSearchControl, { once: true }); } } @@ -262,17 +269,35 @@ async function initialize(term, mode) { // it sends, so allow waiting. const ensureLoaded = new Promise(ready => searchSuggestionsWorker.addEventListener("message", ready, { once: true })); hanziBox.addEventListener('input', suggestSearches); - hanziBox.addEventListener('blur', function () { - if (skipBlur) { - skipBlur = false; - document.addEventListener('mousedown', clearIfOutsideSearchControl, { once: true }); + hanziBox.addEventListener('blur', function (event) { + document.dispatchEvent(new Event('skip-graph-resize')); + setTimeout(function () { + const recentPointerTarget = lastPointerDown && Date.now() - lastPointerDown.time < 500 + ? lastPointerDown.target + : null; + if ( + isInSearchUi(event.relatedTarget) || + isInSearchUi(document.activeElement) || + isInSearchUi(recentPointerTarget) + ) { + return; + } + clearSuggestions(); + }, 0); + }); + hanziBox.addEventListener('focus', showControlsIfEligible); + document.addEventListener('pointerdown', handleDocumentPointerDown); + document.addEventListener('touchstart', function (event) { + if (window.PointerEvent) { return; } - clearSuggestions() + handleDocumentPointerDown(event); }); - hanziBox.addEventListener('focus', showControlsIfEligible); - searchControl.addEventListener('mousedown', function () { - skipBlur = true; + document.addEventListener('mousedown', function (event) { + if (window.PointerEvent) { + return; + } + handleDocumentPointerDown(event); }); if (term) { await ensureLoaded; From 9bc019218bd2d1cfbd1f55eddabcfc5ae41cb3f4 Mon Sep 17 00:00:00 2001 From: Matthew Reichhoff Date: Sun, 14 Jun 2026 16:31:36 -0400 Subject: [PATCH 2/2] More on-screen keyboard workarounds --- public/js/modules/graph.js | 10 ++++++++-- public/js/modules/main.js | 6 ------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/public/js/modules/graph.js b/public/js/modules/graph.js index b30c7507..899287a7 100644 --- a/public/js/modules/graph.js +++ b/public/js/modules/graph.js @@ -1,6 +1,7 @@ import { switchToState, stateKeys, diagramKeys, switchDiagramView } from "./ui-orchestrator"; import { getActiveGraph } from "./options"; import { parsePinyin, trimTone, findPinyinRelationships } from "./pronunciation-parser"; +import { hanziBox } from "./dom.js"; import cytoscape from "cytoscape"; import fcose from 'cytoscape-fcose'; @@ -455,16 +456,21 @@ function toggleColorCodeVisibility() { let skipResize = false; let pendingSkipResizeTimeout = null; +function isSearchFocusedOnTouchDevice() { + return document.activeElement === hanziBox && window.matchMedia('(pointer: coarse)').matches; +} + function handleResize() { clearTimeout(pendingResizeTimeout); pendingResizeTimeout = setTimeout(() => { + const shouldSkipLayout = skipResize || isSearchFocusedOnTouchDevice(); // if the window resizes with the graph collapsed, re-expand it // note that switchDiagramView no-ops if we're going main-->main if (!window.matchMedia('(max-width:664px)').matches) { switchDiagramView(diagramKeys.main); } // TODO: probably want a sizeDirty bit we can check for when the graph isn't shown and a resize happens - if (!skipResize && cy && showingGraph) { + if (!shouldSkipLayout && cy && showingGraph) { cy.layout(mode === modes.graph ? layout(cy.nodes().length) : bfsLayout(root)).run(); } skipResize = false; @@ -519,4 +525,4 @@ function initialize() { matchMedia("(prefers-color-scheme: dark)").addEventListener("change", updateColorScheme); } -export { initialize, isInGraph } \ No newline at end of file +export { initialize, isInGraph } diff --git a/public/js/modules/main.js b/public/js/modules/main.js index fb88516f..5e84b89d 100644 --- a/public/js/modules/main.js +++ b/public/js/modules/main.js @@ -104,12 +104,6 @@ Promise.all( hanziBox.blur(); }); - // The soft keyboard can trigger a resize when it opens. Skip the next resize event to - // avoid relayouting the graph just because the search input gained focus. - hanziBox.addEventListener('focus', function () { - document.dispatchEvent(new Event('skip-graph-resize')); - }); - // TODO(refactor): this belongs in explore rather than main? let oldState = readExploreState(); // precedence goes to the direct URL entered first, then to anything hanging around in history, then localstorage.