diff options
Diffstat (limited to 'src')
19 files changed, 258 insertions, 128 deletions
| diff --git a/src/components/molecules/forms/select-with-tooltip.tsx b/src/components/molecules/forms/select-with-tooltip.tsx index cf7b041..f576a15 100644 --- a/src/components/molecules/forms/select-with-tooltip.tsx +++ b/src/components/molecules/forms/select-with-tooltip.tsx @@ -1,4 +1,5 @@ -import { FC, useState } from 'react'; +import useClickOutside from '@utils/hooks/use-click-outside'; +import { FC, useRef, useState } from 'react';  import HelpButton from '../buttons/help-button';  import Tooltip, { type TooltipProps } from '../modals/tooltip';  import LabelledSelect, { type LabelledSelectProps } from './labelled-select'; @@ -28,11 +29,17 @@ const SelectWithTooltip: FC<SelectWithTooltipProps> = ({    ...props  }) => {    const [isTooltipOpened, setIsTooltipOpened] = useState<boolean>(false); +  const tooltipRef = useRef<HTMLDivElement>(null);    const buttonModifier = isTooltipOpened ? styles['btn--activated'] : '';    const tooltipModifier = isTooltipOpened      ? styles['tooltip--visible']      : styles['tooltip--hidden']; +  useClickOutside( +    tooltipRef, +    () => isTooltipOpened && setIsTooltipOpened(false) +  ); +    return (      <div className={styles.wrapper}>        <LabelledSelect @@ -50,6 +57,7 @@ const SelectWithTooltip: FC<SelectWithTooltipProps> = ({          content={content}          icon="?"          className={`${styles.tooltip} ${tooltipModifier} ${tooltipClassName}`} +        ref={tooltipRef}        />      </div>    ); diff --git a/src/components/molecules/modals/tooltip.tsx b/src/components/molecules/modals/tooltip.tsx index 80721f3..efb3009 100644 --- a/src/components/molecules/modals/tooltip.tsx +++ b/src/components/molecules/modals/tooltip.tsx @@ -1,5 +1,5 @@  import List, { type ListItem } from '@components/atoms/lists/list'; -import { FC, ReactNode } from 'react'; +import { forwardRef, ForwardRefRenderFunction, ReactNode } from 'react';  import styles from './tooltip.module.scss';  export type TooltipProps = { @@ -26,12 +26,10 @@ export type TooltipProps = {   *   * Render a tooltip modal.   */ -const Tooltip: FC<TooltipProps> = ({ -  className = '', -  content, -  icon, -  title, -}) => { +const Tooltip: ForwardRefRenderFunction<HTMLDivElement, TooltipProps> = ( +  { className = '', content, icon, title }, +  ref +) => {    /**     * Format an array of strings to an array of object with id and value.     * @@ -45,7 +43,7 @@ const Tooltip: FC<TooltipProps> = ({    };    return ( -    <div className={`${styles.wrapper} ${className}`}> +    <div className={`${styles.wrapper} ${className}`} ref={ref}>        <div className={styles.title}>          <span className={styles.icon}>{icon}</span>          {title} @@ -59,4 +57,4 @@ const Tooltip: FC<TooltipProps> = ({    );  }; -export default Tooltip; +export default forwardRef(Tooltip); diff --git a/src/components/organisms/forms/settings-form.module.scss b/src/components/organisms/forms/settings-form.module.scss new file mode 100644 index 0000000..a6a2077 --- /dev/null +++ b/src/components/organisms/forms/settings-form.module.scss @@ -0,0 +1,11 @@ +@use "@styles/abstracts/mixins" as mix; + +.label { +  margin-right: auto; + +  @include mix.media("screen") { +    @include mix.dimensions(null, "2xs", "height") { +      font-size: var(--font-size-sm); +    } +  } +} diff --git a/src/components/organisms/forms/settings-form.stories.tsx b/src/components/organisms/forms/settings-form.stories.tsx new file mode 100644 index 0000000..46305e7 --- /dev/null +++ b/src/components/organisms/forms/settings-form.stories.tsx @@ -0,0 +1,47 @@ +import { ComponentMeta, ComponentStory } from '@storybook/react'; +import SettingsForm from './settings-form'; + +/** + * SettingsModal - Storybook Meta + */ +export default { +  title: 'Organisms/Forms', +  component: SettingsForm, +  argTypes: { +    className: { +      control: { +        type: 'text', +      }, +      description: 'Set additional classnames to the modal wrapper.', +      table: { +        category: 'Styles', +      }, +      type: { +        name: 'string', +        required: false, +      }, +    }, +    tooltipClassName: { +      control: { +        type: 'text', +      }, +      description: 'Set additional classnames to the tooltip wrapper.', +      table: { +        category: 'Styles', +      }, +      type: { +        name: 'string', +        required: false, +      }, +    }, +  }, +} as ComponentMeta<typeof SettingsForm>; + +const Template: ComponentStory<typeof SettingsForm> = (args) => ( +  <SettingsForm {...args} /> +); + +/** + * Form Stories - Settings + */ +export const Settings = Template.bind({}); diff --git a/src/components/organisms/forms/settings-form.test.tsx b/src/components/organisms/forms/settings-form.test.tsx new file mode 100644 index 0000000..beb65ec --- /dev/null +++ b/src/components/organisms/forms/settings-form.test.tsx @@ -0,0 +1,39 @@ +import { render, screen } from '@test-utils'; +import SettingsForm from './settings-form'; + +describe('SettingsForm', () => { +  it('renders a form', () => { +    render(<SettingsForm />); +    expect( +      screen.getByRole('form', { name: /^Settings form/i }) +    ).toBeInTheDocument(); +  }); + +  it('renders a theme toggle setting', () => { +    render(<SettingsForm />); +    expect( +      screen.getByRole('checkbox', { name: /^Theme:/i }) +    ).toBeInTheDocument(); +  }); + +  it('renders a code blocks toggle setting', () => { +    render(<SettingsForm />); +    expect( +      screen.getByRole('checkbox', { name: /^Code blocks:/i }) +    ).toBeInTheDocument(); +  }); + +  it('renders a motion setting', () => { +    render(<SettingsForm />); +    expect( +      screen.getByRole('checkbox', { name: /^Animations:/i }) +    ).toBeInTheDocument(); +  }); + +  it('renders a Ackee setting', () => { +    render(<SettingsForm />); +    expect( +      screen.getByRole('combobox', { name: /^Tracking:/i }) +    ).toBeInTheDocument(); +  }); +}); diff --git a/src/components/organisms/forms/settings-form.tsx b/src/components/organisms/forms/settings-form.tsx new file mode 100644 index 0000000..0a34601 --- /dev/null +++ b/src/components/organisms/forms/settings-form.tsx @@ -0,0 +1,36 @@ +import Form from '@components/atoms/forms/form'; +import AckeeSelect, { +  type AckeeSelectProps, +} from '@components/molecules/forms/ackee-select'; +import MotionToggle from '@components/molecules/forms/motion-toggle'; +import PrismThemeToggle from '@components/molecules/forms/prism-theme-toggle'; +import ThemeToggle from '@components/molecules/forms/theme-toggle'; +import { FC } from 'react'; +import { useIntl } from 'react-intl'; +import styles from './settings-form.module.scss'; + +export type SettingsFormProps = Pick<AckeeSelectProps, 'tooltipClassName'>; + +const SettingsForm: FC<SettingsFormProps> = ({ tooltipClassName }) => { +  const intl = useIntl(); +  const ariaLabel = intl.formatMessage({ +    defaultMessage: 'Settings form', +    id: 'gX+YVy', +    description: 'SettingsForm: an accessible form name', +  }); + +  return ( +    <Form aria-label={ariaLabel} onSubmit={() => null}> +      <ThemeToggle labelClassName={styles.label} value={false} /> +      <PrismThemeToggle labelClassName={styles.label} value={false} /> +      <MotionToggle labelClassName={styles.label} value={false} /> +      <AckeeSelect +        initialValue="full" +        labelClassName={styles.label} +        tooltipClassName={tooltipClassName} +      /> +    </Form> +  ); +}; + +export default SettingsForm; diff --git a/src/components/organisms/modals/search-modal.tsx b/src/components/organisms/modals/search-modal.tsx index 866bc25..e92bf1b 100644 --- a/src/components/organisms/modals/search-modal.tsx +++ b/src/components/organisms/modals/search-modal.tsx @@ -1,9 +1,18 @@ +import Spinner from '@components/atoms/loaders/spinner';  import Modal, { type ModalProps } from '@components/molecules/modals/modal'; +import dynamic from 'next/dynamic';  import { FC } from 'react';  import { useIntl } from 'react-intl'; -import SearchForm, { SearchFormProps } from '../forms/search-form'; +import { type SearchFormProps } from '../forms/search-form';  import styles from './search-modal.module.scss'; +const DynamicSearchForm = dynamic( +  () => import('@components/organisms/forms/search-form'), +  { +    loading: () => <Spinner />, +  } +); +  export type SearchModalProps = Pick<SearchFormProps, 'searchPage'> & {    /**     * Set additional classnames to modal wrapper. @@ -26,7 +35,7 @@ const SearchModal: FC<SearchModalProps> = ({ className, searchPage }) => {    return (      <Modal title={modalTitle} className={`${styles.wrapper} ${className}`}> -      <SearchForm hideLabel={true} searchPage={searchPage} /> +      <DynamicSearchForm hideLabel={true} searchPage={searchPage} />      </Modal>    );  }; diff --git a/src/components/organisms/modals/settings-modal.module.scss b/src/components/organisms/modals/settings-modal.module.scss index ebae3da..a6a2077 100644 --- a/src/components/organisms/modals/settings-modal.module.scss +++ b/src/components/organisms/modals/settings-modal.module.scss @@ -1,21 +1,11 @@  @use "@styles/abstracts/mixins" as mix; -.wrapper { -  .label { -    margin-right: auto; -  } +.label { +  margin-right: auto;    @include mix.media("screen") {      @include mix.dimensions(null, "2xs", "height") {        font-size: var(--font-size-sm); - -      .heading { -        font-size: var(--font-size-lg); -      } - -      .label { -        font-size: var(--font-size-sm); -      }      }    }  } diff --git a/src/components/organisms/modals/settings-modal.stories.tsx b/src/components/organisms/modals/settings-modal.stories.tsx index 0abe004..0fe8c18 100644 --- a/src/components/organisms/modals/settings-modal.stories.tsx +++ b/src/components/organisms/modals/settings-modal.stories.tsx @@ -1,5 +1,4 @@  import { ComponentMeta, ComponentStory } from '@storybook/react'; -import { IntlProvider } from 'react-intl';  import SettingsModal from './settings-modal';  /** @@ -36,13 +35,6 @@ export default {        },      },    }, -  decorators: [ -    (Story) => ( -      <IntlProvider locale="en"> -        <Story /> -      </IntlProvider> -    ), -  ],  } as ComponentMeta<typeof SettingsModal>;  const Template: ComponentStory<typeof SettingsModal> = (args) => ( diff --git a/src/components/organisms/modals/settings-modal.test.tsx b/src/components/organisms/modals/settings-modal.test.tsx index 6291e54..acbf7d1 100644 --- a/src/components/organisms/modals/settings-modal.test.tsx +++ b/src/components/organisms/modals/settings-modal.test.tsx @@ -2,31 +2,8 @@ import { render, screen } from '@test-utils';  import SettingsModal from './settings-modal';  describe('SettingsModal', () => { -  it('renders a theme toggle setting', () => { +  it('renders a fake heading', () => {      render(<SettingsModal />); -    expect( -      screen.getByRole('checkbox', { name: /^Theme:/i }) -    ).toBeInTheDocument(); -  }); - -  it('renders a code blocks toggle setting', () => { -    render(<SettingsModal />); -    expect( -      screen.getByRole('checkbox', { name: /^Code blocks:/i }) -    ).toBeInTheDocument(); -  }); - -  it('renders a motion setting', () => { -    render(<SettingsModal />); -    expect( -      screen.getByRole('checkbox', { name: /^Animations:/i }) -    ).toBeInTheDocument(); -  }); - -  it('renders a Ackee setting', () => { -    render(<SettingsModal />); -    expect( -      screen.getByRole('combobox', { name: /^Tracking:/i }) -    ).toBeInTheDocument(); +    expect(screen.getByText(/Settings/i)).toBeInTheDocument();    });  }); diff --git a/src/components/organisms/modals/settings-modal.tsx b/src/components/organisms/modals/settings-modal.tsx index 20d2605..e724076 100644 --- a/src/components/organisms/modals/settings-modal.tsx +++ b/src/components/organisms/modals/settings-modal.tsx @@ -1,25 +1,20 @@ -import Form from '@components/atoms/forms/form'; -import AckeeSelect, { -  type AckeeSelectProps, -} from '@components/molecules/forms/ackee-select'; -import MotionToggle from '@components/molecules/forms/motion-toggle'; -import PrismThemeToggle from '@components/molecules/forms/prism-theme-toggle'; -import ThemeToggle from '@components/molecules/forms/theme-toggle'; +import Spinner from '@components/atoms/loaders/spinner';  import Modal, { type ModalProps } from '@components/molecules/modals/modal'; +import dynamic from 'next/dynamic';  import { FC } from 'react';  import { useIntl } from 'react-intl'; +import { type SettingsFormProps } from '../forms/settings-form';  import styles from './settings-modal.module.scss'; -export type SettingsModalProps = { -  /** -   * Set additional classnames to the modal wrapper. -   */ -  className?: ModalProps['className']; -  /** -   * Set additional classnames to the tooltip wrapper. -   */ -  tooltipClassName?: AckeeSelectProps['tooltipClassName']; -}; +const DynamicSettingsForm = dynamic( +  () => import('@components/organisms/forms/settings-form'), +  { +    loading: () => <Spinner />, +  } +); + +export type SettingsModalProps = Pick<ModalProps, 'className'> & +  Pick<SettingsFormProps, 'tooltipClassName'>;  /**   * SettingsModal component @@ -28,7 +23,7 @@ export type SettingsModalProps = {   */  const SettingsModal: FC<SettingsModalProps> = ({    className = '', -  tooltipClassName = '', +  ...props  }) => {    const intl = useIntl();    const title = intl.formatMessage({ @@ -44,16 +39,7 @@ const SettingsModal: FC<SettingsModalProps> = ({        className={`${styles.wrapper} ${className}`}        headingClassName={styles.heading}      > -      <Form onSubmit={() => null}> -        <ThemeToggle labelClassName={styles.label} value={false} /> -        <PrismThemeToggle labelClassName={styles.label} value={false} /> -        <MotionToggle labelClassName={styles.label} value={false} /> -        <AckeeSelect -          initialValue="full" -          labelClassName={styles.label} -          tooltipClassName={tooltipClassName} -        /> -      </Form> +      <DynamicSettingsForm {...props} />      </Modal>    );  }; diff --git a/src/components/organisms/toolbar/main-nav.tsx b/src/components/organisms/toolbar/main-nav.tsx index 35e3fd6..d205112 100644 --- a/src/components/organisms/toolbar/main-nav.tsx +++ b/src/components/organisms/toolbar/main-nav.tsx @@ -5,7 +5,7 @@ import Nav, {    type NavProps,    type NavItem,  } from '@components/molecules/nav/nav'; -import { FC } from 'react'; +import { forwardRef, ForwardRefRenderFunction } from 'react';  import { useIntl } from 'react-intl';  import mainNavStyles from './main-nav.module.scss';  import sharedStyles from './toolbar-items.module.scss'; @@ -34,12 +34,10 @@ export type MainNavProps = {   *   * Render the main navigation.   */ -const MainNav: FC<MainNavProps> = ({ -  className = '', -  isActive, -  items, -  setIsActive, -}) => { +const MainNav: ForwardRefRenderFunction<HTMLDivElement, MainNavProps> = ( +  { className = '', isActive, items, setIsActive }, +  ref +) => {    const intl = useIntl();    const label = isActive      ? intl.formatMessage({ @@ -54,7 +52,7 @@ const MainNav: FC<MainNavProps> = ({        });    return ( -    <div className={`${sharedStyles.item} ${mainNavStyles.item}`}> +    <div className={`${sharedStyles.item} ${mainNavStyles.item}`} ref={ref}>        <Checkbox          id="main-nav-button"          name="main-nav-button" @@ -79,4 +77,4 @@ const MainNav: FC<MainNavProps> = ({    );  }; -export default MainNav; +export default forwardRef(MainNav); diff --git a/src/components/organisms/toolbar/search.stories.tsx b/src/components/organisms/toolbar/search.stories.tsx index c6063a0..6aaffde 100644 --- a/src/components/organisms/toolbar/search.stories.tsx +++ b/src/components/organisms/toolbar/search.stories.tsx @@ -1,6 +1,5 @@  import { ComponentMeta, ComponentStory } from '@storybook/react';  import { useState } from 'react'; -import { IntlProvider } from 'react-intl';  import Search from './search';  /** diff --git a/src/components/organisms/toolbar/search.test.tsx b/src/components/organisms/toolbar/search.test.tsx index a18b679..7c77eac 100644 --- a/src/components/organisms/toolbar/search.test.tsx +++ b/src/components/organisms/toolbar/search.test.tsx @@ -11,9 +11,4 @@ describe('Search', () => {      render(<Search searchPage="#" isActive={true} setIsActive={() => null} />);      expect(screen.getByRole('checkbox')).toHaveAccessibleName('Close search');    }); - -  it('renders a search form', () => { -    render(<Search searchPage="#" isActive={true} setIsActive={() => null} />); -    expect(screen.getByRole('searchbox')).toBeInTheDocument(); -  });  }); diff --git a/src/components/organisms/toolbar/search.tsx b/src/components/organisms/toolbar/search.tsx index a1471ef..5695348 100644 --- a/src/components/organisms/toolbar/search.tsx +++ b/src/components/organisms/toolbar/search.tsx @@ -1,7 +1,7 @@  import Checkbox, { type CheckboxProps } from '@components/atoms/forms/checkbox';  import Label from '@components/atoms/forms/label';  import MagnifyingGlass from '@components/atoms/icons/magnifying-glass'; -import { FC } from 'react'; +import { forwardRef, ForwardRefRenderFunction } from 'react';  import { useIntl } from 'react-intl';  import SearchModal, { type SearchModalProps } from '../modals/search-modal';  import searchStyles from './search.module.scss'; @@ -26,12 +26,10 @@ export type SearchProps = {    setIsActive: CheckboxProps['setValue'];  }; -const Search: FC<SearchProps> = ({ -  className = '', -  isActive, -  searchPage, -  setIsActive, -}) => { +const Search: ForwardRefRenderFunction<HTMLDivElement, SearchProps> = ( +  { className = '', isActive, searchPage, setIsActive }, +  ref +) => {    const intl = useIntl();    const label = isActive      ? intl.formatMessage({ @@ -46,7 +44,7 @@ const Search: FC<SearchProps> = ({        });    return ( -    <div className={`${sharedStyles.item} ${searchStyles.item}`}> +    <div className={`${sharedStyles.item} ${searchStyles.item}`} ref={ref}>        <Checkbox          id="search-button"          name="search-button" @@ -69,4 +67,4 @@ const Search: FC<SearchProps> = ({    );  }; -export default Search; +export default forwardRef(Search); diff --git a/src/components/organisms/toolbar/settings.stories.tsx b/src/components/organisms/toolbar/settings.stories.tsx index 1ec0897..aab4b9e 100644 --- a/src/components/organisms/toolbar/settings.stories.tsx +++ b/src/components/organisms/toolbar/settings.stories.tsx @@ -1,6 +1,5 @@  import { ComponentMeta, ComponentStory } from '@storybook/react';  import { useState } from 'react'; -import { IntlProvider } from 'react-intl';  import Settings from './settings';  /** @@ -57,13 +56,6 @@ export default {        },      },    }, -  decorators: [ -    (Story) => ( -      <IntlProvider locale="en"> -        <Story /> -      </IntlProvider> -    ), -  ],  } as ComponentMeta<typeof Settings>;  const Template: ComponentStory<typeof Settings> = ({ diff --git a/src/components/organisms/toolbar/settings.tsx b/src/components/organisms/toolbar/settings.tsx index 3b10226..43d3190 100644 --- a/src/components/organisms/toolbar/settings.tsx +++ b/src/components/organisms/toolbar/settings.tsx @@ -1,7 +1,7 @@  import Checkbox, { type CheckboxProps } from '@components/atoms/forms/checkbox';  import Label from '@components/atoms/forms/label';  import Cog from '@components/atoms/icons/cog'; -import { FC } from 'react'; +import { FC, forwardRef, ForwardRefRenderFunction } from 'react';  import { useIntl } from 'react-intl';  import SettingsModal, {    type SettingsModalProps, @@ -28,12 +28,10 @@ export type SettingsProps = {    tooltipClassName?: SettingsModalProps['tooltipClassName'];  }; -const Settings: FC<SettingsProps> = ({ -  className = '', -  isActive, -  setIsActive, -  tooltipClassName = '', -}) => { +const Settings: ForwardRefRenderFunction<HTMLDivElement, SettingsProps> = ( +  { className = '', isActive, setIsActive, tooltipClassName = '' }, +  ref +) => {    const intl = useIntl();    const label = isActive      ? intl.formatMessage({ @@ -48,7 +46,7 @@ const Settings: FC<SettingsProps> = ({        });    return ( -    <div className={`${sharedStyles.item} ${settingsStyles.item}`}> +    <div className={`${sharedStyles.item} ${settingsStyles.item}`} ref={ref}>        <Checkbox          id="settings-button"          name="settings-button" @@ -71,4 +69,4 @@ const Settings: FC<SettingsProps> = ({    );  }; -export default Settings; +export default forwardRef(Settings); diff --git a/src/components/organisms/toolbar/toolbar.tsx b/src/components/organisms/toolbar/toolbar.tsx index 6593055..e4188fe 100644 --- a/src/components/organisms/toolbar/toolbar.tsx +++ b/src/components/organisms/toolbar/toolbar.tsx @@ -1,4 +1,5 @@ -import { FC, useState } from 'react'; +import useClickOutside from '@utils/hooks/use-click-outside'; +import { FC, useRef, useState } from 'react';  import MainNav, { type MainNavProps } from '../toolbar/main-nav';  import Search, { type SearchProps } from '../toolbar/search';  import Settings from '../toolbar/settings'; @@ -22,8 +23,18 @@ export type ToolbarProps = Pick<SearchProps, 'searchPage'> & {   */  const Toolbar: FC<ToolbarProps> = ({ className = '', nav, searchPage }) => {    const [isNavOpened, setIsNavOpened] = useState<boolean>(false); -  const [isSettingsOpened, setIsSettingsOpened] = useState<boolean>(false);    const [isSearchOpened, setIsSearchOpened] = useState<boolean>(false); +  const [isSettingsOpened, setIsSettingsOpened] = useState<boolean>(false); +  const mainNavRef = useRef<HTMLDivElement>(null); +  const searchRef = useRef<HTMLDivElement>(null); +  const settingsRef = useRef<HTMLDivElement>(null); + +  useClickOutside(mainNavRef, () => isNavOpened && setIsNavOpened(false)); +  useClickOutside(searchRef, () => isSearchOpened && setIsSearchOpened(false)); +  useClickOutside( +    settingsRef, +    () => isSettingsOpened && setIsSettingsOpened(false) +  );    return (      <div className={`${styles.wrapper} ${className}`}> @@ -32,18 +43,21 @@ const Toolbar: FC<ToolbarProps> = ({ className = '', nav, searchPage }) => {          isActive={isNavOpened}          setIsActive={setIsNavOpened}          className={styles.modal} +        ref={mainNavRef}        />        <Search          searchPage={searchPage}          isActive={isSearchOpened}          setIsActive={setIsSearchOpened}          className={`${styles.modal} ${styles['modal--search']}`} +        ref={searchRef}        />        <Settings          isActive={isSettingsOpened}          setIsActive={setIsSettingsOpened}          className={`${styles.modal} ${styles['modal--settings']}`}          tooltipClassName={styles.tooltip} +        ref={settingsRef}        />      </div>    ); 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; | 
