fix(ios): can not open keyboard in editor (#11401)

Close [BS-2917](https://linear.app/affine-design/issue/BS-2917/【移动端】ios-唤起键盘的edge-case)

This PR fixes an issue where the keyboard cannot be re-triggered on iOS devices after the keyboard toolbar is hidden or executing some actions in keyboard toolbar.

Key changes:
- Preserve and restore the initial input mode when keyboard toolbar shows/hides
- Improve virtual keyboard service interface to better handle keyboard state
- Add proper cleanup of input mode state in component lifecycle
This commit is contained in:
L-Sun
2025-04-03 01:51:56 +00:00
parent 2026f12daa
commit 5109ceccec
6 changed files with 71 additions and 42 deletions

View File

@@ -1,9 +1,13 @@
import { getDocTitleByEditorHost } from '@blocksuite/affine-fragment-doc-title'; import { getDocTitleByEditorHost } from '@blocksuite/affine-fragment-doc-title';
import type { RootBlockModel } from '@blocksuite/affine-model'; import type { RootBlockModel } from '@blocksuite/affine-model';
import { FeatureFlagService } from '@blocksuite/affine-shared/services'; import {
FeatureFlagService,
VirtualKeyboardProvider,
type VirtualKeyboardProviderWithAction,
} from '@blocksuite/affine-shared/services';
import { IS_MOBILE } from '@blocksuite/global/env'; import { IS_MOBILE } from '@blocksuite/global/env';
import { WidgetComponent } from '@blocksuite/std'; import { WidgetComponent } from '@blocksuite/std';
import { signal } from '@preact/signals-core'; import { effect, signal } from '@preact/signals-core';
import { html, nothing } from 'lit'; import { html, nothing } from 'lit';
import type { PageRootBlockComponent } from '../../page/page-root-block.js'; import type { PageRootBlockComponent } from '../../page/page-root-block.js';
@@ -22,6 +26,7 @@ export class AffineKeyboardToolbarWidget extends WidgetComponent<
if (blur) { if (blur) {
if (document.activeElement === this._docTitle?.inlineEditorContainer) { if (document.activeElement === this._docTitle?.inlineEditorContainer) {
this._docTitle?.inlineEditor?.setInlineRange(null); this._docTitle?.inlineEditor?.setInlineRange(null);
this._docTitle?.inlineEditor?.eventSource?.blur();
} else if (document.activeElement === this.block?.rootComponent) { } else if (document.activeElement === this.block?.rootComponent) {
this.std.selection.clear(); this.std.selection.clear();
} }
@@ -31,6 +36,27 @@ export class AffineKeyboardToolbarWidget extends WidgetComponent<
private readonly _show$ = signal(false); private readonly _show$ = signal(false);
private _initialInputMode: string = '';
get keyboard(): VirtualKeyboardProviderWithAction {
return {
// fallback keyboard actions
show: () => {
const rootComponent = this.block?.rootComponent;
if (rootComponent && rootComponent === document.activeElement) {
rootComponent.inputMode = this._initialInputMode;
}
},
hide: () => {
const rootComponent = this.block?.rootComponent;
if (rootComponent && rootComponent === document.activeElement) {
rootComponent.inputMode = 'none';
}
},
...this.std.get(VirtualKeyboardProvider),
};
}
private get _docTitle() { private get _docTitle() {
return getDocTitleByEditorHost(this.std.host); return getDocTitleByEditorHost(this.std.host);
} }
@@ -48,12 +74,25 @@ export class AffineKeyboardToolbarWidget extends WidgetComponent<
const rootComponent = this.block?.rootComponent; const rootComponent = this.block?.rootComponent;
if (rootComponent) { if (rootComponent) {
this._initialInputMode = rootComponent.inputMode;
this.disposables.add(() => {
rootComponent.inputMode = this._initialInputMode;
});
this.disposables.addFromEvent(rootComponent, 'focus', () => { this.disposables.addFromEvent(rootComponent, 'focus', () => {
this._show$.value = true; this._show$.value = true;
}); });
this.disposables.addFromEvent(rootComponent, 'blur', () => { this.disposables.addFromEvent(rootComponent, 'blur', () => {
this._show$.value = false; this._show$.value = false;
}); });
this.disposables.add(
effect(() => {
// recover input mode when keyboard toolbar is hidden
if (!this._show$.value) {
rootComponent.inputMode = this._initialInputMode;
}
})
);
} }
if (this._docTitle) { if (this._docTitle) {
@@ -84,10 +123,11 @@ export class AffineKeyboardToolbarWidget extends WidgetComponent<
return html`<blocksuite-portal return html`<blocksuite-portal
.shadowDom=${false} .shadowDom=${false}
.template=${html`<affine-keyboard-toolbar .template=${html`<affine-keyboard-toolbar
.keyboard=${this.keyboard}
.config=${this.config} .config=${this.config}
.rootComponent=${this.block.rootComponent} .rootComponent=${this.block.rootComponent}
.close=${this._close} .close=${this._close}
></affine-keyboard-toolbar> `} ></affine-keyboard-toolbar>`}
></blocksuite-portal>`; ></blocksuite-portal>`;
} }
} }

View File

@@ -1,5 +1,5 @@
import { getSelectedModelsCommand } from '@blocksuite/affine-shared/commands'; import { getSelectedModelsCommand } from '@blocksuite/affine-shared/commands';
import { VirtualKeyboardProvider } from '@blocksuite/affine-shared/services'; import { type VirtualKeyboardProviderWithAction } from '@blocksuite/affine-shared/services';
import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit'; import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit';
import { ArrowLeftBigIcon, KeyboardIcon } from '@blocksuite/icons/lit'; import { ArrowLeftBigIcon, KeyboardIcon } from '@blocksuite/icons/lit';
import { import {
@@ -50,10 +50,6 @@ export class AffineKeyboardToolbar extends SignalWatcher(
return this.rootComponent.std; return this.rootComponent.std;
} }
get keyboard() {
return this._context.std.get(VirtualKeyboardProvider);
}
get panelOpened() { get panelOpened() {
return this._currentPanelIndex$.value !== -1; return this._currentPanelIndex$.value !== -1;
} }
@@ -324,6 +320,9 @@ export class AffineKeyboardToolbar extends SignalWatcher(
`; `;
} }
@property({ attribute: false })
accessor keyboard!: VirtualKeyboardProviderWithAction;
@property({ attribute: false }) @property({ attribute: false })
accessor close: (blur: boolean) => void = () => {}; accessor close: (blur: boolean) => void = () => {};

View File

@@ -230,9 +230,6 @@ export class AffineMobileLinkedDocMenu extends SignalWatcher(
} }
override firstUpdated() { override firstUpdated() {
if (!this.keyboard.visible$.value) {
this.keyboard.show();
}
this._scrollInputToTop(); this._scrollInputToTop();
} }

View File

@@ -2,11 +2,16 @@ import { createIdentifier } from '@blocksuite/global/di';
import type { ReadonlySignal } from '@preact/signals-core'; import type { ReadonlySignal } from '@preact/signals-core';
export interface VirtualKeyboardProvider { export interface VirtualKeyboardProvider {
show: () => void;
hide: () => void;
readonly visible$: ReadonlySignal<boolean>; readonly visible$: ReadonlySignal<boolean>;
readonly height$: ReadonlySignal<number>; readonly height$: ReadonlySignal<number>;
} }
export const VirtualKeyboardProvider = export interface VirtualKeyboardProviderWithAction
createIdentifier<VirtualKeyboardProvider>('VirtualKeyboardProvider'); extends VirtualKeyboardProvider {
show: () => void;
hide: () => void;
}
export const VirtualKeyboardProvider = createIdentifier<
VirtualKeyboardProvider | VirtualKeyboardProviderWithAction
>('VirtualKeyboardProvider');

View File

@@ -12,9 +12,6 @@ import { NbStoreNativeDBApis } from './plugins/nbstore';
bindNativeDBApis(NbStoreNativeDBApis); bindNativeDBApis(NbStoreNativeDBApis);
// TODO(@L-Sun) Uncomment this when the `show` method implement by `@capacitor/keyboard` in ios
// import './virtual-keyboard';
function mountApp() { function mountApp() {
// oxlint-disable-next-line @typescript-eslint/no-non-null-assertion // oxlint-disable-next-line @typescript-eslint/no-non-null-assertion
const root = document.getElementById('app')!; const root = document.getElementById('app')!;

View File

@@ -10,9 +10,9 @@ import type {
} from '@blocksuite/affine/global/di'; } from '@blocksuite/affine/global/di';
import { DisposableGroup } from '@blocksuite/affine/global/disposable'; import { DisposableGroup } from '@blocksuite/affine/global/disposable';
import { import {
DocModeProvider,
FeatureFlagService, FeatureFlagService,
VirtualKeyboardProvider as BSVirtualKeyboardProvider, VirtualKeyboardProvider as BSVirtualKeyboardProvider,
type VirtualKeyboardProviderWithAction,
} from '@blocksuite/affine/shared/services'; } from '@blocksuite/affine/shared/services';
import type { SpecBuilder } from '@blocksuite/affine/shared/utils'; import type { SpecBuilder } from '@blocksuite/affine/shared/utils';
import { import {
@@ -69,35 +69,12 @@ function KeyboardToolbarExtension(framework: FrameworkProvider): ExtensionType {
private readonly _disposables = new DisposableGroup(); private readonly _disposables = new DisposableGroup();
private get _rootContentEditable() {
const editorMode = this.std.get(DocModeProvider).getEditorMode();
if (editorMode !== 'page') return null;
if (!this.std.host.doc.root) return;
return this.std.view.getBlock(this.std.host.doc.root.id);
}
// eslint-disable-next-line rxjs/finnish // eslint-disable-next-line rxjs/finnish
readonly visible$ = signal(false); readonly visible$ = signal(false);
// eslint-disable-next-line rxjs/finnish // eslint-disable-next-line rxjs/finnish
readonly height$ = signal(0); readonly height$ = signal(0);
show() {
if ('show' in affineVirtualKeyboardProvider) {
affineVirtualKeyboardProvider.show();
} else if (this._rootContentEditable) {
this._rootContentEditable.inputMode = '';
}
}
hide() {
if ('hide' in affineVirtualKeyboardProvider) {
affineVirtualKeyboardProvider.hide();
} else if (this._rootContentEditable) {
this._rootContentEditable.inputMode = 'none';
}
}
static override setup(di: Container) { static override setup(di: Container) {
super.setup(di); super.setup(di);
di.addImpl(BSVirtualKeyboardProvider, provider => { di.addImpl(BSVirtualKeyboardProvider, provider => {
@@ -125,6 +102,20 @@ function KeyboardToolbarExtension(framework: FrameworkProvider): ExtensionType {
} }
} }
if ('show' in affineVirtualKeyboardProvider) {
return class
extends BSVirtualKeyboardService
implements VirtualKeyboardProviderWithAction
{
show() {
affineVirtualKeyboardProvider.show();
}
hide() {
affineVirtualKeyboardProvider.hide();
}
};
}
return BSVirtualKeyboardService; return BSVirtualKeyboardService;
} }