From 58cb40f031f395ca9efccff674ba0f2dae723f50 Mon Sep 17 00:00:00 2001 From: Armand Philippot Date: Wed, 8 Jun 2022 12:07:08 +0200 Subject: fix(settings): close tooltip when modal is closing The event was not captured so the tooltip remained open when the settings was closed. It prevented to click on the toolbar buttons. --- src/components/molecules/forms/fieldset.tsx | 25 ++++++----- src/components/molecules/forms/radio-group.tsx | 10 +++-- .../organisms/modals/settings-modal.module.scss | 1 - src/components/organisms/modals/settings-modal.tsx | 1 - src/components/organisms/toolbar/toolbar.tsx | 21 ++++----- src/utils/hooks/use-click-outside.tsx | 46 ------------------- src/utils/hooks/use-on-click-outside.tsx | 52 ++++++++++++++++++++++ 7 files changed, 83 insertions(+), 73 deletions(-) delete mode 100644 src/components/organisms/modals/settings-modal.module.scss delete mode 100644 src/utils/hooks/use-click-outside.tsx create mode 100644 src/utils/hooks/use-on-click-outside.tsx (limited to 'src') diff --git a/src/components/molecules/forms/fieldset.tsx b/src/components/molecules/forms/fieldset.tsx index 670cfa1..dae3ead 100644 --- a/src/components/molecules/forms/fieldset.tsx +++ b/src/components/molecules/forms/fieldset.tsx @@ -1,4 +1,4 @@ -import useClickOutside from '@utils/hooks/use-click-outside'; +import useOnClickOutside from '@utils/hooks/use-on-click-outside'; import { cloneElement, FC, @@ -33,11 +33,15 @@ export type FieldsetProps = { */ legendClassName?: string; /** - * The legend position. Default: stacked. + * The legend position. + * + * @default 'stacked' */ legendPosition?: 'inline' | 'stacked'; /** - * An accessible role. Default: group. + * An accessible role. + * + * @default 'group' */ role?: 'group' | 'radiogroup' | 'presentation' | 'none'; /** @@ -63,7 +67,6 @@ const Fieldset: FC = ({ }) => { const [isTooltipOpened, setIsTooltipOpened] = useState(false); const buttonRef = useRef(null); - const tooltipRef = useRef(null); const wrapperModifier = `wrapper--${legendPosition}`; const buttonModifier = isTooltipOpened ? styles['btn--activated'] : ''; const legendModifier = @@ -73,19 +76,17 @@ const Fieldset: FC = ({ : 'tooltip--hidden'; /** - * Close the tooltip if the event target is outside. + * Close the tooltip if the target is not the button. * - * @param {EventTarget} target - The event target. + * @param {Node} target - The event target. */ - const closeTooltip = (target: EventTarget) => { - if (buttonRef.current && !buttonRef.current.contains(target as Node)) + const closeTooltip = (target: Node) => { + if (buttonRef.current && !buttonRef.current.contains(target)) { setIsTooltipOpened(false); + } }; - useClickOutside( - tooltipRef, - (target) => isTooltipOpened && closeTooltip(target) - ); + const tooltipRef = useOnClickOutside(closeTooltip); return (
import('@components/organisms/forms/settings-form'), diff --git a/src/components/organisms/toolbar/toolbar.tsx b/src/components/organisms/toolbar/toolbar.tsx index c18b8ea..50fc7f2 100644 --- a/src/components/organisms/toolbar/toolbar.tsx +++ b/src/components/organisms/toolbar/toolbar.tsx @@ -1,9 +1,9 @@ -import useClickOutside from '@utils/hooks/use-click-outside'; +import useOnClickOutside from '@utils/hooks/use-on-click-outside'; import useRouteChange from '@utils/hooks/use-route-change'; -import { FC, useRef, useState } from 'react'; +import { FC, useState } from 'react'; import MainNav, { type MainNavProps } from '../toolbar/main-nav'; import Search, { type SearchProps } from '../toolbar/search'; -import Settings, { SettingsProps } from '../toolbar/settings'; +import Settings, { type SettingsProps } from '../toolbar/settings'; import styles from './toolbar.module.scss'; export type ToolbarProps = Pick & @@ -33,16 +33,17 @@ const Toolbar: FC = ({ const [isNavOpened, setIsNavOpened] = useState(false); const [isSearchOpened, setIsSearchOpened] = useState(false); const [isSettingsOpened, setIsSettingsOpened] = useState(false); - const mainNavRef = useRef(null); - const searchRef = useRef(null); - const settingsRef = useRef(null); - useClickOutside(mainNavRef, () => isNavOpened && setIsNavOpened(false)); - useClickOutside(searchRef, () => isSearchOpened && setIsSearchOpened(false)); - useClickOutside( - settingsRef, + const mainNavRef = useOnClickOutside( + () => isNavOpened && setIsNavOpened(false) + ); + const searchRef = useOnClickOutside( + () => isSearchOpened && setIsSearchOpened(false) + ); + const settingsRef = useOnClickOutside( () => isSettingsOpened && setIsSettingsOpened(false) ); + useRouteChange(() => setIsSearchOpened(false)); return ( diff --git a/src/utils/hooks/use-click-outside.tsx b/src/utils/hooks/use-click-outside.tsx deleted file mode 100644 index cead98b..0000000 --- a/src/utils/hooks/use-click-outside.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { RefObject, useCallback, useEffect } from 'react'; - -/** - * Listen for click/focus outside an element and execute the given callback. - * - * @param el - A React reference to an element. - * @param callback - A callback function to execute on click outside. - */ -const useClickOutside = ( - el: RefObject, - callback: (target: EventTarget) => void -) => { - /** - * Check if an event target is outside an element. - * - * @param {RefObject} ref - A React reference object. - * @param {EventTarget} target - An event target. - * @returns {boolean} True if the event target is outside the ref object. - */ - const isTargetOutside = ( - ref: RefObject, - target: EventTarget - ): boolean => { - if (!ref.current) return false; - return !ref.current.contains(target as Node); - }; - - const handleEvent = useCallback( - (e: MouseEvent | FocusEvent) => { - if (e.target && isTargetOutside(el, e.target)) callback(e.target); - }, - [el, callback] - ); - - useEffect(() => { - document.addEventListener('mousedown', handleEvent); - document.addEventListener('focusin', handleEvent); - - return () => { - document.removeEventListener('mousedown', handleEvent); - document.removeEventListener('focusin', handleEvent); - }; - }, [handleEvent]); -}; - -export default useClickOutside; diff --git a/src/utils/hooks/use-on-click-outside.tsx b/src/utils/hooks/use-on-click-outside.tsx new file mode 100644 index 0000000..04827b8 --- /dev/null +++ b/src/utils/hooks/use-on-click-outside.tsx @@ -0,0 +1,52 @@ +import { RefObject, useCallback, useEffect, useRef } from 'react'; + +export type UseOnClickOutsideCallback = (target: Node) => void; + +/** + * Detect click/focus outside an element and fire a callback function. + * + * @param {UseOnClickOutsideCallback} callback - A callback function to fire. + * @param {boolean} useCapture - Define event propagation method. Default: true. + * @returns {RefObject} A React reference object. + */ +const useOnClickOutside = ( + callback: UseOnClickOutsideCallback, + useCapture: boolean = true +): RefObject => { + const ref = useRef(null); + + /** + * Check if the target is outside the ref. + * + * @param {Node} target - The event target. + * @returns {boolean | null} True if the target is outside the ref. + */ + const isTargetOutside = (target: Node): boolean | null => { + return ref.current && !ref.current.contains(target); + }; + + /** + * Fire the callback if the event target is outside. + */ + const handler = useCallback( + (e: MouseEvent | FocusEvent) => { + if (e.target && isTargetOutside(e.target as Node)) + callback(e.target as Node); + }, + [callback] + ); + + useEffect(() => { + document.addEventListener('click', handler, useCapture); + document.addEventListener('focusin', handler, useCapture); + + return () => { + document.removeEventListener('click', handler); + document.removeEventListener('focusin', handler); + }; + }, [handler, useCapture]); + + return ref; +}; + +export default useOnClickOutside; -- cgit v1.2.3