Repository: toeverything/AFFiNE
Pull request #11410: fix(ios): adjust bootom padding of body when keyboard toolbar existed
Author: login: L-Sun
Labels: name: mod:dev
name: test
description: Related to test cases
name: app:core
Created: 2025-04-02T09:31:23Z | Merged: 2025-04-03T02:04:30Z

Close [BS-2919](https://linear.app/affine-design/issue/BS-2919/【移动端】ios-页面底部点几，页面定位有问题，光标和浮层重叠，并且看不到新添加的-block)
Close [BS-2918](https://linear.app/affine-design/issue/BS-2918/【移动端】ios-添加标题无法展示对应-block，滑动页面也无效，光标位置也不对)

## Changes
- Fixed body padding calculation when keyboard toolbar is present
- Removed redundant position controller logic and simplified the implementation



Diff:
diff --git a/blocksuite/affine/blocks/block-root/src/widgets/keyboard-toolbar/keyboard-tool-panel.ts b/blocksuite/affine/blocks/block-root/src/widgets/keyboard-toolbar/keyboard-tool-panel.ts
index d27d9bd89042e..ea1495f14b85b 100644
--- a/blocksuite/affine/blocks/block-root/src/widgets/keyboard-toolbar/keyboard-tool-panel.ts
+++ b/blocksuite/affine/blocks/block-root/src/widgets/keyboard-toolbar/keyboard-tool-panel.ts
@@ -95,6 +95,6 @@ export class AffineKeyboardToolPanel extends SignalWatcher(
   @property({ attribute: false })
   accessor context!: KeyboardToolbarContext;
 
-  @property({ type: Number })
+  @property({ attribute: false })
   accessor height = 0;
 }
diff --git a/blocksuite/affine/blocks/block-root/src/widgets/keyboard-toolbar/keyboard-toolbar.ts b/blocksuite/affine/blocks/block-root/src/widgets/keyboard-toolbar/keyboard-toolbar.ts
index 6f67b0e055e30..45e646cdea404 100644
--- a/blocksuite/affine/blocks/block-root/src/widgets/keyboard-toolbar/keyboard-toolbar.ts
+++ b/blocksuite/affine/blocks/block-root/src/widgets/keyboard-toolbar/keyboard-toolbar.ts
@@ -315,7 +315,7 @@ export class AffineKeyboardToolbar extends SignalWatcher(
       <affine-keyboard-tool-panel
         .config=${this._currentPanelConfig}
         .context=${this._context}
-        height=${this.panelHeight$.value}
+        .height=${this.panelHeight$.value}
       ></affine-keyboard-tool-panel>
     `;
   }
diff --git a/blocksuite/affine/blocks/block-root/src/widgets/keyboard-toolbar/position-controller.ts b/blocksuite/affine/blocks/block-root/src/widgets/keyboard-toolbar/position-controller.ts
index ae3105708b4c1..bf8a7337d33df 100644
--- a/blocksuite/affine/blocks/block-root/src/widgets/keyboard-toolbar/position-controller.ts
+++ b/blocksuite/affine/blocks/block-root/src/widgets/keyboard-toolbar/position-controller.ts
@@ -4,8 +4,6 @@ import type { BlockStdScope, ShadowlessElement } from '@blocksuite/std';
 import { effect, type Signal } from '@preact/signals-core';
 import type { ReactiveController, ReactiveControllerHost } from 'lit';
 
-import { TOOLBAR_HEIGHT } from './styles';
-
 /**
  * This controller is used to control the keyboard toolbar position
  */
@@ -25,7 +23,7 @@ export class PositionController implements ReactiveController {
   }
 
   hostConnected() {
-    const { keyboard, panelOpened } = this.host;
+    const { keyboard } = this.host;
 
     this._disposables.add(
       effect(() => {
@@ -36,20 +34,6 @@ export class PositionController implements ReactiveController {
     );
 
     this.host.style.bottom = '0px';
-    this._disposables.add(
-      effect(() => {
-        if (keyboard.visible$.value) {
-          document.body.style.paddingBottom = `${keyboard.height$.value + TOOLBAR_HEIGHT}px`;
-        } else if (panelOpened) {
-          document.body.style.paddingBottom = `${this.host.panelHeight$.peek() + TOOLBAR_HEIGHT}px`;
-        } else {
-          document.body.style.paddingBottom = '';
-        }
-      })
-    );
-    this._disposables.add(() => {
-      document.body.style.paddingBottom = '';
-    });
   }
 
   hostDisconnected() {
diff --git a/blocksuite/affine/blocks/block-root/src/widgets/keyboard-toolbar/styles.ts b/blocksuite/affine/blocks/block-root/src/widgets/keyboard-toolbar/styles.ts
index 858ed9d9f75f5..774bbed2b3b2e 100644
--- a/blocksuite/affine/blocks/block-root/src/widgets/keyboard-toolbar/styles.ts
+++ b/blocksuite/affine/blocks/block-root/src/widgets/keyboard-toolbar/styles.ts
@@ -2,8 +2,6 @@ import { scrollbarStyle } from '@blocksuite/affine-shared/styles';
 import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
 import { css } from 'lit';
 
-export const TOOLBAR_HEIGHT = 46;
-
 export const keyboardToolbarStyles = css`
   affine-keyboard-toolbar {
     position: fixed;
@@ -13,7 +11,7 @@ export const keyboardToolbarStyles = css`
 
   .keyboard-toolbar {
     width: 100%;
-    height: ${TOOLBAR_HEIGHT}px;
+    height: 46px;
     display: inline-flex;
     align-items: center;
     padding: 0px 8px;
diff --git a/packages/frontend/core/src/mobile/components/app-tabs/styles.css.ts b/packages/frontend/core/src/mobile/components/app-tabs/styles.css.ts
index 26f676fd7f4a5..b0ef3c7c345df 100644
--- a/packages/frontend/core/src/mobile/components/app-tabs/styles.css.ts
+++ b/packages/frontend/core/src/mobile/components/app-tabs/styles.css.ts
@@ -1,7 +1,7 @@
 import { cssVarV2 } from '@toeverything/theme/v2';
 import { createVar, style } from '@vanilla-extract/css';
 
-import { globalVars } from '../../styles/mobile.css';
+import { globalVars } from '../../styles/variables.css';
 
 export const appTabsBackground = createVar('appTabsBackground');
 
diff --git a/packages/frontend/core/src/mobile/modules/virtual-keyboard/services/virtual-keyboard.ts b/packages/frontend/core/src/mobile/modules/virtual-keyboard/services/virtual-keyboard.ts
index 356d6381ca623..4eaf5ade15bb8 100644
--- a/packages/frontend/core/src/mobile/modules/virtual-keyboard/services/virtual-keyboard.ts
+++ b/packages/frontend/core/src/mobile/modules/virtual-keyboard/services/virtual-keyboard.ts
@@ -1,10 +1,12 @@
 import { LiveData, Service } from '@toeverything/infra';
+import { setElementVars } from '@vanilla-extract/dynamic';
+import { distinctUntilChanged, scan } from 'rxjs';
 
+import { globalVars } from '../../../styles/variables.css';
 import type { VirtualKeyboardProvider } from '../providers/virtual-keyboard';
 
 export class VirtualKeyboardService extends Service {
   readonly visible$ = new LiveData(false);
-
   readonly height$ = new LiveData(0);
 
   constructor(
@@ -21,5 +23,20 @@ export class VirtualKeyboardService extends Service {
         this.height$.next(info.height);
       })
     );
+
+    // record the static keyboard height to css var
+    const subscription = this.height$
+      .pipe(
+        scan((lastHeight, currentHeight) =>
+          this.visible$.value ? currentHeight : lastHeight
+        ),
+        distinctUntilChanged()
+      )
+      .subscribe(height => {
+        setElementVars(document.body, {
+          [globalVars.appKeyboardHeight]: `${height}px`,
+        });
+      });
+    this.disposables.push(() => subscription.unsubscribe());
   }
 }
diff --git a/packages/frontend/core/src/mobile/styles/mobile.css.ts b/packages/frontend/core/src/mobile/styles/mobile.css.ts
index 7beba3e0198c2..83b46ca1b483b 100644
--- a/packages/frontend/core/src/mobile/styles/mobile.css.ts
+++ b/packages/frontend/core/src/mobile/styles/mobile.css.ts
@@ -1,10 +1,7 @@
 import { cssVarV2 } from '@toeverything/theme/v2';
-import { createVar, globalStyle } from '@vanilla-extract/css';
+import { globalStyle } from '@vanilla-extract/css';
 
-export const globalVars = {
-  appTabHeight: createVar('appTabHeight'),
-  appTabSafeArea: createVar('appTabSafeArea'),
-};
+import { globalVars } from './variables.css';
 
 globalStyle(':root', {
   vars: {
@@ -20,15 +17,19 @@ globalStyle('body', {
   minHeight: '100dvh',
   overflowY: 'unset',
 });
-globalStyle('body:has(> #app-tabs)', {
+globalStyle('body:has(>#app-tabs):not(:has(affine-keyboard-toolbar))', {
   paddingBottom: globalVars.appTabSafeArea,
 });
-globalStyle('body:has(#app-tabs) affine-keyboard-tool-panel', {
-  paddingBottom: `calc(${globalVars.appTabHeight} + env(safe-area-inset-bottom) + 8px)`,
+globalStyle('body:has(affine-keyboard-toolbar)', {
+  paddingBottom: `calc(${globalVars.appKeyboardHeight} + 46px)`,
 });
-globalStyle('body:has(#app-tabs) edgeless-toolbar-widget', {
+globalStyle('body:has(>#app-tabs) affine-keyboard-tool-panel', {
+  paddingBottom: `calc(${globalVars.appTabSafeArea} + 8px)`,
+});
+globalStyle('body:has(>#app-tabs) edgeless-toolbar-widget', {
   bottom: globalVars.appTabSafeArea,
 });
+
 globalStyle('html', {
   height: '100dvh',
   overflowY: 'auto',
diff --git a/packages/frontend/core/src/mobile/styles/variables.css.ts b/packages/frontend/core/src/mobile/styles/variables.css.ts
new file mode 100644
index 0000000000000..730611570fd9d
--- /dev/null
+++ b/packages/frontend/core/src/mobile/styles/variables.css.ts
@@ -0,0 +1,7 @@
+import { createVar } from '@vanilla-extract/css';
+
+export const globalVars = {
+  appKeyboardHeight: createVar('appKeyboardHeight'),
+  appTabHeight: createVar('appTabHeight'),
+  appTabSafeArea: createVar('appTabSafeArea'),
+};


Reviews:
user: login: graphite-app[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-02T09:38:05Z
user: login: graphite-app[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-02T09:50:07Z
user: login: graphite-app[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-02T11:16:10Z
user: login: Saul-Mirone
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-03T01:37:15Z

Review comments:
diff_hunk: @@ -21,5 +23,21 @@ export class VirtualKeyboardService extends Service {
         this.height$.next(info.height);
       })
     );
+
+    // record the static keyboard height to css var
+    this.disposables.push(
+      this.height$
+        .pipe(
+          scan((lastHeight, currentHeight) =>
+            this.visible$.value ? currentHeight : lastHeight
+          ),
+          distinctUntilChanged()
+        )
+        .subscribe(height => {
+          setElementVars(document.body, {
+            [globalVars.appKeyboardHeight]: `${height}px`,
+          });
+        }).unsubscribe
path: packages/frontend/core/src/mobile/modules/virtual-keyboard/services/virtual-keyboard.ts
user: login: graphite-app[bot]
body: There appears to be an issue with the subscription handling. The code is accessing the `unsubscribe` property rather than calling it as a method, which means the subscription won't be properly cleaned up. 

To fix this, please change:
```typescript
}).unsubscribe
```

to:
```typescript
}),
```

This will ensure the subscription is correctly added to the disposables collection and will be properly cleaned up when the service is disposed.
```suggestion
        .subscribe(height => {
          setElementVars(document.body, {
            [globalVars.appKeyboardHeight]: `${height}px`,
          });
        }),
```
  

*Spotted by [Diamond](https://app.graphite.dev/diamond/?org=toeverything&ref=ai-review-comment) (based on [custom rules](https://app.graphite.dev/diamond/?org=toeverything&ref=ai-review-comment&view=settings))*<i class='graphite__hidden'><br /><br />Is this helpful? React 👍 or 👎 to let us know.</i>
created_at: 2025-04-02T09:38:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -13,7 +11,7 @@ export const keyboardToolbarStyles = css`
 
   .keyboard-toolbar {
     width: 100%;
-    height: ${TOOLBAR_HEIGHT}px;
+    height: 46px;
     display: inline-flex;
     align-items: center;
path: blocksuite/affine/blocks/block-root/src/widgets/keyboard-toolbar/styles.ts
user: login: graphite-app[bot]
body: Consider replacing the hard-coded value `46px` with the previously defined `TOOLBAR_HEIGHT` constant. Using constants rather than magic numbers improves maintainability, as height changes would only need to be updated in one place. This ensures consistent behavior throughout the codebase and makes future modifications more reliable.

*Spotted by [Diamond](https://app.graphite.dev/diamond/?org=toeverything&ref=ai-review-comment) (based on [custom rules](https://app.graphite.dev/diamond/?org=toeverything&ref=ai-review-comment&view=settings))*<i class='graphite__hidden'><br /><br />Is this helpful? React 👍 or 👎 to let us know.</i>
created_at: 2025-04-02T09:49:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -21,5 +23,20 @@ export class VirtualKeyboardService extends Service {
         this.height$.next(info.height);
       })
     );
+
+    // record the static keyboard height to css var
+    const subscription = this.height$
+      .pipe(
+        scan((lastHeight, currentHeight) =>
+          this.visible$.value ? currentHeight : lastHeight
+        ),
+        distinctUntilChanged()
+      )
+      .subscribe(height => {
+        setElementVars(document.body, {
+          [globalVars.appKeyboardHeight]: `${height}px`,
+        });
+      });
+    this.disposables.push(() => subscription.unsubscribe);
path: packages/frontend/core/src/mobile/modules/virtual-keyboard/services/virtual-keyboard.ts
user: login: graphite-app[bot]
body: The function passed to `disposables.push()` is missing parentheses when calling `unsubscribe`. This creates a memory leak since the function reference is pushed but never executed during cleanup.

```diff
- this.disposables.push(() => subscription.unsubscribe);
+ this.disposables.push(() => subscription.unsubscribe());
```

Without the parentheses, the cleanup handler is pushing the `unsubscribe` method reference rather than a function that calls it.
```suggestion
    this.disposables.push(() => subscription.unsubscribe());
```
  

*Spotted by [Diamond](https://app.graphite.dev/diamond/?org=toeverything&ref=ai-review-comment) (based on [custom rules](https://app.graphite.dev/diamond/?org=toeverything&ref=ai-review-comment&view=settings))*<i class='graphite__hidden'><br /><br />Is this helpful? React 👍 or 👎 to let us know.</i>
created_at: 2025-04-02T09:49:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -913,6 +913,7 @@ test.describe('Customize linked doc title and description', () => {
     await embedEditPopup.getByRole('button', { name: 'Cancel' }).click();
     await expect(embedEditPopup).toBeHidden();
 
+    await embedLink.click();
     await embedLink.click();
path: tests/affine-local/e2e/links.spec.ts
user: login: graphite-app[bot]
body: It appears there are duplicate click operations on the same element. The PR adds an additional `await embedLink.click()` line directly above an existing identical call. This creates two consecutive clicks on the same element, which may cause test instability or unexpected behavior. Consider removing one of these duplicate click operations to maintain the original test behavior.

The same issue occurs later in the test with duplicate `await cardLink.click()` calls.
```suggestion
    await embedLink.click();
```
  

*Spotted by [Diamond](https://app.graphite.dev/diamond/?org=toeverything&ref=ai-review-comment)*<i class='graphite__hidden'><br /><br />Is this helpful? React 👍 or 👎 to let us know.</i>
created_at: 2025-04-02T11:16:09Z
author_association: CONTRIBUTOR

PR comments:
user: login: L-Sun
created_at: 2025-04-02T09:31:39Z
body: * **#11427** <a href="https://app.graphite.dev/github/pr/toeverything/AFFiNE/11427?utm_source=stack-comment-icon" target="_blank"><img src="https://static.graphite.dev/graphite-32x32-black.png" alt="Graphite" width="10px" height="10px"/></a>
* **#11410** <a href="https://app.graphite.dev/github/pr/toeverything/AFFiNE/11410?utm_source=stack-comment-icon" target="_blank"><img src="https://static.graphite.dev/graphite-32x32-black.png" alt="Graphite" width="10px" height="10px"/></a> 👈 <a href="https://app.graphite.dev/github/pr/toeverything/AFFiNE/11410?utm_source=stack-comment-view-in-graphite" target="_blank">(View in Graphite)</a>
* **#11401** <a href="https://app.graphite.dev/github/pr/toeverything/AFFiNE/11401?utm_source=stack-comment-icon" target="_blank"><img src="https://static.graphite.dev/graphite-32x32-black.png" alt="Graphite" width="10px" height="10px"/></a>
* `canary`
<hr />

## How to use the Graphite Merge Queue

Add either label to this PR to merge it via the merge queue:
- <kbd>merge</kbd> - adds this PR to the back of the merge queue
- <kbd>hotfix</kbd> - for urgent hot fixes, skip the queue and merge this PR next

You must have a Graphite account in order to use the merge queue. Sign up using [this link](https://app.graphite.dev/invite/github/toeverything?ref=merge-queue-instructions-in-stack-comment&prId=11410).

<h2></h2>
<sub>

An organization admin has enabled the [Graphite Merge Queue](https://graphite.dev/docs/graphite-merge-queue) in this repository.

</sub>

<sub>

Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue.

</sub>

<h2></h2>

This stack of pull requests is managed by <a href="https://graphite.dev?utm-source=stack-comment"><b>Graphite</b></a>. Learn more about <a href="https://stacking.dev/?utm_source=stack-comment">stacking</a>.
<!-- Current dependencies on/for this PR: -->
author_association: MEMBER
user: login: codecov[bot]
created_at: 2025-04-02T09:42:10Z
body: ## [Codecov](https://app.codecov.io/gh/toeverything/AFFiNE/pull/11410?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=toeverything) Report
Attention: Patch coverage is `0%` with `1 line` in your changes missing coverage. Please review.
> Project coverage is 53.52%. Comparing base [(`5109cec`)](https://app.codecov.io/gh/toeverything/AFFiNE/commit/5109ceccece8a625adac299af680659a464fa7ea?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=toeverything) to head [(`80a663e`)](https://app.codecov.io/gh/toeverything/AFFiNE/commit/80a663efe728eff6ef20f979a7f730acbdbb3a5b?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=toeverything).
> Report is 1 commits behind head on canary.

| [Files with missing lines](https://app.codecov.io/gh/toeverything/AFFiNE/pull/11410?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=toeverything) | Patch % | Lines |
|---|---|---|
| [...rc/widgets/keyboard-toolbar/position-controller.ts](https://app.codecov.io/gh/toeverything/AFFiNE/pull/11410?src=pr&el=tree&filepath=blocksuite%2Faffine%2Fblocks%2Fblock-root%2Fsrc%2Fwidgets%2Fkeyboard-toolbar%2Fposition-controller.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=toeverything#diff-YmxvY2tzdWl0ZS9hZmZpbmUvYmxvY2tzL2Jsb2NrLXJvb3Qvc3JjL3dpZGdldHMva2V5Ym9hcmQtdG9vbGJhci9wb3NpdGlvbi1jb250cm9sbGVyLnRz) | 0.00% | [1 Missing :warning: ](https://app.codecov.io/gh/toeverything/AFFiNE/pull/11410?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=toeverything) |

<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff           @@
##           canary   #11410   +/-   ##
=======================================
  Coverage   53.51%   53.52%           
=======================================
  Files        2572     2572           
  Lines      116514   116505    -9     
  Branches    18991    18989    -2     
=======================================
+ Hits        62354    62360    +6     
+ Misses      52860    52844   -16     
- Partials     1300     1301    +1     
```

| [Flag](https://app.codecov.io/gh/toeverything/AFFiNE/pull/11410/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=toeverything) | Coverage Δ | |
|---|---|---|
| [server-test](https://app.codecov.io/gh/toeverything/AFFiNE/pull/11410/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=toeverything) | `78.36% <ø> (ø)` | |
| [unittest](https://app.codecov.io/gh/toeverything/AFFiNE/pull/11410/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=toeverything) | `29.40% <0.00%> (+0.01%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=toeverything#carryforward-flags-in-the-pull-request-comment) to find out more.

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/toeverything/AFFiNE/pull/11410?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=toeverything).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=toeverything).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE
user: login: graphite-app[bot]
created_at: 2025-04-03T01:37:22Z
body: ### Merge activity

* **Apr 2, 9:37 PM EDT**: `L-Sun` added this pull request to the [Graphite merge queue](https://app.graphite.dev/merges?org=toeverything&repo=AFFiNE).
* **Apr 2, 10:04 PM EDT**: Merged by the [Graphite merge queue](https://app.graphite.dev/merges?org=toeverything&repo=AFFiNE).

author_association: CONTRIBUTOR