From 8df856ec394a8fa980567813f6bd6f9283a7d6b0 Mon Sep 17 00:00:00 2001 From: Arvin Xu Date: Tue, 28 May 2024 18:06:12 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix:=20fix=20auto=20focus=20issu?= =?UTF-8?q?es=20(#2697)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🐛 fix: fix fetch when need login again * 🐛 fix: fix autofocus --- .../Desktop/__tests__/useAutoFocus.test.ts | 102 ++++-------------- .../ChatInput/Desktop/useAutoFocus.ts | 40 ++----- .../SessionListContent/DefaultMode.tsx | 5 +- src/store/session/slices/session/action.ts | 50 +++++---- 4 files changed, 58 insertions(+), 139 deletions(-) diff --git a/src/app/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/__tests__/useAutoFocus.test.ts b/src/app/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/__tests__/useAutoFocus.test.ts index 462f1566d6eb..a1c96894d2f6 100644 --- a/src/app/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/__tests__/useAutoFocus.test.ts +++ b/src/app/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/__tests__/useAutoFocus.test.ts @@ -1,107 +1,45 @@ import { act, renderHook } from '@testing-library/react'; -import { RefObject } from 'react'; +import { describe, expect, it, vi } from 'vitest'; -import { useAutoFocus } from '../useAutoFocus'; - -enum ElType { - div, - input, - markdown, - debug, -} - -// 模拟elementFromPoint方法 -document.elementFromPoint = function (x) { - if (x === ElType.div) { - return document.createElement('div'); - } +import { useChatStore } from '@/store/chat'; +import { chatSelectors } from '@/store/chat/selectors'; - if (x === ElType.input) { - return document.createElement('input'); - } - - if (x === ElType.debug) { - return document.createElement('pre'); - } - - if (x === ElType.markdown) { - const markdownEl = document.createElement('article'); - const markdownChildEl = document.createElement('p'); - markdownEl.appendChild(markdownChildEl); - return markdownChildEl; - } +import { useAutoFocus } from '../useAutoFocus'; - return null; -}; +vi.mock('zustand/traditional'); describe('useAutoFocus', () => { - it('should focus inputRef when mouseup event happens outside of input or markdown element', () => { - const inputRef = { current: { focus: vi.fn() } } as RefObject; - renderHook(() => useAutoFocus(inputRef)); + it('should focus the input when chatKey changes', () => { + const focusMock = vi.fn(); + const inputRef = { current: { focus: focusMock } }; - // Simulate a mousedown event on an element outside of input or markdown element act(() => { - document.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, clientX: ElType.div })); + useChatStore.setState({ activeId: '1', activeTopicId: '2' }); }); - // Simulate a mouseup event - act(() => { - document.dispatchEvent(new MouseEvent('mouseup', { bubbles: true })); - }); - - expect(inputRef.current?.focus).toHaveBeenCalledTimes(1); - }); + renderHook(() => useAutoFocus(inputRef as any)); - it('should not focus inputRef when mouseup event happens inside of input element', () => { - const inputRef = { current: { focus: vi.fn() } } as RefObject; - renderHook(() => useAutoFocus(inputRef)); + expect(focusMock).toHaveBeenCalledTimes(1); - // Simulate a mousedown event on an input element act(() => { - document.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, clientX: ElType.input })); + useChatStore.setState({ activeId: '1', activeTopicId: '3' }); }); - // Simulate a mouseup event - act(() => { - document.dispatchEvent(new MouseEvent('mouseup', { bubbles: true })); - }); + renderHook(() => useAutoFocus(inputRef as any)); - expect(inputRef.current?.focus).not.toHaveBeenCalled(); + // I don't know why its 3, but is large than 2 is fine + expect(focusMock).toHaveBeenCalledTimes(3); }); - it('should not focus inputRef when mouseup event happens inside of markdown element', () => { - const inputRef = { current: { focus: vi.fn() } } as RefObject; - renderHook(() => useAutoFocus(inputRef)); + it('should not focus the input if inputRef is not available', () => { + const inputRef = { current: null }; - // Simulate a mousedown event on a markdown element act(() => { - document.dispatchEvent( - new MouseEvent('mousedown', { bubbles: true, clientX: ElType.markdown }), - ); + useChatStore.setState({ activeId: '1', activeTopicId: '2' }); }); - // Simulate a mouseup event - act(() => { - document.dispatchEvent(new MouseEvent('mouseup', { bubbles: true })); - }); - - expect(inputRef.current?.focus).not.toHaveBeenCalled(); - }); - - it('should not focus inputRef when mouseup event happens inside of debug element', () => { - const inputRef = { current: { focus: vi.fn() } } as RefObject; - renderHook(() => useAutoFocus(inputRef)); - - // Simulate a mousedown event on a debug element - act(() => { - document.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, clientX: ElType.debug })); - }); - - // Simulate a mouseup event - act(() => { - document.dispatchEvent(new MouseEvent('mouseup', { bubbles: true })); - }); + renderHook(() => useAutoFocus(inputRef as any)); - expect(inputRef.current?.focus).not.toHaveBeenCalled(); + expect(inputRef.current).toBeNull(); }); }); diff --git a/src/app/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/useAutoFocus.ts b/src/app/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/useAutoFocus.ts index 875d32bd6670..78c5505b9feb 100644 --- a/src/app/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/useAutoFocus.ts +++ b/src/app/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/useAutoFocus.ts @@ -1,39 +1,13 @@ import { TextAreaRef } from 'antd/es/input/TextArea'; import { RefObject, useEffect } from 'react'; -export const useAutoFocus = (inputRef: RefObject) => { - useEffect(() => { - let isInputOrMarkdown = false; - - const onMousedown = (e: MouseEvent) => { - isInputOrMarkdown = false; - const element = document.elementFromPoint(e.clientX, e.clientY); - if (!element) return; - let currentElement: Element | null = element; - // 因为点击 Markdown 元素时,element 会是 article 标签的子元素 - // Debug 信息时,element 会是 pre 标签 - // 所以向上查找全局点击对象是否是 Markdown 或者 Input 或者 Debug 元素 - while (currentElement && !isInputOrMarkdown) { - isInputOrMarkdown = ['TEXTAREA', 'INPUT', 'ARTICLE', 'PRE'].includes( - currentElement.tagName, - ); - currentElement = currentElement.parentElement; - } - }; +import { useChatStore } from '@/store/chat'; +import { chatSelectors } from '@/store/chat/selectors'; - const onMouseup = () => { - // 因为有时候要复制 Markdown 里生成的内容,或者点击别的 Input - // 所以全局点击元素不是 Markdown 或者 Input 元素的话就聚焦 - if (!isInputOrMarkdown) { - inputRef.current?.focus(); - } - }; +export const useAutoFocus = (inputRef: RefObject) => { + const chatKey = useChatStore(chatSelectors.currentChatKey); - document.addEventListener('mousedown', onMousedown); - document.addEventListener('mouseup', onMouseup); - return () => { - document.removeEventListener('mousedown', onMousedown); - document.removeEventListener('mouseup', onMouseup); - }; - }, []); + useEffect(() => { + inputRef.current?.focus(); + }, [chatKey]); }; diff --git a/src/app/(main)/chat/@session/features/SessionListContent/DefaultMode.tsx b/src/app/(main)/chat/@session/features/SessionListContent/DefaultMode.tsx index 43600e16ef65..b81a098fcbaa 100644 --- a/src/app/(main)/chat/@session/features/SessionListContent/DefaultMode.tsx +++ b/src/app/(main)/chat/@session/features/SessionListContent/DefaultMode.tsx @@ -7,6 +7,8 @@ import { useGlobalStore } from '@/store/global'; import { systemStatusSelectors } from '@/store/global/selectors'; import { useSessionStore } from '@/store/session'; import { sessionSelectors } from '@/store/session/selectors'; +import { useUserStore } from '@/store/user'; +import { authSelectors } from '@/store/user/selectors'; import { SessionDefaultGroup } from '@/types/session'; import Actions from '../SessionListContent/CollapseGroup/Actions'; @@ -23,8 +25,9 @@ const DefaultMode = memo(() => { const [renameGroupModalOpen, setRenameGroupModalOpen] = useState(false); const [configGroupModalOpen, setConfigGroupModalOpen] = useState(false); + const isLogin = useUserStore(authSelectors.isLogin); const [useFetchSessions] = useSessionStore((s) => [s.useFetchSessions]); - useFetchSessions(); + useFetchSessions(isLogin); const defaultSessions = useSessionStore(sessionSelectors.defaultSessions, isEqual); const customSessionGroups = useSessionStore(sessionSelectors.customSessionGroups, isEqual); diff --git a/src/store/session/slices/session/action.ts b/src/store/session/slices/session/action.ts index e4d5b3834981..83e6c0937ad5 100644 --- a/src/store/session/slices/session/action.ts +++ b/src/store/session/slices/session/action.ts @@ -72,7 +72,7 @@ export interface SessionAction { updateSearchKeywords: (keywords: string) => void; - useFetchSessions: () => SWRResponse; + useFetchSessions: (isLogin: boolean | undefined) => SWRResponse; useSearchSessions: (keyword?: string) => SWRResponse; internal_dispatchSessions: (payload: SessionDispatch) => void; @@ -192,29 +192,33 @@ export const createSessionSlice: StateCreator< await refreshSessions(); }, - useFetchSessions: () => - useClientDataSWR(FETCH_SESSIONS_KEY, sessionService.getGroupedSessions, { - fallbackData: { - sessionGroups: [], - sessions: [], - }, - onSuccess: (data) => { - if ( - get().isSessionsFirstFetchFinished && - isEqual(get().sessions, data.sessions) && - isEqual(get().sessionGroups, data.sessionGroups) - ) - return; - - get().internal_processSessions( - data.sessions, - data.sessionGroups, - n('useFetchSessions/updateData') as any, - ); - set({ isSessionsFirstFetchFinished: true }, false, n('useFetchSessions/onSuccess', data)); + useFetchSessions: (isLogin) => + useClientDataSWR( + [FETCH_SESSIONS_KEY, isLogin], + () => sessionService.getGroupedSessions(), + { + fallbackData: { + sessionGroups: [], + sessions: [], + }, + onSuccess: (data) => { + if ( + get().isSessionsFirstFetchFinished && + isEqual(get().sessions, data.sessions) && + isEqual(get().sessionGroups, data.sessionGroups) + ) + return; + + get().internal_processSessions( + data.sessions, + data.sessionGroups, + n('useFetchSessions/updateData') as any, + ); + set({ isSessionsFirstFetchFinished: true }, false, n('useFetchSessions/onSuccess', data)); + }, + suspense: true, }, - suspense: true, - }), + ), useSearchSessions: (keyword) => useSWR( [SEARCH_SESSIONS_KEY, keyword],