From 99c1cb77815be5232af989ff22344595f950b863 Mon Sep 17 00:00:00 2001 From: yyh <92089059+lyzno1@users.noreply.github.com> Date: Sun, 26 Jul 2026 15:08:48 +0800 Subject: [PATCH] fix(web): prevent search hotkey hydration mismatch (#39574) --- .../__tests__/search-button.spec.tsx | 34 +++++++++++++++++++ .../main-nav/components/search-button.tsx | 22 ++++++++++-- 2 files changed, 54 insertions(+), 2 deletions(-) create mode 100644 web/app/components/main-nav/components/__tests__/search-button.spec.tsx diff --git a/web/app/components/main-nav/components/__tests__/search-button.spec.tsx b/web/app/components/main-nav/components/__tests__/search-button.spec.tsx new file mode 100644 index 00000000000..0e8d74c1a72 --- /dev/null +++ b/web/app/components/main-nav/components/__tests__/search-button.spec.tsx @@ -0,0 +1,34 @@ +import { act, waitFor, within } from '@testing-library/react' +import { hydrateRoot } from 'react-dom/client' +import { renderToString } from 'react-dom/server' +import { MainNavSearchButton } from '../search-button' + +describe('MainNavSearchButton', () => { + afterEach(() => { + vi.restoreAllMocks() + }) + + it('renders Ctrl during SSR and corrects it after Mac hydration without a mismatch', async () => { + vi.spyOn(window.navigator, 'platform', 'get').mockReturnValue('MacIntel') + const app = + const container = document.createElement('div') + container.innerHTML = renderToString(app) + const getSearchButton = () => + within(container).getByRole('button', { name: 'app.gotoAnything.searchTitle' }) + + expect(getSearchButton()).toHaveTextContent('CtrlK') + expect(getSearchButton()).not.toHaveTextContent('⌘') + + const onRecoverableError = vi.fn() + const root = hydrateRoot(container, app, { + onRecoverableError, + }) + + try { + await waitFor(() => expect(getSearchButton()).toHaveTextContent('⌘K')) + expect(onRecoverableError).not.toHaveBeenCalled() + } finally { + act(() => root.unmount()) + } + }) +}) diff --git a/web/app/components/main-nav/components/search-button.tsx b/web/app/components/main-nav/components/search-button.tsx index 1539d38369f..c8378e1f79c 100644 --- a/web/app/components/main-nav/components/search-button.tsx +++ b/web/app/components/main-nav/components/search-button.tsx @@ -2,13 +2,31 @@ import { DialogTrigger } from '@langgenius/dify-ui/dialog' import { Kbd } from '@langgenius/dify-ui/kbd' -import { formatForDisplay } from '@tanstack/react-hotkeys' +import { detectPlatform, formatForDisplay } from '@tanstack/react-hotkeys' +import { useSyncExternalStore } from 'react' import { useTranslation } from 'react-i18next' import { gotoAnythingDialogHandle } from '@/app/components/goto-anything/dialog-handle' import { GOTO_ANYTHING_HOTKEY } from '@/app/components/goto-anything/hotkeys' +function noopSubscribe() { + return () => {} +} + +function getPlatformSnapshot() { + return detectPlatform() +} + +function getServerPlatformSnapshot(): ReturnType { + return 'linux' +} + +function useDisplayPlatform() { + return useSyncExternalStore(noopSubscribe, getPlatformSnapshot, getServerPlatformSnapshot) +} + export function MainNavSearchButton() { const { t } = useTranslation() + const displayPlatform = useDisplayPlatform() return ( {GOTO_ANYTHING_HOTKEY.split('+').map((key) => ( - {formatForDisplay(key)} + {formatForDisplay(key, { platform: displayPlatform })} ))}