This is an automated email from the ASF dual-hosted git repository.
lizhimins pushed a commit to branch rocketmq-studio
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git
The following commit(s) were added to refs/heads/rocketmq-studio by this push:
new aaad83836 fix(layout): ignore composition keys in navigation search
(#6617)
aaad83836 is described below
commit aaad83836421f39432452a0e846242435bc97bb6
Author: bai mengxuan <[email protected]>
AuthorDate: Thu Oct 15 18:17:21 2026 +0800
fix(layout): ignore composition keys in navigation search (#6617)
---
web/src/layouts/MainLayout.navigation.test.tsx | 135 +++++++++++++++++++++++++
web/src/layouts/MainLayout.tsx | 2 +
2 files changed, 137 insertions(+)
diff --git a/web/src/layouts/MainLayout.navigation.test.tsx
b/web/src/layouts/MainLayout.navigation.test.tsx
new file mode 100644
index 000000000..8efb4063e
--- /dev/null
+++ b/web/src/layouts/MainLayout.navigation.test.tsx
@@ -0,0 +1,135 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { fireEvent, render, screen, waitFor, within } from
'@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { App, ConfigProvider } from 'antd';
+import { MemoryRouter, Route, Routes } from 'react-router-dom';
+import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
+import { LangProvider } from '../i18n/LangContext';
+import useAuthStore from '../stores/authStore';
+import { ThemeProvider } from '../theme/ThemeProvider';
+import MainLayout from './MainLayout';
+
+beforeAll(() => {
+ Object.defineProperty(window, 'matchMedia', {
+ writable: true,
+ value: vi.fn().mockImplementation((query: string) => ({
+ matches: false,
+ media: query,
+ onchange: null,
+ addListener: vi.fn(),
+ removeListener: vi.fn(),
+ addEventListener: vi.fn(),
+ removeEventListener: vi.fn(),
+ dispatchEvent: vi.fn(),
+ })),
+ });
+ Element.prototype.scrollIntoView = vi.fn();
+});
+
+async function openSearch() {
+ const user = userEvent.setup();
+ render(
+ <ConfigProvider theme={{ token: { motion: false } }}>
+ <App>
+ <LangProvider>
+ <ThemeProvider>
+ <MemoryRouter initialEntries={['/']}>
+ <Routes>
+ <Route path="/" element={<MainLayout />}>
+ <Route index element={<div>Home route content</div>} />
+ <Route path="settings" element={<div>Settings route
content</div>} />
+ <Route path="instance" element={<div>Instance list route
content</div>} />
+ <Route path="instance/topic" element={<div>Topic route
content</div>} />
+ </Route>
+ </Routes>
+ </MemoryRouter>
+ </ThemeProvider>
+ </LangProvider>
+ </App>
+ </ConfigProvider>,
+ );
+ await user.click(screen.getByRole('button', { name: '打开导航搜索' }));
+ const dialog = await screen.findByRole('dialog');
+ const input = within(dialog).getByPlaceholderText('搜索页面、功能...');
+ await waitFor(() => expect(input).toBeVisible());
+ return { input, dialog };
+}
+
+describe('MainLayout navigation search keyboard input', () => {
+ beforeEach(() => {
+ useAuthStore.setState({ user: null, userId: null, admin: null });
+ });
+
+ it.each(['composing', 'process-key'])(
+ 'keepsNavigationSearchOpenWhileConfirmingCompositionTest: %s',
+ async (sequence) => {
+ const { input, dialog } = await openSearch();
+ fireEvent.compositionStart(input);
+ fireEvent.compositionUpdate(input, { data: '设置' });
+ fireEvent.change(input, { target: { value: '设置' } });
+ expect(within(dialog).getByRole('button', { name: '设置' })).toBeVisible();
+ if (sequence === 'process-key') fireEvent.compositionEnd(input, { data:
'设置' });
+
+ // Exercise the production DOM handler; this does not emulate an OS
input method.
+ const confirmingKey = new KeyboardEvent('keydown', {
+ key: 'Enter',
+ keyCode: sequence === 'process-key' ? 229 : 13,
+ isComposing: sequence === 'composing',
+ bubbles: true,
+ cancelable: true,
+ });
+ fireEvent(input, confirmingKey);
+
+ expect(screen.queryByText('Settings route
content')).not.toBeInTheDocument();
+ expect(screen.getByText('Home route content')).toBeInTheDocument();
+ expect(dialog).toBeVisible();
+ expect(input).toHaveValue('设置');
+ expect(confirmingKey.defaultPrevented).toBe(false);
+ if (sequence === 'composing') fireEvent.compositionEnd(input, { data:
'设置' });
+ fireEvent.keyDown(input, { key: 'Enter', keyCode: 13, isComposing: false
});
+ expect(await screen.findByText('Settings route
content')).toBeInTheDocument();
+ },
+ );
+
+ it('leavesComposingArrowKeysToTheInputMethodTest', async () => {
+ const { input } = await openSearch();
+ fireEvent.change(input, { target: { value: 'instance' } });
+ fireEvent.compositionStart(input);
+ const down = new KeyboardEvent('keydown', {
+ key: 'ArrowDown',
+ isComposing: true,
+ bubbles: true,
+ cancelable: true,
+ });
+ fireEvent(input, down);
+ fireEvent.compositionEnd(input);
+ fireEvent.keyDown(input, { key: 'Enter', keyCode: 13, isComposing: false
});
+ expect(screen.queryByText('Topic route content')).not.toBeInTheDocument();
+ expect(await screen.findByText('Instance list route
content')).toBeInTheDocument();
+ expect(down.defaultPrevented).toBe(false);
+ });
+
+ it('keepsOrdinaryArrowSelectionAndEnterNavigationTest', async () => {
+ const { input } = await openSearch();
+ fireEvent.change(input, { target: { value: 'instance' } });
+ fireEvent.keyDown(input, { key: 'ArrowDown', isComposing: false });
+ fireEvent.keyDown(input, { key: 'Enter', keyCode: 13, isComposing: false
});
+ expect(await screen.findByText('Topic route content')).toBeInTheDocument();
+ });
+});
diff --git a/web/src/layouts/MainLayout.tsx b/web/src/layouts/MainLayout.tsx
index 1e6bbd75a..2e30abb67 100644
--- a/web/src/layouts/MainLayout.tsx
+++ b/web/src/layouts/MainLayout.tsx
@@ -679,6 +679,8 @@ const MainLayout = () => {
setActiveIndex(0);
}}
onKeyDown={(e) => {
+ // Composition keys select or confirm IME text, not navigation
results.
+ if (e.nativeEvent.isComposing || e.nativeEvent.keyCode === 229)
return;
if (e.key === 'ArrowDown') {
e.preventDefault();
setActiveIndex((i) => Math.min(i + 1,
Math.max(searchResults.length - 1, 0)));