From dc597ac765f8d943db8fed8fdf911ff69c45ec96 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 6 Feb 2023 17:17:12 -0600 Subject: [PATCH] Add useClickOutside hook for Floating UI elements --- .../ui/components/profile-dropdown.tsx | 24 +++------------ app/soapbox/hooks/index.ts | 1 + app/soapbox/hooks/useClickOutside.ts | 29 +++++++++++++++++++ 3 files changed, 34 insertions(+), 20 deletions(-) create mode 100644 app/soapbox/hooks/useClickOutside.ts diff --git a/app/soapbox/features/ui/components/profile-dropdown.tsx b/app/soapbox/features/ui/components/profile-dropdown.tsx index 732d35994..65c321a13 100644 --- a/app/soapbox/features/ui/components/profile-dropdown.tsx +++ b/app/soapbox/features/ui/components/profile-dropdown.tsx @@ -8,7 +8,7 @@ import { Link } from 'react-router-dom'; import { fetchOwnAccounts, logOut, switchAccount } from 'soapbox/actions/auth'; import Account from 'soapbox/components/account'; import { MenuDivider } from 'soapbox/components/ui'; -import { useAppDispatch, useAppSelector, useFeatures } from 'soapbox/hooks'; +import { useAppDispatch, useAppSelector, useClickOutside, useFeatures } from 'soapbox/hooks'; import { makeGetAccount } from 'soapbox/selectors'; import ThemeToggle from './theme-toggle'; @@ -102,29 +102,13 @@ const ProfileDropdown: React.FC = ({ account, children }) => { const toggleVisible = () => setVisible(!visible); - const handleWindowClick = (e: MouseEvent) => { - if (e.target) { - const clickWithin = [ - refs.floating.current?.contains(e.target as Node), - (refs.reference.current as HTMLButtonElement | undefined)?.contains(e.target as Node), - ].some(Boolean); - - if (!clickWithin) { - setVisible(false); - } - } - }; - useEffect(() => { fetchOwnAccountThrottled(); }, [account, authUsers]); - useEffect(() => { - window.addEventListener('click', handleWindowClick); - return () => { - window.removeEventListener('click', handleWindowClick); - }; - }, []); + useClickOutside(refs, () => { + setVisible(false); + }); return ( <> diff --git a/app/soapbox/hooks/index.ts b/app/soapbox/hooks/index.ts index 7c662bca2..6460938b6 100644 --- a/app/soapbox/hooks/index.ts +++ b/app/soapbox/hooks/index.ts @@ -2,6 +2,7 @@ export { useAccount } from './useAccount'; export { useApi } from './useApi'; export { useAppDispatch } from './useAppDispatch'; export { useAppSelector } from './useAppSelector'; +export { useClickOutside } from './useClickOutside'; export { useCompose } from './useCompose'; export { useDebounce } from './useDebounce'; export { useDimensions } from './useDimensions'; diff --git a/app/soapbox/hooks/useClickOutside.ts b/app/soapbox/hooks/useClickOutside.ts new file mode 100644 index 000000000..0bb7f387d --- /dev/null +++ b/app/soapbox/hooks/useClickOutside.ts @@ -0,0 +1,29 @@ +import { ExtendedRefs } from '@floating-ui/react'; +import { useCallback, useEffect } from 'react'; + +/** Trigger `callback` when a Floating UI element is clicked outside from. */ +export const useClickOutside = ( + refs: ExtendedRefs, + callback: (e: MouseEvent) => void, +) => { + const handleWindowClick = useCallback((e: MouseEvent) => { + if (e.target) { + const target = e.target as Node; + + const floating = refs.floating.current; + const reference = refs.reference.current as T | undefined; + + if (!(floating?.contains(target) || reference?.contains(target))) { + callback(e); + } + } + }, [refs.floating.current, refs.reference.current]); + + useEffect(() => { + window.addEventListener('click', handleWindowClick); + + return () => { + window.removeEventListener('click', handleWindowClick); + }; + }, []); +}; \ No newline at end of file