mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-18 02:21:51 +08:00
fix(editor): prevent backspace in icon picker search from deleting editor content (#15089)
## Problem
When the callout block's icon picker is open and the user types in the
search input, pressing backspace deletes content in the main editor
instead of the search text.
## Root Cause
The callout icon picker is mounted via `createPopup` inside
`editor-host`. `PageKeyboardManager` registers a global `Backspace`
handler on the editor host (`keyboard-manager.ts`) with `{ global: true
}`, which fires on every backspace keydown regardless of what element is
focused. Without `stopPropagation`, the backspace event from the search
input bubbles up through the DOM and triggers block deletion.
Other keys are unaffected because the editor handles character input
through `contenteditable` focus, those handlers only act when a
contenteditable node is active.
## Fix
Add `onKeyDown` with `e.stopPropagation()` to the search inputs in both
`EmojiPicker` and `AffineIconPicker`. This matches the existing pattern
already used by `MenuComponent` (`menu-renderer.ts:107`) and all other
interactive components (`date-picker`, `inline-edit`, `prompt-modal`).
## Why not affected elsewhere
`DocIconPicker` uses the same pickers but wraps them in a Radix UI
`Menu` with `modal: true`, which portals outside `editor-host` — so
backspace events never reach the editor's global handler there.
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Bug Fixes**
* Improved keyboard event handling in search inputs for icon and emoji
pickers
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
+15
-1
@@ -1,7 +1,13 @@
|
|||||||
import keywords from '@blocksuite/icons/keywords/en.json';
|
import keywords from '@blocksuite/icons/keywords/en.json';
|
||||||
import * as allIcons from '@blocksuite/icons/rc';
|
import * as allIcons from '@blocksuite/icons/rc';
|
||||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||||
import { startTransition, useCallback, useEffect, useState } from 'react';
|
import {
|
||||||
|
type KeyboardEvent,
|
||||||
|
startTransition,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
import { IconButton } from '../../../button';
|
import { IconButton } from '../../../button';
|
||||||
import Input from '../../../input';
|
import Input from '../../../input';
|
||||||
@@ -89,6 +95,13 @@ export const AffineIconPicker = ({
|
|||||||
[addRecentIcon, onSelect, color]
|
[addRecentIcon, onSelect, color]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleSearchKeyDown = useCallback(
|
||||||
|
(e: KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={pickerStyles.root}>
|
<div className={pickerStyles.root}>
|
||||||
{/* Search */}
|
{/* Search */}
|
||||||
@@ -96,6 +109,7 @@ export const AffineIconPicker = ({
|
|||||||
<Input
|
<Input
|
||||||
value={keyword}
|
value={keyword}
|
||||||
onChange={setKeyword}
|
onChange={setKeyword}
|
||||||
|
onKeyDown={handleSearchKeyDown}
|
||||||
className={pickerStyles.searchInput}
|
className={pickerStyles.searchInput}
|
||||||
preFix={
|
preFix={
|
||||||
<div style={{ marginLeft: 10, lineHeight: 0 }}>
|
<div style={{ marginLeft: 10, lineHeight: 0 }}>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { SearchIcon } from '@blocksuite/icons/rc';
|
import { SearchIcon } from '@blocksuite/icons/rc';
|
||||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||||
import { useCallback, useState } from 'react';
|
import { type KeyboardEvent, useCallback, useState } from 'react';
|
||||||
|
|
||||||
import { IconButton } from '../../../button';
|
import { IconButton } from '../../../button';
|
||||||
import Input from '../../../input';
|
import Input from '../../../input';
|
||||||
@@ -38,12 +38,20 @@ export const EmojiPicker = ({
|
|||||||
[addRecent, onSelect]
|
[addRecent, onSelect]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleSearchKeyDown = useCallback(
|
||||||
|
(e: KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={pickerStyles.root}>
|
<div className={pickerStyles.root}>
|
||||||
<header className={pickerStyles.searchContainer}>
|
<header className={pickerStyles.searchContainer}>
|
||||||
<Input
|
<Input
|
||||||
value={keyword}
|
value={keyword}
|
||||||
onChange={setKeyword}
|
onChange={setKeyword}
|
||||||
|
onKeyDown={handleSearchKeyDown}
|
||||||
className={pickerStyles.searchInput}
|
className={pickerStyles.searchInput}
|
||||||
preFix={
|
preFix={
|
||||||
<div style={{ marginLeft: 10, lineHeight: 0 }}>
|
<div style={{ marginLeft: 10, lineHeight: 0 }}>
|
||||||
|
|||||||
Reference in New Issue
Block a user