Skip to content

fix(ios): keyboard overlay exit + build chunk fix#74

Draft
moodyjmz wants to merge 2 commits into
mainfrom
fix/ios-wkwebview-keyboard
Draft

fix(ios): keyboard overlay exit + build chunk fix#74
moodyjmz wants to merge 2 commits into
mainfrom
fix/ios-wkwebview-keyboard

Conversation

@moodyjmz
Copy link
Copy Markdown
Member

@moodyjmz moodyjmz commented Jun 2, 2026

Summary

Two fixes for the iOS keyboard integration:

  1. Keyboard persists after exiting edit modeturnOnViewerMode() was not disabling the iOS contenteditable overlay, so any subsequent tap refocused it and showed keyboard. Adds preventVirtualKeyboard_Hard() on exit.

  2. Spurious webpack chunk — dynamic import('./less/app.less') was creating a separate JS chunk not referenced by index.html. CSS loading silently broke (no FAB, black document area) when deploying app.js alone. Changed to static import.

⚠️ Known: the new build causes a FAB regression in the Nextcloud iOS app. The deployed bundle is currently the original Docker image build with the iOS fix Python-patched in. See detail section.


Painful level of detail

iOS Keyboard Investigation — EuroOffice Mobile Editor

Status: RESOLVED for browser / partially open for Nextcloud iOS app (2026-06-02)

Keyboard shows, stays up, cursor moves on tap, hides when exiting edit mode — verified in Mobile Safari and desktop Chrome mobile emulation via the same-origin proxy.

Nextcloud iOS app: keyboard fix works, but a separate FAB (edit button) regression exists with the new app.js build — see "Known Regressions" below.


⚠️ Testing Gotchas

iOS Simulator — software keyboard may be off

If the keyboard shows only a thin bar / tick mark (~20px) and immediately dismisses, check whether the Simulator software keyboard is enabled:

  • Hardware → Keyboard → Toggle Software Keyboard (or ⌘K)
  • When the software keyboard is off, iOS shows only the accessory bar. This looks identical to a "keyboard dismissed by UIKit" bug. Several observations during this investigation may have been Simulator keyboard-off behaviour, not real iOS dismissals.
  • Always verify on a physical iPhone before concluding a keyboard bug is real.

Typing slowness — re-test after keyboard fix

Earlier sessions noted "typing slowness" and attributed it to WebSocket round-trips. This should be re-tested: the keyboard was broken during those sessions (repeated dismiss/re-show cycles would cause erratic input processing). With the fix in place the slowness may be gone.


Problem

On iOS (Mobile Safari, WKWebView), tapping in the document canvas in edit mode caused the keyboard to briefly flash then dismiss.

Environment

  • EuroOffice DocumentServer proxied through Nextcloud at http://192.168.178.126:8081/ds/
  • Nextcloud at http://192.168.178.126:8081
  • Same-origin proxy required (see Local Dev Proxy section)
  • Physical iPhone + iOS Simulator

Root Cause (confirmed)

iOS evaluates the gesture at touchend (~300ms). If the tap started on a canvas (not a text input), iOS decides "not a text input tap" → native UIKit keyboard dismissal (empty JS stack trace on blur).

The fix: place a transparent contenteditable div covering the entire canvas area so every user tap lands on a text input element. iOS then treats it as a text-input tap and keeps the keyboard.

Critical detail: opacity:0 causes iOS to exclude elements from hit-testing entirely — iOS still dismisses keyboard even for a tap on an opacity:0 contenteditable. Must use color:transparent; caret-color:transparent; background:transparent (opacity:1) instead.


Solution — sdkjs changes (fix/ios-wkwebview-keyboard branch)

common/browser.js (commit 745db40)

Added AscBrowser.isSafariMobile = (isSafari || isAppleDevices) && isMobile to cover iOS WKWebView apps (Nextcloud iOS) that omit "safari" from their UA.

common/text_input2.js

ElementType switch: For isSafariMobile, use ContentEditableDiv instead of TextArea:

this.ElementType = AscCommon.AscBrowser.isSafariMobile
    ? InputTextElementType.ContentEditableDiv
    : InputTextElementType.TextArea;

CSS: Must use color:transparent NOT opacity:0:

_style = "left:0;top:0;width:100%;height:100%;color:transparent;caret-color:transparent;background:transparent;outline:none;";

DOM placement: Append inside oHtmlParent (= id_main_view) NOT oHtmlParent.parentNode. This makes the overlay a direct sibling of id_viewer_overlay at the correct z-index level.

Touch forwarding: Dispatch PointerEvent (NOT TouchEvent) to id_viewer_overlay. The mobile touch manager registers pointerdown/pointermove/pointerup handlers there (because isUsePointerEvents = true for modern iOS Safari).

showKeyboard(): For iOS ContentEditableDiv, enable overlay via setReadOnlyWrapper(false) and return without calling focusHtmlElement() — programmatic focus is dismissed by UIKit outside a direct user gesture.

setInterfaceEnableKeyEvents guard: Ignore false calls while contenteditable has focus — these come from Framework7 sheet/popover events, not real exit-edit-mode.

setReadOnlyWrapper: For ContentEditableDiv, call HtmlArea.blur() when disabling so iOS hides keyboard on edit mode exit.

move(): Return early for ContentEditableDiv — full-canvas overlay doesn't need repositioning.

common/Scrolls/mobileTouchManagerBase.js

Suppress the bottom formatting sheet when keyboard is visible — the sheet would steal focus and dismiss keyboard:

if (AscCommon.AscBrowser.isSafariMobile && window.visualViewport &&
    window.visualViewport.height < window.innerHeight * 0.8)
{
    return;
}

Key Architecture Discoveries

DOM structure (word editor)

id_panel_top
  id_hor_ruler
  id_main_view                    ← oHtmlParent (InitBrowserInputContext target_id="id_target_cursor")
    id_viewer (canvas, z-index:1)
    id_viewer_overlay (canvas, z-index:2)  ← touch manager registers pointerdown here
    id_target_cursor (div, z-index:4)
    area_id_main (our overlay, z-index:10) ← appended here, set by HtmlPage.initEventsMobile
      area_id_parent (HtmlDiv)
        area_id (contenteditable, our keyboard input)

HtmlPage.initEventsMobile() (isUseOldMobileVersion=true path):

  • Sets area_id_main.style.zIndex = 10 (via TextBoxBackground.HtmlElement.parentNode.parentNode)
  • Calls MobileTouchManager.initEvents("area_id") — registers iScroll on area_id

Event routing

  • mainOnTouchStart / mainOnTouchEnd are registered on id_viewer_overlay via pointerdown/pointerup
  • isUsePointerEvents = true for Safari ≥ 15 → touch manager uses pointer events, not touch events
  • Forwarding must dispatch PointerEvent, not TouchEvent
  • Synthetic PointerEvent has isTrusted=false but no isTrusted check exists in the touch manager

Why cross-origin iframe was a red herring

iOS Safari does restrict keyboard in cross-origin iframes, but that only prevented document loading. The keyboard issue exists on same-origin too because the fundamental problem is the canvas tap not being recognized as a text-input tap.


Local Dev Proxy (same-origin setup)

iOS Safari blocks keyboard in cross-origin iframes. For local dev, proxy EuroOffice through Nextcloud:

File: develop/setup/eo-proxy.conf (Apache config, mounted into nextcloud container)

<Location /ds/>
    ProxyPass http://eo/ upgrade=websocket
    ProxyPassReverse http://eo/
    ProxyPreserveHost Off
    RequestHeader set X-Forwarded-Prefix "/ds"
</Location>

occ settings:

php occ config:app:set eurooffice DocumentServerUrl --value='http://192.168.178.126:8081/ds'
php occ config:app:set eurooffice DocumentServerInternalUrl --value='http://eo'

Persistence: Add to docker-compose.override.yml:

services:
  nextcloud:
    volumes:
      - ./setup/eo-proxy.conf:/etc/apache2/conf-available/eo-proxy.conf:ro

Then in the container: a2enmod proxy proxy_http proxy_wstunnel headers && a2enconf eo-proxy

Note: X-Forwarded-Prefix: /ds is critical — without it, the DS constructs cache/Editor.bin URLs without the /ds prefix and they 404.


Related Repos

iOS app — ios-builds/ios

Context doc: /Users/jamesmanuel/ios-builds/ios/.claude/eurooffice.md

Relevant branches:

  • feature/eurooffice-fixes: double-tap guard (isNavigatingMetadata), spinner-on-exit fix, isScrollEnabled=true, isInspectable=true
  • Private API swizzle (NCViewerDirectEditingKeyboard.swift) — investigated but not needed with this fix

web-apps — DocumentServer/web-apps

EuroOffice-specific additions in apps/documenteditor/mobile/src/:

  • page/main.jsx: showKeyboard() called in turnOffViewerMode() for iOS
  • view/Toolbar.jsx: keyboard toggle button (ctx.HtmlArea.focus() in onTouchEnd) — this is what triggers keyboard on FAB tap since programmatic focus from showKeyboard() alone can't show keyboard

sdkjs — DocumentServer/sdkjs

Branch: fix/ios-wkwebview-keyboard
2 commits ahead of main:

  1. 745db40isSafariMobile detection
  2. (staged/committed after this session) — ContentEditableDiv + proxy setup

Approaches That Failed

  1. isSafariMobile detection only — correct but insufficient (textarea still offscreen)
  2. isScrollEnabled=true — no effect on keyboard
  3. tabIndex=-1 on <a> elements — doesn't prevent iOS auto-focus
  4. MutationObserver to set tabindex — broke styling, performance issues
  5. pointer-events:none on document — worse
  6. focus intercept in document.focus handler — iOS blurs at UIKit level before JS fires
  7. blur re-focus handler — same
  8. opacity:0 contenteditable — iOS excludes from hit-testing, keyboard dismissed
  9. Touch forwarding to word_mobile_element — element doesn't exist in this editor path
  10. TouchEvent forwarding to id_viewer_overlay — touch manager uses pointer events, not touch events

What Remains

  • Typing slowness — needs re-test on physical iPhone with keyboard fix in place; earlier observation may have been an artefact of the broken keyboard state
  • Context menu sheet — appears on 500ms timer after cursor placement; visualViewport guard suppresses it when keyboard is fully up, but may still appear if keyboard is only partially shown at 500ms mark
  • Simulator testing — always verify keyboard behaviour on a physical device; Simulator with software keyboard off looks identical to a real UIKit dismissal bug
  • Cell/Slide editors — ContentEditableDiv keyboard fix is gated to Word only (editorId === Word); cell and slide have different canvas z-index layouts and forwarding targets that have not been validated

Known Regressions (2026-06-02)

web-apps new build breaks FAB in Nextcloud iOS app

Symptom: FAB (edit/pencil button) does not appear in the Nextcloud iOS native app when using the app.js produced by npm run deploy-word from the current source. The FAB IS present in Mobile Safari and desktop Chrome. The original Docker image's app.js (with our Python-patched iOS fix) works correctly everywhere.

What we ruled out:

  • window.native.editorConfig.mobileForceView injection — window.native is undefined in the editor frame; not the cause
  • customization.mobile.forceView from PHP — not set in the PHP app
  • Same-origin proxy causing window.native injection to work — window.native is still undefined with same-origin
  • JavaScript logic difference — all changeViewerMode, isForceView, isViewer, isEdit, p= FAB condition code is byte-for-byte identical between original and new bundle
  • index.html difference — only the CSS hash differs; identical otherwise

Most likely cause: Module execution order. The original bundle has async CSS chunk loading infrastructure (__webpack_require__.e(526)) from the dynamic import('./less/app.less'). Our static import change removed this async runtime. This shifts the order in which webpack modules initialise, causing changeViewerMode(false) to fire at a different point in the lifecycle relative to React's first render. The localStorage debug patch (which would have confirmed the exact call stack) couldn't be applied because the debug bundle caused the iOS app to open documents in the browser instead.

Current mitigation: The deployed app.js is the original Docker image bundle with our iOS fix Python-patched in. This works correctly on all tested platforms. The new build's app.js is not deployed.

Source state: web-apps/apps/documenteditor/mobile/src/app.js has the static import (import './less/app.less'). dist/js/app.js is the working patched bundle. These are intentionally out of sync. dist/ is gitignored so this is safe; the discrepancy is documented here.

To investigate properly:

  1. The localStorage debug approach (patching changeViewerMode to persist call stacks) is the right tool — just needs to be deployed without the JWT error that caused browser redirect
  2. Alternatively: revert app.js back to dynamic import (import('./less/app.less')) and test whether the new build then works in the iOS app — if yes, confirms execution-order hypothesis

Nextcloud iOS app — direct edit mode / FAB visibility

When the Nextcloud iOS app opens a document, it starts with isViewer = true (FAB shown) and the FAB transitions visible briefly, then may be removed if changeViewerMode(false) fires before the first paint. This is distinct from the build regression above and depends on the initialization race condition.

The FAB is shown when: a.isViewer && !u.disabledSettings && !u.disabledControls && !e.users.isDisconnected && d && a.isEdit && (!a.isProtected || ...). All these conditions are satisfied for normal editable documents.

moodyjmz and others added 2 commits June 2, 2026 01:29
When the user taps Done to exit edit mode, the iOS contenteditable
overlay (used for keyboard persistence) was not being deactivated.
Any subsequent tap in the document refocused the contenteditable and
showed the keyboard unexpectedly.

Call preventVirtualKeyboard_Hard() in turnOnViewerMode to disable the
overlay, mirroring the showKeyboard() call in turnOffViewerMode.

Uses window.AscCommon.AscBrowser.isSafariMobile rather than Device.ios
as Device is minified in the production bundle.

Note: dist/js/app.js is gitignored. The deployed bundle was patched
directly from the original production image. A full rebuild via
vendor/framework7-react/build (deploy-word) produces an extra CSS
chunk (apps_..._less.js) that breaks the FAB on all platforms — this
build config issue needs investigation before the source change can
be rebuilt cleanly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
Dynamic import('./less/app.less') caused webpack to emit a separate
apps_documenteditor_mobile_src_less_app_less.js chunk. This chunk is
not referenced by index.html, so deploying app.js without it broke
CSS loading entirely — no FAB, black document area on all platforms.

Change to a static import so MiniCssExtractPlugin extracts the LESS
to the main CSS output file with no separate JS chunk. A clean build
now produces only app.js + css/app.[hash].css, matching the original
deployment structure.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: James Manuel <moodyjmz@users.noreply.github.com>
@moodyjmz moodyjmz self-assigned this Jun 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant