aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/components/organisms/forms/search-form/search-form.test.tsx17
-rw-r--r--src/components/organisms/forms/search-form/search-form.tsx32
-rw-r--r--src/components/organisms/navbar/index.ts1
-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
-rw-r--r--src/components/organisms/navbar/navbar.module.scss2
-rw-r--r--src/components/organisms/navbar/navbar.stories.tsx117
-rw-r--r--src/components/organisms/navbar/navbar.test.tsx47
-rw-r--r--src/components/organisms/navbar/navbar.tsx31
-rw-r--r--src/components/templates/layout/layout.tsx125
-rw-r--r--src/utils/hooks/index.ts1
-rw-r--r--src/utils/hooks/use-autofocus/index.ts1
-rw-r--r--src/utils/hooks/use-autofocus/use-autofocus.test.ts79
-rw-r--r--src/utils/hooks/use-autofocus/use-autofocus.ts40
15 files changed, 216 insertions, 404 deletions
diff --git a/src/components/organisms/forms/search-form/search-form.test.tsx b/src/components/organisms/forms/search-form/search-form.test.tsx
index d1fdfa9..8b4379b 100644
--- a/src/components/organisms/forms/search-form/search-form.test.tsx
+++ b/src/components/organisms/forms/search-form/search-form.test.tsx
@@ -1,7 +1,8 @@
import { describe, expect, it, jest } from '@jest/globals';
import { userEvent } from '@testing-library/user-event';
-import { render, screen as rtlScreen } from '../../../../../tests/utils';
-import { SearchForm } from './search-form';
+import type { Ref } from 'react';
+import { act, render, screen as rtlScreen } from '../../../../../tests/utils';
+import { SearchForm, type SearchFormRef } from './search-form';
describe('SearchForm', () => {
it('renders a search input with a submit button', () => {
@@ -36,4 +37,16 @@ describe('SearchForm', () => {
expect(onSubmit).toHaveBeenCalledTimes(1);
expect(onSubmit).toHaveBeenCalledWith({ query });
});
+
+ it('can give focus to the search input', () => {
+ const ref: Ref<SearchFormRef> = { current: null };
+
+ render(<SearchForm ref={ref} />);
+
+ act(() => {
+ ref.current?.focus();
+ });
+
+ expect(rtlScreen.getByRole('searchbox')).toHaveFocus();
+ });
});
diff --git a/src/components/organisms/forms/search-form/search-form.tsx b/src/components/organisms/forms/search-form/search-form.tsx
index 3f16ad0..3d0efa2 100644
--- a/src/components/organisms/forms/search-form/search-form.tsx
+++ b/src/components/organisms/forms/search-form/search-form.tsx
@@ -1,4 +1,10 @@
-import { forwardRef, type ForwardRefRenderFunction, useId } from 'react';
+import {
+ forwardRef,
+ type ForwardRefRenderFunction,
+ useId,
+ useImperativeHandle,
+ useRef,
+} from 'react';
import { useIntl } from 'react-intl';
import { type FormSubmitHandler, useForm } from '../../../../utils/hooks';
import {
@@ -29,8 +35,15 @@ export type SearchFormProps = Omit<FormProps, 'children' | 'onSubmit'> & {
onSubmit?: SearchFormSubmit;
};
+export type SearchFormRef = {
+ /**
+ * A method to focus the search input.
+ */
+ focus: () => void;
+};
+
const SearchFormWithRef: ForwardRefRenderFunction<
- HTMLInputElement,
+ SearchFormRef,
SearchFormProps
> = ({ className = '', isLabelHidden = false, onSubmit, ...props }, ref) => {
const intl = useIntl();
@@ -39,6 +52,7 @@ const SearchFormWithRef: ForwardRefRenderFunction<
submitHandler: onSubmit,
});
const id = useId();
+ const inputRef = useRef<HTMLInputElement>(null);
const formClass = [
styles.wrapper,
styles[isLabelHidden ? 'wrapper--no-label' : 'wrapper--has-label'],
@@ -57,6 +71,18 @@ const SearchFormWithRef: ForwardRefRenderFunction<
}),
};
+ useImperativeHandle(
+ ref,
+ () => {
+ return {
+ focus() {
+ inputRef.current?.focus();
+ },
+ };
+ },
+ []
+ );
+
return (
<Form {...props} className={formClass} onSubmit={submit}>
<LabelledField
@@ -68,7 +94,7 @@ const SearchFormWithRef: ForwardRefRenderFunction<
// eslint-disable-next-line react/jsx-no-literals
name="query"
onChange={update}
- ref={ref}
+ ref={inputRef}
// eslint-disable-next-line react/jsx-no-literals
type="search"
value={values.query}
diff --git a/src/components/organisms/navbar/index.ts b/src/components/organisms/navbar/index.ts
index f5899d0..afa9cb6 100644
--- a/src/components/organisms/navbar/index.ts
+++ b/src/components/organisms/navbar/index.ts
@@ -1 +1,2 @@
export * from './navbar';
+export * from './navbar-item';
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}
/>
diff --git a/src/components/organisms/navbar/navbar.module.scss b/src/components/organisms/navbar/navbar.module.scss
index 4041825..5af884e 100644
--- a/src/components/organisms/navbar/navbar.module.scss
+++ b/src/components/organisms/navbar/navbar.module.scss
@@ -47,7 +47,7 @@
}
}
-.item {
+:where(.wrapper) > * {
display: flex;
justify-content: flex-end;
}
diff --git a/src/components/organisms/navbar/navbar.stories.tsx b/src/components/organisms/navbar/navbar.stories.tsx
index fef995e..95b71ef 100644
--- a/src/components/organisms/navbar/navbar.stories.tsx
+++ b/src/components/organisms/navbar/navbar.stories.tsx
@@ -1,5 +1,6 @@
import type { ComponentMeta, ComponentStory } from '@storybook/react';
import { Navbar as NavbarComponent } from './navbar';
+import { NavbarItem } from './navbar-item';
/**
* Navbar - Storybook Meta
@@ -7,28 +8,15 @@ import { Navbar as NavbarComponent } from './navbar';
export default {
title: 'Organisms/Navbar',
component: NavbarComponent,
- args: {
- searchPage: '#',
- },
argTypes: {
- nav: {
- description: 'The main nav items.',
+ children: {
+ description: 'The navbar items.',
type: {
name: 'object',
required: true,
value: {},
},
},
- searchPage: {
- control: {
- type: 'text',
- },
- description: 'The search results page url.',
- type: {
- name: 'string',
- required: true,
- },
- },
},
parameters: {
layout: 'fullscreen',
@@ -39,72 +27,51 @@ const Template: ComponentStory<typeof NavbarComponent> = (args) => (
<NavbarComponent {...args} />
);
-const doNothing = () => {
- // do nothing;
+/**
+ * Navbar Stories - 1 item
+ */
+export const OneItem = Template.bind({});
+OneItem.args = {
+ children: (
+ <NavbarItem icon="hamburger" id="main-nav" label="Nav">
+ The main nav contents
+ </NavbarItem>
+ ),
};
/**
- * Navbar Stories - With all items inactive
+ * Navbar Stories - 2 items
*/
-export const NavbarInactiveItems = Template.bind({});
-NavbarInactiveItems.args = {
- items: [
- {
- icon: 'hamburger',
- id: 'main-nav',
- isActive: false,
- label: 'Nav',
- contents: 'Main nav contents',
- onToggle: doNothing,
- },
- {
- icon: 'magnifying-glass',
- id: 'search',
- isActive: false,
- label: 'Search',
- contents: 'Search contents',
- onToggle: doNothing,
- },
- {
- icon: 'cog',
- id: 'settings',
- isActive: false,
- label: 'Settings',
- contents: 'Settings contents',
- onToggle: doNothing,
- },
- ],
+export const TwoItems = Template.bind({});
+TwoItems.args = {
+ children: (
+ <>
+ <NavbarItem icon="hamburger" id="main-nav" label="Nav">
+ The main nav contents
+ </NavbarItem>
+ <NavbarItem icon="magnifying-glass" id="search" label="Search">
+ A search form
+ </NavbarItem>
+ </>
+ ),
};
/**
- * Navbar Stories - With one item active
+ * Navbar Stories - 3 items
*/
-export const NavbarActiveItem = Template.bind({});
-NavbarActiveItem.args = {
- items: [
- {
- icon: 'hamburger',
- id: 'main-nav',
- isActive: true,
- label: 'Nav',
- contents: 'Main nav contents',
- onToggle: doNothing,
- },
- {
- icon: 'magnifying-glass',
- id: 'search',
- isActive: false,
- label: 'Search',
- contents: 'Search contents',
- onToggle: doNothing,
- },
- {
- icon: 'cog',
- id: 'settings',
- isActive: false,
- label: 'Settings',
- contents: 'Settings contents',
- onToggle: doNothing,
- },
- ],
+export const ThreeItems = Template.bind({});
+ThreeItems.args = {
+ children: (
+ <>
+ <NavbarItem icon="hamburger" id="main-nav" label="Nav">
+ The main nav contents
+ </NavbarItem>
+ <NavbarItem icon="magnifying-glass" id="search" label="Search">
+ A search form
+ </NavbarItem>
+ <NavbarItem icon="cog" id="settings" label="Settings">
+ A settings form
+ </NavbarItem>
+ </>
+ ),
};
diff --git a/src/components/organisms/navbar/navbar.test.tsx b/src/components/organisms/navbar/navbar.test.tsx
index 35b33f2..6578672 100644
--- a/src/components/organisms/navbar/navbar.test.tsx
+++ b/src/components/organisms/navbar/navbar.test.tsx
@@ -1,42 +1,21 @@
import { describe, expect, it } from '@jest/globals';
import { render, screen as rtlScreen } from '@testing-library/react';
-import { Navbar, type NavbarItems } from './navbar';
-
-const doNothing = () => {
- // do nothing;
-};
-
-const items: NavbarItems = [
- {
- icon: 'hamburger',
- id: 'main-nav',
- isActive: false,
- label: 'Nav',
- contents: 'Main nav contents',
- onToggle: doNothing,
- },
- {
- icon: 'magnifying-glass',
- id: 'search',
- isActive: false,
- label: 'Search',
- contents: 'Search contents',
- onToggle: doNothing,
- },
- {
- icon: 'cog',
- id: 'settings',
- isActive: false,
- label: 'Settings',
- contents: 'Settings contents',
- onToggle: doNothing,
- },
-];
+import { Navbar } from './navbar';
+import { NavbarItem } from './navbar-item';
describe('Navbar', () => {
it('renders the given items', () => {
- render(<Navbar items={items} />);
+ render(
+ <Navbar>
+ <NavbarItem icon="hamburger" id="main-nav" label="Main nav">
+ Main nav
+ </NavbarItem>
+ <NavbarItem icon="magnifying-glass" id="search" label="Search">
+ Search form
+ </NavbarItem>
+ </Navbar>
+ );
- expect(rtlScreen.getAllByRole('listitem')).toHaveLength(items.length);
+ expect(rtlScreen.getAllByRole('listitem')).toHaveLength(2);
});
});
diff --git a/src/components/organisms/navbar/navbar.tsx b/src/components/organisms/navbar/navbar.tsx
index ee379e9..39f3c45 100644
--- a/src/components/organisms/navbar/navbar.tsx
+++ b/src/components/organisms/navbar/navbar.tsx
@@ -4,26 +4,8 @@ import {
type ReactNode,
} from 'react';
import { List, type ListProps } from '../../atoms';
-import { NavbarItem, type NavbarItemProps } from './navbar-item';
import styles from './navbar.module.scss';
-export type NavbarItemData = Pick<
- NavbarItemProps,
- | 'icon'
- | 'id'
- | 'isActive'
- | 'label'
- | 'modalHeading'
- | 'modalVisibleFrom'
- | 'onDeactivate'
- | 'onToggle'
- | 'showIconOnModal'
-> & {
- contents: ReactNode;
-};
-
-export type NavbarItems = [NavbarItemData, NavbarItemData?, NavbarItemData?];
-
export type NavbarProps = Omit<
ListProps<false, false>,
'children' | 'hideMarker' | 'isHierarchical' | 'isInline' | 'isOrdered'
@@ -34,25 +16,18 @@ export type NavbarProps = Omit<
* The number of items should not exceed 3 because of the modal position on
* small screens.
*/
- items: NavbarItems;
+ children: ReactNode;
};
const NavbarWithRef: ForwardRefRenderFunction<HTMLUListElement, NavbarProps> = (
- { className = '', items, ...props },
+ { children, className = '', ...props },
ref
) => {
const wrapperClass = `${styles.wrapper} ${className}`;
- const navItems = items.filter(
- (item): item is NavbarItemData => item !== undefined
- );
return (
<List {...props} className={wrapperClass} hideMarker isInline ref={ref}>
- {navItems.map(({ contents, ...item }) => (
- <NavbarItem {...item} className={styles.item} key={item.id}>
- {contents}
- </NavbarItem>
- ))}
+ {children}
</List>
);
};
diff --git a/src/components/templates/layout/layout.tsx b/src/components/templates/layout/layout.tsx
index 953b0db..ce7f1fa 100644
--- a/src/components/templates/layout/layout.tsx
+++ b/src/components/templates/layout/layout.tsx
@@ -16,12 +16,7 @@ import type { Person, SearchAction, WebSite, WithContext } from 'schema-dts';
import type { NextPageWithLayoutOptions } from '../../../types';
import { CONFIG } from '../../../utils/config';
import { ROUTES } from '../../../utils/constants';
-import {
- useAutofocus,
- useBoolean,
- useOnRouteChange,
- useScrollPosition,
-} from '../../../utils/hooks';
+import { useOnRouteChange, useScrollPosition } from '../../../utils/hooks';
import {
ButtonLink,
Footer,
@@ -45,8 +40,10 @@ import {
MainNav,
SearchForm,
SettingsForm,
- type NavbarItems,
type SearchFormSubmit,
+ NavbarItem,
+ type SearchFormRef,
+ type NavbarItemActivationHandler,
} from '../../organisms';
import styles from './layout.module.scss';
@@ -174,21 +171,6 @@ export const Layout: FC<LayoutProps> = ({ children, isHome }) => {
},
];
- 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 labels = {
mainNavItem: intl.formatMessage({
defaultMessage: 'Open menu',
@@ -231,10 +213,13 @@ export const Layout: FC<LayoutProps> = ({ children, isHome }) => {
e.preventDefault();
}, []);
- const searchInputRef = useAutofocus<HTMLInputElement>({
- condition: () => isSearchOpen,
- delay: 360,
- });
+ const searchFormRef = useRef<SearchFormRef>(null);
+ const giveFocusToSearchInput: NavbarItemActivationHandler = useCallback(
+ (isActive) => {
+ if (isActive) searchFormRef.current?.focus();
+ },
+ []
+ );
const searchSubmitHandler: SearchFormSubmit = useCallback(
({ query }) => {
if (!query)
@@ -256,55 +241,6 @@ export const Layout: FC<LayoutProps> = ({ children, isHome }) => {
[intl, router]
);
- useOnRouteChange(deactivateSearch);
-
- const navbarItems: NavbarItems = [
- {
- contents: <MainNav aria-label={labels.mainNavModal} items={mainNav} />,
- icon: 'hamburger',
- id: 'main-nav',
- isActive: isMainNavOpen,
- label: labels.mainNavItem,
- modalVisibleFrom: 'md',
- onDeactivate: deactivateMainNav,
- onToggle: toggleMainNav,
- },
- {
- contents: (
- <SearchForm
- className={styles.search}
- isLabelHidden
- onSubmit={searchSubmitHandler}
- ref={searchInputRef}
- />
- ),
- icon: 'magnifying-glass',
- id: 'search',
- isActive: isSearchOpen,
- label: labels.searchItem,
- onDeactivate: deactivateSearch,
- onToggle: toggleSearch,
- modalHeading: labels.searchModal,
- },
- {
- contents: (
- <SettingsForm
- aria-label={labels.settingsForm}
- className={styles.settings}
- onSubmit={settingsSubmitHandler}
- />
- ),
- icon: 'cog',
- id: 'settings',
- isActive: isSettingsOpen,
- label: labels.settingsItem,
- onDeactivate: deactivateSettings,
- onToggle: toggleSettings,
- modalHeading: labels.settingsModal,
- showIconOnModal: true,
- },
- ];
-
const legalNoticeLabel = intl.formatMessage({
defaultMessage: 'Legal notice',
description: 'Layout: Legal notice label',
@@ -436,7 +372,44 @@ export const Layout: FC<LayoutProps> = ({ children, isHome }) => {
}
url="/"
/>
- <Navbar items={navbarItems} />
+ <Navbar>
+ <NavbarItem
+ icon="hamburger"
+ id="main-nav"
+ label={labels.mainNavItem}
+ modalVisibleFrom="md"
+ >
+ <MainNav aria-label={labels.mainNavModal} items={mainNav} />
+ </NavbarItem>
+ <NavbarItem
+ activationHandlerDelay={350}
+ icon="magnifying-glass"
+ id="search"
+ label={labels.searchItem}
+ modalHeading={labels.searchModal}
+ onActivation={giveFocusToSearchInput}
+ >
+ <SearchForm
+ className={styles.search}
+ isLabelHidden
+ onSubmit={searchSubmitHandler}
+ ref={searchFormRef}
+ />
+ </NavbarItem>
+ <NavbarItem
+ icon="cog"
+ id="settings"
+ label={labels.settingsItem}
+ modalHeading={labels.settingsModal}
+ showIconOnModal
+ >
+ <SettingsForm
+ aria-label={labels.settingsForm}
+ className={styles.settings}
+ onSubmit={settingsSubmitHandler}
+ />
+ </NavbarItem>
+ </Navbar>
</div>
</Header>
<Main id="main" className={styles.main}>
diff --git a/src/utils/hooks/index.ts b/src/utils/hooks/index.ts
index 240a092..f3bfd75 100644
--- a/src/utils/hooks/index.ts
+++ b/src/utils/hooks/index.ts
@@ -1,6 +1,5 @@
export * from './use-ackee';
export * from './use-article';
-export * from './use-autofocus';
export * from './use-boolean';
export * from './use-breadcrumb';
export * from './use-comments';
diff --git a/src/utils/hooks/use-autofocus/index.ts b/src/utils/hooks/use-autofocus/index.ts
deleted file mode 100644
index bb23089..0000000
--- a/src/utils/hooks/use-autofocus/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export * from './use-autofocus';
diff --git a/src/utils/hooks/use-autofocus/use-autofocus.test.ts b/src/utils/hooks/use-autofocus/use-autofocus.test.ts
deleted file mode 100644
index 1a9a3be..0000000
--- a/src/utils/hooks/use-autofocus/use-autofocus.test.ts
+++ /dev/null
@@ -1,79 +0,0 @@
-import {
- afterEach,
- beforeEach,
- describe,
- expect,
- it,
- jest,
-} from '@jest/globals';
-import { renderHook, screen as rtlScreen } from '@testing-library/react';
-import { useAutofocus } from './use-autofocus';
-
-describe('useAutofocus', () => {
- // When less than 1ms, setTimeout use 1. Default delay is 0ms.
- const defaultTimeoutDelay = 1;
- const input = document.createElement('input');
- input.type = 'text';
-
- beforeEach(() => {
- document.body.append(input);
- jest.useFakeTimers();
- });
-
- afterEach(() => {
- document.body.removeChild(input);
- jest.runOnlyPendingTimers();
- jest.useRealTimers();
- });
-
- it('gives focus to the element without condition', () => {
- const { result } = renderHook(() => useAutofocus<HTMLInputElement>());
- result.current.current = input;
-
- jest.advanceTimersByTime(defaultTimeoutDelay);
-
- expect(rtlScreen.getByRole('textbox')).toHaveFocus();
- });
-
- it('can give focus to the element with custom delay', () => {
- const delay = 2000;
- const { result } = renderHook(() =>
- useAutofocus<HTMLInputElement>({ delay })
- );
- result.current.current = input;
-
- jest.advanceTimersByTime(defaultTimeoutDelay);
-
- expect(rtlScreen.getByRole('textbox')).not.toHaveFocus();
-
- jest.advanceTimersByTime(delay);
-
- expect(rtlScreen.getByRole('textbox')).toHaveFocus();
- });
-
- it('can give focus to the element when the condition is met', () => {
- const condition = jest.fn(() => true);
- const { result } = renderHook(() =>
- useAutofocus<HTMLInputElement>({ condition })
- );
- result.current.current = input;
-
- jest.advanceTimersByTime(defaultTimeoutDelay);
-
- expect(rtlScreen.getByRole('textbox')).toHaveFocus();
- expect(condition).toHaveBeenCalledTimes(1);
- });
-
- it('does not give focus to the element when the condition is not met', () => {
- const condition = jest.fn(() => false);
- const { result } = renderHook(() =>
- useAutofocus<HTMLInputElement>({ condition })
- );
- result.current.current = input;
-
- jest.advanceTimersByTime(defaultTimeoutDelay);
-
- expect(rtlScreen.getByRole('textbox')).not.toHaveFocus();
- expect(condition).toHaveBeenCalledTimes(1);
- });
-});
diff --git a/src/utils/hooks/use-autofocus/use-autofocus.ts b/src/utils/hooks/use-autofocus/use-autofocus.ts
deleted file mode 100644
index 0d21a59..0000000
--- a/src/utils/hooks/use-autofocus/use-autofocus.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-import { useCallback, useRef, type MutableRefObject } from 'react';
-import { useTimeout } from '../use-timeout';
-
-export type UseAutofocusCondition = () => boolean;
-
-export type UseAutofocusConfig = {
- /**
- * A condition to met before giving focus to the element.
- */
- condition?: UseAutofocusCondition;
- /**
- * A delay in ms before giving focus to the element.
- */
- delay?: number;
-};
-
-/**
- * React hook to give focus to an element automatically.
- *
- * @param {UseAutofocusConfig} [config] - A configuration object.
- * @returns {RefObject<T>} The element reference.
- */
-export const useAutofocus = <T extends HTMLElement>(
- config?: UseAutofocusConfig
-): MutableRefObject<T | null> => {
- const { condition, delay } = config ?? {};
- const ref = useRef<T | null>(null);
-
- const setFocus = useCallback(() => {
- const shouldFocus = condition ? condition() : true;
-
- if (ref.current && shouldFocus) {
- ref.current.focus();
- }
- }, [condition]);
-
- useTimeout(setFocus, delay);
-
- return ref;
-};