diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/components/molecules/forms/flipping-label/flipping-label.stories.tsx | 7 | ||||
| -rw-r--r-- | src/components/organisms/forms/ackee-toggle/ackee-toggle.tsx | 17 | ||||
| -rw-r--r-- | src/components/organisms/forms/comment-form/comment-form.tsx | 17 | ||||
| -rw-r--r-- | src/components/organisms/forms/contact-form/contact-form.tsx | 17 | ||||
| -rw-r--r-- | src/components/organisms/layout/comment.tsx | 13 | ||||
| -rw-r--r-- | src/components/organisms/toolbar/main-nav.stories.tsx | 8 | ||||
| -rw-r--r-- | src/components/organisms/toolbar/search.stories.tsx | 16 | ||||
| -rw-r--r-- | src/components/organisms/toolbar/settings.stories.tsx | 8 | ||||
| -rw-r--r-- | src/components/organisms/toolbar/toolbar.tsx | 53 | ||||
| -rw-r--r-- | src/utils/hooks/index.ts | 2 | ||||
| -rw-r--r-- | src/utils/hooks/use-boolean/index.ts | 1 | ||||
| -rw-r--r-- | src/utils/hooks/use-boolean/use-boolean.test.ts | 45 | ||||
| -rw-r--r-- | src/utils/hooks/use-boolean/use-boolean.ts | 44 | ||||
| -rw-r--r-- | src/utils/hooks/use-toggle/index.ts | 1 | ||||
| -rw-r--r-- | src/utils/hooks/use-toggle/use-toggle.test.ts | 24 | ||||
| -rw-r--r-- | src/utils/hooks/use-toggle/use-toggle.ts | 15 | 
16 files changed, 204 insertions, 84 deletions
| diff --git a/src/components/molecules/forms/flipping-label/flipping-label.stories.tsx b/src/components/molecules/forms/flipping-label/flipping-label.stories.tsx index c3c4f9a..906a488 100644 --- a/src/components/molecules/forms/flipping-label/flipping-label.stories.tsx +++ b/src/components/molecules/forms/flipping-label/flipping-label.stories.tsx @@ -1,5 +1,5 @@  import type { ComponentMeta, ComponentStory } from '@storybook/react'; -import { useCallback, useState } from 'react'; +import { useToggle } from '../../../../utils/hooks';  import { Button, Icon } from '../../../atoms';  import { FlippingLabel } from './flipping-label'; @@ -74,11 +74,10 @@ const Template: ComponentStory<typeof FlippingLabel> = ({    isActive,    ...args  }) => { -  const [active, setActive] = useState<boolean>(isActive); -  const updateState = useCallback(() => setActive((prev) => !prev), []); +  const [active, toggle] = useToggle(isActive);    return ( -    <Button kind="neutral" onClick={updateState} shape="initial" type="button"> +    <Button kind="neutral" onClick={toggle} shape="initial" type="button">        <FlippingLabel {...args} isActive={active} />      </Button>    ); diff --git a/src/components/organisms/forms/ackee-toggle/ackee-toggle.tsx b/src/components/organisms/forms/ackee-toggle/ackee-toggle.tsx index 9493095..2fea0a7 100644 --- a/src/components/organisms/forms/ackee-toggle/ackee-toggle.tsx +++ b/src/components/organisms/forms/ackee-toggle/ackee-toggle.tsx @@ -1,7 +1,7 @@  /* eslint-disable max-statements */ -import { type FC, useState, useCallback } from 'react'; +import type { FC } from 'react';  import { useIntl } from 'react-intl'; -import { useAckee } from '../../../../utils/hooks'; +import { useAckee, useBoolean } from '../../../../utils/hooks';  import { Legend, List, ListItem } from '../../../atoms';  import {    Switch, @@ -25,7 +25,11 @@ export type AckeeToggleProps = Omit<  export const AckeeToggle: FC<AckeeToggleProps> = ({ direction, ...props }) => {    const intl = useIntl();    const [tracking, toggleTracking] = useAckee(); -  const [isTooltipOpened, setIsTooltipOpened] = useState(false); +  const { +    deactivate: closeTooltip, +    state: isTooltipOpened, +    toggle: toggleTooltip, +  } = useBoolean(false);    const ackeeLabel = intl.formatMessage({      defaultMessage: 'Tracking:', @@ -64,13 +68,6 @@ export const AckeeToggle: FC<AckeeToggleProps> = ({ direction, ...props }) => {      { id: 'ackee-partial' as const, label: partialLabel, value: 'partial' },    ] satisfies [SwitchOption, SwitchOption]; -  const closeTooltip = useCallback(() => { -    setIsTooltipOpened(false); -  }, []); -  const toggleTooltip = useCallback(() => { -    setIsTooltipOpened((prev) => !prev); -  }, []); -    return (      <Switch        {...props} diff --git a/src/components/organisms/forms/comment-form/comment-form.tsx b/src/components/organisms/forms/comment-form/comment-form.tsx index b5f2d64..9059cbc 100644 --- a/src/components/organisms/forms/comment-form/comment-form.tsx +++ b/src/components/organisms/forms/comment-form/comment-form.tsx @@ -10,6 +10,7 @@ import {    useId,  } from 'react';  import { useIntl } from 'react-intl'; +import { useBoolean } from '../../../../utils/hooks';  import {    Button,    Form, @@ -77,15 +78,19 @@ export const CommentForm: FC<CommentFormProps> = ({      };    }, [parentId]);    const [data, setData] = useState(emptyForm); -  const [isSubmitting, setIsSubmitting] = useState<boolean>(false); +  const { +    activate: activateNotice, +    deactivate: deactivateNotice, +    state: isSubmitting, +  } = useBoolean(false);    /**     * Reset all the form fields.     */    const resetForm = useCallback(() => {      setData(emptyForm); -    setIsSubmitting(false); -  }, [emptyForm]); +    deactivateNotice(); +  }, [deactivateNotice, emptyForm]);    const nameLabel = intl.formatMessage({      defaultMessage: 'Name:', @@ -160,10 +165,10 @@ export const CommentForm: FC<CommentFormProps> = ({    const sendForm = useCallback(      (e: FormEvent) => {        e.preventDefault(); -      setIsSubmitting(true); -      saveComment(data, resetForm).then(() => setIsSubmitting(false)); +      activateNotice(); +      saveComment(data, resetForm).then(() => deactivateNotice());      }, -    [data, resetForm, saveComment] +    [activateNotice, data, deactivateNotice, resetForm, saveComment]    );    return ( diff --git a/src/components/organisms/forms/contact-form/contact-form.tsx b/src/components/organisms/forms/contact-form/contact-form.tsx index 89fd331..ed23aad 100644 --- a/src/components/organisms/forms/contact-form/contact-form.tsx +++ b/src/components/organisms/forms/contact-form/contact-form.tsx @@ -9,6 +9,7 @@ import {    useMemo,  } from 'react';  import { useIntl } from 'react-intl'; +import { useBoolean } from '../../../../utils/hooks';  import { Button, Form, Input, Label, Spinner, TextArea } from '../../../atoms';  import { LabelledField } from '../../../molecules';  import styles from './contact-form.module.scss'; @@ -56,15 +57,19 @@ export const ContactForm: FC<ContactFormProps> = ({      };    }, []);    const [data, setData] = useState(emptyForm); -  const [isSubmitting, setIsSubmitting] = useState<boolean>(false); +  const { +    activate: activateNotice, +    deactivate: deactivateNotice, +    state: isSubmitting, +  } = useBoolean(false);    /**     * Reset all the form fields.     */    const resetForm = useCallback(() => {      setData(emptyForm); -    setIsSubmitting(false); -  }, [emptyForm]); +    deactivateNotice(); +  }, [deactivateNotice, emptyForm]);    const updateForm = useCallback(      (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => { @@ -135,10 +140,10 @@ export const ContactForm: FC<ContactFormProps> = ({    const submitHandler = useCallback(      async (e: FormEvent) => {        e.preventDefault(); -      setIsSubmitting(true); -      await sendMail(data, resetForm).then(() => setIsSubmitting(false)); +      activateNotice(); +      await sendMail(data, resetForm).then(() => deactivateNotice());      }, -    [data, resetForm, sendMail] +    [activateNotice, data, deactivateNotice, resetForm, sendMail]    );    return ( diff --git a/src/components/organisms/layout/comment.tsx b/src/components/organisms/layout/comment.tsx index db7cb3a..adbb2cc 100644 --- a/src/components/organisms/layout/comment.tsx +++ b/src/components/organisms/layout/comment.tsx @@ -1,11 +1,11 @@  /* eslint-disable max-statements */  import NextImage from 'next/image';  import Script from 'next/script'; -import { type FC, useCallback, useState } from 'react'; +import type { FC } from 'react';  import { useIntl } from 'react-intl';  import type { Comment as CommentSchema, WithContext } from 'schema-dts';  import type { SingleComment } from '../../../types'; -import { useSettings } from '../../../utils/hooks'; +import { useSettings, useToggle } from '../../../utils/hooks';  import { Button, Link, Time } from '../../atoms';  import {    Card, @@ -49,12 +49,7 @@ export const UserComment: FC<UserCommentProps> = ({  }) => {    const intl = useIntl();    const { website } = useSettings(); -  const [isReplying, setIsReplying] = useState<boolean>(false); - -  const handleReply = useCallback( -    () => setIsReplying((prevState) => !prevState), -    [] -  ); +  const [isReplying, toggleIsReplying] = useToggle(false);    if (!approved) {      return ( @@ -170,7 +165,7 @@ export const UserComment: FC<UserCommentProps> = ({          {canReply ? (            <CardFooter>              <CardActions> -              <Button kind="tertiary" onClick={handleReply}> +              <Button kind="tertiary" onClick={toggleIsReplying}>                  {buttonLabel}                </Button>              </CardActions> diff --git a/src/components/organisms/toolbar/main-nav.stories.tsx b/src/components/organisms/toolbar/main-nav.stories.tsx index d79addf..31e2b65 100644 --- a/src/components/organisms/toolbar/main-nav.stories.tsx +++ b/src/components/organisms/toolbar/main-nav.stories.tsx @@ -1,5 +1,5 @@  import type { ComponentMeta, ComponentStory } from '@storybook/react'; -import { useCallback, useState } from 'react'; +import { useToggle } from '../../../utils/hooks';  import { MainNavItem } from './main-nav';  /** @@ -61,11 +61,7 @@ const Template: ComponentStory<typeof MainNavItem> = ({    setIsActive: _setIsActive,    ...args  }) => { -  const [isOpen, setIsOpen] = useState<boolean>(isActive); - -  const toggle = useCallback(() => { -    setIsOpen((prevState) => !prevState); -  }, []); +  const [isOpen, toggle] = useToggle(isActive);    return <MainNavItem isActive={isOpen} setIsActive={toggle} {...args} />;  }; diff --git a/src/components/organisms/toolbar/search.stories.tsx b/src/components/organisms/toolbar/search.stories.tsx index 2c8dd10..0f211bd 100644 --- a/src/components/organisms/toolbar/search.stories.tsx +++ b/src/components/organisms/toolbar/search.stories.tsx @@ -1,5 +1,5 @@ -import { ComponentMeta, ComponentStory } from '@storybook/react'; -import { useState } from 'react'; +import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import { useToggle } from '../../../utils/hooks';  import { Search } from './search';  /** @@ -66,17 +66,9 @@ const Template: ComponentStory<typeof Search> = ({    setIsActive: _setIsActive,    ...args  }) => { -  const [isOpen, setIsOpen] = useState<boolean>(isActive); +  const [isOpen, toggle] = useToggle(isActive); -  return ( -    <Search -      isActive={isOpen} -      setIsActive={() => { -        setIsOpen(!isOpen); -      }} -      {...args} -    /> -  ); +  return <Search isActive={isOpen} setIsActive={toggle} {...args} />;  };  /** diff --git a/src/components/organisms/toolbar/settings.stories.tsx b/src/components/organisms/toolbar/settings.stories.tsx index 793c521..c1fe37d 100644 --- a/src/components/organisms/toolbar/settings.stories.tsx +++ b/src/components/organisms/toolbar/settings.stories.tsx @@ -1,5 +1,5 @@  import type { ComponentMeta, ComponentStory } from '@storybook/react'; -import { useCallback, useState } from 'react'; +import { useToggle } from '../../../utils/hooks';  import { Settings } from './settings';  /** @@ -66,11 +66,7 @@ const Template: ComponentStory<typeof Settings> = ({    setIsActive: _setIsActive,    ...args  }) => { -  const [isOpen, setIsOpen] = useState<boolean>(isActive); - -  const toggle = useCallback(() => { -    setIsOpen((prevState) => !prevState); -  }, []); +  const [isOpen, toggle] = useToggle(isActive);    return <Settings isActive={isOpen} setIsActive={toggle} {...args} />;  }; diff --git a/src/components/organisms/toolbar/toolbar.tsx b/src/components/organisms/toolbar/toolbar.tsx index c400285..c0be464 100644 --- a/src/components/organisms/toolbar/toolbar.tsx +++ b/src/components/organisms/toolbar/toolbar.tsx @@ -1,6 +1,10 @@  /* eslint-disable max-statements */ -import { type FC, useState, useCallback } from 'react'; -import { useOnClickOutside, useRouteChange } from '../../../utils/hooks'; +import type { FC } from 'react'; +import { +  useBoolean, +  useOnClickOutside, +  useRouteChange, +} from '../../../utils/hooks';  import { MainNavItem, type MainNavItemProps } from './main-nav';  import { Search, type SearchProps } from './search';  import { Settings } from './settings'; @@ -27,54 +31,53 @@ export const Toolbar: FC<ToolbarProps> = ({    nav,    searchPage,  }) => { -  const [isNavOpened, setIsNavOpened] = useState<boolean>(false); -  const [isSearchOpened, setIsSearchOpened] = useState<boolean>(false); -  const [isSettingsOpened, setIsSettingsOpened] = useState<boolean>(false); +  const { +    deactivate: deactivateMainNav, +    state: isMainNavOpen, +    toggle: toggleMainNav, +  } = useBoolean(false); +  const { +    deactivate: deactivateSearch, +    state: isSearchOpen, +    toggle: toggleSearch, +  } = useBoolean(false); +  const { +    deactivate: deactivateSettings, +    state: isSettingsOpen, +    toggle: toggleSettings, +  } = useBoolean(false);    const mainNavRef = useOnClickOutside<HTMLDivElement>( -    () => isNavOpened && setIsNavOpened(false) +    () => isMainNavOpen && deactivateMainNav()    );    const searchRef = useOnClickOutside<HTMLDivElement>( -    () => isSearchOpened && setIsSearchOpened(false) +    () => isSearchOpen && deactivateSearch()    );    const settingsRef = useOnClickOutside<HTMLDivElement>( -    () => isSettingsOpened && setIsSettingsOpened(false) +    () => isSettingsOpen && deactivateSettings()    ); -  const toggleMainNav = useCallback( -    () => setIsNavOpened((prevState) => !prevState), -    [] -  ); -  const toggleSearch = useCallback( -    () => setIsSearchOpened((prevState) => !prevState), -    [] -  ); -  const toggleSettings = useCallback( -    () => setIsSettingsOpened((prevState) => !prevState), -    [] -  ); - -  useRouteChange(() => setIsSearchOpened(false)); +  useRouteChange(deactivateSearch);    return (      <div className={`${styles.wrapper} ${className}`}>        <MainNavItem          className={styles.modal} -        isActive={isNavOpened} +        isActive={isMainNavOpen}          items={nav}          ref={mainNavRef}          setIsActive={toggleMainNav}        />        <Search          className={`${styles.modal} ${styles['modal--search']}`} -        isActive={isSearchOpened} +        isActive={isSearchOpen}          ref={searchRef}          searchPage={searchPage}          setIsActive={toggleSearch}        />        <Settings          className={`${styles.modal} ${styles['modal--settings']}`} -        isActive={isSettingsOpened} +        isActive={isSettingsOpen}          ref={settingsRef}          setIsActive={toggleSettings}        /> diff --git a/src/utils/hooks/index.ts b/src/utils/hooks/index.ts index f1bb31e..b98be04 100644 --- a/src/utils/hooks/index.ts +++ b/src/utils/hooks/index.ts @@ -1,5 +1,6 @@  export * from './use-ackee';  export * from './use-article'; +export * from './use-boolean';  export * from './use-breadcrumb';  export * from './use-comments';  export * from './use-data-from-api'; @@ -23,3 +24,4 @@ export * from './use-settings';  export * from './use-state-change';  export * from './use-system-color-scheme';  export * from './use-theme'; +export * from './use-toggle'; diff --git a/src/utils/hooks/use-boolean/index.ts b/src/utils/hooks/use-boolean/index.ts new file mode 100644 index 0000000..a210294 --- /dev/null +++ b/src/utils/hooks/use-boolean/index.ts @@ -0,0 +1 @@ +export * from './use-boolean'; diff --git a/src/utils/hooks/use-boolean/use-boolean.test.ts b/src/utils/hooks/use-boolean/use-boolean.test.ts new file mode 100644 index 0000000..22d3cdc --- /dev/null +++ b/src/utils/hooks/use-boolean/use-boolean.test.ts @@ -0,0 +1,45 @@ +import { describe, expect, it } from '@jest/globals'; +import { act, renderHook } from '@testing-library/react'; +import { useBoolean } from './use-boolean'; + +describe('use-boolean', () => { +  it('returns the initial state', () => { +    const initialState = true; +    const { result } = renderHook(() => useBoolean(initialState)); + +    expect(result.current.state).toBe(initialState); +  }); + +  it('can set the state to false', () => { +    const { result } = renderHook(() => useBoolean()); + +    act(() => { +      result.current.deactivate(); +    }); + +    expect(result.current.state).toBe(false); +  }); + +  it('can set the state to true', () => { +    const { result } = renderHook(() => useBoolean()); + +    act(() => { +      result.current.activate(); +    }); + +    expect(result.current.state).toBe(true); +  }); + +  it('can switch the state', () => { +    const initialState = true; +    const { result } = renderHook(() => useBoolean(initialState)); + +    expect(result.current.state).toBe(initialState); + +    act(() => { +      result.current.toggle(); +    }); + +    expect(result.current.state).toBe(!initialState); +  }); +}); diff --git a/src/utils/hooks/use-boolean/use-boolean.ts b/src/utils/hooks/use-boolean/use-boolean.ts new file mode 100644 index 0000000..35cb00c --- /dev/null +++ b/src/utils/hooks/use-boolean/use-boolean.ts @@ -0,0 +1,44 @@ +import { useCallback, useState } from 'react'; + +export type UseBooleanReturn = { +  /** +   * Set state as true. +   */ +  activate: () => void; +  /** +   * Set state as false. +   */ +  deactivate: () => void; +  /** +   * Current state. +   */ +  state: boolean; +  /** +   * Switch state. +   */ +  toggle: () => void; +}; + +/** + * React hook to deal with boolean states. + * + * @param {boolean} [initialState] - The initial state. + * @returns {UseBooleanReturn} The state and utility functions to update it. + */ +export const useBoolean = (initialState = false): UseBooleanReturn => { +  const [state, setState] = useState(initialState); + +  const activate = useCallback(() => { +    setState(true); +  }, []); + +  const deactivate = useCallback(() => { +    setState(false); +  }, []); + +  const toggle = useCallback(() => { +    setState((prevState) => !prevState); +  }, []); + +  return { activate, deactivate, state, toggle }; +}; diff --git a/src/utils/hooks/use-toggle/index.ts b/src/utils/hooks/use-toggle/index.ts new file mode 100644 index 0000000..01a0e57 --- /dev/null +++ b/src/utils/hooks/use-toggle/index.ts @@ -0,0 +1 @@ +export * from './use-toggle'; diff --git a/src/utils/hooks/use-toggle/use-toggle.test.ts b/src/utils/hooks/use-toggle/use-toggle.test.ts new file mode 100644 index 0000000..b2feeab --- /dev/null +++ b/src/utils/hooks/use-toggle/use-toggle.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, it } from '@jest/globals'; +import { act, renderHook } from '@testing-library/react'; +import { useToggle } from './use-toggle'; + +describe('use-toggle', () => { +  it('returns the default state', () => { +    const { result } = renderHook(() => useToggle()); + +    expect(result.current[0]).toBe(false); +  }); + +  it('can switch the state', () => { +    const initialState = true; +    const { result } = renderHook(() => useToggle(initialState)); + +    expect(result.current[0]).toBe(initialState); + +    act(() => { +      result.current[1](); +    }); + +    expect(result.current[0]).toBe(!initialState); +  }); +}); diff --git a/src/utils/hooks/use-toggle/use-toggle.ts b/src/utils/hooks/use-toggle/use-toggle.ts new file mode 100644 index 0000000..c07c1e2 --- /dev/null +++ b/src/utils/hooks/use-toggle/use-toggle.ts @@ -0,0 +1,15 @@ +import { useBoolean } from '../use-boolean'; + +export type UseToggleReturn = readonly [boolean, () => void]; + +/** + * React hook to toggle boolean states. + * + * @param {boolean} [initialState] - The initial state. + * @returns {UseToggleReturn} The state and a function to switch state. + */ +export const useToggle = (initialState = false): UseToggleReturn => { +  const { state, toggle } = useBoolean(initialState); + +  return [state, toggle] as const; +}; | 
