mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-27 02:42:25 +08:00
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:
@@ -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>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 = () => {};
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -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')!;
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user