aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/components/molecules/forms/flipping-label/flipping-label.stories.tsx7
-rw-r--r--src/components/organisms/forms/ackee-toggle/ackee-toggle.tsx17
-rw-r--r--src/components/organisms/forms/comment-form/comment-form.tsx17
-rw-r--r--src/components/organisms/forms/contact-form/contact-form.tsx17
-rw-r--r--src/components/organisms/layout/comment.tsx13
-rw-r--r--src/components/organisms/toolbar/main-nav.stories.tsx8
-rw-r--r--src/components/organisms/toolbar/search.stories.tsx16
-rw-r--r--src/components/organisms/toolbar/settings.stories.tsx8
-rw-r--r--src/components/organisms/toolbar/toolbar.tsx53
-rw-r--r--src/utils/hooks/index.ts2
-rw-r--r--src/utils/hooks/use-boolean/index.ts1
-rw-r--r--src/utils/hooks/use-boolean/use-boolean.test.ts45
-rw-r--r--src/utils/hooks/use-boolean/use-boolean.ts44
-rw-r--r--src/utils/hooks/use-toggle/index.ts1
-rw-r--r--src/utils/hooks/use-toggle/use-toggle.test.ts24
-rw-r--r--src/utils/hooks/use-toggle/use-toggle.ts15
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;
+};