mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-01-22 18:30:09 +00:00
fix(ios): prevent keyboard from overlapping inputs
Use Capacitor's native WebView resize mode on iOS instead of CSS-based workarounds. When keyboard appears, the WebView itself shrinks so 100vh automatically fits above the keyboard. - Configure iOS to use `resize: 'native'` (Android keeps `resize: 'body'`) - Add scrollIntoViewIfNeeded() to scroll focused inputs into view - Add proper cleanup for keyboard event listeners - Improve flexbox shrinking in fullscreen markdown dialog
This commit is contained in:
parent
806dbc2dc3
commit
1421151724
3 changed files with 76 additions and 5 deletions
|
|
@ -13,9 +13,8 @@ const config: CapacitorConfig = {
|
|||
smallIcon: 'ic_stat_sp',
|
||||
},
|
||||
Keyboard: {
|
||||
// Resize the web view when keyboard appears (iOS)
|
||||
// Default: resize body (Android)
|
||||
resize: 'body',
|
||||
// Style keyboard accessory bar
|
||||
resizeOnFullScreen: true,
|
||||
},
|
||||
StatusBar: {
|
||||
|
|
@ -33,6 +32,15 @@ const config: CapacitorConfig = {
|
|||
allowsLinkPreview: true,
|
||||
// Scroll behavior
|
||||
scrollEnabled: true,
|
||||
// iOS-specific plugin overrides
|
||||
plugins: {
|
||||
Keyboard: {
|
||||
// Resize the native WebView when keyboard appears
|
||||
// This shrinks the viewport so 100vh/100% automatically fits above keyboard
|
||||
resize: 'native',
|
||||
resizeOnFullScreen: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -31,7 +31,8 @@ import { IS_ANDROID_WEB_VIEW } from '../../util/is-android-web-view';
|
|||
import { androidInterface } from '../../features/android/android-interface';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { CapacitorPlatformService } from '../platform/capacitor-platform.service';
|
||||
import { Keyboard } from '@capacitor/keyboard';
|
||||
import { Keyboard, KeyboardInfo } from '@capacitor/keyboard';
|
||||
import { PluginListenerHandle } from '@capacitor/core';
|
||||
import { StatusBar, Style } from '@capacitor/status-bar';
|
||||
import { LS } from '../persistence/storage-keys.const';
|
||||
import { CustomThemeService } from './custom-theme.service';
|
||||
|
|
@ -59,6 +60,8 @@ export class GlobalThemeService {
|
|||
private _environmentInjector = inject(EnvironmentInjector);
|
||||
private _destroyRef = inject(DestroyRef);
|
||||
private _hasInitialized = false;
|
||||
private _keyboardListenerHandles: PluginListenerHandle[] = [];
|
||||
private _focusinListener: ((event: FocusEvent) => void) | null = null;
|
||||
|
||||
darkMode = signal<DarkModeCfg>(
|
||||
(localStorage.getItem(LS.DARK_MODE) as DarkModeCfg) || 'system',
|
||||
|
|
@ -429,7 +432,7 @@ export class GlobalThemeService {
|
|||
* Adds/removes CSS classes when keyboard shows/hides.
|
||||
*/
|
||||
private _initIOSKeyboardHandling(): void {
|
||||
Keyboard.addListener('keyboardWillShow', (info) => {
|
||||
Keyboard.addListener('keyboardWillShow', (info: KeyboardInfo) => {
|
||||
Log.log('iOS keyboard will show', info);
|
||||
this.document.body.classList.add(BodyClass.isKeyboardVisible);
|
||||
// Set CSS variable for keyboard height to adjust layout
|
||||
|
|
@ -437,15 +440,67 @@ export class GlobalThemeService {
|
|||
'--keyboard-height',
|
||||
`${info.keyboardHeight}px`,
|
||||
);
|
||||
});
|
||||
}).then((handle) => this._keyboardListenerHandles.push(handle));
|
||||
|
||||
// Use keyboardDidShow for scroll (after animation completes)
|
||||
Keyboard.addListener('keyboardDidShow', () => {
|
||||
this._scrollActiveInputIntoView();
|
||||
}).then((handle) => this._keyboardListenerHandles.push(handle));
|
||||
|
||||
Keyboard.addListener('keyboardWillHide', () => {
|
||||
Log.log('iOS keyboard will hide');
|
||||
this.document.body.classList.remove(BodyClass.isKeyboardVisible);
|
||||
this.document.documentElement.style.setProperty('--keyboard-height', '0px');
|
||||
}).then((handle) => this._keyboardListenerHandles.push(handle));
|
||||
|
||||
// Also handle focus changes while keyboard is already visible
|
||||
this._focusinListener = (event: FocusEvent): void => {
|
||||
const target = event.target as HTMLElement;
|
||||
if (
|
||||
this.document.body.classList.contains(BodyClass.isKeyboardVisible) &&
|
||||
this._isInputElement(target)
|
||||
) {
|
||||
// Small delay to let CSS padding apply, validate element is still focused
|
||||
setTimeout(() => {
|
||||
if (this.document.activeElement === target) {
|
||||
this._scrollActiveInputIntoView();
|
||||
}
|
||||
}, 50);
|
||||
}
|
||||
};
|
||||
this.document.addEventListener('focusin', this._focusinListener, { passive: true });
|
||||
|
||||
// Cleanup listeners on destroy
|
||||
this._destroyRef.onDestroy(() => {
|
||||
this._keyboardListenerHandles.forEach((handle) => handle.remove());
|
||||
if (this._focusinListener) {
|
||||
this.document.removeEventListener('focusin', this._focusinListener);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private _isInputElement(el: HTMLElement): boolean {
|
||||
const tagName = el.tagName.toLowerCase();
|
||||
return (
|
||||
tagName === 'input' ||
|
||||
tagName === 'textarea' ||
|
||||
tagName === 'select' ||
|
||||
el.isContentEditable
|
||||
);
|
||||
}
|
||||
|
||||
private _scrollActiveInputIntoView(): void {
|
||||
const activeEl = this.document.activeElement as HTMLElement;
|
||||
if (activeEl && this._isInputElement(activeEl)) {
|
||||
// scrollIntoViewIfNeeded is non-standard but well-supported in iOS WebView
|
||||
if ('scrollIntoViewIfNeeded' in activeEl) {
|
||||
(activeEl as any).scrollIntoViewIfNeeded(true);
|
||||
} else {
|
||||
activeEl.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize iOS status bar styling.
|
||||
* Syncs status bar style with app dark/light mode.
|
||||
|
|
|
|||
|
|
@ -69,11 +69,17 @@
|
|||
background-color: var(--bg-lightest);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
// Allow proper flex shrinking
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
|
||||
.editor-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
// Allow proper flex shrinking
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
|
||||
textarea {
|
||||
flex-grow: 1;
|
||||
|
|
@ -87,6 +93,8 @@
|
|||
display: block;
|
||||
resize: none;
|
||||
font-size: 14px;
|
||||
// Allow proper flex shrinking
|
||||
min-height: 0;
|
||||
|
||||
@include scrollY;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue