aboutsummaryrefslogtreecommitdiffstats
path: root/src/components/organisms/navbar/navbar-item
diff options
context:
space:
mode:
authorArmand Philippot <git@armandphilippot.com>2023-11-21 16:10:20 +0100
committerArmand Philippot <git@armandphilippot.com>2023-11-21 18:17:47 +0100
commitc6212f927daf3c928f479afa052e4772216a2d8a (patch)
tree6bb71d9cea03e110d10120ba4bf24e3a6f4ff7d0 /src/components/organisms/navbar/navbar-item
parent70b4f633a6fbedb58c8b9134ac64ede854d489de (diff)
refactor(components): replace items prop in Navbar component
* replace `items` prop with `children` prop: it is more readable this way, * handle navbar item state inside NavbarItem component: it avoid using three differents states and their methods to do exactly the same thing * remove useAutofocus hook since we can't use it anymore * add `onActivation` and `activationHandlerDelay` prop to NavbarItem component to be able to focus the search input only when the item is activated (it replicates the functioning of useAutofocus hook) * replace `ref` type in SearchForm component: it does not make sense to use an input ref for a form. Instead I use useImperativeHandle to provide different a focus method to the given ref.
Diffstat (limited to 'src/components/organisms/navbar/navbar-item')
-rw-r--r--src/components/organisms/navbar/navbar-item/navbar-item.stories.tsx23
-rw-r--r--src/components/organisms/navbar/navbar-item/navbar-item.test.tsx63
-rw-r--r--src/components/organisms/navbar/navbar-item/navbar-item.tsx41
3 files changed, 63 insertions, 64 deletions
diff --git a/src/components/organisms/navbar/navbar-item/navbar-item.stories.tsx b/src/components/organisms/navbar/navbar-item/navbar-item.stories.tsx
index 1c56768..93b7281 100644
--- a/src/components/organisms/navbar/navbar-item/navbar-item.stories.tsx
+++ b/src/components/organisms/navbar/navbar-item/navbar-item.stories.tsx
@@ -1,5 +1,4 @@
import type { ComponentMeta, ComponentStory } from '@storybook/react';
-import { useBoolean } from '../../../../utils/hooks';
import { NavbarItem } from './navbar-item';
/**
@@ -11,23 +10,9 @@ export default {
argTypes: {},
} as ComponentMeta<typeof NavbarItem>;
-const Template: ComponentStory<typeof NavbarItem> = ({
- isActive,
- onDeactivate,
- onToggle,
- ...args
-}) => {
- const { deactivate, state, toggle } = useBoolean(isActive);
-
- return (
- <NavbarItem
- {...args}
- isActive={state}
- onDeactivate={deactivate}
- onToggle={toggle}
- />
- );
-};
+const Template: ComponentStory<typeof NavbarItem> = (args) => (
+ <NavbarItem {...args} />
+);
/**
* NavbarItem Stories - Default
@@ -37,7 +22,6 @@ Default.args = {
children: 'The modal contents.',
icon: 'cog',
id: 'default',
- isActive: false,
label: 'Open example',
};
@@ -49,7 +33,6 @@ ModalVisibleAfterBreakpoint.args = {
children: 'The modal contents.',
icon: 'cog',
id: 'modal-visible',
- isActive: false,
label: 'Open example',
modalVisibleFrom: 'md',
};
diff --git a/src/components/organisms/navbar/navbar-item/navbar-item.test.tsx b/src/components/organisms/navbar/navbar-item/navbar-item.test.tsx
index 2e7edea..e531ff6 100644
--- a/src/components/organisms/navbar/navbar-item/navbar-item.test.tsx
+++ b/src/components/organisms/navbar/navbar-item/navbar-item.test.tsx
@@ -1,24 +1,7 @@
-import { describe, expect, it } from '@jest/globals';
+import { describe, expect, it, jest } from '@jest/globals';
import { render, screen as rtlScreen } from '@testing-library/react';
import { userEvent } from '@testing-library/user-event';
-import { useBoolean } from '../../../../utils/hooks';
-import { NavbarItem, type NavbarItemProps } from './navbar-item';
-
-const ControlledNavbarItem = ({
- isActive,
- ...props
-}: Omit<NavbarItemProps, 'onDeactivate' | 'onToggle'>) => {
- const { deactivate, state, toggle } = useBoolean(isActive);
-
- return (
- <NavbarItem
- {...props}
- isActive={state}
- onDeactivate={deactivate}
- onToggle={toggle}
- />
- );
-};
+import { NavbarItem } from './navbar-item';
describe('NavbarItem', () => {
it('renders a labelled checkbox to open/close a modal', async () => {
@@ -27,14 +10,9 @@ describe('NavbarItem', () => {
const user = userEvent.setup();
render(
- <ControlledNavbarItem
- icon="arrow"
- id="vel"
- isActive={false}
- label={label}
- >
+ <NavbarItem icon="arrow" id="vel" label={label}>
{modal}
- </ControlledNavbarItem>
+ </NavbarItem>
);
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
@@ -58,9 +36,9 @@ describe('NavbarItem', () => {
const user = userEvent.setup();
render(
- <ControlledNavbarItem icon="arrow" id="et" isActive label={label}>
+ <NavbarItem icon="arrow" id="et" label={label}>
{modal}
- </ControlledNavbarItem>
+ </NavbarItem>
);
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
@@ -68,6 +46,8 @@ describe('NavbarItem', () => {
const controller = rtlScreen.getByRole('checkbox', { name: label });
+ await user.click(controller);
+
expect(controller).toBeChecked();
if (controller.parentElement) await user.click(controller.parentElement);
@@ -76,4 +56,31 @@ describe('NavbarItem', () => {
// Since the visibility is declared in CSS we cannot use this assertion.
//expect(rtlScreen.getByText(modal)).not.toBeVisible();
});
+
+ /* eslint-disable max-statements */
+ it('accepts an activation handler', async () => {
+ const handler = jest.fn();
+ const user = userEvent.setup();
+ const label = 'qui';
+
+ render(
+ <NavbarItem icon="arrow" id="aut" label={label} onActivation={handler}>
+ Some contents
+ </NavbarItem>
+ );
+
+ // eslint-disable-next-line @typescript-eslint/no-magic-numbers
+ expect.assertions(3);
+
+ expect(handler).not.toHaveBeenCalled();
+
+ await user.click(rtlScreen.getByLabelText(label));
+
+ /* For some reasons (probably setTimeout) it is called twice but if I use
+ jest fake timers the test throws `Exceeded timeout`... So I leave it with 2
+ for now. */
+ expect(handler).toHaveBeenCalledTimes(2);
+ expect(handler).toHaveBeenCalledWith(true);
+ });
+ /* eslint-enable max-statements */
});
diff --git a/src/components/organisms/navbar/navbar-item/navbar-item.tsx b/src/components/organisms/navbar/navbar-item/navbar-item.tsx
index 8ef6ce3..993b613 100644
--- a/src/components/organisms/navbar/navbar-item/navbar-item.tsx
+++ b/src/components/organisms/navbar/navbar-item/navbar-item.tsx
@@ -6,8 +6,11 @@ import {
useRef,
} from 'react';
import {
+ useBoolean,
useOnClickOutside,
+ useOnRouteChange,
type useOnClickOutsideHandler,
+ useTimeout,
} from '../../../../utils/hooks';
import {
Checkbox,
@@ -24,11 +27,17 @@ import {
import { Modal } from '../../../molecules';
import styles from './navbar-item.module.scss';
+export type NavbarItemActivationHandler = (isActive: boolean) => void;
+
export type NavbarItemProps = Omit<
ListItemProps,
'children' | 'hideMarker' | 'id'
> & {
/**
+ * Add a delay (in ms) before triggering the `onActivation` handler.
+ */
+ activationHandlerDelay?: number;
+ /**
* The modal contents.
*/
children: ReactNode;
@@ -41,10 +50,6 @@ export type NavbarItemProps = Omit<
*/
id: string;
/**
- * Should the modal be visible?
- */
- isActive: boolean;
- /**
* An accessible name for the nav item.
*/
label: string;
@@ -57,13 +62,9 @@ export type NavbarItemProps = Omit<
*/
modalVisibleFrom?: 'sm' | 'md';
/**
- * A callback function to handle modal deactivation.
+ * A callback function to handle item activation.
*/
- onDeactivate?: () => void;
- /**
- * A callback function to handle modal toggle.
- */
- onToggle: () => void;
+ onActivation?: NavbarItemActivationHandler;
/**
* Should we add the icon on the modal?
*
@@ -77,16 +78,15 @@ const NavbarItemWithRef: ForwardRefRenderFunction<
NavbarItemProps
> = (
{
+ activationHandlerDelay,
children,
className = '',
icon,
id,
- isActive,
label,
modalHeading,
modalVisibleFrom,
- onDeactivate,
- onToggle,
+ onActivation,
showIconOnModal = false,
...props
},
@@ -99,6 +99,7 @@ const NavbarItemWithRef: ForwardRefRenderFunction<
: '',
className,
].join(' ');
+ const { deactivate, state: isActive, toggle } = useBoolean(false);
const labelRef = useRef<HTMLLabelElement>(null);
const checkboxRef = useRef<HTMLInputElement>(null);
const deactivateItem: useOnClickOutsideHandler = useCallback(
@@ -107,12 +108,20 @@ const NavbarItemWithRef: ForwardRefRenderFunction<
e.target && checkboxRef.current?.contains(e.target as Node);
const isLabel = e.target && labelRef.current?.contains(e.target as Node);
- if (onDeactivate && !isCheckbox && !isLabel) onDeactivate();
+ if (!isCheckbox && !isLabel) deactivate();
},
- [onDeactivate]
+ [deactivate]
);
const modalRef = useOnClickOutside<HTMLDivElement>(deactivateItem);
+ useOnRouteChange(deactivate, 'end');
+
+ const handleActivation = useCallback(() => {
+ if (onActivation) onActivation(isActive);
+ }, [isActive, onActivation]);
+
+ useTimeout(handleActivation, activationHandlerDelay);
+
return (
<ListItem {...props} className={itemClass} hideMarker ref={ref}>
<Checkbox
@@ -120,7 +129,7 @@ const NavbarItemWithRef: ForwardRefRenderFunction<
id={id}
isChecked={isActive}
name={id}
- onChange={onToggle}
+ onChange={toggle}
ref={checkboxRef}
value={id}
/>