Skip to content

Commit

Permalink
🐛 fix: fix auto focus issues (lobehub#2697)
Browse files Browse the repository at this point in the history
* 🐛 fix: fix fetch when need login again

* 🐛 fix: fix autofocus
  • Loading branch information
arvinxx authored May 28, 2024
1 parent 920a70d commit 8df856e
Show file tree
Hide file tree
Showing 4 changed files with 58 additions and 139 deletions.
Original file line number Diff line number Diff line change
@@ -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<any>;
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<any>;
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<any>;
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<any>;
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();
});
});
Original file line number Diff line number Diff line change
@@ -1,39 +1,13 @@
import { TextAreaRef } from 'antd/es/input/TextArea';
import { RefObject, useEffect } from 'react';

export const useAutoFocus = (inputRef: RefObject<TextAreaRef>) => {
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<TextAreaRef>) => {
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]);
};
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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);
Expand Down
50 changes: 27 additions & 23 deletions src/store/session/slices/session/action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export interface SessionAction {

updateSearchKeywords: (keywords: string) => void;

useFetchSessions: () => SWRResponse<ChatSessionList>;
useFetchSessions: (isLogin: boolean | undefined) => SWRResponse<ChatSessionList>;
useSearchSessions: (keyword?: string) => SWRResponse<any>;

internal_dispatchSessions: (payload: SessionDispatch) => void;
Expand Down Expand Up @@ -192,29 +192,33 @@ export const createSessionSlice: StateCreator<
await refreshSessions();
},

useFetchSessions: () =>
useClientDataSWR<ChatSessionList>(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<ChatSessionList>(
[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<LobeSessions>(
[SEARCH_SESSIONS_KEY, keyword],
Expand Down

0 comments on commit 8df856e

Please sign in to comment.