From 1973cea035b3acbba057ec44f4062743255dddd5 Mon Sep 17 00:00:00 2001 From: L-Sun Date: Fri, 1 Nov 2024 05:48:30 +0000 Subject: [PATCH] fix(mobile): incorrect height of keyboard toolbar in ios (#8653) ### What Changes: - Fix keyboard height calculation with the `KeyboardToolBarConfig.useScreenHeight = true`. More detials in: https://github.com/toeverything/blocksuite/pull/8645 - Add safe bottom padding for shrinked keyboard toolbar - Add `VirtualKeyboard` polyfill. Close [AF-1573](https://linear.app/affine-design/issue/AF-1573/virtualkeyboard-polyfill-with-capacitor) - Since the `@capacitor/keyboard` dose not implement the `Keyboard.show()` method, this polyfill is not enabled now. --- packages/frontend/apps/ios/App/Podfile | 1 + packages/frontend/apps/ios/App/Podfile.lock | 10 +- .../frontend/apps/ios/capacitor.config.ts | 4 + packages/frontend/apps/ios/package.json | 1 + packages/frontend/apps/ios/src/setup.ts | 3 + .../frontend/apps/ios/src/virtual-keyboard.ts | 152 ++++++++++++++++++ .../specs/custom/root-block.ts | 11 +- .../specs/custom/widgets/keyboard-toolbar.ts | 11 ++ yarn.lock | 10 ++ 9 files changed, 198 insertions(+), 5 deletions(-) create mode 100644 packages/frontend/apps/ios/src/virtual-keyboard.ts create mode 100644 packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/custom/widgets/keyboard-toolbar.ts diff --git a/packages/frontend/apps/ios/App/Podfile b/packages/frontend/apps/ios/App/Podfile index 3dd2bb02a..804c58d3b 100644 --- a/packages/frontend/apps/ios/App/Podfile +++ b/packages/frontend/apps/ios/App/Podfile @@ -13,6 +13,7 @@ def capacitor_pods pod 'CapacitorCordova', :path => '../../../../../node_modules/@capacitor/ios' pod 'CapacitorApp', :path => '../../../../../node_modules/@capacitor/app' pod 'CapacitorBrowser', :path => '../../../../../node_modules/@capacitor/browser' + pod 'CapacitorKeyboard', :path => '../../../../../node_modules/@capacitor/keyboard' end target 'App' do diff --git a/packages/frontend/apps/ios/App/Podfile.lock b/packages/frontend/apps/ios/App/Podfile.lock index b142a3306..5a7ee08b8 100644 --- a/packages/frontend/apps/ios/App/Podfile.lock +++ b/packages/frontend/apps/ios/App/Podfile.lock @@ -6,6 +6,8 @@ PODS: - CapacitorBrowser (6.0.3): - Capacitor - CapacitorCordova (6.1.2) + - CapacitorKeyboard (6.0.2): + - Capacitor - CryptoSwift (1.8.3) DEPENDENCIES: @@ -13,6 +15,7 @@ DEPENDENCIES: - "CapacitorApp (from `../../../../../node_modules/@capacitor/app`)" - "CapacitorBrowser (from `../../../../../node_modules/@capacitor/browser`)" - "CapacitorCordova (from `../../../../../node_modules/@capacitor/ios`)" + - "CapacitorKeyboard (from `../../../../../node_modules/@capacitor/keyboard`)" - CryptoSwift (~> 1.8.3) SPEC REPOS: @@ -28,14 +31,17 @@ EXTERNAL SOURCES: :path: "../../../../../node_modules/@capacitor/browser" CapacitorCordova: :path: "../../../../../node_modules/@capacitor/ios" + CapacitorKeyboard: + :path: "../../../../../node_modules/@capacitor/keyboard" SPEC CHECKSUMS: Capacitor: 679f9673fdf30597493a6362a5d5bf233d46abc2 CapacitorApp: 0bc633b4eae40a1f32cd2834788fad3bc42da6a1 CapacitorBrowser: aab1ed943b01c0365c4810538a8b3477e2d9f72e CapacitorCordova: f48c89f96c319101cd2f0ce8a2b7449b5fb8b3dd + CapacitorKeyboard: 2700f9b18687be021e28b5a09b59eb151a46d5e0 CryptoSwift: 967f37cea5a3294d9cce358f78861652155be483 -PODFILE CHECKSUM: 763e3dac392c17bcf42dab97a9225ea234e8416a +PODFILE CHECKSUM: 1b0d3fe81862c0e9ce712ddd0c5a0accd0097698 -COCOAPODS: 1.15.2 +COCOAPODS: 1.16.1 diff --git a/packages/frontend/apps/ios/capacitor.config.ts b/packages/frontend/apps/ios/capacitor.config.ts index 236dd793c..8712cc2f9 100644 --- a/packages/frontend/apps/ios/capacitor.config.ts +++ b/packages/frontend/apps/ios/capacitor.config.ts @@ -1,4 +1,5 @@ import type { CapacitorConfig } from '@capacitor/cli'; +import { KeyboardResize } from '@capacitor/keyboard'; const config: CapacitorConfig = { appId: 'app.affine.pro', @@ -17,6 +18,9 @@ const config: CapacitorConfig = { CapacitorHttp: { enabled: true, }, + Keyboard: { + resize: KeyboardResize.Native, + }, }, }; diff --git a/packages/frontend/apps/ios/package.json b/packages/frontend/apps/ios/package.json index a751762d9..17e8aba38 100644 --- a/packages/frontend/apps/ios/package.json +++ b/packages/frontend/apps/ios/package.json @@ -19,6 +19,7 @@ "@capacitor/browser": "^6.0.3", "@capacitor/core": "^6.1.2", "@capacitor/ios": "^6.1.2", + "@capacitor/keyboard": "^6.0.2", "@sentry/react": "^8.0.0", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/packages/frontend/apps/ios/src/setup.ts b/packages/frontend/apps/ios/src/setup.ts index 27bd3f467..6cf4000d5 100644 --- a/packages/frontend/apps/ios/src/setup.ts +++ b/packages/frontend/apps/ios/src/setup.ts @@ -1,3 +1,6 @@ import '@affine/core/bootstrap/browser'; import '@affine/component/theme'; import '@affine/core/mobile/styles/mobile.css'; + +// TODO(@L-Sun) Uncomment this when the `show` method implement by `@capacitor/keyboard` in ios +// import './virtual-keyboard'; diff --git a/packages/frontend/apps/ios/src/virtual-keyboard.ts b/packages/frontend/apps/ios/src/virtual-keyboard.ts new file mode 100644 index 000000000..dd1d6f663 --- /dev/null +++ b/packages/frontend/apps/ios/src/virtual-keyboard.ts @@ -0,0 +1,152 @@ +import type { PluginListenerHandle } from '@capacitor/core/types/definitions'; +import { Keyboard } from '@capacitor/keyboard'; + +type VirtualKeyboardCallback = + | (( + this: VirtualKeyboard, + ev: VirtualKeyboardEventMap[K] + ) => any) + | EventListenerOrEventListenerObject; + +class NavigatorVirtualKeyboard implements VirtualKeyboard { + private readonly _boundingRect = new DOMRect(); + + private readonly _overlaysContent = false; + + private _listeners = new Map< + string, + Set<{ + cb: VirtualKeyboardCallback; + options?: boolean | AddEventListenerOptions; + }> + >(); + + private _capacitorListenerHandles: PluginListenerHandle[] = []; + + private async _bindListener() { + const updateBoundingRect = (info?: { keyboardHeight: number }) => { + this.boundingRect.x = 0; + this.boundingRect.y = info ? window.innerHeight - info.keyboardHeight : 0; + this.boundingRect.width = window.innerWidth; + this.boundingRect.height = info ? info.keyboardHeight : 0; + this.dispatchEvent(new Event('geometrychange')); + }; + + this._capacitorListenerHandles = [ + await Keyboard.addListener('keyboardDidShow', updateBoundingRect), + await Keyboard.addListener('keyboardDidHide', updateBoundingRect), + ]; + } + + dispatchEvent = (event: Event) => { + const listeners = this._listeners.get(event.type); + if (listeners) { + for (const l of listeners) { + if (typeof l.cb === 'function') { + l.cb.call(this, event); + } else { + l.cb.handleEvent(event); + } + } + } + return !(event.cancelable && event.defaultPrevented); + }; + + constructor() { + this._bindListener(); + } + + destroy() { + this._capacitorListenerHandles.forEach(handle => handle.remove()); + } + + get boundingRect(): DOMRect { + return this._boundingRect; + } + + get overlaysContent(): boolean { + return this._overlaysContent; + } + + set overlaysContent(_: boolean) { + console.warn( + 'overlaysContent is read-only in polyfill based on @capacitor/keyboard' + ); + } + + hide() { + Keyboard.hide(); + } + + show() { + Keyboard.show(); + } + + ongeometrychange: ((this: VirtualKeyboard, ev: Event) => any) | null = null; + + addEventListener( + type: K, + listener: VirtualKeyboardCallback, + options?: boolean | AddEventListenerOptions + ) { + if (!this._listeners.has(type)) { + this._listeners.set(type, new Set()); + } + + const listeners = this._listeners.get(type); + if (!listeners) return; + + listeners.add({ cb: listener, options }); + } + + removeEventListener( + type: K, + listener: VirtualKeyboardCallback, + options?: boolean | EventListenerOptions + ) { + const listeners = this._listeners.get(type); + if (!listeners) return; + + const sameCapture = ( + a?: boolean | AddEventListenerOptions, + b?: boolean | EventListenerOptions + ) => { + if (a === undefined && b === undefined) { + return true; + } + + if (typeof a === 'boolean' && typeof b === 'boolean') { + return a === b; + } + + if (typeof a === 'object' && typeof b === 'object') { + return a.capture === b.capture; + } + + if (typeof a === 'object' && typeof b === 'boolean') { + return a.capture === b; + } + + if (typeof a === 'boolean' && typeof b === 'object') { + return a === b.capture; + } + + return false; + }; + + let target = null; + for (const l of listeners) { + if (l.cb === listener && sameCapture(l.options, options)) { + target = l; + break; + } + } + + if (target) { + listeners.delete(target); + } + } +} + +// @ts-expect-error polyfill +navigator.virtualKeyboard = new NavigatorVirtualKeyboard(); diff --git a/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/custom/root-block.ts b/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/custom/root-block.ts index fe993dd98..c79a6efb9 100644 --- a/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/custom/root-block.ts +++ b/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/custom/root-block.ts @@ -10,6 +10,11 @@ import { LifeCycleWatcher, StdIdentifier, } from '@blocksuite/affine/block-std'; +import type { + RootBlockConfig, + TelemetryEventMap, + ThemeExtension, +} from '@blocksuite/affine/blocks'; import { ColorScheme, EdgelessBuiltInManager, @@ -18,9 +23,7 @@ import { EditorSettingExtension, FontLoaderService, PageRootBlockSpec, - type TelemetryEventMap, TelemetryProvider, - type ThemeExtension, ThemeExtensionIdentifier, } from '@blocksuite/affine/blocks'; import { @@ -39,6 +42,7 @@ import { combineLatest, map } from 'rxjs'; import { getFontConfigExtension } from '../font-extension'; import { createDatabaseOptionsConfig } from './database-block'; +import { createKeyboardToolbarConfig } from './widgets/keyboard-toolbar'; import { createLinkedWidgetConfig } from './widgets/linked'; import { createToolbarMoreMenuConfig } from './widgets/toolbar'; @@ -144,7 +148,8 @@ function getEditorConfigExtension( linkedWidget: createLinkedWidgetConfig(framework), toolbarMoreMenu: createToolbarMoreMenuConfig(framework), databaseOptions: createDatabaseOptionsConfig(framework), - }), + keyboardToolbar: createKeyboardToolbarConfig(), + } satisfies RootBlockConfig), ]; } diff --git a/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/custom/widgets/keyboard-toolbar.ts b/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/custom/widgets/keyboard-toolbar.ts new file mode 100644 index 000000000..88b7ba16b --- /dev/null +++ b/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/custom/widgets/keyboard-toolbar.ts @@ -0,0 +1,11 @@ +import { globalVars } from '@affine/core/mobile/styles/mobile.css'; +import { type KeyboardToolbarConfig } from '@blocksuite/affine/blocks'; + +export function createKeyboardToolbarConfig(): Partial { + return { + // TODO(@L-Sun): check android following the PR + // https://github.com/toeverything/blocksuite/pull/8645 + useScreenHeight: BUILD_CONFIG.isIOS, + safeBottomPadding: BUILD_CONFIG.isIOS ? globalVars.appTabHeight : '0px', + }; +} diff --git a/yarn.lock b/yarn.lock index 90532964b..8dacd24e0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -606,6 +606,7 @@ __metadata: "@capacitor/cli": "npm:^6.1.2" "@capacitor/core": "npm:^6.1.2" "@capacitor/ios": "npm:^6.1.2" + "@capacitor/keyboard": "npm:^6.0.2" "@sentry/react": "npm:^8.0.0" "@types/react": "npm:^18.2.75" "@types/react-dom": "npm:^18.2.24" @@ -2991,6 +2992,15 @@ __metadata: languageName: node linkType: hard +"@capacitor/keyboard@npm:^6.0.2": + version: 6.0.2 + resolution: "@capacitor/keyboard@npm:6.0.2" + peerDependencies: + "@capacitor/core": ^6.0.0 + checksum: 10/cef4cce5c703fd67ceed3f04736a6513e05423006a5b4571d88d07a5849b79fc9f1ec98759606b7498ec1b604e5ee7987e5e30e61e9835538a29238ffbe3b036 + languageName: node + linkType: hard + "@chromatic-com/storybook@npm:^3.0.0": version: 3.0.0 resolution: "@chromatic-com/storybook@npm:3.0.0"