From 0f0bfb9f062e86398f089ba88e230c8a154efc33 Mon Sep 17 00:00:00 2001 From: "passabilities.eth" Date: Sun, 1 Feb 2026 05:43:28 -0600 Subject: [PATCH] fix(editor): slash menu on mobile browser (#14328) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Fixed the slash menu not appearing when typing `/` on mobile web browsers. ## Problem Mobile browsers don't reliably fire keyboard events (`keyDown`) when using virtual keyboards. This caused the slash menu trigger to fail on mobile devices. ## Solution - Changed from handling `keyDown` events to `beforeInput` events - `InputEvent` is fired consistently across all platforms (mobile and desktop) - Added proper handling for IME composition to avoid duplicate triggers - Uses `waitForUpdate()` to ensure the input is processed before checking for the trigger ## Test plan - [x] Tested on mobile Safari (iOS) - [x] Tested on mobile Chrome (Android) - [x] Verified desktop browsers still work correctly - [x] Verified IME input (e.g., Chinese/Japanese) doesn't trigger false positives Fixes #12910 ## Summary by CodeRabbit * **Bug Fixes** * Improved slash menu input handling for better reliability and enhanced IME (input method editor) composition support. ✏️ Tip: You can customize this high-level summary in your review settings. --- .../affine/widgets/slash-menu/src/widget.ts | 35 ++++++++++++------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/blocksuite/affine/widgets/slash-menu/src/widget.ts b/blocksuite/affine/widgets/slash-menu/src/widget.ts index 04a8065f81..3b2749390c 100644 --- a/blocksuite/affine/widgets/slash-menu/src/widget.ts +++ b/blocksuite/affine/widgets/slash-menu/src/widget.ts @@ -1,8 +1,11 @@ import { getInlineEditorByModel } from '@blocksuite/affine-rich-text'; import type { AffineInlineEditor } from '@blocksuite/affine-shared/types'; import { DisposableGroup } from '@blocksuite/global/disposable'; -import type { UIEventStateContext } from '@blocksuite/std'; -import { TextSelection, WidgetComponent } from '@blocksuite/std'; +import { + TextSelection, + type UIEventStateContext, + WidgetComponent, +} from '@blocksuite/std'; import { InlineEditor } from '@blocksuite/std/inline'; import debounce from 'lodash-es/debounce'; @@ -59,9 +62,7 @@ const showSlashMenu = debounce( ); export class AffineSlashMenuWidget extends WidgetComponent { - private readonly _getInlineEditor = ( - evt: KeyboardEvent | CompositionEvent - ) => { + private readonly _getInlineEditor = (evt: CompositionEvent | InputEvent) => { if (evt.target instanceof HTMLElement) { const editor = ( evt.target.closest('.inline-editor') as { @@ -152,18 +153,27 @@ export class AffineSlashMenuWidget extends WidgetComponent { this._handleInput(inlineEditor, true); }; - private readonly _onKeyDown = (ctx: UIEventStateContext) => { - const eventState = ctx.get('keyboardState'); - const event = eventState.raw; + private readonly _onBeforeInput = (ctx: UIEventStateContext) => { + const event = ctx.get('defaultState').event; + if (!(event instanceof InputEvent)) return; - const key = event.key; + // Skip non-character inputs and IME composition (handled by _onCompositionEnd) + if (event.data === null || event.isComposing) return; - if (event.isComposing || key !== AFFINE_SLASH_MENU_TRIGGER_KEY) return; + // Quick check: only proceed if the input contains the trigger key + if (!event.data.includes(AFFINE_SLASH_MENU_TRIGGER_KEY)) return; const inlineEditor = this._getInlineEditor(event); if (!inlineEditor) return; - this._handleInput(inlineEditor, false); + // Wait for the input to be processed, then handle it + // Pass true because after waitForUpdate(), the range is already synced + inlineEditor + .waitForUpdate() + .then(() => { + this._handleInput(inlineEditor, true); + }) + .catch(console.error); }; get config() { @@ -177,8 +187,7 @@ export class AffineSlashMenuWidget extends WidgetComponent { override connectedCallback() { super.connectedCallback(); - // this.handleEvent('beforeInput', this._onBeforeInput); - this.handleEvent('keyDown', this._onKeyDown); + this.handleEvent('beforeInput', this._onBeforeInput); this.handleEvent('compositionEnd', this._onCompositionEnd); } }