From a1e8f1e4426ed3560ce1b76fb73a6969388ed253 Mon Sep 17 00:00:00 2001
From: Armand Philippot <git@armandphilippot.com>
Date: Thu, 7 Apr 2022 22:57:15 +0200
Subject: chore: add a SelectWithTooltip component

---
 .../molecules/forms/select-with-tooltip.tsx        | 58 ++++++++++++++++++++++
 1 file changed, 58 insertions(+)
 create mode 100644 src/components/molecules/forms/select-with-tooltip.tsx

(limited to 'src/components/molecules/forms/select-with-tooltip.tsx')

diff --git a/src/components/molecules/forms/select-with-tooltip.tsx b/src/components/molecules/forms/select-with-tooltip.tsx
new file mode 100644
index 0000000..5e48d62
--- /dev/null
+++ b/src/components/molecules/forms/select-with-tooltip.tsx
@@ -0,0 +1,58 @@
+import Select, { SelectProps } from '@components/atoms/forms/select';
+import { FC, useState } from 'react';
+import HelpButton from '../buttons/help-button';
+import Tooltip, { TooltipProps } from '../modals/tooltip';
+import styles from './select-with-tooltip.module.scss';
+
+export type SelectWithTooltipProps = SelectProps &
+  Pick<TooltipProps, 'title' | 'content'> & {
+    /**
+     * The select label.
+     */
+    label: string;
+    /**
+     * Set additional classes to the tooltip wrapper.
+     */
+    tooltipClasses?: string;
+  };
+
+/**
+ * SelectWithTooltip component
+ *
+ * Render a select with a button to display a tooltip about options.
+ */
+const SelectWithTooltip: FC<SelectWithTooltipProps> = ({
+  title,
+  content,
+  id,
+  label,
+  tooltipClasses = '',
+  ...props
+}) => {
+  const [isTooltipOpened, setIsTooltipOpened] = useState<boolean>(false);
+  const buttonModifier = isTooltipOpened ? styles['btn--activated'] : '';
+  const tooltipModifier = isTooltipOpened
+    ? styles['tooltip--visible']
+    : styles['tooltip--hidden'];
+
+  return (
+    <div className={styles.wrapper}>
+      <label htmlFor={id} className={styles.label}>
+        {label}
+      </label>
+      <Select id={id} {...props} classes={styles.select} />
+      <HelpButton
+        onClick={() => setIsTooltipOpened(!isTooltipOpened)}
+        classes={buttonModifier}
+      />
+      <Tooltip
+        title={title}
+        content={content}
+        icon="?"
+        classes={`${styles.tooltip} ${tooltipModifier} ${tooltipClasses}`}
+      />
+    </div>
+  );
+};
+
+export default SelectWithTooltip;
-- 
cgit v1.2.3


From 0b3146f7278929c4d1b33dd8f94f34e351e5e5a9 Mon Sep 17 00:00:00 2001
From: Armand Philippot <git@armandphilippot.com>
Date: Fri, 8 Apr 2022 22:36:24 +0200
Subject: chore: add a Settings modal component

---
 src/components/atoms/forms/field.stories.tsx       | 45 +++++++++---
 src/components/atoms/forms/field.tsx               | 21 ++++--
 src/components/atoms/forms/form.test.tsx           |  9 +++
 src/components/atoms/forms/form.tsx                | 73 ++++++++++++++++++++
 src/components/atoms/forms/forms.module.scss       | 23 +++----
 src/components/atoms/forms/label.module.scss       | 17 +++++
 src/components/atoms/forms/label.stories.tsx       | 42 ++++++++++--
 src/components/atoms/forms/label.test.tsx          |  2 +-
 src/components/atoms/forms/label.tsx               | 32 +++++++--
 src/components/atoms/forms/select.stories.tsx      | 44 +++++++++---
 src/components/atoms/forms/select.tsx              | 25 +++++--
 src/components/atoms/forms/toggle.module.scss      |  2 +-
 src/components/atoms/forms/toggle.tsx              |  7 +-
 .../molecules/forms/labelled-field.module.scss     |  9 +++
 .../molecules/forms/labelled-field.stories.tsx     | 33 +++++++--
 src/components/molecules/forms/labelled-field.tsx  | 34 +++++++--
 .../molecules/forms/labelled-select.module.scss    |  9 +++
 .../molecules/forms/labelled-select.stories.tsx    | 80 ++++++++++++++++++++--
 src/components/molecules/forms/labelled-select.tsx | 51 ++++++++++++--
 .../molecules/forms/motion-toggle.stories.tsx      | 15 ++++
 src/components/molecules/forms/motion-toggle.tsx   |  8 ++-
 .../molecules/forms/prism-theme-toggle.stories.tsx | 15 ++++
 .../molecules/forms/prism-theme-toggle.tsx         | 11 ++-
 .../forms/select-with-tooltip.module.scss          | 10 +--
 .../forms/select-with-tooltip.stories.tsx          | 77 +++++++++++++++++----
 .../molecules/forms/select-with-tooltip.tsx        | 34 +++++----
 .../molecules/forms/theme-toggle.stories.tsx       | 15 ++++
 src/components/molecules/forms/theme-toggle.tsx    |  8 ++-
 .../organisms/modals/settings-modal.module.scss    | 14 ++++
 .../organisms/modals/settings-modal.stories.tsx    | 31 +++++++++
 .../organisms/modals/settings-modal.test.tsx       | 34 +++++++++
 src/components/organisms/modals/settings-modal.tsx | 51 ++++++++++++++
 32 files changed, 761 insertions(+), 120 deletions(-)
 create mode 100644 src/components/atoms/forms/form.test.tsx
 create mode 100644 src/components/atoms/forms/form.tsx
 create mode 100644 src/components/atoms/forms/label.module.scss
 create mode 100644 src/components/molecules/forms/labelled-field.module.scss
 create mode 100644 src/components/molecules/forms/labelled-select.module.scss
 create mode 100644 src/components/organisms/modals/settings-modal.module.scss
 create mode 100644 src/components/organisms/modals/settings-modal.stories.tsx
 create mode 100644 src/components/organisms/modals/settings-modal.test.tsx
 create mode 100644 src/components/organisms/modals/settings-modal.tsx

(limited to 'src/components/molecules/forms/select-with-tooltip.tsx')

diff --git a/src/components/atoms/forms/field.stories.tsx b/src/components/atoms/forms/field.stories.tsx
index 02681e7..ec81922 100644
--- a/src/components/atoms/forms/field.stories.tsx
+++ b/src/components/atoms/forms/field.stories.tsx
@@ -1,4 +1,5 @@
 import { ComponentMeta, ComponentStory } from '@storybook/react';
+import { useState } from 'react';
 import FieldComponent from './field';
 
 export default {
@@ -7,11 +8,35 @@ export default {
   args: {
     disabled: false,
     required: false,
-    setValue: () => null,
     type: 'text',
-    value: '',
   },
   argTypes: {
+    'aria-labelledby': {
+      control: {
+        type: 'text',
+      },
+      description: 'One or more ids that refers to the field name.',
+      table: {
+        category: 'Accessibility',
+      },
+      type: {
+        name: 'string',
+        required: false,
+      },
+    },
+    className: {
+      control: {
+        type: 'text',
+      },
+      description: 'Add classnames to the field.',
+      table: {
+        category: 'Styles',
+      },
+      type: {
+        name: 'string',
+        required: false,
+      },
+    },
     disabled: {
       control: {
         type: 'boolean',
@@ -148,7 +173,7 @@ export default {
     },
     value: {
       control: {
-        type: 'text',
+        type: null,
       },
       description: 'Field value.',
       type: {
@@ -159,14 +184,18 @@ export default {
   },
 } as ComponentMeta<typeof FieldComponent>;
 
-const Template: ComponentStory<typeof FieldComponent> = (args) => (
-  <FieldComponent {...args} />
-);
+const Template: ComponentStory<typeof FieldComponent> = ({
+  value: _value,
+  setValue: _setValue,
+  ...args
+}) => {
+  const [value, setValue] = useState<string>('');
+
+  return <FieldComponent value={value} setValue={setValue} {...args} />;
+};
 
 export const Field = Template.bind({});
 Field.args = {
   id: 'field-storybook',
   name: 'field-storybook',
-  setValue: () => null,
-  value: '',
 };
diff --git a/src/components/atoms/forms/field.tsx b/src/components/atoms/forms/field.tsx
index 513d2ba..2e75d0f 100644
--- a/src/components/atoms/forms/field.tsx
+++ b/src/components/atoms/forms/field.tsx
@@ -1,4 +1,4 @@
-import { ChangeEvent, FC, SetStateAction } from 'react';
+import { ChangeEvent, SetStateAction, VFC } from 'react';
 import styles from './forms.module.scss';
 
 export type FieldType =
@@ -13,6 +13,14 @@ export type FieldType =
   | 'url';
 
 export type FieldProps = {
+  /**
+   * One or more ids that refers to the field name.
+   */
+  'aria-labelledby'?: string;
+  /**
+   * Add classnames to the field.
+   */
+  className?: string;
   /**
    * Field state. Either enabled (false) or disabled (true).
    */
@@ -64,7 +72,12 @@ export type FieldProps = {
  *
  * Render either an input or a textarea.
  */
-const Field: FC<FieldProps> = ({ setValue, type, ...props }) => {
+const Field: VFC<FieldProps> = ({
+  className = '',
+  setValue,
+  type,
+  ...props
+}) => {
   /**
    * Update select value when an option is selected.
    * @param e - The option change event.
@@ -78,14 +91,14 @@ const Field: FC<FieldProps> = ({ setValue, type, ...props }) => {
   return type === 'textarea' ? (
     <textarea
       onChange={updateValue}
-      className={`${styles.field} ${styles['field--textarea']}`}
+      className={`${styles.field} ${styles['field--textarea']} ${className}`}
       {...props}
     />
   ) : (
     <input
       type={type}
       onChange={updateValue}
-      className={styles.field}
+      className={`${styles.field} ${className}`}
       {...props}
     />
   );
diff --git a/src/components/atoms/forms/form.test.tsx b/src/components/atoms/forms/form.test.tsx
new file mode 100644
index 0000000..9cd3c58
--- /dev/null
+++ b/src/components/atoms/forms/form.test.tsx
@@ -0,0 +1,9 @@
+import { render, screen } from '@test-utils';
+import Form from './form';
+
+describe('Form', () => {
+  it('renders a form', () => {
+    render(<Form aria-label="Jest form" onSubmit={() => null}></Form>);
+    expect(screen.getByRole('form', { name: 'Jest form' })).toBeInTheDocument();
+  });
+});
diff --git a/src/components/atoms/forms/form.tsx b/src/components/atoms/forms/form.tsx
new file mode 100644
index 0000000..8e80930
--- /dev/null
+++ b/src/components/atoms/forms/form.tsx
@@ -0,0 +1,73 @@
+import { Children, FC, FormEvent, Fragment } from 'react';
+import styles from './forms.module.scss';
+
+export type FormProps = {
+  /**
+   * An accessible name.
+   */
+  'aria-label'?: string;
+  /**
+   * One or more ids that refers to the form name.
+   */
+  'aria-labelledby'?: string;
+  /**
+   * Set additional classnames to the form wrapper.
+   */
+  className?: string;
+  /**
+   * Wrap each items with a div. Default: true.
+   */
+  grouped?: boolean;
+  /**
+   * A callback function to execute on submit.
+   */
+  onSubmit: () => void;
+};
+
+/**
+ * Form component.
+ *
+ * Render children wrapped in a form element.
+ */
+const Form: FC<FormProps> = ({
+  children,
+  className = '',
+  grouped = true,
+  onSubmit,
+  ...props
+}) => {
+  const arrayChildren = Children.toArray(children);
+
+  /**
+   * Get the form items.
+   * @returns {JSX.Element[]} An array of child elements wrapped in a div.
+   */
+  const getFormItems = (): JSX.Element[] => {
+    return arrayChildren.map((child, index) =>
+      grouped ? (
+        <div key={`item-${index}`} className={styles.item}>
+          {child}
+        </div>
+      ) : (
+        <Fragment key={`item-${index}`}>{child}</Fragment>
+      )
+    );
+  };
+
+  /**
+   * Handle form submit.
+   * @param {FormEvent} e - The form event.
+   */
+  const handleSubmit = (e: FormEvent) => {
+    e.preventDefault();
+    onSubmit();
+  };
+
+  return (
+    <form onSubmit={handleSubmit} className={className} {...props}>
+      {getFormItems()}
+    </form>
+  );
+};
+
+export default Form;
diff --git a/src/components/atoms/forms/forms.module.scss b/src/components/atoms/forms/forms.module.scss
index 689a318..279c185 100644
--- a/src/components/atoms/forms/forms.module.scss
+++ b/src/components/atoms/forms/forms.module.scss
@@ -1,7 +1,12 @@
 @use "@styles/abstracts/functions" as fun;
+@use "@styles/abstracts/mixins" as mix;
+
+.item {
+  margin: var(--spacing-xs) 0;
+  max-width: 45ch;
+}
 
 .field {
-  width: 100%;
   padding: var(--spacing-2xs) var(--spacing-xs);
   background: var(--color-bg-tertiary);
   border: fun.convert-px(2) solid var(--color-border);
@@ -10,6 +15,10 @@
 
   &--select {
     cursor: pointer;
+
+    @include mix.pointer("fine") {
+      padding: fun.convert-px(3) var(--spacing-xs);
+    }
   }
 
   &--textarea {
@@ -41,15 +50,3 @@
     }
   }
 }
-
-.label {
-  display: block;
-  color: var(--color-primary-darker);
-  font-size: var(--font-size-sm);
-  font-variant: small-caps;
-  font-weight: 600;
-}
-
-.required {
-  color: var(--color-secondary);
-}
diff --git a/src/components/atoms/forms/label.module.scss b/src/components/atoms/forms/label.module.scss
new file mode 100644
index 0000000..f900925
--- /dev/null
+++ b/src/components/atoms/forms/label.module.scss
@@ -0,0 +1,17 @@
+.label {
+  color: var(--color-primary-darker);
+  font-weight: 600;
+
+  &--small {
+    font-size: var(--font-size-sm);
+    font-variant: small-caps;
+  }
+
+  &--medium {
+    font-size: var(--font-size-md);
+  }
+}
+
+.required {
+  color: var(--color-secondary);
+}
diff --git a/src/components/atoms/forms/label.stories.tsx b/src/components/atoms/forms/label.stories.tsx
index 06e8eb9..463e8ac 100644
--- a/src/components/atoms/forms/label.stories.tsx
+++ b/src/components/atoms/forms/label.stories.tsx
@@ -4,7 +4,24 @@ import LabelComponent from './label';
 export default {
   title: 'Atoms/Forms',
   component: LabelComponent,
+  args: {
+    required: false,
+    size: 'small',
+  },
   argTypes: {
+    className: {
+      control: {
+        type: 'text',
+      },
+      description: 'Add classnames to the label.',
+      table: {
+        category: 'Styles',
+      },
+      type: {
+        name: 'string',
+        required: false,
+      },
+    },
     children: {
       control: {
         type: 'text',
@@ -32,22 +49,37 @@ export default {
       description: 'Set to true if the field is required.',
       table: {
         category: 'Options',
+        defaultValue: { summary: false },
       },
       type: {
         name: 'boolean',
         required: false,
       },
     },
+    size: {
+      control: {
+        type: 'select',
+      },
+      description: 'The label size.',
+      options: ['medium', 'small'],
+      table: {
+        category: 'Options',
+        defaultValue: { summary: 'small' },
+      },
+      type: {
+        name: 'string',
+        required: false,
+      },
+    },
   },
 } as ComponentMeta<typeof LabelComponent>;
 
-const Template: ComponentStory<typeof LabelComponent> = (args) => {
-  const { children, ...props } = args;
-  return <LabelComponent {...props}>{children}</LabelComponent>;
-};
+const Template: ComponentStory<typeof LabelComponent> = ({
+  children,
+  ...args
+}) => <LabelComponent {...args}>{children}</LabelComponent>;
 
 export const Label = Template.bind({});
 Label.args = {
   children: 'A label',
-  htmlFor: 'a-field-id',
 };
diff --git a/src/components/atoms/forms/label.test.tsx b/src/components/atoms/forms/label.test.tsx
index fcf1731..14257c3 100644
--- a/src/components/atoms/forms/label.test.tsx
+++ b/src/components/atoms/forms/label.test.tsx
@@ -3,7 +3,7 @@ import Label from './label';
 
 describe('Label', () => {
   it('renders a field label', () => {
-    render(<Label htmlFor="a-field-id">A label</Label>);
+    render(<Label>A label</Label>);
     expect(screen.getByText('A label')).toBeDefined();
   });
 });
diff --git a/src/components/atoms/forms/label.tsx b/src/components/atoms/forms/label.tsx
index 860cd73..8d57ee2 100644
--- a/src/components/atoms/forms/label.tsx
+++ b/src/components/atoms/forms/label.tsx
@@ -1,9 +1,23 @@
 import { FC } from 'react';
-import styles from './forms.module.scss';
+import styles from './label.module.scss';
 
-type LabelProps = {
-  htmlFor: string;
+export type LabelProps = {
+  /**
+   * Add classnames to the label.
+   */
+  className?: string;
+  /**
+   * The field id.
+   */
+  htmlFor?: string;
+  /**
+   * Is the field required? Default: false.
+   */
   required?: boolean;
+  /**
+   * The label size. Default: small.
+   */
+  size?: 'medium' | 'small';
 };
 
 /**
@@ -11,9 +25,17 @@ type LabelProps = {
  *
  * Render a HTML label element.
  */
-const Label: FC<LabelProps> = ({ children, required = false, ...props }) => {
+const Label: FC<LabelProps> = ({
+  children,
+  className = '',
+  required = false,
+  size = 'small',
+  ...props
+}) => {
+  const sizeClass = styles[`label--${size}`];
+
   return (
-    <label className={styles.label} {...props}>
+    <label className={`${styles.label} ${sizeClass} ${className}`} {...props}>
       {children}
       {required && <span className={styles.required}> *</span>}
     </label>
diff --git a/src/components/atoms/forms/select.stories.tsx b/src/components/atoms/forms/select.stories.tsx
index c7bb253..c2fb8c6 100644
--- a/src/components/atoms/forms/select.stories.tsx
+++ b/src/components/atoms/forms/select.stories.tsx
@@ -1,4 +1,5 @@
 import { ComponentMeta, ComponentStory } from '@storybook/react';
+import { useState } from 'react';
 import SelectComponent from './select';
 
 const selectOptions = [
@@ -10,14 +11,31 @@ const selectOptions = [
 export default {
   title: 'Atoms/Forms',
   component: SelectComponent,
+  args: {
+    disabled: false,
+    required: false,
+  },
   argTypes: {
-    classes: {
+    'aria-labelledby': {
       control: {
         type: 'text',
       },
-      description: 'Set additional classes',
+      description: 'One or more ids that refers to the select field name.',
       table: {
-        category: 'Options',
+        category: 'Accessibility',
+      },
+      type: {
+        name: 'string',
+        required: false,
+      },
+    },
+    className: {
+      control: {
+        type: 'text',
+      },
+      description: 'Add classnames to the select field.',
+      table: {
+        category: 'Styles',
       },
       type: {
         name: 'string',
@@ -59,9 +77,6 @@ export default {
       },
     },
     options: {
-      control: {
-        type: null,
-      },
       description: 'Select options.',
       type: {
         name: 'array',
@@ -100,7 +115,7 @@ export default {
     },
     value: {
       control: {
-        type: 'text',
+        type: null,
       },
       description: 'Field value.',
       type: {
@@ -111,13 +126,20 @@ export default {
   },
 } as ComponentMeta<typeof SelectComponent>;
 
-const Template: ComponentStory<typeof SelectComponent> = (args) => (
-  <SelectComponent {...args} />
-);
+const Template: ComponentStory<typeof SelectComponent> = ({
+  value,
+  setValue: _setValue,
+  ...args
+}) => {
+  const [selected, setSelected] = useState<string>(value);
+
+  return <SelectComponent value={selected} setValue={setSelected} {...args} />;
+};
 
 export const Select = Template.bind({});
 Select.args = {
+  id: 'storybook-select',
+  name: 'storybook-select',
   options: selectOptions,
-  setValue: () => null,
   value: 'option2',
 };
diff --git a/src/components/atoms/forms/select.tsx b/src/components/atoms/forms/select.tsx
index a42dbda..25e86e0 100644
--- a/src/components/atoms/forms/select.tsx
+++ b/src/components/atoms/forms/select.tsx
@@ -1,17 +1,30 @@
-import { ChangeEvent, FC, SetStateAction } from 'react';
+import { ChangeEvent, SetStateAction, VFC } from 'react';
 import styles from './forms.module.scss';
 
 export type SelectOptions = {
+  /**
+   * The option id.
+   */
   id: string;
+  /**
+   * The option name.
+   */
   name: string;
+  /**
+   * The option value.
+   */
   value: string;
 };
 
 export type SelectProps = {
   /**
-   * Set additional classes.
+   * One or more ids that refers to the select field name.
+   */
+  'aria-labelledby'?: string;
+  /**
+   * Add classnames to the select field.
    */
-  classes?: string;
+  className?: string;
   /**
    * Field state. Either enabled (false) or disabled (true).
    */
@@ -47,8 +60,8 @@ export type SelectProps = {
  *
  * Render a HTML select element.
  */
-const Select: FC<SelectProps> = ({
-  classes = '',
+const Select: VFC<SelectProps> = ({
+  className = '',
   options,
   setValue,
   ...props
@@ -74,7 +87,7 @@ const Select: FC<SelectProps> = ({
 
   return (
     <select
-      className={`${styles.field} ${styles['field--select']} ${classes}`}
+      className={`${styles.field} ${styles['field--select']} ${className}`}
       onChange={updateValue}
       {...props}
     >
diff --git a/src/components/atoms/forms/toggle.module.scss b/src/components/atoms/forms/toggle.module.scss
index 24b867e..2e8a49f 100644
--- a/src/components/atoms/forms/toggle.module.scss
+++ b/src/components/atoms/forms/toggle.module.scss
@@ -10,7 +10,7 @@
 }
 
 .title {
-  margin-right: auto;
+  margin-right: var(--spacing-2xs);
 }
 
 .toggle {
diff --git a/src/components/atoms/forms/toggle.tsx b/src/components/atoms/forms/toggle.tsx
index 7ef40ed..c3bc09d 100644
--- a/src/components/atoms/forms/toggle.tsx
+++ b/src/components/atoms/forms/toggle.tsx
@@ -26,6 +26,10 @@ export type ToggleProps = {
    * The toggle label.
    */
   label: string;
+  /**
+   * Set additional classnames to the label.
+   */
+  labelClassName?: string;
   /**
    * The label size.
    */
@@ -53,6 +57,7 @@ const Toggle: VFC<ToggleProps> = ({
   choices,
   id,
   label,
+  labelClassName = '',
   labelSize,
   name,
   setValue,
@@ -69,7 +74,7 @@ const Toggle: VFC<ToggleProps> = ({
         className={styles.checkbox}
       />
       <Label size={labelSize} htmlFor={id} className={styles.label}>
-        <span className={styles.title}>{label}</span>
+        <span className={`${styles.title} ${labelClassName}`}>{label}</span>
         {choices.left}
         <span className={styles.toggle}></span>
         {choices.right}
diff --git a/src/components/molecules/forms/labelled-field.module.scss b/src/components/molecules/forms/labelled-field.module.scss
new file mode 100644
index 0000000..64ef3d0
--- /dev/null
+++ b/src/components/molecules/forms/labelled-field.module.scss
@@ -0,0 +1,9 @@
+.label {
+  &--left {
+    margin-right: var(--spacing-2xs);
+  }
+
+  &--top {
+    display: block;
+  }
+}
diff --git a/src/components/molecules/forms/labelled-field.stories.tsx b/src/components/molecules/forms/labelled-field.stories.tsx
index eb7f8b5..b77d71e 100644
--- a/src/components/molecules/forms/labelled-field.stories.tsx
+++ b/src/components/molecules/forms/labelled-field.stories.tsx
@@ -1,4 +1,5 @@
 import { ComponentMeta, ComponentStory } from '@storybook/react';
+import { useState } from 'react';
 import LabelledFieldComponent from './labelled-field';
 
 export default {
@@ -6,6 +7,7 @@ export default {
   component: LabelledFieldComponent,
   args: {
     disabled: false,
+    labelPosition: 'top',
     required: false,
   },
   argTypes: {
@@ -43,6 +45,21 @@ export default {
         required: true,
       },
     },
+    labelPosition: {
+      control: {
+        type: 'select',
+      },
+      description: 'The label position.',
+      options: ['left', 'top'],
+      table: {
+        category: 'Options',
+        defaultValue: { summary: 'top' },
+      },
+      type: {
+        name: 'string',
+        required: false,
+      },
+    },
     max: {
       control: {
         type: 'number',
@@ -155,7 +172,7 @@ export default {
     },
     value: {
       control: {
-        type: 'text',
+        type: null,
       },
       description: 'Field value.',
       type: {
@@ -166,15 +183,19 @@ export default {
   },
 } as ComponentMeta<typeof LabelledFieldComponent>;
 
-const Template: ComponentStory<typeof LabelledFieldComponent> = (args) => (
-  <LabelledFieldComponent {...args} />
-);
+const Template: ComponentStory<typeof LabelledFieldComponent> = ({
+  value: _value,
+  setValue: _setValue,
+  ...args
+}) => {
+  const [value, setValue] = useState<string>('');
+
+  return <LabelledFieldComponent value={value} setValue={setValue} {...args} />;
+};
 
 export const LabelledField = Template.bind({});
 LabelledField.args = {
   id: 'labelled-field-storybook',
   label: 'Labelled field',
   name: 'labelled-field-storybook',
-  setValue: () => null,
-  value: '',
 };
diff --git a/src/components/molecules/forms/labelled-field.tsx b/src/components/molecules/forms/labelled-field.tsx
index 7f81e23..08d0126 100644
--- a/src/components/molecules/forms/labelled-field.tsx
+++ b/src/components/molecules/forms/labelled-field.tsx
@@ -1,20 +1,46 @@
 import Field, { type FieldProps } from '@components/atoms/forms/field';
 import Label from '@components/atoms/forms/label';
-import { FC } from 'react';
+import { VFC } from 'react';
+import styles from './labelled-field.module.scss';
 
-type LabelledFieldProps = FieldProps & {
+export type LabelledFieldProps = FieldProps & {
+  /**
+   * Visually hide the field label. Default: false.
+   */
+  hideLabel?: boolean;
+  /**
+   * The field label.
+   */
   label: string;
+  /**
+   * The label position. Default: top.
+   */
+  labelPosition?: 'left' | 'top';
 };
 
-const LabelledField: FC<LabelledFieldProps> = ({
+/**
+ * LabelledField component
+ *
+ * Render a field tied to a label.
+ */
+const LabelledField: VFC<LabelledFieldProps> = ({
+  hideLabel = false,
   id,
   label,
+  labelPosition = 'top',
   required,
   ...props
 }) => {
+  const positionModifier = `label--${labelPosition}`;
+  const visibilityClass = hideLabel ? 'screen-reader-text' : '';
+
   return (
     <>
-      <Label htmlFor={id} required={required}>
+      <Label
+        htmlFor={id}
+        required={required}
+        className={`${visibilityClass} ${styles[positionModifier]}`}
+      >
         {label}
       </Label>
       <Field id={id} required={required} {...props} />
diff --git a/src/components/molecules/forms/labelled-select.module.scss b/src/components/molecules/forms/labelled-select.module.scss
new file mode 100644
index 0000000..64ef3d0
--- /dev/null
+++ b/src/components/molecules/forms/labelled-select.module.scss
@@ -0,0 +1,9 @@
+.label {
+  &--left {
+    margin-right: var(--spacing-2xs);
+  }
+
+  &--top {
+    display: block;
+  }
+}
diff --git a/src/components/molecules/forms/labelled-select.stories.tsx b/src/components/molecules/forms/labelled-select.stories.tsx
index 0966e13..0c569f5 100644
--- a/src/components/molecules/forms/labelled-select.stories.tsx
+++ b/src/components/molecules/forms/labelled-select.stories.tsx
@@ -1,4 +1,5 @@
 import { ComponentMeta, ComponentStory } from '@storybook/react';
+import { useState } from 'react';
 import LabelledSelectComponent from './labelled-select';
 
 const selectOptions = [
@@ -12,6 +13,7 @@ export default {
   component: LabelledSelectComponent,
   args: {
     disabled: false,
+    labelPosition: 'top',
     required: false,
   },
   argTypes: {
@@ -49,6 +51,48 @@ export default {
         required: true,
       },
     },
+    labelClassName: {
+      control: {
+        type: 'text',
+      },
+      description: 'Set additional classnames to the label.',
+      table: {
+        category: 'Styles',
+      },
+      type: {
+        name: 'string',
+        required: false,
+      },
+    },
+    labelPosition: {
+      control: {
+        type: 'select',
+      },
+      description: 'The label position.',
+      options: ['left', 'top'],
+      table: {
+        category: 'Options',
+        defaultValue: { summary: 'top' },
+      },
+      type: {
+        name: 'string',
+        required: false,
+      },
+    },
+    labelSize: {
+      control: {
+        type: 'select',
+      },
+      description: 'The label size.',
+      options: ['medium', 'small'],
+      table: {
+        category: 'Options',
+      },
+      type: {
+        name: 'string',
+        required: false,
+      },
+    },
     name: {
       control: {
         type: 'text',
@@ -86,6 +130,19 @@ export default {
         required: false,
       },
     },
+    selectClassName: {
+      control: {
+        type: 'text',
+      },
+      description: 'Set additional classnames to the select field.',
+      table: {
+        category: 'Styles',
+      },
+      type: {
+        name: 'string',
+        required: false,
+      },
+    },
     setValue: {
       control: {
         type: null,
@@ -101,7 +158,7 @@ export default {
     },
     value: {
       control: {
-        type: 'text',
+        type: null,
       },
       description: 'Field value.',
       type: {
@@ -112,9 +169,21 @@ export default {
   },
 } as ComponentMeta<typeof LabelledSelectComponent>;
 
-const Template: ComponentStory<typeof LabelledSelectComponent> = (args) => (
-  <LabelledSelectComponent {...args} />
-);
+const Template: ComponentStory<typeof LabelledSelectComponent> = ({
+  value,
+  setValue: _setValue,
+  ...args
+}) => {
+  const [selected, setSelected] = useState<string>(value);
+
+  return (
+    <LabelledSelectComponent
+      value={selected}
+      setValue={setSelected}
+      {...args}
+    />
+  );
+};
 
 export const LabelledSelect = Template.bind({});
 LabelledSelect.args = {
@@ -122,6 +191,5 @@ LabelledSelect.args = {
   label: 'Labelled select',
   name: 'labelled-select-storybook',
   options: selectOptions,
-  setValue: () => null,
-  value: '',
+  value: 'option1',
 };
diff --git a/src/components/molecules/forms/labelled-select.tsx b/src/components/molecules/forms/labelled-select.tsx
index 442e91a..7d4237a 100644
--- a/src/components/molecules/forms/labelled-select.tsx
+++ b/src/components/molecules/forms/labelled-select.tsx
@@ -1,23 +1,62 @@
-import Label from '@components/atoms/forms/label';
+import Label, { LabelProps } from '@components/atoms/forms/label';
 import Select, { type SelectProps } from '@components/atoms/forms/select';
-import { FC } from 'react';
+import { VFC } from 'react';
+import styles from './labelled-select.module.scss';
 
-type LabelledSelectProps = SelectProps & {
+export type LabelledSelectProps = Omit<
+  SelectProps,
+  'aria-labelledby' | 'className'
+> & {
+  /**
+   * The field label.
+   */
   label: string;
+  /**
+   * Set additional classnames to the label.
+   */
+  labelClassName?: string;
+  /**
+   * The label position. Default: top.
+   */
+  labelPosition?: 'left' | 'top';
+  /**
+   * The label size.
+   */
+  labelSize?: LabelProps['size'];
+  /**
+   * Set additional classnames to the select field.
+   */
+  selectClassName?: string;
 };
 
-const LabelledSelect: FC<LabelledSelectProps> = ({
+const LabelledSelect: VFC<LabelledSelectProps> = ({
   id,
   label,
+  labelClassName = '',
+  labelPosition = 'top',
+  labelSize,
   required,
+  selectClassName = '',
   ...props
 }) => {
+  const positionModifier = `label--${labelPosition}`;
+
   return (
     <>
-      <Label htmlFor={id} required={required}>
+      <Label
+        htmlFor={id}
+        required={required}
+        size={labelSize}
+        className={`${styles[positionModifier]} ${labelClassName}`}
+      >
         {label}
       </Label>
-      <Select id={id} required={required} {...props} />
+      <Select
+        id={id}
+        required={required}
+        {...props}
+        className={selectClassName}
+      />
     </>
   );
 };
diff --git a/src/components/molecules/forms/motion-toggle.stories.tsx b/src/components/molecules/forms/motion-toggle.stories.tsx
index 4fc199a..dc4d2a9 100644
--- a/src/components/molecules/forms/motion-toggle.stories.tsx
+++ b/src/components/molecules/forms/motion-toggle.stories.tsx
@@ -5,6 +5,18 @@ import MotionToggleComponent from './motion-toggle';
 export default {
   title: 'Molecules/Forms',
   component: MotionToggleComponent,
+  argTypes: {
+    value: {
+      control: {
+        type: null,
+      },
+      description: 'The reduce motion value.',
+      type: {
+        name: 'boolean',
+        required: true,
+      },
+    },
+  },
 } as ComponentMeta<typeof MotionToggleComponent>;
 
 const Template: ComponentStory<typeof MotionToggleComponent> = (args) => (
@@ -14,3 +26,6 @@ const Template: ComponentStory<typeof MotionToggleComponent> = (args) => (
 );
 
 export const MotionToggle = Template.bind({});
+MotionToggle.args = {
+  value: false,
+};
diff --git a/src/components/molecules/forms/motion-toggle.tsx b/src/components/molecules/forms/motion-toggle.tsx
index 77291ed..d4f7d11 100644
--- a/src/components/molecules/forms/motion-toggle.tsx
+++ b/src/components/molecules/forms/motion-toggle.tsx
@@ -2,17 +2,17 @@ import Toggle, {
   ToggleChoices,
   ToggleProps,
 } from '@components/atoms/forms/toggle';
-import { FC, useState } from 'react';
+import { useState, VFC } from 'react';
 import { useIntl } from 'react-intl';
 
-export type MotionToggleProps = Pick<ToggleProps, 'value'>;
+export type MotionToggleProps = Pick<ToggleProps, 'labelClassName' | 'value'>;
 
 /**
  * MotionToggle component
  *
  * Render a Toggle component to set reduce motion.
  */
-const MotionToggle: FC<MotionToggleProps> = ({ value }) => {
+const MotionToggle: VFC<MotionToggleProps> = ({ value, ...props }) => {
   const intl = useIntl();
   const [isDeactivated, setIsDeactivated] = useState<boolean>(value);
   const reduceMotionLabel = intl.formatMessage({
@@ -40,9 +40,11 @@ const MotionToggle: FC<MotionToggleProps> = ({ value }) => {
       id="reduce-motion-settings"
       name="reduce-motion-settings"
       label={reduceMotionLabel}
+      labelSize="medium"
       choices={reduceMotionChoices}
       value={isDeactivated}
       setValue={setIsDeactivated}
+      {...props}
     />
   );
 };
diff --git a/src/components/molecules/forms/prism-theme-toggle.stories.tsx b/src/components/molecules/forms/prism-theme-toggle.stories.tsx
index fee54ef..dc9090b 100644
--- a/src/components/molecules/forms/prism-theme-toggle.stories.tsx
+++ b/src/components/molecules/forms/prism-theme-toggle.stories.tsx
@@ -5,6 +5,18 @@ import PrismThemeToggleComponent from './prism-theme-toggle';
 export default {
   title: 'Molecules/Forms',
   component: PrismThemeToggleComponent,
+  argTypes: {
+    value: {
+      control: {
+        type: null,
+      },
+      description: 'The prism theme value.',
+      type: {
+        name: 'boolean',
+        required: true,
+      },
+    },
+  },
 } as ComponentMeta<typeof PrismThemeToggleComponent>;
 
 const Template: ComponentStory<typeof PrismThemeToggleComponent> = (args) => (
@@ -14,3 +26,6 @@ const Template: ComponentStory<typeof PrismThemeToggleComponent> = (args) => (
 );
 
 export const PrismThemeToggle = Template.bind({});
+PrismThemeToggle.args = {
+  value: false,
+};
diff --git a/src/components/molecules/forms/prism-theme-toggle.tsx b/src/components/molecules/forms/prism-theme-toggle.tsx
index cedb71a..81a211b 100644
--- a/src/components/molecules/forms/prism-theme-toggle.tsx
+++ b/src/components/molecules/forms/prism-theme-toggle.tsx
@@ -4,17 +4,20 @@ import Toggle, {
 } from '@components/atoms/forms/toggle';
 import Moon from '@components/atoms/icons/moon';
 import Sun from '@components/atoms/icons/sun';
-import { FC, useState } from 'react';
+import { useState, VFC } from 'react';
 import { useIntl } from 'react-intl';
 
-export type PrismThemeToggleProps = Pick<ToggleProps, 'value'>;
+export type PrismThemeToggleProps = Pick<
+  ToggleProps,
+  'labelClassName' | 'value'
+>;
 
 /**
  * PrismThemeToggle component
  *
  * Render a Toggle component to set code blocks theme.
  */
-const PrismThemeToggle: FC<PrismThemeToggleProps> = ({ value }) => {
+const PrismThemeToggle: VFC<PrismThemeToggleProps> = ({ value, ...props }) => {
   const intl = useIntl();
   const [isDarkTheme, setIsDarkTheme] = useState<boolean>(value);
   const themeLabel = intl.formatMessage({
@@ -42,9 +45,11 @@ const PrismThemeToggle: FC<PrismThemeToggleProps> = ({ value }) => {
       id="prism-theme-settings"
       name="prism-theme-settings"
       label={themeLabel}
+      labelSize="medium"
       choices={themeChoices}
       value={isDarkTheme}
       setValue={setIsDarkTheme}
+      {...props}
     />
   );
 };
diff --git a/src/components/molecules/forms/select-with-tooltip.module.scss b/src/components/molecules/forms/select-with-tooltip.module.scss
index 1f91f74..bfadece 100644
--- a/src/components/molecules/forms/select-with-tooltip.module.scss
+++ b/src/components/molecules/forms/select-with-tooltip.module.scss
@@ -5,14 +5,9 @@
   display: flex;
   flex-flow: row wrap;
   align-items: center;
-  gap: var(--spacing-xs);
   position: relative;
 }
 
-.label {
-  margin-right: auto;
-}
-
 .select {
   width: auto;
 
@@ -22,6 +17,8 @@
 }
 
 .btn {
+  margin-left: var(--spacing-xs);
+
   &--activated {
     background: var(--color-primary);
 
@@ -34,8 +31,7 @@
 .tooltip {
   position: absolute;
   top: calc(100% + var(--spacing-xs));
-  right: 0;
-  transform-origin: top right;
+  transform-origin: top;
   transition: all 0.75s ease-in-out 0s;
 
   &--hidden {
diff --git a/src/components/molecules/forms/select-with-tooltip.stories.tsx b/src/components/molecules/forms/select-with-tooltip.stories.tsx
index d2d36fa..c63e9b8 100644
--- a/src/components/molecules/forms/select-with-tooltip.stories.tsx
+++ b/src/components/molecules/forms/select-with-tooltip.stories.tsx
@@ -17,11 +17,25 @@ export default {
         required: true,
       },
     },
-    title: {
+    disabled: {
+      control: {
+        type: 'boolean',
+      },
+      description: 'Field state: either enabled or disabled.',
+      table: {
+        category: 'Options',
+        defaultValue: { summary: false },
+      },
+      type: {
+        name: 'boolean',
+        required: false,
+      },
+    },
+    id: {
       control: {
         type: 'text',
       },
-      description: 'The tooltip title',
+      description: 'Field id.',
       type: {
         name: 'string',
         required: true,
@@ -37,28 +51,31 @@ export default {
         required: true,
       },
     },
-    disabled: {
+    labelClassName: {
       control: {
-        type: 'boolean',
+        type: 'text',
       },
-      description: 'Field state: either enabled or disabled.',
+      description: 'Set additional classnames to the label.',
       table: {
-        category: 'Options',
-        defaultValue: { summary: false },
+        category: 'Styles',
       },
       type: {
-        name: 'boolean',
+        name: 'string',
         required: false,
       },
     },
-    id: {
+    labelSize: {
       control: {
-        type: 'text',
+        type: 'select',
+      },
+      description: 'The label size.',
+      options: ['medium', 'small'],
+      table: {
+        category: 'Options',
       },
-      description: 'Field id.',
       type: {
         name: 'string',
-        required: true,
+        required: false,
       },
     },
     name: {
@@ -98,6 +115,19 @@ export default {
         required: false,
       },
     },
+    selectClassName: {
+      control: {
+        type: 'text',
+      },
+      description: 'Set additional classnames to the select field.',
+      table: {
+        category: 'Styles',
+      },
+      type: {
+        name: 'string',
+        required: false,
+      },
+    },
     setValue: {
       control: {
         type: null,
@@ -111,6 +141,29 @@ export default {
         required: true,
       },
     },
+    title: {
+      control: {
+        type: 'text',
+      },
+      description: 'The tooltip title',
+      type: {
+        name: 'string',
+        required: true,
+      },
+    },
+    tooltipClassName: {
+      control: {
+        type: 'text',
+      },
+      description: 'Set additional classnames to the tooltip.',
+      table: {
+        category: 'Styles',
+      },
+      type: {
+        name: 'string',
+        required: false,
+      },
+    },
     value: {
       control: {
         type: 'text',
diff --git a/src/components/molecules/forms/select-with-tooltip.tsx b/src/components/molecules/forms/select-with-tooltip.tsx
index 5e48d62..f537e1e 100644
--- a/src/components/molecules/forms/select-with-tooltip.tsx
+++ b/src/components/molecules/forms/select-with-tooltip.tsx
@@ -1,19 +1,22 @@
-import Select, { SelectProps } from '@components/atoms/forms/select';
-import { FC, useState } from 'react';
+import { useState, VFC } from 'react';
 import HelpButton from '../buttons/help-button';
-import Tooltip, { TooltipProps } from '../modals/tooltip';
+import Tooltip, { type TooltipProps } from '../modals/tooltip';
+import LabelledSelect, { type LabelledSelectProps } from './labelled-select';
 import styles from './select-with-tooltip.module.scss';
 
-export type SelectWithTooltipProps = SelectProps &
+export type SelectWithTooltipProps = Omit<
+  LabelledSelectProps,
+  'labelPosition'
+> &
   Pick<TooltipProps, 'title' | 'content'> & {
     /**
      * The select label.
      */
     label: string;
     /**
-     * Set additional classes to the tooltip wrapper.
+     * Set additional classnames to the tooltip wrapper.
      */
-    tooltipClasses?: string;
+    tooltipClassName?: string;
   };
 
 /**
@@ -21,12 +24,11 @@ export type SelectWithTooltipProps = SelectProps &
  *
  * Render a select with a button to display a tooltip about options.
  */
-const SelectWithTooltip: FC<SelectWithTooltipProps> = ({
+const SelectWithTooltip: VFC<SelectWithTooltipProps> = ({
   title,
   content,
   id,
-  label,
-  tooltipClasses = '',
+  tooltipClassName = '',
   ...props
 }) => {
   const [isTooltipOpened, setIsTooltipOpened] = useState<boolean>(false);
@@ -37,19 +39,21 @@ const SelectWithTooltip: FC<SelectWithTooltipProps> = ({
 
   return (
     <div className={styles.wrapper}>
-      <label htmlFor={id} className={styles.label}>
-        {label}
-      </label>
-      <Select id={id} {...props} classes={styles.select} />
+      <LabelledSelect
+        labelPosition="left"
+        id={id}
+        labelClassName={styles.label}
+        {...props}
+      />
       <HelpButton
         onClick={() => setIsTooltipOpened(!isTooltipOpened)}
-        classes={buttonModifier}
+        className={`${styles.btn} ${buttonModifier}`}
       />
       <Tooltip
         title={title}
         content={content}
         icon="?"
-        classes={`${styles.tooltip} ${tooltipModifier} ${tooltipClasses}`}
+        className={`${styles.tooltip} ${tooltipModifier} ${tooltipClassName}`}
       />
     </div>
   );
diff --git a/src/components/molecules/forms/theme-toggle.stories.tsx b/src/components/molecules/forms/theme-toggle.stories.tsx
index 5970afd..a9bcf73 100644
--- a/src/components/molecules/forms/theme-toggle.stories.tsx
+++ b/src/components/molecules/forms/theme-toggle.stories.tsx
@@ -5,6 +5,18 @@ import ThemeToggleComponent from './theme-toggle';
 export default {
   title: 'Molecules/Forms',
   component: ThemeToggleComponent,
+  argTypes: {
+    value: {
+      control: {
+        type: null,
+      },
+      description: 'The theme value.',
+      type: {
+        name: 'boolean',
+        required: true,
+      },
+    },
+  },
 } as ComponentMeta<typeof ThemeToggleComponent>;
 
 const Template: ComponentStory<typeof ThemeToggleComponent> = (args) => (
@@ -14,3 +26,6 @@ const Template: ComponentStory<typeof ThemeToggleComponent> = (args) => (
 );
 
 export const ThemeToggle = Template.bind({});
+ThemeToggle.args = {
+  value: false,
+};
diff --git a/src/components/molecules/forms/theme-toggle.tsx b/src/components/molecules/forms/theme-toggle.tsx
index c927151..6d54591 100644
--- a/src/components/molecules/forms/theme-toggle.tsx
+++ b/src/components/molecules/forms/theme-toggle.tsx
@@ -4,17 +4,17 @@ import Toggle, {
 } from '@components/atoms/forms/toggle';
 import Moon from '@components/atoms/icons/moon';
 import Sun from '@components/atoms/icons/sun';
-import { FC, useState } from 'react';
+import { useState, VFC } from 'react';
 import { useIntl } from 'react-intl';
 
-export type ThemeToggleProps = Pick<ToggleProps, 'value'>;
+export type ThemeToggleProps = Pick<ToggleProps, 'labelClassName' | 'value'>;
 
 /**
  * ThemeToggle component
  *
  * Render a Toggle component to set theme.
  */
-const ThemeToggle: FC<ThemeToggleProps> = ({ value }) => {
+const ThemeToggle: VFC<ThemeToggleProps> = ({ value, ...props }) => {
   const intl = useIntl();
   const [isDarkTheme, setIsDarkTheme] = useState<boolean>(value);
   const themeLabel = intl.formatMessage({
@@ -42,9 +42,11 @@ const ThemeToggle: FC<ThemeToggleProps> = ({ value }) => {
       id="theme-settings"
       name="theme-settings"
       label={themeLabel}
+      labelSize="medium"
       choices={themeChoices}
       value={isDarkTheme}
       setValue={setIsDarkTheme}
+      {...props}
     />
   );
 };
diff --git a/src/components/organisms/modals/settings-modal.module.scss b/src/components/organisms/modals/settings-modal.module.scss
new file mode 100644
index 0000000..f17c9b3
--- /dev/null
+++ b/src/components/organisms/modals/settings-modal.module.scss
@@ -0,0 +1,14 @@
+.wrapper {
+  max-width: 30ch;
+
+  .label {
+    margin-right: auto;
+  }
+}
+
+.tooltip {
+  width: 120%;
+  top: calc(100% + var(--spacing-sm));
+  right: -10%;
+  transform-origin: top right;
+}
diff --git a/src/components/organisms/modals/settings-modal.stories.tsx b/src/components/organisms/modals/settings-modal.stories.tsx
new file mode 100644
index 0000000..c19a6d7
--- /dev/null
+++ b/src/components/organisms/modals/settings-modal.stories.tsx
@@ -0,0 +1,31 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import { IntlProvider } from 'react-intl';
+import SettingsModal from './settings-modal';
+
+export default {
+  title: 'Organisms/Modals',
+  component: SettingsModal,
+  argTypes: {
+    className: {
+      control: {
+        type: 'text',
+      },
+      description: 'Set additional classnames to the modal wrapper.',
+      table: {
+        category: 'Styles',
+      },
+      type: {
+        name: 'string',
+        required: false,
+      },
+    },
+  },
+} as ComponentMeta<typeof SettingsModal>;
+
+const Template: ComponentStory<typeof SettingsModal> = (args) => (
+  <IntlProvider locale="en">
+    <SettingsModal {...args} />
+  </IntlProvider>
+);
+
+export const Settings = Template.bind({});
diff --git a/src/components/organisms/modals/settings-modal.test.tsx b/src/components/organisms/modals/settings-modal.test.tsx
new file mode 100644
index 0000000..44695d7
--- /dev/null
+++ b/src/components/organisms/modals/settings-modal.test.tsx
@@ -0,0 +1,34 @@
+import { render, screen } from '@test-utils';
+import SettingsModal from './settings-modal';
+
+jest.mock('next/dynamic', () => () => 'dynamic-import');
+
+describe('SettingsModal', () => {
+  it('renders a theme toggle setting', () => {
+    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();
+  });
+});
diff --git a/src/components/organisms/modals/settings-modal.tsx b/src/components/organisms/modals/settings-modal.tsx
new file mode 100644
index 0000000..0fac332
--- /dev/null
+++ b/src/components/organisms/modals/settings-modal.tsx
@@ -0,0 +1,51 @@
+import Form from '@components/atoms/forms/form';
+import AckeeSelect 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 Modal from '@components/molecules/modals/modal';
+import { VFC } from 'react';
+import { useIntl } from 'react-intl';
+import styles from './settings-modal.module.scss';
+
+export type SettingsModalProps = {
+  /**
+   * Set additional classnames to modal wrapper.
+   */
+  className?: string;
+};
+
+/**
+ * SettingsModal component
+ *
+ * Render a modal with settings options.
+ */
+const SettingsModal: VFC<SettingsModalProps> = ({ className }) => {
+  const intl = useIntl();
+  const title = intl.formatMessage({
+    defaultMessage: 'Settings',
+    description: 'SettingsModal: title',
+    id: 'gPfT/K',
+  });
+
+  return (
+    <Modal
+      title={title}
+      icon="cogs"
+      className={`${styles.wrapper} ${className}`}
+    >
+      <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={styles.tooltip}
+        />
+      </Form>
+    </Modal>
+  );
+};
+
+export default SettingsModal;
-- 
cgit v1.2.3


From 5a6e4eea16047083e2de0e91a1b3ed9be8d6eb68 Mon Sep 17 00:00:00 2001
From: Armand Philippot <git@armandphilippot.com>
Date: Sat, 16 Apr 2022 16:08:49 +0200
Subject: refactor: support React 18

I replaced the deprecated VFC type with FC type and made all children
explicits.
Formatjs is still not compatible with React 18 so I need to skip type
checking when comitting. There are some type errors because of
IntlProvider in Storybook stories.
---
 jest.setup.js                                      |  1 +
 src/components/atoms/buttons/button-link.tsx       |  6 +-
 src/components/atoms/buttons/button.tsx            |  6 +-
 src/components/atoms/forms/checkbox.tsx            |  4 +-
 src/components/atoms/forms/field.tsx               |  4 +-
 src/components/atoms/forms/form.test.tsx           |  6 +-
 src/components/atoms/forms/form.tsx                |  6 +-
 src/components/atoms/forms/label.tsx               |  6 +-
 src/components/atoms/forms/select.tsx              |  4 +-
 src/components/atoms/headings/heading.tsx          |  6 +-
 src/components/atoms/icons/arrow.tsx               |  4 +-
 src/components/atoms/icons/career.tsx              |  4 +-
 src/components/atoms/icons/cc-by-sa.tsx            |  4 +-
 src/components/atoms/icons/close.tsx               |  4 +-
 src/components/atoms/icons/cog.tsx                 |  4 +-
 src/components/atoms/icons/computer-screen.tsx     |  4 +-
 src/components/atoms/icons/envelop.tsx             |  4 +-
 src/components/atoms/icons/hamburger.tsx           |  2 +-
 src/components/atoms/icons/home.tsx                |  4 +-
 src/components/atoms/icons/magnifying-glass.tsx    |  4 +-
 src/components/atoms/icons/moon.tsx                |  6 +-
 src/components/atoms/icons/plus-minus.tsx          |  2 +-
 src/components/atoms/icons/posts-stack.tsx         |  4 +-
 src/components/atoms/icons/sun.tsx                 |  6 +-
 src/components/atoms/images/logo.tsx               |  6 +-
 src/components/atoms/layout/copyright.tsx          |  4 +-
 src/components/atoms/layout/main.tsx               |  6 +-
 src/components/atoms/layout/no-script.tsx          |  4 +-
 src/components/atoms/layout/notice.tsx             |  4 +-
 src/components/atoms/layout/section.tsx            |  4 +-
 src/components/atoms/links/link.module.scss        | 20 ++++--
 src/components/atoms/links/link.tsx                |  6 +-
 src/components/atoms/links/nav-link.tsx            |  4 +-
 src/components/atoms/links/sharing-link.tsx        |  4 +-
 src/components/atoms/links/social-link.tsx         |  4 +-
 src/components/atoms/lists/description-list.tsx    |  4 +-
 src/components/atoms/lists/list.tsx                |  4 +-
 src/components/atoms/loaders/progress-bar.tsx      |  4 +-
 src/components/atoms/loaders/spinner.tsx           |  4 +-
 src/components/molecules/buttons/back-to-top.tsx   | 14 ++--
 .../molecules/buttons/heading-button.tsx           |  4 +-
 src/components/molecules/buttons/help-button.tsx   |  6 +-
 .../molecules/forms/ackee-select.test.tsx          | 16 +++--
 src/components/molecules/forms/ackee-select.tsx    |  6 +-
 src/components/molecules/forms/labelled-field.tsx  |  4 +-
 src/components/molecules/forms/labelled-select.tsx | 15 +++--
 src/components/molecules/forms/motion-toggle.tsx   |  8 +--
 .../molecules/forms/prism-theme-toggle.tsx         |  8 +--
 .../molecules/forms/select-with-tooltip.tsx        | 10 +--
 src/components/molecules/forms/theme-toggle.tsx    |  8 +--
 src/components/molecules/forms/toggle.tsx          | 18 ++---
 .../molecules/images/responsive-image.tsx          | 10 +--
 src/components/molecules/layout/branding.tsx       | 12 ++--
 src/components/molecules/layout/card.tsx           |  8 +--
 src/components/molecules/layout/flipping-logo.tsx  | 10 +--
 src/components/molecules/layout/meta.tsx           |  4 +-
 src/components/molecules/layout/widget.tsx         | 10 ++-
 src/components/molecules/modals/modal.test.tsx     | 13 +++-
 src/components/molecules/modals/modal.tsx          | 14 ++--
 src/components/molecules/modals/tooltip.tsx        |  4 +-
 src/components/molecules/nav/breadcrumb.tsx        |  4 +-
 src/components/molecules/nav/nav.tsx               |  4 +-
 .../organisms/forms/comment-form.stories.tsx       |  1 -
 src/components/organisms/forms/comment-form.tsx    |  4 +-
 src/components/organisms/forms/contact-form.tsx    |  4 +-
 src/components/organisms/forms/search-form.tsx     | 11 +++-
 src/components/organisms/layout/cards-list.tsx     |  4 +-
 src/components/organisms/layout/footer.tsx         | 13 ++--
 src/components/organisms/layout/overview.tsx       |  4 +-
 .../organisms/layout/summary.stories.tsx           |  2 +-
 src/components/organisms/layout/summary.test.tsx   |  2 +-
 src/components/organisms/layout/summary.tsx        | 77 +++++++++++++---------
 src/components/organisms/modals/search-modal.tsx   |  8 +--
 src/components/organisms/modals/settings-modal.tsx | 14 ++--
 src/components/organisms/toolbar/main-nav.tsx      | 18 +++--
 src/components/organisms/toolbar/search.tsx        | 16 ++---
 src/components/organisms/toolbar/settings.tsx      | 14 ++--
 src/components/organisms/toolbar/toolbar.tsx       |  4 +-
 src/components/organisms/widgets/image-widget.tsx  | 34 ++++------
 .../organisms/widgets/links-list-widget.tsx        |  9 ++-
 src/components/organisms/widgets/sharing.tsx       |  6 +-
 81 files changed, 353 insertions(+), 289 deletions(-)

(limited to 'src/components/molecules/forms/select-with-tooltip.tsx')

diff --git a/jest.setup.js b/jest.setup.js
index 5c5ecbc..d50c988 100644
--- a/jest.setup.js
+++ b/jest.setup.js
@@ -2,3 +2,4 @@ import '@testing-library/jest-dom/extend-expect';
 import './__tests__/jest/__mocks__/matchMedia.mock';
 
 jest.mock('next/dist/client/router', () => require('next-router-mock'));
+jest.mock('next/dynamic', () => () => 'dynamic-import');
diff --git a/src/components/atoms/buttons/button-link.tsx b/src/components/atoms/buttons/button-link.tsx
index 77a7f7b..906fa76 100644
--- a/src/components/atoms/buttons/button-link.tsx
+++ b/src/components/atoms/buttons/button-link.tsx
@@ -1,5 +1,5 @@
 import Link from 'next/link';
-import { FC } from 'react';
+import { FC, ReactNode } from 'react';
 import styles from './buttons.module.scss';
 
 export type ButtonLinkProps = {
@@ -7,6 +7,10 @@ export type ButtonLinkProps = {
    * ButtonLink accessible label.
    */
   'aria-label'?: string;
+  /**
+   * The button link body.
+   */
+  children: ReactNode;
   /**
    * Set additional classnames to the button link.
    */
diff --git a/src/components/atoms/buttons/button.tsx b/src/components/atoms/buttons/button.tsx
index 545c5c5..a6eef8b 100644
--- a/src/components/atoms/buttons/button.tsx
+++ b/src/components/atoms/buttons/button.tsx
@@ -1,7 +1,11 @@
-import { FC, MouseEventHandler } from 'react';
+import { FC, MouseEventHandler, ReactNode } from 'react';
 import styles from './buttons.module.scss';
 
 export type ButtonProps = {
+  /**
+   * The button body.
+   */
+  children: ReactNode;
   /**
    * Set additional classnames to the button wrapper.
    */
diff --git a/src/components/atoms/forms/checkbox.tsx b/src/components/atoms/forms/checkbox.tsx
index 8babcc8..aec97f0 100644
--- a/src/components/atoms/forms/checkbox.tsx
+++ b/src/components/atoms/forms/checkbox.tsx
@@ -1,4 +1,4 @@
-import { SetStateAction, VFC } from 'react';
+import { FC, SetStateAction } from 'react';
 
 export type CheckboxProps = {
   /**
@@ -32,7 +32,7 @@ export type CheckboxProps = {
  *
  * Render a checkbox type input.
  */
-const Checkbox: VFC<CheckboxProps> = ({ value, setValue, ...props }) => {
+const Checkbox: FC<CheckboxProps> = ({ value, setValue, ...props }) => {
   return (
     <input
       type="checkbox"
diff --git a/src/components/atoms/forms/field.tsx b/src/components/atoms/forms/field.tsx
index 2e75d0f..e45a8a7 100644
--- a/src/components/atoms/forms/field.tsx
+++ b/src/components/atoms/forms/field.tsx
@@ -1,4 +1,4 @@
-import { ChangeEvent, SetStateAction, VFC } from 'react';
+import { ChangeEvent, FC, SetStateAction } from 'react';
 import styles from './forms.module.scss';
 
 export type FieldType =
@@ -72,7 +72,7 @@ export type FieldProps = {
  *
  * Render either an input or a textarea.
  */
-const Field: VFC<FieldProps> = ({
+const Field: FC<FieldProps> = ({
   className = '',
   setValue,
   type,
diff --git a/src/components/atoms/forms/form.test.tsx b/src/components/atoms/forms/form.test.tsx
index 9cd3c58..8b534f1 100644
--- a/src/components/atoms/forms/form.test.tsx
+++ b/src/components/atoms/forms/form.test.tsx
@@ -3,7 +3,11 @@ import Form from './form';
 
 describe('Form', () => {
   it('renders a form', () => {
-    render(<Form aria-label="Jest form" onSubmit={() => null}></Form>);
+    render(
+      <Form aria-label="Jest form" onSubmit={() => null}>
+        Fields
+      </Form>
+    );
     expect(screen.getByRole('form', { name: 'Jest form' })).toBeInTheDocument();
   });
 });
diff --git a/src/components/atoms/forms/form.tsx b/src/components/atoms/forms/form.tsx
index 8e80930..ef8dce4 100644
--- a/src/components/atoms/forms/form.tsx
+++ b/src/components/atoms/forms/form.tsx
@@ -1,4 +1,4 @@
-import { Children, FC, FormEvent, Fragment } from 'react';
+import { Children, FC, FormEvent, Fragment, ReactNode } from 'react';
 import styles from './forms.module.scss';
 
 export type FormProps = {
@@ -10,6 +10,10 @@ export type FormProps = {
    * One or more ids that refers to the form name.
    */
   'aria-labelledby'?: string;
+  /**
+   * The form body.
+   */
+  children: ReactNode;
   /**
    * Set additional classnames to the form wrapper.
    */
diff --git a/src/components/atoms/forms/label.tsx b/src/components/atoms/forms/label.tsx
index 8d57ee2..ce4c70f 100644
--- a/src/components/atoms/forms/label.tsx
+++ b/src/components/atoms/forms/label.tsx
@@ -1,7 +1,11 @@
-import { FC } from 'react';
+import { FC, ReactNode } from 'react';
 import styles from './label.module.scss';
 
 export type LabelProps = {
+  /**
+   * The label body.
+   */
+  children: ReactNode;
   /**
    * Add classnames to the label.
    */
diff --git a/src/components/atoms/forms/select.tsx b/src/components/atoms/forms/select.tsx
index 25e86e0..dbe9b37 100644
--- a/src/components/atoms/forms/select.tsx
+++ b/src/components/atoms/forms/select.tsx
@@ -1,4 +1,4 @@
-import { ChangeEvent, SetStateAction, VFC } from 'react';
+import { ChangeEvent, FC, SetStateAction } from 'react';
 import styles from './forms.module.scss';
 
 export type SelectOptions = {
@@ -60,7 +60,7 @@ export type SelectProps = {
  *
  * Render a HTML select element.
  */
-const Select: VFC<SelectProps> = ({
+const Select: FC<SelectProps> = ({
   className = '',
   options,
   setValue,
diff --git a/src/components/atoms/headings/heading.tsx b/src/components/atoms/headings/heading.tsx
index 4703b5d..c5bf4ca 100644
--- a/src/components/atoms/headings/heading.tsx
+++ b/src/components/atoms/headings/heading.tsx
@@ -1,9 +1,13 @@
-import { FC } from 'react';
+import { FC, ReactNode } from 'react';
 import styles from './heading.module.scss';
 
 export type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6;
 
 export type HeadingProps = {
+  /**
+   * The heading body.
+   */
+  children: ReactNode;
   /**
    * Set additional classnames.
    */
diff --git a/src/components/atoms/icons/arrow.tsx b/src/components/atoms/icons/arrow.tsx
index 5f3c460..2962530 100644
--- a/src/components/atoms/icons/arrow.tsx
+++ b/src/components/atoms/icons/arrow.tsx
@@ -1,4 +1,4 @@
-import { VFC } from 'react';
+import { FC } from 'react';
 import styles from './arrow.module.scss';
 
 export type ArrowDirection = 'top' | 'right' | 'bottom' | 'left';
@@ -19,7 +19,7 @@ export type ArrowProps = {
  *
  * Render a svg arrow icon.
  */
-const Arrow: VFC<ArrowProps> = ({ className = '', direction }) => {
+const Arrow: FC<ArrowProps> = ({ className = '', direction }) => {
   const directionClass = styles[`icon--${direction}`];
   const classes = `${styles.icon} ${directionClass} ${className}`;
 
diff --git a/src/components/atoms/icons/career.tsx b/src/components/atoms/icons/career.tsx
index 28edcc7..f28d399 100644
--- a/src/components/atoms/icons/career.tsx
+++ b/src/components/atoms/icons/career.tsx
@@ -1,4 +1,4 @@
-import { VFC } from 'react';
+import { FC } from 'react';
 import styles from './career.module.scss';
 
 export type CareerProps = {
@@ -13,7 +13,7 @@ export type CareerProps = {
  *
  * Render a career svg icon.
  */
-const Career: VFC<CareerProps> = ({ className = '' }) => {
+const Career: FC<CareerProps> = ({ className = '' }) => {
   return (
     <svg
       viewBox="0 0 100 100"
diff --git a/src/components/atoms/icons/cc-by-sa.tsx b/src/components/atoms/icons/cc-by-sa.tsx
index 552504e..8239154 100644
--- a/src/components/atoms/icons/cc-by-sa.tsx
+++ b/src/components/atoms/icons/cc-by-sa.tsx
@@ -1,4 +1,4 @@
-import { VFC } from 'react';
+import { FC } from 'react';
 import { useIntl } from 'react-intl';
 import styles from './cc-by-sa.module.scss';
 
@@ -14,7 +14,7 @@ export type CCBySAProps = {
  *
  * Render a CC BY SA svg icon.
  */
-const CCBySA: VFC<CCBySAProps> = ({ className = '' }) => {
+const CCBySA: FC<CCBySAProps> = ({ className = '' }) => {
   const intl = useIntl();
 
   return (
diff --git a/src/components/atoms/icons/close.tsx b/src/components/atoms/icons/close.tsx
index eb9ce7c..3e0adb5 100644
--- a/src/components/atoms/icons/close.tsx
+++ b/src/components/atoms/icons/close.tsx
@@ -1,4 +1,4 @@
-import { VFC } from 'react';
+import { FC } from 'react';
 import styles from './close.module.scss';
 
 export type CloseProps = {
@@ -13,7 +13,7 @@ export type CloseProps = {
  *
  * Render a close svg icon.
  */
-const Close: VFC<CloseProps> = ({ className = '' }) => {
+const Close: FC<CloseProps> = ({ className = '' }) => {
   return (
     <svg
       viewBox="0 0 100 100"
diff --git a/src/components/atoms/icons/cog.tsx b/src/components/atoms/icons/cog.tsx
index df6d54d..9e78a7b 100644
--- a/src/components/atoms/icons/cog.tsx
+++ b/src/components/atoms/icons/cog.tsx
@@ -1,4 +1,4 @@
-import { VFC } from 'react';
+import { FC } from 'react';
 import styles from './cog.module.scss';
 
 export type CogProps = {
@@ -13,7 +13,7 @@ export type CogProps = {
  *
  * Render a cog svg icon.
  */
-const Cog: VFC<CogProps> = ({ className = '' }) => {
+const Cog: FC<CogProps> = ({ className = '' }) => {
   return (
     <svg
       viewBox="0 0 100 100"
diff --git a/src/components/atoms/icons/computer-screen.tsx b/src/components/atoms/icons/computer-screen.tsx
index 310836f..8786139 100644
--- a/src/components/atoms/icons/computer-screen.tsx
+++ b/src/components/atoms/icons/computer-screen.tsx
@@ -1,4 +1,4 @@
-import { VFC } from 'react';
+import { FC } from 'react';
 import styles from './computer-screen.module.scss';
 
 export type ComputerScreenProps = {
@@ -13,7 +13,7 @@ export type ComputerScreenProps = {
  *
  * Render a computer screen svg icon.
  */
-const ComputerScreen: VFC<ComputerScreenProps> = ({ className = '' }) => {
+const ComputerScreen: FC<ComputerScreenProps> = ({ className = '' }) => {
   return (
     <svg
       viewBox="0 0 100 100"
diff --git a/src/components/atoms/icons/envelop.tsx b/src/components/atoms/icons/envelop.tsx
index 7b50d1d..84dca97 100644
--- a/src/components/atoms/icons/envelop.tsx
+++ b/src/components/atoms/icons/envelop.tsx
@@ -1,4 +1,4 @@
-import { VFC } from 'react';
+import { FC } from 'react';
 import styles from './envelop.module.scss';
 
 export type EnvelopProps = {
@@ -13,7 +13,7 @@ export type EnvelopProps = {
  *
  * Render an envelop svg icon.
  */
-const Envelop: VFC<EnvelopProps> = ({ className = '' }) => {
+const Envelop: FC<EnvelopProps> = ({ className = '' }) => {
   return (
     <svg
       viewBox="0 0 100 100"
diff --git a/src/components/atoms/icons/hamburger.tsx b/src/components/atoms/icons/hamburger.tsx
index 7e7c2c9..93aed2a 100644
--- a/src/components/atoms/icons/hamburger.tsx
+++ b/src/components/atoms/icons/hamburger.tsx
@@ -1,7 +1,7 @@
 import { FC } from 'react';
 import styles from './hamburger.module.scss';
 
-type HamburgerProps = {
+export type HamburgerProps = {
   /**
    * Set additional classnames to the icon wrapper.
    */
diff --git a/src/components/atoms/icons/home.tsx b/src/components/atoms/icons/home.tsx
index 71bbc4a..3b6732d 100644
--- a/src/components/atoms/icons/home.tsx
+++ b/src/components/atoms/icons/home.tsx
@@ -1,4 +1,4 @@
-import { VFC } from 'react';
+import { FC } from 'react';
 import styles from './home.module.scss';
 
 export type HomeProps = {
@@ -13,7 +13,7 @@ export type HomeProps = {
  *
  * Render a home svg icon.
  */
-const Home: VFC<HomeProps> = ({ className = '' }) => {
+const Home: FC<HomeProps> = ({ className = '' }) => {
   return (
     <svg
       viewBox="0 0 100 100"
diff --git a/src/components/atoms/icons/magnifying-glass.tsx b/src/components/atoms/icons/magnifying-glass.tsx
index 445ef10..1ca2a44 100644
--- a/src/components/atoms/icons/magnifying-glass.tsx
+++ b/src/components/atoms/icons/magnifying-glass.tsx
@@ -1,4 +1,4 @@
-import { VFC } from 'react';
+import { FC } from 'react';
 import styles from './magnifying-glass.module.scss';
 
 export type MagnifyingGlassProps = {
@@ -13,7 +13,7 @@ export type MagnifyingGlassProps = {
  *
  * Render a magnifying glass svg icon.
  */
-const MagnifyingGlass: VFC<MagnifyingGlassProps> = ({ className = '' }) => {
+const MagnifyingGlass: FC<MagnifyingGlassProps> = ({ className = '' }) => {
   return (
     <svg
       viewBox="0 0 100 100"
diff --git a/src/components/atoms/icons/moon.tsx b/src/components/atoms/icons/moon.tsx
index 4f52319..ec4fa0c 100644
--- a/src/components/atoms/icons/moon.tsx
+++ b/src/components/atoms/icons/moon.tsx
@@ -1,7 +1,7 @@
-import { VFC } from 'react';
+import { FC } from 'react';
 import styles from './moon.module.scss';
 
-type MoonProps = {
+export type MoonProps = {
   /**
    * Set additional classnames to the icon.
    */
@@ -12,7 +12,7 @@ type MoonProps = {
   title?: string;
 };
 
-const Moon: VFC<MoonProps> = ({ className = '', title }) => {
+const Moon: FC<MoonProps> = ({ className = '', title }) => {
   return (
     <svg
       className={`${styles.icon} ${className}`}
diff --git a/src/components/atoms/icons/plus-minus.tsx b/src/components/atoms/icons/plus-minus.tsx
index 78aa14a..e8897b7 100644
--- a/src/components/atoms/icons/plus-minus.tsx
+++ b/src/components/atoms/icons/plus-minus.tsx
@@ -1,7 +1,7 @@
 import { FC } from 'react';
 import styles from './plus-minus.module.scss';
 
-type PlusMinusProps = {
+export type PlusMinusProps = {
   /**
    * Set additional classnames to the icon.
    */
diff --git a/src/components/atoms/icons/posts-stack.tsx b/src/components/atoms/icons/posts-stack.tsx
index 1998d25..ab21323 100644
--- a/src/components/atoms/icons/posts-stack.tsx
+++ b/src/components/atoms/icons/posts-stack.tsx
@@ -1,4 +1,4 @@
-import { VFC } from 'react';
+import { FC } from 'react';
 import styles from './posts-stack.module.scss';
 
 export type PostsStackProps = {
@@ -13,7 +13,7 @@ export type PostsStackProps = {
  *
  * Render a posts stack svg icon.
  */
-const PostsStack: VFC<PostsStackProps> = ({ className = '' }) => {
+const PostsStack: FC<PostsStackProps> = ({ className = '' }) => {
   return (
     <svg
       viewBox="0 0 100 100"
diff --git a/src/components/atoms/icons/sun.tsx b/src/components/atoms/icons/sun.tsx
index fa9d922..ca31747 100644
--- a/src/components/atoms/icons/sun.tsx
+++ b/src/components/atoms/icons/sun.tsx
@@ -1,7 +1,7 @@
-import { VFC } from 'react';
+import { FC } from 'react';
 import styles from './sun.module.scss';
 
-type SunProps = {
+export type SunProps = {
   /**
    * Set additional classnames to the icon.
    */
@@ -17,7 +17,7 @@ type SunProps = {
  *
  * Render a svg sun icon.
  */
-const Sun: VFC<SunProps> = ({ className = '', title }) => {
+const Sun: FC<SunProps> = ({ className = '', title }) => {
   return (
     <svg
       className={`${styles.sun} ${className}`}
diff --git a/src/components/atoms/images/logo.tsx b/src/components/atoms/images/logo.tsx
index 2e52110..3978882 100644
--- a/src/components/atoms/images/logo.tsx
+++ b/src/components/atoms/images/logo.tsx
@@ -1,7 +1,7 @@
-import { VFC } from 'react';
+import { FC } from 'react';
 import styles from './logo.module.scss';
 
-type LogoProps = {
+export type LogoProps = {
   /**
    * SVG Image title.
    */
@@ -13,7 +13,7 @@ type LogoProps = {
  *
  * Render a SVG logo.
  */
-const Logo: VFC<LogoProps> = ({ title }) => {
+const Logo: FC<LogoProps> = ({ title }) => {
   return (
     <svg
       viewBox="0 0 512 512"
diff --git a/src/components/atoms/layout/copyright.tsx b/src/components/atoms/layout/copyright.tsx
index 76252ee..f70695d 100644
--- a/src/components/atoms/layout/copyright.tsx
+++ b/src/components/atoms/layout/copyright.tsx
@@ -1,4 +1,4 @@
-import { ReactNode, VFC } from 'react';
+import { FC, ReactNode } from 'react';
 import styles from './copyright.module.scss';
 
 export type CopyrightDates = {
@@ -32,7 +32,7 @@ export type CopyrightProps = {
  *
  * Renders a copyright information (owner, dates, license icon).
  */
-const Copyright: VFC<CopyrightProps> = ({ owner, dates, icon }) => {
+const Copyright: FC<CopyrightProps> = ({ owner, dates, icon }) => {
   const getFormattedDate = (date: string) => {
     const datetime = new Date(date).toISOString();
 
diff --git a/src/components/atoms/layout/main.tsx b/src/components/atoms/layout/main.tsx
index 4549328..d92a5c7 100644
--- a/src/components/atoms/layout/main.tsx
+++ b/src/components/atoms/layout/main.tsx
@@ -1,6 +1,10 @@
-import { FC } from 'react';
+import { FC, ReactNode } from 'react';
 
 export type MainProps = {
+  /**
+   * The main body.
+   */
+  children: ReactNode;
   /**
    * Set additional classnames to the main element.
    */
diff --git a/src/components/atoms/layout/no-script.tsx b/src/components/atoms/layout/no-script.tsx
index 6358cf8..a503e0c 100644
--- a/src/components/atoms/layout/no-script.tsx
+++ b/src/components/atoms/layout/no-script.tsx
@@ -1,4 +1,4 @@
-import { VFC } from 'react';
+import { FC } from 'react';
 import styles from './no-script.module.scss';
 
 export type NoScriptProps = {
@@ -12,7 +12,7 @@ export type NoScriptProps = {
   position?: 'initial' | 'top';
 };
 
-const NoScript: VFC<NoScriptProps> = ({ message, position = 'initial' }) => {
+const NoScript: FC<NoScriptProps> = ({ message, position = 'initial' }) => {
   const positionClass = styles[`noscript--${position}`];
 
   return <div className={`${styles.noscript} ${positionClass}`}>{message}</div>;
diff --git a/src/components/atoms/layout/notice.tsx b/src/components/atoms/layout/notice.tsx
index b6e09c5..115bd9c 100644
--- a/src/components/atoms/layout/notice.tsx
+++ b/src/components/atoms/layout/notice.tsx
@@ -1,4 +1,4 @@
-import { VFC } from 'react';
+import { FC } from 'react';
 import styles from './notice.module.scss';
 
 export type NoticeKind = 'error' | 'info' | 'success' | 'warning';
@@ -19,7 +19,7 @@ export type NoticeProps = {
  *
  * Render a colored message depending on notice kind.
  */
-const Notice: VFC<NoticeProps> = ({ kind, message }) => {
+const Notice: FC<NoticeProps> = ({ kind, message }) => {
   const kindClass = `wrapper--${kind}`;
 
   return (
diff --git a/src/components/atoms/layout/section.tsx b/src/components/atoms/layout/section.tsx
index f1bbb34..cb727ff 100644
--- a/src/components/atoms/layout/section.tsx
+++ b/src/components/atoms/layout/section.tsx
@@ -1,4 +1,4 @@
-import { ReactNode, VFC } from 'react';
+import { FC, ReactNode } from 'react';
 import Heading from '../headings/heading';
 import styles from './section.module.scss';
 
@@ -32,7 +32,7 @@ export type SectionProps = {
  *
  * Render a section element.
  */
-const Section: VFC<SectionProps> = ({
+const Section: FC<SectionProps> = ({
   className = '',
   content,
   title,
diff --git a/src/components/atoms/links/link.module.scss b/src/components/atoms/links/link.module.scss
index e7ead86..d23667a 100644
--- a/src/components/atoms/links/link.module.scss
+++ b/src/components/atoms/links/link.module.scss
@@ -5,7 +5,9 @@
   &[hreflang] {
     &::after {
       display: inline-block;
-      content: "\0000a0["attr(hreflang) "]";
+      /* Prettier is removing spacing between content parts. */
+      /* prettier-ignore */
+      content: "\0000a0[" attr(hreflang) "]";
       font-size: var(--font-size-sm);
     }
   }
@@ -13,22 +15,30 @@
   &--external {
     &::after {
       display: inline-block;
-      content: "\0000a0"url(fun.encode-svg('<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$light-theme_blue}" d="M100 0 59.543 5.887l20.8 6.523-51.134 51.134 7.249 7.248L87.59 19.66l6.522 20.798z"/><path fill="#{var.$light-theme_blue}" d="M4 10a4 4 0 0 0-4 4v82a4 4 0 0 0 4 4h82a4 4 0 0 0 4-4V62.314h-8V92H8V18h29.686v-8z"/></svg>'));
+      /* Prettier is removing spacing between content parts. */
+      /* prettier-ignore */
+      content: "\0000a0" url(fun.encode-svg('<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$light-theme_blue}" d="M100 0 59.543 5.887l20.8 6.523-51.134 51.134 7.249 7.248L87.59 19.66l6.522 20.798z"/><path fill="#{var.$light-theme_blue}" d="M4 10a4 4 0 0 0-4 4v82a4 4 0 0 0 4 4h82a4 4 0 0 0 4-4V62.314h-8V92H8V18h29.686v-8z"/></svg>'));
     }
 
     &:focus:not(:active)::after {
-      content: "\0000a0"url(fun.encode-svg('<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$light-theme_white}" d="M100 0 59.543 5.887l20.8 6.523-51.134 51.134 7.249 7.248L87.59 19.66l6.522 20.798z"/><path fill="#{var.$light-theme_white}" d="M4 10a4 4 0 0 0-4 4v82a4 4 0 0 0 4 4h82a4 4 0 0 0 4-4V62.314h-8V92H8V18h29.686v-8z"/></svg>'));
+      /* Prettier is removing spacing between content parts. */
+      /* prettier-ignore */
+      content: "\0000a0" url(fun.encode-svg('<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$light-theme_white}" d="M100 0 59.543 5.887l20.8 6.523-51.134 51.134 7.249 7.248L87.59 19.66l6.522 20.798z"/><path fill="#{var.$light-theme_white}" d="M4 10a4 4 0 0 0-4 4v82a4 4 0 0 0 4 4h82a4 4 0 0 0 4-4V62.314h-8V92H8V18h29.686v-8z"/></svg>'));
     }
 
     &[hreflang] {
       &::after {
-        content: "\0000a0["attr(hreflang) "]\0000a0"url(fun.encode-svg(
+        /* Prettier is removing spacing between content parts. */
+        /* prettier-ignore */
+        content: "\0000a0[" attr(hreflang) "]\0000a0" url(fun.encode-svg(
             '<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$light-theme_blue}" d="M100 0 59.543 5.887l20.8 6.523-51.134 51.134 7.249 7.248L87.59 19.66l6.522 20.798z"/><path fill="#{var.$light-theme_blue}" d="M4 10a4 4 0 0 0-4 4v82a4 4 0 0 0 4 4h82a4 4 0 0 0 4-4V62.314h-8V92H8V18h29.686v-8z"/></svg>'
           ));
       }
 
       &:focus:not(:active)::after {
-        content: "\0000a0["attr(hreflang) "]\0000a0"url(fun.encode-svg(
+        /* Prettier is removing spacing between content parts. */
+        /* prettier-ignore */
+        content: "\0000a0[" attr(hreflang) "]\0000a0" url(fun.encode-svg(
             '<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$light-theme_white}" d="M100 0 59.543 5.887l20.8 6.523-51.134 51.134 7.249 7.248L87.59 19.66l6.522 20.798z"/><path fill="#{var.$light-theme_white}" d="M4 10a4 4 0 0 0-4 4v82a4 4 0 0 0 4 4h82a4 4 0 0 0 4-4V62.314h-8V92H8V18h29.686v-8z"/></svg>'
           ));
       }
diff --git a/src/components/atoms/links/link.tsx b/src/components/atoms/links/link.tsx
index 87f11fc..674c07b 100644
--- a/src/components/atoms/links/link.tsx
+++ b/src/components/atoms/links/link.tsx
@@ -1,8 +1,12 @@
 import NextLink from 'next/link';
-import { FC } from 'react';
+import { FC, ReactNode } from 'react';
 import styles from './link.module.scss';
 
 export type LinkProps = {
+  /**
+   * The link body.
+   */
+  children: ReactNode;
   /**
    * Set additional classnames to the link.
    */
diff --git a/src/components/atoms/links/nav-link.tsx b/src/components/atoms/links/nav-link.tsx
index 25c0e7d..7c6fede 100644
--- a/src/components/atoms/links/nav-link.tsx
+++ b/src/components/atoms/links/nav-link.tsx
@@ -1,5 +1,5 @@
 import Link from 'next/link';
-import { VFC, ReactNode } from 'react';
+import { FC, ReactNode } from 'react';
 import styles from './nav-link.module.scss';
 
 export type NavLinkProps = {
@@ -22,7 +22,7 @@ export type NavLinkProps = {
  *
  * Render a navigation link.
  */
-const NavLink: VFC<NavLinkProps> = ({ href, label, logo }) => {
+const NavLink: FC<NavLinkProps> = ({ href, label, logo }) => {
   return (
     <Link href={href}>
       <a className={styles.link}>
diff --git a/src/components/atoms/links/sharing-link.tsx b/src/components/atoms/links/sharing-link.tsx
index 3cd2dd1..ca53ef9 100644
--- a/src/components/atoms/links/sharing-link.tsx
+++ b/src/components/atoms/links/sharing-link.tsx
@@ -1,4 +1,4 @@
-import { VFC } from 'react';
+import { FC } from 'react';
 import { useIntl } from 'react-intl';
 import styles from './sharing-link.module.scss';
 
@@ -26,7 +26,7 @@ export type SharingLinkProps = {
  *
  * Render a sharing link.
  */
-const SharingLink: VFC<SharingLinkProps> = ({ medium, url }) => {
+const SharingLink: FC<SharingLinkProps> = ({ medium, url }) => {
   const intl = useIntl();
   const text = intl.formatMessage(
     {
diff --git a/src/components/atoms/links/social-link.tsx b/src/components/atoms/links/social-link.tsx
index 8c7c790..464bc60 100644
--- a/src/components/atoms/links/social-link.tsx
+++ b/src/components/atoms/links/social-link.tsx
@@ -2,7 +2,7 @@ import GithubIcon from '@assets/images/social-media/github.svg';
 import GitlabIcon from '@assets/images/social-media/gitlab.svg';
 import LinkedInIcon from '@assets/images/social-media/linkedin.svg';
 import TwitterIcon from '@assets/images/social-media/twitter.svg';
-import { VFC } from 'react';
+import { FC } from 'react';
 import styles from './social-link.module.scss';
 
 export type SocialWebsite = 'Github' | 'Gitlab' | 'LinkedIn' | 'Twitter';
@@ -23,7 +23,7 @@ export type SocialLinkProps = {
  *
  * Render a social icon link.
  */
-const SocialLink: VFC<SocialLinkProps> = ({ name, url }) => {
+const SocialLink: FC<SocialLinkProps> = ({ name, url }) => {
   /**
    * Retrieve a social link icon by id.
    * @param {string} id - The social website id.
diff --git a/src/components/atoms/lists/description-list.tsx b/src/components/atoms/lists/description-list.tsx
index 0a92465..a60a6a1 100644
--- a/src/components/atoms/lists/description-list.tsx
+++ b/src/components/atoms/lists/description-list.tsx
@@ -1,4 +1,4 @@
-import { VFC } from 'react';
+import { FC } from 'react';
 import styles from './description-list.module.scss';
 
 export type DescriptionListItem = {
@@ -52,7 +52,7 @@ export type DescriptionListProps = {
  *
  * Render a description list.
  */
-const DescriptionList: VFC<DescriptionListProps> = ({
+const DescriptionList: FC<DescriptionListProps> = ({
   className = '',
   descriptionClassName = '',
   groupClassName = '',
diff --git a/src/components/atoms/lists/list.tsx b/src/components/atoms/lists/list.tsx
index d100a31..6726802 100644
--- a/src/components/atoms/lists/list.tsx
+++ b/src/components/atoms/lists/list.tsx
@@ -1,4 +1,4 @@
-import { VFC } from 'react';
+import { FC } from 'react';
 import styles from './list.module.scss';
 
 export type ListItem = {
@@ -44,7 +44,7 @@ export type ListProps = {
  *
  * Render either an ordered or an unordered list.
  */
-const List: VFC<ListProps> = ({
+const List: FC<ListProps> = ({
   className = '',
   items,
   itemsClassName = '',
diff --git a/src/components/atoms/loaders/progress-bar.tsx b/src/components/atoms/loaders/progress-bar.tsx
index 1b1ff06..9bac847 100644
--- a/src/components/atoms/loaders/progress-bar.tsx
+++ b/src/components/atoms/loaders/progress-bar.tsx
@@ -1,4 +1,4 @@
-import { VFC } from 'react';
+import { FC } from 'react';
 import styles from './progress-bar.module.scss';
 
 export type ProgressBarProps = {
@@ -29,7 +29,7 @@ export type ProgressBarProps = {
  *
  * Render a progress bar.
  */
-const ProgressBar: VFC<ProgressBarProps> = ({
+const ProgressBar: FC<ProgressBarProps> = ({
   current,
   info,
   min,
diff --git a/src/components/atoms/loaders/spinner.tsx b/src/components/atoms/loaders/spinner.tsx
index bff0f25..6655141 100644
--- a/src/components/atoms/loaders/spinner.tsx
+++ b/src/components/atoms/loaders/spinner.tsx
@@ -1,4 +1,4 @@
-import { VFC } from 'react';
+import { FC } from 'react';
 import { useIntl } from 'react-intl';
 import styles from './spinner.module.scss';
 
@@ -14,7 +14,7 @@ export type SpinnerProps = {
  *
  * Render a loading message with animation.
  */
-const Spinner: VFC<SpinnerProps> = ({ message }) => {
+const Spinner: FC<SpinnerProps> = ({ message }) => {
   const intl = useIntl();
 
   return (
diff --git a/src/components/molecules/buttons/back-to-top.tsx b/src/components/molecules/buttons/back-to-top.tsx
index 8a52231..bd1925a 100644
--- a/src/components/molecules/buttons/back-to-top.tsx
+++ b/src/components/molecules/buttons/back-to-top.tsx
@@ -1,18 +1,16 @@
-import ButtonLink from '@components/atoms/buttons/button-link';
+import ButtonLink, {
+  type ButtonLinkProps,
+} from '@components/atoms/buttons/button-link';
 import Arrow from '@components/atoms/icons/arrow';
-import { VFC } from 'react';
+import { FC } from 'react';
 import { useIntl } from 'react-intl';
 import styles from './back-to-top.module.scss';
 
-export type BackToTopProps = {
+export type BackToTopProps = Pick<ButtonLinkProps, 'target'> & {
   /**
    * Set additional classnames to the button wrapper.
    */
   className?: string;
-  /**
-   * An element id (without hashtag) to use as anchor.
-   */
-  target: string;
 };
 
 /**
@@ -20,7 +18,7 @@ export type BackToTopProps = {
  *
  * Render a back to top link.
  */
-const BackToTop: VFC<BackToTopProps> = ({ className = '', target }) => {
+const BackToTop: FC<BackToTopProps> = ({ className = '', target }) => {
   const intl = useIntl();
   const linkName = intl.formatMessage({
     defaultMessage: 'Back to top',
diff --git a/src/components/molecules/buttons/heading-button.tsx b/src/components/molecules/buttons/heading-button.tsx
index fc79749..0ed9a76 100644
--- a/src/components/molecules/buttons/heading-button.tsx
+++ b/src/components/molecules/buttons/heading-button.tsx
@@ -1,6 +1,6 @@
 import Heading, { type HeadingProps } from '@components/atoms/headings/heading';
 import PlusMinus from '@components/atoms/icons/plus-minus';
-import { SetStateAction, VFC } from 'react';
+import { FC, SetStateAction } from 'react';
 import { useIntl } from 'react-intl';
 import styles from './heading-button.module.scss';
 
@@ -28,7 +28,7 @@ export type HeadingButtonProps = Pick<HeadingProps, 'level'> & {
  *
  * Render a button as accordion title to toggle body.
  */
-const HeadingButton: VFC<HeadingButtonProps> = ({
+const HeadingButton: FC<HeadingButtonProps> = ({
   className = '',
   expanded,
   level,
diff --git a/src/components/molecules/buttons/help-button.tsx b/src/components/molecules/buttons/help-button.tsx
index aeb84ec..f19322f 100644
--- a/src/components/molecules/buttons/help-button.tsx
+++ b/src/components/molecules/buttons/help-button.tsx
@@ -1,5 +1,5 @@
-import Button, { ButtonProps } from '@components/atoms/buttons/button';
-import { VFC } from 'react';
+import Button, { type ButtonProps } from '@components/atoms/buttons/button';
+import { FC } from 'react';
 import { useIntl } from 'react-intl';
 import styles from './help-button.module.scss';
 
@@ -15,7 +15,7 @@ export type HelpButtonProps = Pick<ButtonProps, 'onClick'> & {
  *
  * Render a button with an interrogation mark icon.
  */
-const HelpButton: VFC<HelpButtonProps> = ({ className = '', onClick }) => {
+const HelpButton: FC<HelpButtonProps> = ({ className = '', onClick }) => {
   const intl = useIntl();
   const text = intl.formatMessage({
     defaultMessage: 'Help',
diff --git a/src/components/molecules/forms/ackee-select.test.tsx b/src/components/molecules/forms/ackee-select.test.tsx
index e1e6b2d..ec27922 100644
--- a/src/components/molecules/forms/ackee-select.test.tsx
+++ b/src/components/molecules/forms/ackee-select.test.tsx
@@ -1,5 +1,5 @@
-import userEvent from '@testing-library/user-event';
-import { render, screen } from '@test-utils';
+import user from '@testing-library/user-event';
+import { act, render, screen } from '@test-utils';
 import AckeeSelect from './ackee-select';
 
 describe('Select', () => {
@@ -9,13 +9,15 @@ describe('Select', () => {
     expect(screen.queryByRole('combobox')).not.toHaveValue('partial');
   });
 
-  it('should correctly change value when user choose another option', () => {
+  it('should correctly change value when user choose another option', async () => {
     render(<AckeeSelect initialValue="full" />);
 
-    userEvent.selectOptions(
-      screen.getByRole('combobox'),
-      screen.getByRole('option', { name: 'Partial' })
-    );
+    await act(async () => {
+      await user.selectOptions(
+        screen.getByRole('combobox'),
+        screen.getByRole('option', { name: 'Partial' })
+      );
+    });
 
     expect(screen.getByRole('combobox')).toHaveValue('partial');
     expect(screen.queryByRole('combobox')).not.toHaveValue('full');
diff --git a/src/components/molecules/forms/ackee-select.tsx b/src/components/molecules/forms/ackee-select.tsx
index 4a8410c..101e5b5 100644
--- a/src/components/molecules/forms/ackee-select.tsx
+++ b/src/components/molecules/forms/ackee-select.tsx
@@ -1,8 +1,8 @@
 import { SelectOptions } from '@components/atoms/forms/select';
-import { Dispatch, SetStateAction, useState, VFC } from 'react';
+import { Dispatch, FC, SetStateAction, useState } from 'react';
 import { useIntl } from 'react-intl';
 import SelectWithTooltip, {
-  SelectWithTooltipProps,
+  type SelectWithTooltipProps,
 } from './select-with-tooltip';
 
 export type AckeeOptions = 'full' | 'partial';
@@ -22,7 +22,7 @@ export type AckeeSelectProps = Pick<
  *
  * Render a select to set Ackee settings.
  */
-const AckeeSelect: VFC<AckeeSelectProps> = ({ initialValue, ...props }) => {
+const AckeeSelect: FC<AckeeSelectProps> = ({ initialValue, ...props }) => {
   const intl = useIntl();
   const [value, setValue] = useState<AckeeOptions>(initialValue);
 
diff --git a/src/components/molecules/forms/labelled-field.tsx b/src/components/molecules/forms/labelled-field.tsx
index 08d0126..ecc9255 100644
--- a/src/components/molecules/forms/labelled-field.tsx
+++ b/src/components/molecules/forms/labelled-field.tsx
@@ -1,6 +1,6 @@
 import Field, { type FieldProps } from '@components/atoms/forms/field';
 import Label from '@components/atoms/forms/label';
-import { VFC } from 'react';
+import { FC } from 'react';
 import styles from './labelled-field.module.scss';
 
 export type LabelledFieldProps = FieldProps & {
@@ -23,7 +23,7 @@ export type LabelledFieldProps = FieldProps & {
  *
  * Render a field tied to a label.
  */
-const LabelledField: VFC<LabelledFieldProps> = ({
+const LabelledField: FC<LabelledFieldProps> = ({
   hideLabel = false,
   id,
   label,
diff --git a/src/components/molecules/forms/labelled-select.tsx b/src/components/molecules/forms/labelled-select.tsx
index 7d4237a..23057d0 100644
--- a/src/components/molecules/forms/labelled-select.tsx
+++ b/src/components/molecules/forms/labelled-select.tsx
@@ -1,6 +1,6 @@
-import Label, { LabelProps } from '@components/atoms/forms/label';
+import Label, { type LabelProps } from '@components/atoms/forms/label';
 import Select, { type SelectProps } from '@components/atoms/forms/select';
-import { VFC } from 'react';
+import { FC } from 'react';
 import styles from './labelled-select.module.scss';
 
 export type LabelledSelectProps = Omit<
@@ -14,7 +14,7 @@ export type LabelledSelectProps = Omit<
   /**
    * Set additional classnames to the label.
    */
-  labelClassName?: string;
+  labelClassName?: LabelProps['className'];
   /**
    * The label position. Default: top.
    */
@@ -26,10 +26,15 @@ export type LabelledSelectProps = Omit<
   /**
    * Set additional classnames to the select field.
    */
-  selectClassName?: string;
+  selectClassName?: SelectProps['className'];
 };
 
-const LabelledSelect: VFC<LabelledSelectProps> = ({
+/**
+ * LabelledSelect component
+ *
+ * Render a select with a label.
+ */
+const LabelledSelect: FC<LabelledSelectProps> = ({
   id,
   label,
   labelClassName = '',
diff --git a/src/components/molecules/forms/motion-toggle.tsx b/src/components/molecules/forms/motion-toggle.tsx
index 9f30b42..24b54ae 100644
--- a/src/components/molecules/forms/motion-toggle.tsx
+++ b/src/components/molecules/forms/motion-toggle.tsx
@@ -1,8 +1,8 @@
 import Toggle, {
-  ToggleChoices,
-  ToggleProps,
+  type ToggleChoices,
+  type ToggleProps,
 } from '@components/molecules/forms/toggle';
-import { useState, VFC } from 'react';
+import { FC, useState } from 'react';
 import { useIntl } from 'react-intl';
 
 export type MotionToggleProps = Pick<ToggleProps, 'labelClassName' | 'value'>;
@@ -12,7 +12,7 @@ export type MotionToggleProps = Pick<ToggleProps, 'labelClassName' | 'value'>;
  *
  * Render a Toggle component to set reduce motion.
  */
-const MotionToggle: VFC<MotionToggleProps> = ({ value, ...props }) => {
+const MotionToggle: FC<MotionToggleProps> = ({ value, ...props }) => {
   const intl = useIntl();
   const [isDeactivated, setIsDeactivated] = useState<boolean>(value);
   const reduceMotionLabel = intl.formatMessage({
diff --git a/src/components/molecules/forms/prism-theme-toggle.tsx b/src/components/molecules/forms/prism-theme-toggle.tsx
index daee6bd..0b9c447 100644
--- a/src/components/molecules/forms/prism-theme-toggle.tsx
+++ b/src/components/molecules/forms/prism-theme-toggle.tsx
@@ -1,10 +1,10 @@
 import Moon from '@components/atoms/icons/moon';
 import Sun from '@components/atoms/icons/sun';
 import Toggle, {
-  ToggleChoices,
-  ToggleProps,
+  type ToggleChoices,
+  type ToggleProps,
 } from '@components/molecules/forms/toggle';
-import { useState, VFC } from 'react';
+import { FC, useState } from 'react';
 import { useIntl } from 'react-intl';
 
 export type PrismThemeToggleProps = Pick<
@@ -17,7 +17,7 @@ export type PrismThemeToggleProps = Pick<
  *
  * Render a Toggle component to set code blocks theme.
  */
-const PrismThemeToggle: VFC<PrismThemeToggleProps> = ({ value, ...props }) => {
+const PrismThemeToggle: FC<PrismThemeToggleProps> = ({ value, ...props }) => {
   const intl = useIntl();
   const [isDarkTheme, setIsDarkTheme] = useState<boolean>(value);
   const themeLabel = intl.formatMessage({
diff --git a/src/components/molecules/forms/select-with-tooltip.tsx b/src/components/molecules/forms/select-with-tooltip.tsx
index f537e1e..cf7b041 100644
--- a/src/components/molecules/forms/select-with-tooltip.tsx
+++ b/src/components/molecules/forms/select-with-tooltip.tsx
@@ -1,4 +1,4 @@
-import { useState, VFC } from 'react';
+import { FC, useState } from 'react';
 import HelpButton from '../buttons/help-button';
 import Tooltip, { type TooltipProps } from '../modals/tooltip';
 import LabelledSelect, { type LabelledSelectProps } from './labelled-select';
@@ -9,14 +9,10 @@ export type SelectWithTooltipProps = Omit<
   'labelPosition'
 > &
   Pick<TooltipProps, 'title' | 'content'> & {
-    /**
-     * The select label.
-     */
-    label: string;
     /**
      * Set additional classnames to the tooltip wrapper.
      */
-    tooltipClassName?: string;
+    tooltipClassName?: TooltipProps['className'];
   };
 
 /**
@@ -24,7 +20,7 @@ export type SelectWithTooltipProps = Omit<
  *
  * Render a select with a button to display a tooltip about options.
  */
-const SelectWithTooltip: VFC<SelectWithTooltipProps> = ({
+const SelectWithTooltip: FC<SelectWithTooltipProps> = ({
   title,
   content,
   id,
diff --git a/src/components/molecules/forms/theme-toggle.tsx b/src/components/molecules/forms/theme-toggle.tsx
index eb56ce9..10c6c47 100644
--- a/src/components/molecules/forms/theme-toggle.tsx
+++ b/src/components/molecules/forms/theme-toggle.tsx
@@ -1,10 +1,10 @@
 import Moon from '@components/atoms/icons/moon';
 import Sun from '@components/atoms/icons/sun';
 import Toggle, {
-  ToggleChoices,
-  ToggleProps,
+  type ToggleChoices,
+  type ToggleProps,
 } from '@components/molecules/forms/toggle';
-import { useState, VFC } from 'react';
+import { FC, useState } from 'react';
 import { useIntl } from 'react-intl';
 
 export type ThemeToggleProps = Pick<ToggleProps, 'labelClassName' | 'value'>;
@@ -14,7 +14,7 @@ export type ThemeToggleProps = Pick<ToggleProps, 'labelClassName' | 'value'>;
  *
  * Render a Toggle component to set theme.
  */
-const ThemeToggle: VFC<ThemeToggleProps> = ({ value, ...props }) => {
+const ThemeToggle: FC<ThemeToggleProps> = ({ value, ...props }) => {
   const intl = useIntl();
   const [isDarkTheme, setIsDarkTheme] = useState<boolean>(value);
   const themeLabel = intl.formatMessage({
diff --git a/src/components/molecules/forms/toggle.tsx b/src/components/molecules/forms/toggle.tsx
index dff2d2d..288062d 100644
--- a/src/components/molecules/forms/toggle.tsx
+++ b/src/components/molecules/forms/toggle.tsx
@@ -1,6 +1,6 @@
-import Checkbox from '@components/atoms/forms/checkbox';
+import Checkbox, { type CheckboxProps } from '@components/atoms/forms/checkbox';
 import Label, { type LabelProps } from '@components/atoms/forms/label';
-import { ReactNode, VFC } from 'react';
+import { FC, ReactNode } from 'react';
 import styles from './toggle.module.scss';
 
 export type ToggleChoices = {
@@ -14,15 +14,11 @@ export type ToggleChoices = {
   right: ReactNode;
 };
 
-export type ToggleProps = {
+export type ToggleProps = Pick<CheckboxProps, 'id' | 'name'> & {
   /**
    * The toggle choices.
    */
   choices: ToggleChoices;
-  /**
-   * The input id.
-   */
-  id: string;
   /**
    * The toggle label.
    */
@@ -30,15 +26,11 @@ export type ToggleProps = {
   /**
    * Set additional classnames to the label.
    */
-  labelClassName?: string;
+  labelClassName?: LabelProps['className'];
   /**
    * The label size.
    */
   labelSize?: LabelProps['size'];
-  /**
-   * The input name.
-   */
-  name: string;
   /**
    * The toggle value. True if checked.
    */
@@ -54,7 +46,7 @@ export type ToggleProps = {
  *
  * Render a toggle with a label and two choices.
  */
-const Toggle: VFC<ToggleProps> = ({
+const Toggle: FC<ToggleProps> = ({
   choices,
   id,
   label,
diff --git a/src/components/molecules/images/responsive-image.tsx b/src/components/molecules/images/responsive-image.tsx
index 1d8787e..31cbcd1 100644
--- a/src/components/molecules/images/responsive-image.tsx
+++ b/src/components/molecules/images/responsive-image.tsx
@@ -1,6 +1,6 @@
-import Link from '@components/atoms/links/link';
-import Image, { ImageProps } from 'next/image';
-import { VFC } from 'react';
+import Link, { type LinkProps } from '@components/atoms/links/link';
+import Image, { type ImageProps } from 'next/image';
+import { FC } from 'react';
 import styles from './responsive-image.module.scss';
 
 export type ResponsiveImageProps = Omit<
@@ -26,7 +26,7 @@ export type ResponsiveImageProps = Omit<
   /**
    * A link target.
    */
-  target?: string;
+  target?: LinkProps['href'];
   /**
    * The image width.
    */
@@ -38,7 +38,7 @@ export type ResponsiveImageProps = Omit<
  *
  * Render a responsive image wrapped in a figure element.
  */
-const ResponsiveImage: VFC<ResponsiveImageProps> = ({
+const ResponsiveImage: FC<ResponsiveImageProps> = ({
   alt,
   caption,
   className = '',
diff --git a/src/components/molecules/layout/branding.tsx b/src/components/molecules/layout/branding.tsx
index 9f564bf..9fe89e7 100644
--- a/src/components/molecules/layout/branding.tsx
+++ b/src/components/molecules/layout/branding.tsx
@@ -1,11 +1,11 @@
 import Heading from '@components/atoms/headings/heading';
 import Link from 'next/link';
-import { VFC } from 'react';
+import { FC } from 'react';
 import { useIntl } from 'react-intl';
 import styles from './branding.module.scss';
-import FlippingLogo from './flipping-logo';
+import FlippingLogo, { type FlippingLogoProps } from './flipping-logo';
 
-type BrandingProps = {
+export type BrandingProps = Pick<FlippingLogoProps, 'photo'> & {
   /**
    * The Branding baseline.
    */
@@ -14,10 +14,6 @@ type BrandingProps = {
    * Use H1 if the current page is homepage. Default: false.
    */
   isHome?: boolean;
-  /**
-   * A photography URL.
-   */
-  photo: string;
   /**
    * The Branding title;
    */
@@ -33,7 +29,7 @@ type BrandingProps = {
  *
  * Render the branding logo, title and optional baseline.
  */
-const Branding: VFC<BrandingProps> = ({
+const Branding: FC<BrandingProps> = ({
   baseline,
   isHome = false,
   photo,
diff --git a/src/components/molecules/layout/card.tsx b/src/components/molecules/layout/card.tsx
index 23a0e54..89f100e 100644
--- a/src/components/molecules/layout/card.tsx
+++ b/src/components/molecules/layout/card.tsx
@@ -1,11 +1,11 @@
 import ButtonLink from '@components/atoms/buttons/button-link';
 import Heading, { type HeadingLevel } from '@components/atoms/headings/heading';
 import DescriptionList, {
-  DescriptionListItem,
+  type DescriptionListItem,
 } from '@components/atoms/lists/description-list';
-import { VFC } from 'react';
+import { FC } from 'react';
 import ResponsiveImage, {
-  ResponsiveImageProps,
+  type ResponsiveImageProps,
 } from '../images/responsive-image';
 import styles from './card.module.scss';
 
@@ -68,7 +68,7 @@ export type CardProps = {
  *
  * Render a link with minimal information about its content.
  */
-const Card: VFC<CardProps> = ({
+const Card: FC<CardProps> = ({
   className = '',
   cover,
   coverFit = 'cover',
diff --git a/src/components/molecules/layout/flipping-logo.tsx b/src/components/molecules/layout/flipping-logo.tsx
index 6f7645f..4a216ef 100644
--- a/src/components/molecules/layout/flipping-logo.tsx
+++ b/src/components/molecules/layout/flipping-logo.tsx
@@ -1,9 +1,9 @@
-import Logo from '@components/atoms/images/logo';
+import Logo, { type LogoProps } from '@components/atoms/images/logo';
 import Image from 'next/image';
-import { VFC } from 'react';
+import { FC } from 'react';
 import styles from './flipping-logo.module.scss';
 
-type FlippingLogoProps = {
+export type FlippingLogoProps = {
   /**
    * Set additional classnames to the logo wrapper.
    */
@@ -15,7 +15,7 @@ type FlippingLogoProps = {
   /**
    * Logo image title.
    */
-  logoTitle?: string;
+  logoTitle?: LogoProps['title'];
   /**
    * Photo url.
    */
@@ -27,7 +27,7 @@ type FlippingLogoProps = {
  *
  * Render a logo and a photo with a flipping effect.
  */
-const FlippingLogo: VFC<FlippingLogoProps> = ({
+const FlippingLogo: FC<FlippingLogoProps> = ({
   className = '',
   altText,
   logoTitle,
diff --git a/src/components/molecules/layout/meta.tsx b/src/components/molecules/layout/meta.tsx
index 218ebd9..fcce473 100644
--- a/src/components/molecules/layout/meta.tsx
+++ b/src/components/molecules/layout/meta.tsx
@@ -2,7 +2,7 @@ import DescriptionList, {
   type DescriptionListProps,
   type DescriptionListItem,
 } from '@components/atoms/lists/description-list';
-import { ReactNode, VFC } from 'react';
+import { FC, ReactNode } from 'react';
 
 export type MetaItem = {
   /**
@@ -43,7 +43,7 @@ export type MetaProps = {
  *
  * Renders the page metadata.
  */
-const Meta: VFC<MetaProps> = ({ data, ...props }) => {
+const Meta: FC<MetaProps> = ({ data, ...props }) => {
   /**
    * Transform the metadata to description list item format.
    *
diff --git a/src/components/molecules/layout/widget.tsx b/src/components/molecules/layout/widget.tsx
index c04362a..feb2add 100644
--- a/src/components/molecules/layout/widget.tsx
+++ b/src/components/molecules/layout/widget.tsx
@@ -1,11 +1,17 @@
-import { FC, useState } from 'react';
-import HeadingButton, { HeadingButtonProps } from '../buttons/heading-button';
+import { FC, ReactNode, useState } from 'react';
+import HeadingButton, {
+  type HeadingButtonProps,
+} from '../buttons/heading-button';
 import styles from './widget.module.scss';
 
 export type WidgetProps = Pick<
   HeadingButtonProps,
   'expanded' | 'level' | 'title'
 > & {
+  /**
+   * The widget body.
+   */
+  children: ReactNode;
   /**
    * Set additional classnames to the widget wrapper.
    */
diff --git a/src/components/molecules/modals/modal.test.tsx b/src/components/molecules/modals/modal.test.tsx
index 14fb224..9a0e237 100644
--- a/src/components/molecules/modals/modal.test.tsx
+++ b/src/components/molecules/modals/modal.test.tsx
@@ -1,9 +1,18 @@
 import { render, screen } from '@test-utils';
 import Modal from './modal';
 
+const title = 'A custom title';
+const children =
+  'Labore ullam delectus sit modi quam dolores. Ratione id sint aliquid facilis ipsum. Unde necessitatibus provident minus.';
+
 describe('Modal', () => {
   it('renders a title', () => {
-    render(<Modal title="A custom title" />);
-    expect(screen.getByText('A custom title')).toBeInTheDocument();
+    render(<Modal title={title}>{children}</Modal>);
+    expect(screen.getByText(title)).toBeInTheDocument();
+  });
+
+  it('renders the modal body', () => {
+    render(<Modal title={title}>{children}</Modal>);
+    expect(screen.getByText(children)).toBeInTheDocument();
   });
 });
diff --git a/src/components/molecules/modals/modal.tsx b/src/components/molecules/modals/modal.tsx
index 52ada57..58f5fa0 100644
--- a/src/components/molecules/modals/modal.tsx
+++ b/src/components/molecules/modals/modal.tsx
@@ -1,13 +1,17 @@
-import Heading from '@components/atoms/headings/heading';
-import { CogProps } from '@components/atoms/icons/cog';
-import { MagnifyingGlassProps } from '@components/atoms/icons/magnifying-glass';
+import Heading, { type HeadingProps } from '@components/atoms/headings/heading';
+import { type CogProps } from '@components/atoms/icons/cog';
+import { type MagnifyingGlassProps } from '@components/atoms/icons/magnifying-glass';
 import dynamic from 'next/dynamic';
-import { FC } from 'react';
+import { FC, ReactNode } from 'react';
 import styles from './modal.module.scss';
 
 export type Icons = 'cogs' | 'search';
 
 export type ModalProps = {
+  /**
+   * The modal body.
+   */
+  children: ReactNode;
   /**
    * Set additional classnames.
    */
@@ -15,7 +19,7 @@ export type ModalProps = {
   /**
    * Set additional classnames to the heading.
    */
-  headingClassName?: string;
+  headingClassName?: HeadingProps['className'];
   /**
    * A icon to illustrate the modal.
    */
diff --git a/src/components/molecules/modals/tooltip.tsx b/src/components/molecules/modals/tooltip.tsx
index 73f36e7..80721f3 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 { ReactNode, VFC } from 'react';
+import { FC, ReactNode } from 'react';
 import styles from './tooltip.module.scss';
 
 export type TooltipProps = {
@@ -26,7 +26,7 @@ export type TooltipProps = {
  *
  * Render a tooltip modal.
  */
-const Tooltip: VFC<TooltipProps> = ({
+const Tooltip: FC<TooltipProps> = ({
   className = '',
   content,
   icon,
diff --git a/src/components/molecules/nav/breadcrumb.tsx b/src/components/molecules/nav/breadcrumb.tsx
index 33af735..6dc86a0 100644
--- a/src/components/molecules/nav/breadcrumb.tsx
+++ b/src/components/molecules/nav/breadcrumb.tsx
@@ -1,7 +1,7 @@
 import Link from '@components/atoms/links/link';
 import { settings } from '@utils/config';
 import Script from 'next/script';
-import { VFC } from 'react';
+import { FC } from 'react';
 import { useIntl } from 'react-intl';
 import { BreadcrumbList, ListItem, WithContext } from 'schema-dts';
 import styles from './breadcrumb.module.scss';
@@ -37,7 +37,7 @@ export type BreadcrumbProps = {
  *
  * Render a breadcrumb navigation.
  */
-const Breadcrumb: VFC<BreadcrumbProps> = ({ items, ...props }) => {
+const Breadcrumb: FC<BreadcrumbProps> = ({ items, ...props }) => {
   const intl = useIntl();
 
   /**
diff --git a/src/components/molecules/nav/nav.tsx b/src/components/molecules/nav/nav.tsx
index 6ef9158..2666ea2 100644
--- a/src/components/molecules/nav/nav.tsx
+++ b/src/components/molecules/nav/nav.tsx
@@ -1,6 +1,6 @@
 import Link from '@components/atoms/links/link';
 import NavLink from '@components/atoms/links/nav-link';
-import { ReactNode, VFC } from 'react';
+import { FC, ReactNode } from 'react';
 import styles from './nav.module.scss';
 
 export type NavItem = {
@@ -46,7 +46,7 @@ export type NavProps = {
  *
  * Render the nav links.
  */
-const Nav: VFC<NavProps> = ({
+const Nav: FC<NavProps> = ({
   className = '',
   items,
   kind,
diff --git a/src/components/organisms/forms/comment-form.stories.tsx b/src/components/organisms/forms/comment-form.stories.tsx
index 1ab7cf2..670176c 100644
--- a/src/components/organisms/forms/comment-form.stories.tsx
+++ b/src/components/organisms/forms/comment-form.stories.tsx
@@ -1,4 +1,3 @@
-import Notice from '@components/atoms/layout/notice';
 import { ComponentMeta, ComponentStory } from '@storybook/react';
 import { IntlProvider } from 'react-intl';
 import CommentFormComponent from './comment-form';
diff --git a/src/components/organisms/forms/comment-form.tsx b/src/components/organisms/forms/comment-form.tsx
index 6acbf94..d7cb0f5 100644
--- a/src/components/organisms/forms/comment-form.tsx
+++ b/src/components/organisms/forms/comment-form.tsx
@@ -3,7 +3,7 @@ import Form from '@components/atoms/forms/form';
 import Heading, { type HeadingLevel } from '@components/atoms/headings/heading';
 import Spinner from '@components/atoms/loaders/spinner';
 import LabelledField from '@components/molecules/forms/labelled-field';
-import { ReactNode, useState, VFC } from 'react';
+import { FC, ReactNode, useState } from 'react';
 import { useIntl } from 'react-intl';
 import styles from './comment-form.module.scss';
 
@@ -31,7 +31,7 @@ export type CommentFormProps = {
   titleLevel?: HeadingLevel;
 };
 
-const CommentForm: VFC<CommentFormProps> = ({
+const CommentForm: FC<CommentFormProps> = ({
   className = '',
   Notice,
   saveComment,
diff --git a/src/components/organisms/forms/contact-form.tsx b/src/components/organisms/forms/contact-form.tsx
index 994244a..4a6902b 100644
--- a/src/components/organisms/forms/contact-form.tsx
+++ b/src/components/organisms/forms/contact-form.tsx
@@ -2,7 +2,7 @@ import Button from '@components/atoms/buttons/button';
 import Form from '@components/atoms/forms/form';
 import Spinner from '@components/atoms/loaders/spinner';
 import LabelledField from '@components/molecules/forms/labelled-field';
-import { ReactNode, useState, VFC } from 'react';
+import { FC, ReactNode, useState } from 'react';
 import { useIntl } from 'react-intl';
 import styles from './contact-form.module.scss';
 
@@ -27,7 +27,7 @@ export type ContactFormProps = {
  *
  * Render a contact form.
  */
-const ContactForm: VFC<ContactFormProps> = ({
+const ContactForm: FC<ContactFormProps> = ({
   className = '',
   Notice,
   sendMail,
diff --git a/src/components/organisms/forms/search-form.tsx b/src/components/organisms/forms/search-form.tsx
index 351d93c..18b7c08 100644
--- a/src/components/organisms/forms/search-form.tsx
+++ b/src/components/organisms/forms/search-form.tsx
@@ -2,15 +2,20 @@ import Button from '@components/atoms/buttons/button';
 import Form from '@components/atoms/forms/form';
 import MagnifyingGlass from '@components/atoms/icons/magnifying-glass';
 import LabelledField, {
-  LabelledFieldProps,
+  type LabelledFieldProps,
 } from '@components/molecules/forms/labelled-field';
-import { useState, VFC } from 'react';
+import { FC, useState } from 'react';
 import { useIntl } from 'react-intl';
 import styles from './search-form.module.scss';
 
 export type SearchFormProps = Pick<LabelledFieldProps, 'hideLabel'>;
 
-const SearchForm: VFC<SearchFormProps> = ({ hideLabel }) => {
+/**
+ * SearchForm component
+ *
+ * Render a search form.
+ */
+const SearchForm: FC<SearchFormProps> = ({ hideLabel }) => {
   const intl = useIntl();
   const fieldLabel = intl.formatMessage({
     defaultMessage: 'Search for:',
diff --git a/src/components/organisms/layout/cards-list.tsx b/src/components/organisms/layout/cards-list.tsx
index a53df0d..33ffe23 100644
--- a/src/components/organisms/layout/cards-list.tsx
+++ b/src/components/organisms/layout/cards-list.tsx
@@ -3,7 +3,7 @@ import List, {
   type ListProps,
 } from '@components/atoms/lists/list';
 import Card, { type CardProps } from '@components/molecules/layout/card';
-import { VFC } from 'react';
+import { FC } from 'react';
 import styles from './cards-list.module.scss';
 
 export type CardsListItem = Omit<
@@ -37,7 +37,7 @@ export type CardsListProps = {
  *
  * Return a list of Card components.
  */
-const CardsList: VFC<CardsListProps> = ({
+const CardsList: FC<CardsListProps> = ({
   coverFit,
   items,
   kind = 'unordered',
diff --git a/src/components/organisms/layout/footer.tsx b/src/components/organisms/layout/footer.tsx
index c9cb067..15bfa24 100644
--- a/src/components/organisms/layout/footer.tsx
+++ b/src/components/organisms/layout/footer.tsx
@@ -1,7 +1,9 @@
-import Copyright, { CopyrightProps } from '@components/atoms/layout/copyright';
+import Copyright, {
+  type CopyrightProps,
+} from '@components/atoms/layout/copyright';
 import BackToTop from '@components/molecules/buttons/back-to-top';
 import Nav, { type NavItem } from '@components/molecules/nav/nav';
-import { VFC } from 'react';
+import { FC } from 'react';
 import styles from './footer.module.scss';
 
 export type FooterProps = {
@@ -28,12 +30,7 @@ export type FooterProps = {
  *
  * Renders a footer with copyright and nav;
  */
-const Footer: VFC<FooterProps> = ({
-  className,
-  copyright,
-  navItems,
-  topId,
-}) => {
+const Footer: FC<FooterProps> = ({ className, copyright, navItems, topId }) => {
   return (
     <footer className={`${styles.wrapper} ${className}`}>
       <Copyright
diff --git a/src/components/organisms/layout/overview.tsx b/src/components/organisms/layout/overview.tsx
index 3f83342..42d0431 100644
--- a/src/components/organisms/layout/overview.tsx
+++ b/src/components/organisms/layout/overview.tsx
@@ -2,7 +2,7 @@ import ResponsiveImage, {
   type ResponsiveImageProps,
 } from '@components/molecules/images/responsive-image';
 import Meta, { type MetaMap } from '@components/molecules/layout/meta';
-import { VFC } from 'react';
+import { FC } from 'react';
 import styles from './overview.module.scss';
 
 export type OverviewProps = {
@@ -15,7 +15,7 @@ export type OverviewProps = {
  *
  * Render an overview.
  */
-const Overview: VFC<OverviewProps> = ({ cover, meta }) => {
+const Overview: FC<OverviewProps> = ({ cover, meta }) => {
   return (
     <div className={styles.wrapper}>
       {cover && (
diff --git a/src/components/organisms/layout/summary.stories.tsx b/src/components/organisms/layout/summary.stories.tsx
index 5214d70..b33acde 100644
--- a/src/components/organisms/layout/summary.stories.tsx
+++ b/src/components/organisms/layout/summary.stories.tsx
@@ -103,7 +103,7 @@ Summary.args = {
   cover: {
     alt: 'A cover',
     height: 480,
-    url: 'http://placeimg.com/640/480',
+    src: 'http://placeimg.com/640/480',
     width: 640,
   },
   excerpt:
diff --git a/src/components/organisms/layout/summary.test.tsx b/src/components/organisms/layout/summary.test.tsx
index ce87c0c..4944805 100644
--- a/src/components/organisms/layout/summary.test.tsx
+++ b/src/components/organisms/layout/summary.test.tsx
@@ -4,7 +4,7 @@ import Summary from './summary';
 const cover = {
   alt: 'A cover',
   height: 480,
-  url: 'http://placeimg.com/640/480',
+  src: 'http://placeimg.com/640/480',
   width: 640,
 };
 
diff --git a/src/components/organisms/layout/summary.tsx b/src/components/organisms/layout/summary.tsx
index 3624e5d..733a660 100644
--- a/src/components/organisms/layout/summary.tsx
+++ b/src/components/organisms/layout/summary.tsx
@@ -2,18 +2,18 @@ import ButtonLink from '@components/atoms/buttons/button-link';
 import Heading, { type HeadingLevel } from '@components/atoms/headings/heading';
 import Arrow from '@components/atoms/icons/arrow';
 import Link from '@components/atoms/links/link';
-import ResponsiveImage from '@components/molecules/images/responsive-image';
+import ResponsiveImage, {
+  type ResponsiveImageProps,
+} from '@components/molecules/images/responsive-image';
 import Meta, { type MetaItem } from '@components/molecules/layout/meta';
-import { VFC } from 'react';
+import { FC } from 'react';
 import { useIntl } from 'react-intl';
 import styles from './summary.module.scss';
 
-export type Cover = {
-  alt: string;
-  height: number;
-  url: string;
-  width: number;
-};
+export type Cover = Pick<
+  ResponsiveImageProps,
+  'alt' | 'src' | 'width' | 'height'
+>;
 
 export type RequiredMetaKey = 'publication';
 
@@ -35,11 +35,29 @@ export type OptionalMeta = {
 export type Meta = RequiredMeta & OptionalMeta;
 
 export type SummaryProps = {
+  /**
+   * The post cover.
+   */
   cover?: Cover;
+  /**
+   * The post excerpt.
+   */
   excerpt: string;
+  /**
+   * The post meta.
+   */
   meta: Meta;
+  /**
+   * The post title.
+   */
   title: string;
+  /**
+   * The heading level (hn).
+   */
   titleLevel?: HeadingLevel;
+  /**
+   * The post url.
+   */
   url: string;
 };
 
@@ -48,7 +66,7 @@ export type SummaryProps = {
  *
  * Render a page summary.
  */
-const Summary: VFC<SummaryProps> = ({
+const Summary: FC<SummaryProps> = ({
   cover,
   excerpt,
   meta,
@@ -57,18 +75,23 @@ const Summary: VFC<SummaryProps> = ({
   url,
 }) => {
   const intl = useIntl();
+  const readMore = intl.formatMessage(
+    {
+      defaultMessage: 'Read more<a11y> about {title}</a11y>',
+      description: 'Summary: read more link',
+      id: 'Zpgv+f',
+    },
+    {
+      title,
+      a11y: (chunks: string) => (
+        <span className="screen-reader-text">{chunks}</span>
+      ),
+    }
+  );
 
   return (
     <article className={styles.wrapper}>
-      {cover && (
-        <ResponsiveImage
-          alt={cover.alt}
-          src={cover.url}
-          width={cover.width}
-          height={cover.height}
-          className={styles.cover}
-        />
-      )}
+      {cover && <ResponsiveImage className={styles.cover} {...cover} />}
       <header className={styles.header}>
         <Link href={url}>
           <Heading level={titleLevel} className={styles.title}>
@@ -79,20 +102,10 @@ const Summary: VFC<SummaryProps> = ({
       <div className={styles.body}>
         {excerpt}
         <ButtonLink target={url} className={styles['read-more']}>
-          {intl.formatMessage(
-            {
-              defaultMessage: 'Read more<a11y> about {title}</a11y>',
-              description: 'Summary: read more link',
-              id: 'Zpgv+f',
-            },
-            {
-              title,
-              a11y: (chunks: string) => (
-                <span className="screen-reader-text">{chunks}</span>
-              ),
-            }
-          )}
-          <Arrow direction="right" />
+          <>
+            {readMore}
+            <Arrow direction="right" />
+          </>
         </ButtonLink>
       </div>
       <footer className={styles.footer}>
diff --git a/src/components/organisms/modals/search-modal.tsx b/src/components/organisms/modals/search-modal.tsx
index 883b783..0e0ceed 100644
--- a/src/components/organisms/modals/search-modal.tsx
+++ b/src/components/organisms/modals/search-modal.tsx
@@ -1,5 +1,5 @@
-import Modal from '@components/molecules/modals/modal';
-import { VFC } from 'react';
+import Modal, { type ModalProps } from '@components/molecules/modals/modal';
+import { FC } from 'react';
 import { useIntl } from 'react-intl';
 import SearchForm from '../forms/search-form';
 import styles from './search-modal.module.scss';
@@ -8,7 +8,7 @@ export type SearchModalProps = {
   /**
    * Set additional classnames to modal wrapper.
    */
-  className?: string;
+  className?: ModalProps['className'];
 };
 
 /**
@@ -16,7 +16,7 @@ export type SearchModalProps = {
  *
  * Render a search form modal.
  */
-const SearchModal: VFC<SearchModalProps> = ({ className }) => {
+const SearchModal: FC<SearchModalProps> = ({ className }) => {
   const intl = useIntl();
   const modalTitle = intl.formatMessage({
     defaultMessage: 'Search',
diff --git a/src/components/organisms/modals/settings-modal.tsx b/src/components/organisms/modals/settings-modal.tsx
index 25d6f6f..20d2605 100644
--- a/src/components/organisms/modals/settings-modal.tsx
+++ b/src/components/organisms/modals/settings-modal.tsx
@@ -1,10 +1,12 @@
 import Form from '@components/atoms/forms/form';
-import AckeeSelect from '@components/molecules/forms/ackee-select';
+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 Modal from '@components/molecules/modals/modal';
-import { VFC } from 'react';
+import Modal, { type ModalProps } from '@components/molecules/modals/modal';
+import { FC } from 'react';
 import { useIntl } from 'react-intl';
 import styles from './settings-modal.module.scss';
 
@@ -12,11 +14,11 @@ export type SettingsModalProps = {
   /**
    * Set additional classnames to the modal wrapper.
    */
-  className?: string;
+  className?: ModalProps['className'];
   /**
    * Set additional classnames to the tooltip wrapper.
    */
-  tooltipClassName?: string;
+  tooltipClassName?: AckeeSelectProps['tooltipClassName'];
 };
 
 /**
@@ -24,7 +26,7 @@ export type SettingsModalProps = {
  *
  * Render a modal with settings options.
  */
-const SettingsModal: VFC<SettingsModalProps> = ({
+const SettingsModal: FC<SettingsModalProps> = ({
   className = '',
   tooltipClassName = '',
 }) => {
diff --git a/src/components/organisms/toolbar/main-nav.tsx b/src/components/organisms/toolbar/main-nav.tsx
index 50bbe8b..35e3fd6 100644
--- a/src/components/organisms/toolbar/main-nav.tsx
+++ b/src/components/organisms/toolbar/main-nav.tsx
@@ -1,17 +1,20 @@
 import Checkbox, { type CheckboxProps } from '@components/atoms/forms/checkbox';
 import Label from '@components/atoms/forms/label';
 import Hamburger from '@components/atoms/icons/hamburger';
-import Nav, { type NavItem } from '@components/molecules/nav/nav';
-import { VFC } from 'react';
+import Nav, {
+  type NavProps,
+  type NavItem,
+} from '@components/molecules/nav/nav';
+import { FC } from 'react';
 import { useIntl } from 'react-intl';
-import sharedStyles from './toolbar-items.module.scss';
 import mainNavStyles from './main-nav.module.scss';
+import sharedStyles from './toolbar-items.module.scss';
 
 export type MainNavProps = {
   /**
    * Set additional classnames to the nav element.
    */
-  className?: string;
+  className?: NavProps['className'];
   /**
    * The button state.
    */
@@ -26,7 +29,12 @@ export type MainNavProps = {
   setIsActive: CheckboxProps['setValue'];
 };
 
-const MainNav: VFC<MainNavProps> = ({
+/**
+ * MainNav component
+ *
+ * Render the main navigation.
+ */
+const MainNav: FC<MainNavProps> = ({
   className = '',
   isActive,
   items,
diff --git a/src/components/organisms/toolbar/search.tsx b/src/components/organisms/toolbar/search.tsx
index 070bce0..72cd576 100644
--- a/src/components/organisms/toolbar/search.tsx
+++ b/src/components/organisms/toolbar/search.tsx
@@ -1,17 +1,17 @@
-import Checkbox, { CheckboxProps } from '@components/atoms/forms/checkbox';
+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 { VFC } from 'react';
+import { FC } from 'react';
 import { useIntl } from 'react-intl';
-import SearchModal from '../modals/search-modal';
-import sharedStyles from './toolbar-items.module.scss';
+import SearchModal, { type SearchModalProps } from '../modals/search-modal';
 import searchStyles from './search.module.scss';
+import sharedStyles from './toolbar-items.module.scss';
 
 export type SearchProps = {
   /**
    * Set additional classnames to the modal wrapper.
    */
-  className?: string;
+  className?: SearchModalProps['className'];
   /**
    * The button state.
    */
@@ -22,11 +22,7 @@ export type SearchProps = {
   setIsActive: CheckboxProps['setValue'];
 };
 
-const Search: VFC<SearchProps> = ({
-  className = '',
-  isActive,
-  setIsActive,
-}) => {
+const Search: FC<SearchProps> = ({ className = '', isActive, setIsActive }) => {
   const intl = useIntl();
   const label = isActive
     ? intl.formatMessage({
diff --git a/src/components/organisms/toolbar/settings.tsx b/src/components/organisms/toolbar/settings.tsx
index 88539fb..3b10226 100644
--- a/src/components/organisms/toolbar/settings.tsx
+++ b/src/components/organisms/toolbar/settings.tsx
@@ -1,11 +1,13 @@
-import Checkbox, { CheckboxProps } from '@components/atoms/forms/checkbox';
+import Checkbox, { type CheckboxProps } from '@components/atoms/forms/checkbox';
 import Label from '@components/atoms/forms/label';
 import Cog from '@components/atoms/icons/cog';
-import { VFC } from 'react';
+import { FC } from 'react';
 import { useIntl } from 'react-intl';
-import SettingsModal from '../modals/settings-modal';
-import sharedStyles from './toolbar-items.module.scss';
+import SettingsModal, {
+  type SettingsModalProps,
+} from '../modals/settings-modal';
 import settingsStyles from './settings.module.scss';
+import sharedStyles from './toolbar-items.module.scss';
 
 export type SettingsProps = {
   /**
@@ -23,10 +25,10 @@ export type SettingsProps = {
   /**
    * Set additional classnames to the tooltip wrapper.
    */
-  tooltipClassName?: string;
+  tooltipClassName?: SettingsModalProps['tooltipClassName'];
 };
 
-const Settings: VFC<SettingsProps> = ({
+const Settings: FC<SettingsProps> = ({
   className = '',
   isActive,
   setIsActive,
diff --git a/src/components/organisms/toolbar/toolbar.tsx b/src/components/organisms/toolbar/toolbar.tsx
index 81e80cf..f1ce530 100644
--- a/src/components/organisms/toolbar/toolbar.tsx
+++ b/src/components/organisms/toolbar/toolbar.tsx
@@ -1,4 +1,4 @@
-import { useState, VFC } from 'react';
+import { FC, useState } from 'react';
 import MainNav, { type MainNavProps } from '../toolbar/main-nav';
 import Search from '../toolbar/search';
 import Settings from '../toolbar/settings';
@@ -20,7 +20,7 @@ export type ToolbarProps = {
  *
  * Render the website toolbar.
  */
-const Toolbar: VFC<ToolbarProps> = ({ className = '', nav }) => {
+const Toolbar: FC<ToolbarProps> = ({ className = '', nav }) => {
   const [isNavOpened, setIsNavOpened] = useState<boolean>(false);
   const [isSettingsOpened, setIsSettingsOpened] = useState<boolean>(false);
   const [isSearchOpened, setIsSearchOpened] = useState<boolean>(false);
diff --git a/src/components/organisms/widgets/image-widget.tsx b/src/components/organisms/widgets/image-widget.tsx
index 928d5ea..ba04c6a 100644
--- a/src/components/organisms/widgets/image-widget.tsx
+++ b/src/components/organisms/widgets/image-widget.tsx
@@ -1,28 +1,16 @@
-import ResponsiveImage from '@components/molecules/images/responsive-image';
+import ResponsiveImage, {
+  type ResponsiveImageProps,
+} from '@components/molecules/images/responsive-image';
 import Widget, { type WidgetProps } from '@components/molecules/layout/widget';
-import { VFC } from 'react';
+import { FC } from 'react';
 import styles from './image-widget.module.scss';
 
 export type Alignment = 'left' | 'center' | 'right';
 
-export type Image = {
-  /**
-   * An alternative text for the image.
-   */
-  alt: string;
-  /**
-   * The image height.
-   */
-  height: number;
-  /**
-   * The image source.
-   */
-  src: string;
-  /**
-   * The image width.
-   */
-  width: number;
-};
+export type Image = Pick<
+  ResponsiveImageProps,
+  'alt' | 'height' | 'src' | 'width'
+>;
 
 export type ImageWidgetProps = Pick<
   WidgetProps,
@@ -35,7 +23,7 @@ export type ImageWidgetProps = Pick<
   /**
    * Add a caption to the image.
    */
-  description?: string;
+  description?: ResponsiveImageProps['caption'];
   /**
    * An object describing the image.
    */
@@ -43,7 +31,7 @@ export type ImageWidgetProps = Pick<
   /**
    * Add a link to the image.
    */
-  url?: string;
+  url?: ResponsiveImageProps['target'];
 };
 
 /**
@@ -51,7 +39,7 @@ export type ImageWidgetProps = Pick<
  *
  * Renders a widget that print an image and an optional text.
  */
-const ImageWidget: VFC<ImageWidgetProps> = ({
+const ImageWidget: FC<ImageWidgetProps> = ({
   alignment = 'left',
   description,
   img,
diff --git a/src/components/organisms/widgets/links-list-widget.tsx b/src/components/organisms/widgets/links-list-widget.tsx
index 155354e..559d0b6 100644
--- a/src/components/organisms/widgets/links-list-widget.tsx
+++ b/src/components/organisms/widgets/links-list-widget.tsx
@@ -1,8 +1,11 @@
 import Link from '@components/atoms/links/link';
-import List, { ListProps, type ListItem } from '@components/atoms/lists/list';
+import List, {
+  type ListProps,
+  type ListItem,
+} from '@components/atoms/lists/list';
 import Widget, { type WidgetProps } from '@components/molecules/layout/widget';
 import { slugify } from '@utils/helpers/slugify';
-import { VFC } from 'react';
+import { FC } from 'react';
 import styles from './links-list-widget.module.scss';
 
 export type LinksListItems = {
@@ -33,7 +36,7 @@ export type LinksListWidgetProps = Pick<WidgetProps, 'level' | 'title'> &
  *
  * Render a list of links inside a widget.
  */
-const LinksListWidget: VFC<LinksListWidgetProps> = ({
+const LinksListWidget: FC<LinksListWidgetProps> = ({
   items,
   kind = 'unordered',
   ...props
diff --git a/src/components/organisms/widgets/sharing.tsx b/src/components/organisms/widgets/sharing.tsx
index ccd3a21..571c1a4 100644
--- a/src/components/organisms/widgets/sharing.tsx
+++ b/src/components/organisms/widgets/sharing.tsx
@@ -2,7 +2,7 @@ import SharingLink, {
   type SharingMedium,
 } from '@components/atoms/links/sharing-link';
 import Widget, { type WidgetProps } from '@components/molecules/layout/widget';
-import { VFC } from 'react';
+import { FC } from 'react';
 import { useIntl } from 'react-intl';
 import styles from './sharing.module.scss';
 
@@ -21,7 +21,7 @@ export type SharingData = {
   url: string;
 };
 
-export type SharingProps = WidgetProps & {
+export type SharingProps = Omit<WidgetProps, 'children'> & {
   /**
    * The page data to share.
    */
@@ -37,7 +37,7 @@ export type SharingProps = WidgetProps & {
  *
  * Render a list of sharing links inside a widget.
  */
-const Sharing: VFC<SharingProps> = ({ data, media, ...props }) => {
+const Sharing: FC<SharingProps> = ({ data, media, ...props }) => {
   const intl = useIntl();
 
   /**
-- 
cgit v1.2.3


From b214baab3e17d92f784b4f782863deafc5558ee4 Mon Sep 17 00:00:00 2001
From: Armand Philippot <git@armandphilippot.com>
Date: Wed, 18 May 2022 14:27:11 +0200
Subject: chore: close toolbar modals on click/focus outside

---
 .../molecules/forms/select-with-tooltip.tsx        | 10 ++++-
 src/components/molecules/modals/tooltip.tsx        | 16 ++++----
 .../organisms/forms/settings-form.module.scss      | 11 +++++
 .../organisms/forms/settings-form.stories.tsx      | 47 ++++++++++++++++++++++
 .../organisms/forms/settings-form.test.tsx         | 39 ++++++++++++++++++
 src/components/organisms/forms/settings-form.tsx   | 36 +++++++++++++++++
 src/components/organisms/modals/search-modal.tsx   | 13 +++++-
 .../organisms/modals/settings-modal.module.scss    | 14 +------
 .../organisms/modals/settings-modal.stories.tsx    |  8 ----
 .../organisms/modals/settings-modal.test.tsx       | 27 +------------
 src/components/organisms/modals/settings-modal.tsx | 42 +++++++------------
 src/components/organisms/toolbar/main-nav.tsx      | 16 ++++----
 .../organisms/toolbar/search.stories.tsx           |  1 -
 src/components/organisms/toolbar/search.test.tsx   |  5 ---
 src/components/organisms/toolbar/search.tsx        | 16 ++++----
 .../organisms/toolbar/settings.stories.tsx         |  8 ----
 src/components/organisms/toolbar/settings.tsx      | 16 ++++----
 src/components/organisms/toolbar/toolbar.tsx       | 18 ++++++++-
 src/utils/hooks/use-click-outside.tsx              | 43 ++++++++++++++++++++
 19 files changed, 258 insertions(+), 128 deletions(-)
 create mode 100644 src/components/organisms/forms/settings-form.module.scss
 create mode 100644 src/components/organisms/forms/settings-form.stories.tsx
 create mode 100644 src/components/organisms/forms/settings-form.test.tsx
 create mode 100644 src/components/organisms/forms/settings-form.tsx
 create mode 100644 src/utils/hooks/use-click-outside.tsx

(limited to 'src/components/molecules/forms/select-with-tooltip.tsx')

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;
-- 
cgit v1.2.3


From ef2407832202e5451751e26459e6bdcdbd152122 Mon Sep 17 00:00:00 2001
From: Armand Philippot <git@armandphilippot.com>
Date: Sat, 21 May 2022 16:04:06 +0200
Subject: fix: close Ackee tooltip on button click

The tooltip was closing and reopening on click because of a conflict
between the button event and the hook useClickOutside. By checking that
the event target is not the button, the tooltip is now closing as
expected.
---
 src/components/atoms/buttons/button.tsx            | 35 ++++++++++++++--------
 src/components/molecules/buttons/help-button.tsx   | 12 +++++---
 .../molecules/forms/select-with-tooltip.tsx        | 11 +++++--
 src/utils/hooks/use-click-outside.tsx              |  7 +++--
 4 files changed, 44 insertions(+), 21 deletions(-)

(limited to 'src/components/molecules/forms/select-with-tooltip.tsx')

diff --git a/src/components/atoms/buttons/button.tsx b/src/components/atoms/buttons/button.tsx
index a6eef8b..9776687 100644
--- a/src/components/atoms/buttons/button.tsx
+++ b/src/components/atoms/buttons/button.tsx
@@ -1,4 +1,9 @@
-import { FC, MouseEventHandler, ReactNode } from 'react';
+import {
+  forwardRef,
+  ForwardRefRenderFunction,
+  MouseEventHandler,
+  ReactNode,
+} from 'react';
 import styles from './buttons.module.scss';
 
 export type ButtonProps = {
@@ -41,23 +46,27 @@ export type ButtonProps = {
  *
  * Use a button as call to action.
  */
-const Button: FC<ButtonProps> = ({
-  className = '',
-  children,
-  disabled = false,
-  kind = 'secondary',
-  shape = 'rectangle',
-  type = 'button',
-  ...props
-}) => {
+const Button: ForwardRefRenderFunction<HTMLButtonElement, ButtonProps> = (
+  {
+    className = '',
+    children,
+    disabled = false,
+    kind = 'secondary',
+    shape = 'rectangle',
+    type = 'button',
+    ...props
+  },
+  ref
+) => {
   const kindClass = styles[`btn--${kind}`];
   const shapeClass = styles[`btn--${shape}`];
 
   return (
     <button
-      type={type}
-      disabled={disabled}
       className={`${styles.btn} ${kindClass} ${shapeClass} ${className}`}
+      disabled={disabled}
+      ref={ref}
+      type={type}
       {...props}
     >
       {children}
@@ -65,4 +74,4 @@ const Button: FC<ButtonProps> = ({
   );
 };
 
-export default Button;
+export default forwardRef(Button);
diff --git a/src/components/molecules/buttons/help-button.tsx b/src/components/molecules/buttons/help-button.tsx
index f19322f..e351453 100644
--- a/src/components/molecules/buttons/help-button.tsx
+++ b/src/components/molecules/buttons/help-button.tsx
@@ -1,5 +1,5 @@
 import Button, { type ButtonProps } from '@components/atoms/buttons/button';
-import { FC } from 'react';
+import { forwardRef, ForwardRefRenderFunction } from 'react';
 import { useIntl } from 'react-intl';
 import styles from './help-button.module.scss';
 
@@ -15,7 +15,10 @@ export type HelpButtonProps = Pick<ButtonProps, 'onClick'> & {
  *
  * Render a button with an interrogation mark icon.
  */
-const HelpButton: FC<HelpButtonProps> = ({ className = '', onClick }) => {
+const HelpButton: ForwardRefRenderFunction<
+  HTMLButtonElement,
+  HelpButtonProps
+> = ({ className = '', onClick }, ref) => {
   const intl = useIntl();
   const text = intl.formatMessage({
     defaultMessage: 'Help',
@@ -25,9 +28,10 @@ const HelpButton: FC<HelpButtonProps> = ({ className = '', onClick }) => {
 
   return (
     <Button
-      shape="circle"
       className={`${styles.btn} ${className}`}
       onClick={onClick}
+      ref={ref}
+      shape="circle"
     >
       <span className="screen-reader-text">{text}</span>
       <span className={styles.icon}>?</span>
@@ -35,4 +39,4 @@ const HelpButton: FC<HelpButtonProps> = ({ className = '', onClick }) => {
   );
 };
 
-export default HelpButton;
+export default forwardRef(HelpButton);
diff --git a/src/components/molecules/forms/select-with-tooltip.tsx b/src/components/molecules/forms/select-with-tooltip.tsx
index f576a15..29e2563 100644
--- a/src/components/molecules/forms/select-with-tooltip.tsx
+++ b/src/components/molecules/forms/select-with-tooltip.tsx
@@ -29,15 +29,21 @@ const SelectWithTooltip: FC<SelectWithTooltipProps> = ({
   ...props
 }) => {
   const [isTooltipOpened, setIsTooltipOpened] = useState<boolean>(false);
+  const buttonRef = useRef<HTMLButtonElement>(null);
   const tooltipRef = useRef<HTMLDivElement>(null);
   const buttonModifier = isTooltipOpened ? styles['btn--activated'] : '';
   const tooltipModifier = isTooltipOpened
     ? styles['tooltip--visible']
     : styles['tooltip--hidden'];
 
+  const closeTooltip = (target: EventTarget) => {
+    if (buttonRef.current && !buttonRef.current.contains(target as Node))
+      setIsTooltipOpened(false);
+  };
+
   useClickOutside(
     tooltipRef,
-    () => isTooltipOpened && setIsTooltipOpened(false)
+    (target) => isTooltipOpened && closeTooltip(target)
   );
 
   return (
@@ -49,8 +55,9 @@ const SelectWithTooltip: FC<SelectWithTooltipProps> = ({
         {...props}
       />
       <HelpButton
-        onClick={() => setIsTooltipOpened(!isTooltipOpened)}
         className={`${styles.btn} ${buttonModifier}`}
+        onClick={() => setIsTooltipOpened(!isTooltipOpened)}
+        ref={buttonRef}
       />
       <Tooltip
         title={title}
diff --git a/src/utils/hooks/use-click-outside.tsx b/src/utils/hooks/use-click-outside.tsx
index 066c1c2..cead98b 100644
--- a/src/utils/hooks/use-click-outside.tsx
+++ b/src/utils/hooks/use-click-outside.tsx
@@ -6,7 +6,10 @@ import { RefObject, useCallback, useEffect } from 'react';
  * @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) => {
+const useClickOutside = (
+  el: RefObject<HTMLElement>,
+  callback: (target: EventTarget) => void
+) => {
   /**
    * Check if an event target is outside an element.
    *
@@ -24,7 +27,7 @@ const useClickOutside = (el: RefObject<HTMLElement>, callback: () => void) => {
 
   const handleEvent = useCallback(
     (e: MouseEvent | FocusEvent) => {
-      if (e.target && isTargetOutside(el, e.target)) callback();
+      if (e.target && isTargetOutside(el, e.target)) callback(e.target);
     },
     [el, callback]
   );
-- 
cgit v1.2.3

