refactor(core): add keyPress event to fix IME space detection (#11700)

### TL;DR
Refactor space-triggered AI Widget activation logic from `keydown` to `keypress` event listeners

### Background

The `keydown` event triggered by a space may originate from:
1. Normal space insertion
2. Space triggered by input method confirming candidate words

In scenarios like (2), some browsers (see [ISSUE](https://github.com/toeverything/AFFiNE/issues/11541)) and input method callbacks produce events identical to scenario (1),making it impossible to distinguish between the two.

To fix this, the space-activated AI listener uses the `keypress` event:
In scenario 2, `event.which !== 32` (may be `30430` or other values) can be used to differentiate from scenario 1.

> CLOSE BS-3081
This commit is contained in:
yoyoyohamapi
2025-04-15 08:37:27 +00:00
parent fd6c34cfa3
commit 0df584bd5e
4 changed files with 54 additions and 4 deletions
@@ -59,6 +59,21 @@ export class KeyboardControl {
private composition = false;
private readonly _press = (event: KeyboardEvent) => {
if (!this._shouldTrigger(event)) {
return;
}
const keyboardEventState = new KeyboardEventState({
event,
composing: this.composition,
});
this._dispatcher.run(
'keyPress',
this._createContext(event, keyboardEventState)
);
};
constructor(private readonly _dispatcher: UIEventDispatcher) {}
private _createContext(event: Event, keyboardState: KeyboardEventState) {
@@ -105,6 +120,11 @@ export class KeyboardControl {
listen() {
this._dispatcher.disposables.addFromEvent(document, 'keydown', this._down);
this._dispatcher.disposables.addFromEvent(document, 'keyup', this._up);
this._dispatcher.disposables.addFromEvent(
document,
'keypress',
this._press
);
this._dispatcher.disposables.addFromEvent(
document,
'compositionstart',
@@ -46,6 +46,7 @@ const eventNames = [
'keyDown',
'keyUp',
'keyPress',
'selectionChange',
'compositionStart',
@@ -5,20 +5,31 @@ import { AIProvider } from '../../provider';
import type { AffineAIPanelWidget } from '../../widgets/ai-panel/ai-panel';
export function setupSpaceAIEntry(panel: AffineAIPanelWidget) {
panel.handleEvent('keyDown', ctx => {
// Background: The keydown event triggered by a space may originate from:
// 1. Normal space insertion
// 2. Space triggered by input method confirming candidate words
// In scenarios like (2), some browsers (see [ISSUE](https://github.com/toeverything/AFFiNE/issues/11541))
// and input method callbacks produce events identical to scenario (1),
// making it impossible to distinguish between the two.
//
// To fix this, the space-activated AI listener uses the `keypress` event:
// In scenario 2, `event.which !== 32` (may be `30430` or other values) can be used to differentiate from scenario 1.
panel.handleEvent('keyPress', ctx => {
const host = panel.host;
const keyboardState = ctx.get('keyboardState');
const event = keyboardState.raw;
if (
AIProvider.actions.chat &&
keyboardState.raw.key === ' ' &&
!keyboardState.raw.isComposing
event.key === ' ' &&
event.which === 32 &&
!event.isComposing
) {
const selection = host.selection.find(TextSelection);
if (selection && selection.isCollapsed() && selection.from.index === 0) {
const block = host.view.getBlock(selection.blockId);
if (!block?.model?.text || block.model.text?.length > 0) return;
keyboardState.raw.preventDefault();
event.preventDefault();
handleInlineAskAIAction(host);
}
}
@@ -0,0 +1,18 @@
import { expect } from '@playwright/test';
import { test } from '../base/base-test';
test.describe('AIBasic/Guidance', () => {
test.beforeEach(async ({ page, utils }) => {
await utils.testUtils.setupTestEnvironment(page);
});
test('should show AI panel when space is pressed on empty paragraph', async ({
page,
utils,
}) => {
await utils.editor.focusToEditor(page);
await page.keyboard.press('Space');
await expect(page.locator('affine-ai-panel-widget')).toBeVisible();
});
});