diff --git a/packages/x/components/suggestion/__tests__/index.test.tsx b/packages/x/components/suggestion/__tests__/index.test.tsx
index fc182a450..2bbed68bd 100644
--- a/packages/x/components/suggestion/__tests__/index.test.tsx
+++ b/packages/x/components/suggestion/__tests__/index.test.tsx
@@ -2,6 +2,7 @@ import { fireEvent, render, screen } from '@testing-library/react';
import React from 'react';
import mountTest from '../../../tests/shared/mountTest';
import rtlTest from '../../../tests/shared/rtlTest';
+import Sender, { type SenderProps } from '../../sender';
import Suggestion, { type SuggestionProps } from '../index';
describe('Suggestion Component', () => {
@@ -84,6 +85,42 @@ describe('Suggestion Component', () => {
expect(onOpenChange).toHaveBeenCalledWith(false);
});
+ it('should not prevent Space input whether the popup is closed or open', () => {
+ const items = [{ label: 'Suggestion 1', value: 'suggestion1' }];
+ const { container } = render();
+ const input = container.querySelector('input')!;
+
+ expect(fireEvent.keyDown(input, { key: ' ' })).toBe(true);
+
+ fireEvent.keyDown(input, { key: '/' });
+ expect(screen.getByText('Suggestion 1')).toBeInTheDocument();
+ expect(fireEvent.keyDown(input, { key: ' ' })).toBe(true);
+ });
+
+ it.each<[name: string, senderProps: SenderProps, targetSelector: string]>([
+ ['textarea', {}, 'textarea'],
+ [
+ 'input slot',
+ { slotConfig: [{ type: 'input', key: 'input' }] },
+ 'input[data-slot-input="input"]',
+ ],
+ [
+ 'contenteditable slot',
+ { slotConfig: [{ type: 'content', key: 'content' }] },
+ '[data-slot-key="content"][contenteditable="true"]',
+ ],
+ ])('should preserve Space input in a Sender %s', (_, senderProps, targetSelector) => {
+ const items = [{ label: 'Suggestion 1', value: 'suggestion1' }];
+ const { container } = render(
+
+ {({ onKeyDown }) => }
+ ,
+ );
+ const target = container.querySelector(targetSelector)!;
+
+ expect(fireEvent.keyDown(target, { key: ' ' })).toBe(true);
+ });
+
it('open controlled', () => {
const items = [
{ label: 'Suggestion 1', value: 'suggestion1', icon: },
diff --git a/packages/x/components/suggestion/__tests__/useActive.test.tsx b/packages/x/components/suggestion/__tests__/useActive.test.tsx
index d32c48862..4e45b93ad 100644
--- a/packages/x/components/suggestion/__tests__/useActive.test.tsx
+++ b/packages/x/components/suggestion/__tests__/useActive.test.tsx
@@ -270,6 +270,70 @@ describe('useActive', () => {
const [activePaths] = result.current;
expect(activePaths).toEqual([]);
});
+
+ const dispatchSpace = (
+ target: HTMLElement,
+ options: { open?: boolean; altKey?: boolean; ctrlKey?: boolean; metaKey?: boolean } = {},
+ ) => {
+ const items: SuggestionItem[] = [{ label: 'Item 1', value: 'item1' }];
+ const stopPropagation = jest.fn();
+ const preventDefault = jest.fn();
+ const { open = true, ...modifiers } = options;
+ const { result } = renderHook(() => useActive(items, open, false, jest.fn()));
+
+ act(() =>
+ result.current[1]({
+ key: ' ',
+ target,
+ stopPropagation,
+ preventDefault,
+ ...modifiers,
+ } as any),
+ );
+
+ return { preventDefault, stopPropagation };
+ };
+
+ it.each([true, false])(
+ 'should stop plain Space propagation from an editable target when open is %s',
+ (open) => {
+ const { preventDefault, stopPropagation } = dispatchSpace(
+ document.createElement('textarea'),
+ { open },
+ );
+
+ expect(stopPropagation).toHaveBeenCalledTimes(1);
+ expect(preventDefault).not.toHaveBeenCalled();
+ },
+ );
+
+ it('should stop plain Space propagation from a contenteditable target', () => {
+ const target = document.createElement('div');
+ Object.defineProperty(target, 'isContentEditable', { value: true });
+
+ expect(dispatchSpace(target).stopPropagation).toHaveBeenCalledTimes(1);
+ });
+
+ it.each([
+ ['Alt', { altKey: true }],
+ ['Control', { ctrlKey: true }],
+ ['Meta', { metaKey: true }],
+ ])('should not stop %s+Space propagation', (_, modifiers) => {
+ expect(
+ dispatchSpace(document.createElement('textarea'), modifiers).stopPropagation,
+ ).not.toHaveBeenCalled();
+ });
+
+ it.each([
+ ['button', document.createElement('button')],
+ ['button input', Object.assign(document.createElement('input'), { type: 'button' })],
+ ['checkbox input', Object.assign(document.createElement('input'), { type: 'checkbox' })],
+ ['number input', Object.assign(document.createElement('input'), { type: 'number' })],
+ ['readonly input', Object.assign(document.createElement('input'), { readOnly: true })],
+ ['disabled textarea', Object.assign(document.createElement('textarea'), { disabled: true })],
+ ])('should not stop Space propagation from a %s', (_, target) => {
+ expect(dispatchSpace(target).stopPropagation).not.toHaveBeenCalled();
+ });
});
describe('RTL mode', () => {
diff --git a/packages/x/components/suggestion/useActive.ts b/packages/x/components/suggestion/useActive.ts
index 60d6c8ee2..69043f9f2 100644
--- a/packages/x/components/suggestion/useActive.ts
+++ b/packages/x/components/suggestion/useActive.ts
@@ -2,6 +2,24 @@ import { useEvent } from '@rc-component/util';
import React from 'react';
import type { SuggestionItem } from '.';
+const TEXT_INPUT_TYPES = new Set(['email', 'password', 'search', 'tel', 'text', 'url']);
+
+const isEditableTarget = (target: EventTarget | null) => {
+ if (!(target instanceof HTMLElement)) {
+ return false;
+ }
+
+ if (target instanceof HTMLInputElement) {
+ return TEXT_INPUT_TYPES.has(target.type) && !target.disabled && !target.readOnly;
+ }
+
+ if (target instanceof HTMLTextAreaElement) {
+ return !target.disabled && !target.readOnly;
+ }
+
+ return target.isContentEditable;
+};
+
/**
* Since Cascader not support ref active, we use `value` to mock the active item.
*/
@@ -58,6 +76,10 @@ export default function useActive(
};
const onKeyDown: React.KeyboardEventHandler = useEvent((e) => {
+ if (e.key === ' ' && !e.altKey && !e.ctrlKey && !e.metaKey && isEditableTarget(e.target)) {
+ e.stopPropagation();
+ }
+
if (!open) {
return;
}