From b214baab3e17d92f784b4f782863deafc5558ee4 Mon Sep 17 00:00:00 2001
From: Armand Philippot <git@armandphilippot.com>
Date: Wed, 18 May 2022 14:27:11 +0200
Subject: chore: close toolbar modals on click/focus outside

---
 src/utils/hooks/use-click-outside.tsx | 43 +++++++++++++++++++++++++++++++++++
 1 file changed, 43 insertions(+)
 create mode 100644 src/utils/hooks/use-click-outside.tsx

(limited to 'src/utils/hooks/use-click-outside.tsx')

diff --git a/src/utils/hooks/use-click-outside.tsx b/src/utils/hooks/use-click-outside.tsx
new file mode 100644
index 0000000..066c1c2
--- /dev/null
+++ b/src/utils/hooks/use-click-outside.tsx
@@ -0,0 +1,43 @@
+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<HTMLElement>, callback: () => void) => {
+  /**
+   * Check if an event target is outside an element.
+   *
+   * @param {RefObject<HTMLElement>} 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<HTMLElement>,
+    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();
+    },
+    [el, callback]
+  );
+
+  useEffect(() => {
+    document.addEventListener('mousedown', handleEvent);
+    document.addEventListener('focusin', handleEvent);
+
+    return () => {
+      document.removeEventListener('mousedown', handleEvent);
+      document.removeEventListener('focusin', handleEvent);
+    };
+  }, [handleEvent]);
+};
+
+export default useClickOutside;
-- 
cgit v1.2.3


From ef2407832202e5451751e26459e6bdcdbd152122 Mon Sep 17 00:00:00 2001
From: Armand Philippot <git@armandphilippot.com>
Date: Sat, 21 May 2022 16:04:06 +0200
Subject: fix: close Ackee tooltip on button click

The tooltip was closing and reopening on click because of a conflict
between the button event and the hook useClickOutside. By checking that
the event target is not the button, the tooltip is now closing as
expected.
---
 src/components/atoms/buttons/button.tsx            | 35 ++++++++++++++--------
 src/components/molecules/buttons/help-button.tsx   | 12 +++++---
 .../molecules/forms/select-with-tooltip.tsx        | 11 +++++--
 src/utils/hooks/use-click-outside.tsx              |  7 +++--
 4 files changed, 44 insertions(+), 21 deletions(-)

(limited to 'src/utils/hooks/use-click-outside.tsx')

diff --git a/src/components/atoms/buttons/button.tsx b/src/components/atoms/buttons/button.tsx
index a6eef8b..9776687 100644
--- a/src/components/atoms/buttons/button.tsx
+++ b/src/components/atoms/buttons/button.tsx
@@ -1,4 +1,9 @@
-import { FC, MouseEventHandler, ReactNode } from 'react';
+import {
+  forwardRef,
+  ForwardRefRenderFunction,
+  MouseEventHandler,
+  ReactNode,
+} from 'react';
 import styles from './buttons.module.scss';
 
 export type ButtonProps = {
@@ -41,23 +46,27 @@ export type ButtonProps = {
  *
  * Use a button as call to action.
  */
-const Button: FC<ButtonProps> = ({
-  className = '',
-  children,
-  disabled = false,
-  kind = 'secondary',
-  shape = 'rectangle',
-  type = 'button',
-  ...props
-}) => {
+const Button: ForwardRefRenderFunction<HTMLButtonElement, ButtonProps> = (
+  {
+    className = '',
+    children,
+    disabled = false,
+    kind = 'secondary',
+    shape = 'rectangle',
+    type = 'button',
+    ...props
+  },
+  ref
+) => {
   const kindClass = styles[`btn--${kind}`];
   const shapeClass = styles[`btn--${shape}`];
 
   return (
     <button
-      type={type}
-      disabled={disabled}
       className={`${styles.btn} ${kindClass} ${shapeClass} ${className}`}
+      disabled={disabled}
+      ref={ref}
+      type={type}
       {...props}
     >
       {children}
@@ -65,4 +74,4 @@ const Button: FC<ButtonProps> = ({
   );
 };
 
-export default Button;
+export default forwardRef(Button);
diff --git a/src/components/molecules/buttons/help-button.tsx b/src/components/molecules/buttons/help-button.tsx
index f19322f..e351453 100644
--- a/src/components/molecules/buttons/help-button.tsx
+++ b/src/components/molecules/buttons/help-button.tsx
@@ -1,5 +1,5 @@
 import Button, { type ButtonProps } from '@components/atoms/buttons/button';
-import { FC } from 'react';
+import { forwardRef, ForwardRefRenderFunction } from 'react';
 import { useIntl } from 'react-intl';
 import styles from './help-button.module.scss';
 
@@ -15,7 +15,10 @@ export type HelpButtonProps = Pick<ButtonProps, 'onClick'> & {
  *
  * Render a button with an interrogation mark icon.
  */
-const HelpButton: FC<HelpButtonProps> = ({ className = '', onClick }) => {
+const HelpButton: ForwardRefRenderFunction<
+  HTMLButtonElement,
+  HelpButtonProps
+> = ({ className = '', onClick }, ref) => {
   const intl = useIntl();
   const text = intl.formatMessage({
     defaultMessage: 'Help',
@@ -25,9 +28,10 @@ const HelpButton: FC<HelpButtonProps> = ({ className = '', onClick }) => {
 
   return (
     <Button
-      shape="circle"
       className={`${styles.btn} ${className}`}
       onClick={onClick}
+      ref={ref}
+      shape="circle"
     >
       <span className="screen-reader-text">{text}</span>
       <span className={styles.icon}>?</span>
@@ -35,4 +39,4 @@ const HelpButton: FC<HelpButtonProps> = ({ className = '', onClick }) => {
   );
 };
 
-export default HelpButton;
+export default forwardRef(HelpButton);
diff --git a/src/components/molecules/forms/select-with-tooltip.tsx b/src/components/molecules/forms/select-with-tooltip.tsx
index f576a15..29e2563 100644
--- a/src/components/molecules/forms/select-with-tooltip.tsx
+++ b/src/components/molecules/forms/select-with-tooltip.tsx
@@ -29,15 +29,21 @@ const SelectWithTooltip: FC<SelectWithTooltipProps> = ({
   ...props
 }) => {
   const [isTooltipOpened, setIsTooltipOpened] = useState<boolean>(false);
+  const buttonRef = useRef<HTMLButtonElement>(null);
   const tooltipRef = useRef<HTMLDivElement>(null);
   const buttonModifier = isTooltipOpened ? styles['btn--activated'] : '';
   const tooltipModifier = isTooltipOpened
     ? styles['tooltip--visible']
     : styles['tooltip--hidden'];
 
+  const closeTooltip = (target: EventTarget) => {
+    if (buttonRef.current && !buttonRef.current.contains(target as Node))
+      setIsTooltipOpened(false);
+  };
+
   useClickOutside(
     tooltipRef,
-    () => isTooltipOpened && setIsTooltipOpened(false)
+    (target) => isTooltipOpened && closeTooltip(target)
   );
 
   return (
@@ -49,8 +55,9 @@ const SelectWithTooltip: FC<SelectWithTooltipProps> = ({
         {...props}
       />
       <HelpButton
-        onClick={() => setIsTooltipOpened(!isTooltipOpened)}
         className={`${styles.btn} ${buttonModifier}`}
+        onClick={() => setIsTooltipOpened(!isTooltipOpened)}
+        ref={buttonRef}
       />
       <Tooltip
         title={title}
diff --git a/src/utils/hooks/use-click-outside.tsx b/src/utils/hooks/use-click-outside.tsx
index 066c1c2..cead98b 100644
--- a/src/utils/hooks/use-click-outside.tsx
+++ b/src/utils/hooks/use-click-outside.tsx
@@ -6,7 +6,10 @@ import { RefObject, useCallback, useEffect } from 'react';
  * @param el - A React reference to an element.
  * @param callback - A callback function to execute on click outside.
  */
-const useClickOutside = (el: RefObject<HTMLElement>, callback: () => void) => {
+const useClickOutside = (
+  el: RefObject<HTMLElement>,
+  callback: (target: EventTarget) => void
+) => {
   /**
    * Check if an event target is outside an element.
    *
@@ -24,7 +27,7 @@ const useClickOutside = (el: RefObject<HTMLElement>, callback: () => void) => {
 
   const handleEvent = useCallback(
     (e: MouseEvent | FocusEvent) => {
-      if (e.target && isTargetOutside(el, e.target)) callback();
+      if (e.target && isTargetOutside(el, e.target)) callback(e.target);
     },
     [el, callback]
   );
-- 
cgit v1.2.3

