App-Ansicht iOS: Chat-Eingabe springt nicht mehr weg, kein Zoom beim Tippen

Auf dem iPhone sprang die Seite beim Tippen ins Chatfeld weg (nur die
Fusszeile war zu sehen), zoomte hinein und liess sich danach seitlich
schieben. Beides sind bekannte Safari-Effekte:

- window.scrollTo(0, 0) bei offener Tastatur kaempft gegen Safaris eigenes
  Hochschieben des Feldes und ist entfernt, ebenso das Verkleinern des
  Chats auf die sichtbare Hoehe, das das schon hochgeschobene Feld wieder
  aus dem Bild rutschen liess. Der Chat nutzt jetzt 100dvh: auf Android
  folgt das der Tastatur (interactive-widget=resizes-content), auf iOS
  bleibt die Hoehe stabil.
- Auf iOS aendert sich beim Fokus keine Hoehe mehr (die Tab-Leiste wird
  nur optisch ausgeblendet). Ist die Chat-Eingabe nach der
  Tastatur-Animation doch verdeckt, wird sie gezielt nachgeschoben
  (gemessen am visualViewport).
- iOS zoomt beim Fokus, wenn die Schrift effektiv unter 16 px liegt, z. B.
  bei Safari-Seitenzoom unter 100 %. maximum-scale=1 nur auf iOS verhindert
  diesen automatischen Zoom; mit zwei Fingern zoomen bleibt moeglich
  (WebKit ignoriert die Sperre dafuer seit iOS 10). Android bleibt
  unveraendert, dort wuerde es das Zoomen sperren.

Im Ruhezustand pixelgleich zu main (28 Ansichten).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-09-26 15:17:36 +00:00
1 parent 163ab88c35
commit d5bfd1be3d
2 files changed
+46 -14

No files matched your search

+38 -10
View File
@@ -1379,6 +1379,22 @@ function onAppViewShown(name) {
updateAppBar()
}
/* Chat-Eingabe ueber der Tastatur halten: nur nachschieben, wenn sie wirklich verdeckt ist, gemessen am
sichtbaren Bereich (visualViewport). Die Seite nicht nach oben pinnen – das kaempft auf iOS gegen
Safaris eigenes Hochschieben und laesst die Eingabe aus dem Bild springen. */
function revealChatComposer() {
const composer = document.getElementById('chat-composer')
const vv = window.visualViewport
if (!composer || !vv || !isAppLayout() || !composer.contains(document.activeElement)) return
const rect = composer.getBoundingClientRect()
const visibleTop = vv.offsetTop
const visibleBottom = vv.offsetTop + vv.height
let delta = 0
if (rect.bottom > visibleBottom - 8) delta = rect.bottom - visibleBottom + 8
else if (rect.top < visibleTop + 8) delta = rect.top - visibleTop - 8
if (Number.isFinite(delta) && Math.abs(delta) > 1) window.scrollBy(0, delta)
}
/* Wartungs-Tab: Hinweis bei fehlgeschlagener Zahlung oder offener Rückfrage zur Gutschrift */
function updateWartungBadge() {
const card = document.getElementById('abo-card')
@@ -1563,9 +1579,16 @@ function initAppShell() {
})
}, { passive: true })
// Tastatur offen (Touch): Tab-Leiste ausblenden, damit Eingabe und Chat Platz haben
// Tastatur offen (Touch): Tab-Leiste ausblenden. Auf Android schrumpft die Seite mit der Tastatur
// (interactive-widget=resizes-content), dort nutzt der Chat den frei werdenden Platz. Auf iOS liegt
// die Tastatur ueber der Seite und Safari schiebt das Feld selbst hoch – dort darf sich beim Fokus
// keine Hoehe aendern (kb-ios, siehe kundenbereich.css), sonst rutscht das Feld wieder aus dem Bild.
if (isIosDevice()) document.body.classList.add('kb-ios')
document.addEventListener('focusin', (ev) => {
if (coarsePointer.matches && ev.target.matches?.(TEXT_INPUT)) document.body.classList.add('kb-typing')
if (!coarsePointer.matches || !ev.target.matches?.(TEXT_INPUT)) return
document.body.classList.add('kb-typing')
// Nach der Tastatur-Animation (iOS ca. 350 ms) pruefen, ob die Chat-Eingabe sichtbar ist
if (ev.target.closest('#chat-composer')) setTimeout(revealChatComposer, 350)
})
document.addEventListener('focusout', () => {
setTimeout(() => {
@@ -1573,7 +1596,6 @@ function initAppShell() {
}, 80)
})
// Sichtbare Hoehe (ohne Tastatur) fuer den Chat.
// Schliesst die Tastatur, ohne dass das Feld den Fokus verliert (Android-Zurueck-Geste),
// kommt die Tab-Leiste zurueck; tippt man erneut hinein, verschwindet sie wieder.
const vv = window.visualViewport
@@ -1581,8 +1603,7 @@ function initAppShell() {
let fullHeight = vv.height
let width = vv.width
let keyboardOpen = false
const syncHeight = () => {
document.documentElement.style.setProperty('--kb-vvh', `${Math.round(vv.height)}px`)
vv.addEventListener('resize', () => {
if (Math.abs(vv.width - width) > 1) {
width = vv.width
fullHeight = vv.height
@@ -1594,11 +1615,8 @@ function initAppShell() {
if (!open) document.body.classList.remove('kb-typing')
else if (coarsePointer.matches && document.activeElement?.matches?.(TEXT_INPUT)) document.body.classList.add('kb-typing')
}
// Chat mit Tastatur: iOS schiebt die Seite hoch; App-Leiste, Verlauf und Eingabe passen genau hinein
if (keyboardOpen && appShell.view === 'chat' && isAppLayout()) window.scrollTo(0, 0)
}
vv.addEventListener('resize', syncHeight)
syncHeight()
if (open) requestAnimationFrame(() => requestAnimationFrame(revealChatComposer))
})
}
// Wird der Verlauf kleiner (Tastatur, Tab-Leiste), bleibt die neueste Nachricht sichtbar – sofern
// man unten war (data-stick setzt ChatComposer.trackScroll)
@@ -1650,6 +1668,16 @@ function initAppShell() {
if (isAppLayout() && getQueryParam('abo') === 'success') showView('wartung')
}
// iOS zoomt beim Tippen in ein Feld hinein, wenn dessen Schrift effektiv unter 16 px liegt – z. B. bei
// verkleinertem Seitenzoom in Safari, obwohl die Felder 16 px haben. maximum-scale=1 verhindert nur
// diesen automatischen Zoom; mit zwei Fingern zoomen bleibt auf iOS moeglich (WebKit ignoriert die
// Sperre dafuer seit iOS 10). Nur iOS: Android wuerde damit jedes Zoomen sperren.
function preventIosFocusZoom() {
const meta = document.querySelector('meta[name="viewport"]')
if (meta && isIosDevice() && !/maximum-scale/.test(meta.content)) meta.content += ', maximum-scale=1'
}
preventIosFocusZoom()
// App-Installation (Homescreen) und Offline-Seite; zwischengespeichert wird nur die Offline-Seite (sw.js)
if ('serviceWorker' in navigator && window.isSecureContext) {
window.addEventListener('load', () => {
+8 -4
View File
@@ -441,7 +441,8 @@ button.kb-avatar:disabled { cursor: default; box-shadow: none; }
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: transparent;
}
.kb-page.kb-typing { --kb-tabbar-h: 0px; }
/* Tastatur offen: auf Android wird der Platz der Tab-Leiste frei; auf iOS keine Hoehenaenderung (app.js) */
.kb-page.kb-typing:not(.kb-ios) { --kb-tabbar-h: 0px; }
html.kb-sheet-open { overflow: hidden; }
.kb-desk-only { display: none; }
.kb-app-only { display: inline; }
@@ -588,10 +589,12 @@ button.kb-avatar:disabled { cursor: default; box-shadow: none; }
.kb-invoices .inv-actions .btn-link:first-child { background: rgba(157, 61, 254, 0.18); border-color: var(--kb-purple-line); }
.kb-invoices .inv-actions .btn-link:first-child:not(:last-child) { flex: 1.7; }
/* Chat: fuellt den Platz zwischen App-Leiste und Tab-Leiste (bzw. Tastatur) */
/* Chat: fuellt den Platz zwischen App-Leiste und Tab-Leiste. 100dvh folgt auf Android der Tastatur
(interactive-widget=resizes-content); auf iOS bleibt die Hoehe, Safari schiebt das Feld selbst hoch */
#view-chat.active {
display: flex; flex-direction: column;
height: calc(var(--kb-vvh, 100dvh) - var(--kb-appbar-h) - var(--kb-tabbar-h) - 20px);
height: calc(100vh - var(--kb-appbar-h) - var(--kb-tabbar-h) - 20px);
height: calc(100dvh - var(--kb-appbar-h) - var(--kb-tabbar-h) - 20px);
}
#view-chat .kb-head { flex: none; margin-bottom: 12px; }
#view-chat .chat-box { flex: 1 1 auto; min-height: 0; height: auto; padding: 10px; border-radius: 20px; }
@@ -631,7 +634,8 @@ button.kb-avatar:disabled { cursor: default; box-shadow: none; }
.kb-sheet { inset: auto 0 0 0; width: 100%; max-width: none; margin: 0; }
.kb-sheet::backdrop { background: rgba(0, 0, 0, 0.55); }
.kb-sheet-panel {
max-height: calc(var(--kb-vvh, 100dvh) - env(safe-area-inset-top) - 24px); overscroll-behavior: contain;
max-height: calc(100vh - env(safe-area-inset-top) - 24px); max-height: calc(100dvh - env(safe-area-inset-top) - 24px);
overscroll-behavior: contain;
padding: 8px max(18px, env(safe-area-inset-right)) calc(18px + env(safe-area-inset-bottom)) max(18px, env(safe-area-inset-left));
border-radius: 24px 24px 0 0; border-bottom: none; box-shadow: 0 -12px 40px rgba(0, 0, 0, 0.5);
transition: transform 0.24s ease-in;