mirror of
https://github.com/NovaOSS/nova-betterchat.git
synced 2024-11-25 19:54:00 +01:00
refactor: Add hideOnClickOutside feature to reuse code (#240)
* remove console log * refactor: Add hideOnClickOutside feature to reuse code This change abstracts the code repetition in three different files and promote the DRY principle by adding the hideOnClickOutside feature to the handleClickOutside function. The feature is designed to hide an element when clicking outside of its area. * refactor: hooks --------- Co-authored-by: Jing Hua <tohjinghua123@gmail.com>
This commit is contained in:
parent
226944b62a
commit
b3f421cde9
|
@ -1,9 +1,12 @@
|
||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
import useStore from '@store/store';
|
import useStore from '@store/store';
|
||||||
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { matchSorter } from 'match-sorter';
|
import { matchSorter } from 'match-sorter';
|
||||||
import { Prompt } from '@type/prompt';
|
import { Prompt } from '@type/prompt';
|
||||||
|
|
||||||
|
import useHideOnOutsideClick from '@hooks/useHideOnOutsideClick';
|
||||||
|
|
||||||
const CommandPrompt = ({
|
const CommandPrompt = ({
|
||||||
_setContent,
|
_setContent,
|
||||||
}: {
|
}: {
|
||||||
|
@ -11,10 +14,10 @@ const CommandPrompt = ({
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const prompts = useStore((state) => state.prompts);
|
const prompts = useStore((state) => state.prompts);
|
||||||
const [dropDown, setDropDown] = useState<boolean>(false);
|
|
||||||
const [_prompts, _setPrompts] = useState<Prompt[]>(prompts);
|
const [_prompts, _setPrompts] = useState<Prompt[]>(prompts);
|
||||||
const [input, setInput] = useState<string>('');
|
const [input, setInput] = useState<string>('');
|
||||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
const [dropDown, setDropDown, dropDownRef] = useHideOnOutsideClick();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const filteredPrompts = matchSorter(useStore.getState().prompts, input, {
|
const filteredPrompts = matchSorter(useStore.getState().prompts, input, {
|
||||||
|
@ -28,29 +31,8 @@ const CommandPrompt = ({
|
||||||
setInput('');
|
setInput('');
|
||||||
}, [prompts]);
|
}, [prompts]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleClickOutside = (event: MouseEvent) => {
|
|
||||||
if (
|
|
||||||
dropdownRef.current &&
|
|
||||||
!dropdownRef.current.contains(event.target as Node)
|
|
||||||
) {
|
|
||||||
setDropDown(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (dropDown) {
|
|
||||||
document.addEventListener('mousedown', handleClickOutside);
|
|
||||||
} else {
|
|
||||||
document.removeEventListener('mousedown', handleClickOutside);
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
document.removeEventListener('mousedown', handleClickOutside);
|
|
||||||
};
|
|
||||||
}, [dropdownRef, dropDown]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='relative max-wd-sm' ref={dropdownRef}>
|
<div className='relative max-wd-sm' ref={dropDownRef}>
|
||||||
<button
|
<button
|
||||||
className='btn btn-neutral btn-small'
|
className='btn btn-neutral btn-small'
|
||||||
onClick={() => setDropDown(!dropDown)}
|
onClick={() => setDropDown(!dropDown)}
|
||||||
|
|
|
@ -5,6 +5,8 @@ import useStore from '@store/store';
|
||||||
import DownChevronArrow from '@icon/DownChevronArrow';
|
import DownChevronArrow from '@icon/DownChevronArrow';
|
||||||
import { ChatInterface, Role, roles } from '@type/chat';
|
import { ChatInterface, Role, roles } from '@type/chat';
|
||||||
|
|
||||||
|
import useHideOnOutsideClick from '@hooks/useHideOnOutsideClick';
|
||||||
|
|
||||||
const RoleSelector = React.memo(
|
const RoleSelector = React.memo(
|
||||||
({
|
({
|
||||||
role,
|
role,
|
||||||
|
@ -20,29 +22,7 @@ const RoleSelector = React.memo(
|
||||||
const setChats = useStore((state) => state.setChats);
|
const setChats = useStore((state) => state.setChats);
|
||||||
const currentChatIndex = useStore((state) => state.currentChatIndex);
|
const currentChatIndex = useStore((state) => state.currentChatIndex);
|
||||||
|
|
||||||
const [dropDown, setDropDown] = useState<boolean>(false);
|
const [dropDown, setDropDown, dropDownRef] = useHideOnOutsideClick();
|
||||||
const dropDownRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
const handleClickOutside = useCallback(
|
|
||||||
(event: any) => {
|
|
||||||
if (dropDownRef.current && !dropDownRef.current.contains(event.target))
|
|
||||||
setDropDown(false);
|
|
||||||
},
|
|
||||||
[dropDownRef, setDropDown, messageIndex]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Bind the event listener only if the dropdown is open.
|
|
||||||
if (dropDown) {
|
|
||||||
document.addEventListener('mousedown', handleClickOutside);
|
|
||||||
} else {
|
|
||||||
document.removeEventListener('mousedown', handleClickOutside);
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
document.removeEventListener('mousedown', handleClickOutside);
|
|
||||||
};
|
|
||||||
}, [dropDownRef, dropDown]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='prose dark:prose-invert relative'>
|
<div className='prose dark:prose-invert relative'>
|
||||||
|
|
|
@ -20,6 +20,8 @@ import RefreshIcon from '@icon/RefreshIcon';
|
||||||
|
|
||||||
import { folderColorOptions } from '@constants/color';
|
import { folderColorOptions } from '@constants/color';
|
||||||
|
|
||||||
|
import useHideOnOutsideClick from '@hooks/useHideOnOutsideClick';
|
||||||
|
|
||||||
const ChatFolder = ({
|
const ChatFolder = ({
|
||||||
folderChats,
|
folderChats,
|
||||||
folderId,
|
folderId,
|
||||||
|
@ -37,13 +39,13 @@ const ChatFolder = ({
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
const folderRef = useRef<HTMLDivElement>(null);
|
const folderRef = useRef<HTMLDivElement>(null);
|
||||||
const gradientRef = useRef<HTMLDivElement>(null);
|
const gradientRef = useRef<HTMLDivElement>(null);
|
||||||
const paletteRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
const [_folderName, _setFolderName] = useState<string>(folderName);
|
const [_folderName, _setFolderName] = useState<string>(folderName);
|
||||||
const [isEdit, setIsEdit] = useState<boolean>(false);
|
const [isEdit, setIsEdit] = useState<boolean>(false);
|
||||||
const [isDelete, setIsDelete] = useState<boolean>(false);
|
const [isDelete, setIsDelete] = useState<boolean>(false);
|
||||||
const [isHover, setIsHover] = useState<boolean>(false);
|
const [isHover, setIsHover] = useState<boolean>(false);
|
||||||
const [showPalette, setShowPalette] = useState<boolean>(false);
|
|
||||||
|
const [showPalette, setShowPalette, paletteRef] = useHideOnOutsideClick();
|
||||||
|
|
||||||
const editTitle = () => {
|
const editTitle = () => {
|
||||||
const updatedFolders: FolderCollection = JSON.parse(
|
const updatedFolders: FolderCollection = JSON.parse(
|
||||||
|
@ -145,27 +147,6 @@ const ChatFolder = ({
|
||||||
if (inputRef && inputRef.current) inputRef.current.focus();
|
if (inputRef && inputRef.current) inputRef.current.focus();
|
||||||
}, [isEdit]);
|
}, [isEdit]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleClickOutside = (event: MouseEvent) => {
|
|
||||||
if (
|
|
||||||
paletteRef.current &&
|
|
||||||
!paletteRef.current.contains(event.target as Node)
|
|
||||||
) {
|
|
||||||
setShowPalette(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (showPalette) {
|
|
||||||
document.addEventListener('mousedown', handleClickOutside);
|
|
||||||
} else {
|
|
||||||
document.removeEventListener('mousedown', handleClickOutside);
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
document.removeEventListener('mousedown', handleClickOutside);
|
|
||||||
};
|
|
||||||
}, [paletteRef, showPalette]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`w-full transition-colors group/folder ${
|
className={`w-full transition-colors group/folder ${
|
||||||
|
|
36
src/hooks/useHideOnOutsideClick.ts
Normal file
36
src/hooks/useHideOnOutsideClick.ts
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
|
const useHideOnOutsideClick = (): [
|
||||||
|
boolean,
|
||||||
|
React.Dispatch<React.SetStateAction<boolean>>,
|
||||||
|
React.RefObject<HTMLDivElement>
|
||||||
|
] => {
|
||||||
|
const elementRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [showElement, setShowElement] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
|
if (
|
||||||
|
elementRef.current &&
|
||||||
|
!elementRef.current.contains(event.target as Node)
|
||||||
|
) {
|
||||||
|
setShowElement(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Bind the event listener only if the element is show.
|
||||||
|
if (showElement) {
|
||||||
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
|
} else {
|
||||||
|
document.removeEventListener('mousedown', handleClickOutside);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('mousedown', handleClickOutside);
|
||||||
|
};
|
||||||
|
}, [showElement, elementRef]);
|
||||||
|
|
||||||
|
return [showElement, setShowElement, elementRef];
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useHideOnOutsideClick;
|
Loading…
Reference in a new issue