Skip to content

Commit

Permalink
front: operationalstudies: focus added on the name input when modal o…
Browse files Browse the repository at this point in the history
…pens

- added focus on mapSearchStation
- added focus on addOrEditProjectModal
- added focus on addOrEditStudyModal
- added focus on addOrEditScenarioModal
- create custom hook useModalFocusTrap in utils/hooks
  • Loading branch information
Caracol3 committed Feb 12, 2024
1 parent b4a0802 commit d204e34
Show file tree
Hide file tree
Showing 6 changed files with 81 additions and 51 deletions.
1 change: 1 addition & 0 deletions front/src/common/Map/Search/MapSearchStation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ const MapSearchStation = ({ updateExtViewport, closeMapSearchPopUp }: MapSearchS
clearButton
noMargin
sm
focus
/>
</span>
<span className="col-md-3 pl-0 mb-2">
Expand Down
10 changes: 8 additions & 2 deletions front/src/modules/project/components/AddOrEditProjectModal.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useContext, useEffect, useState } from 'react';
import React, { useContext, useEffect, useRef, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
Expand Down Expand Up @@ -30,6 +30,7 @@ import type { ProjectWithStudies, ProjectCreateForm } from 'common/api/osrdEdito

import { setFailure, setSuccess } from 'reducers/main';
import { getUserSafeWord } from 'reducers/user/userSelectors';
import useModalFocusTrap from 'utils/hooks/useModalFocusTrap';

const emptyProject: ProjectCreateForm = {
description: '',
Expand Down Expand Up @@ -71,6 +72,8 @@ export default function AddOrEditProjectModal({

const { updateProjectID } = useOsrdConfActions();

const modalRef = useRef<HTMLDivElement>(null);

const removeTag = (idx: number) => {
if (!currentProject.tags) return;
const newTags = Array.from(currentProject.tags);
Expand Down Expand Up @@ -217,8 +220,10 @@ export default function AddOrEditProjectModal({
}
}, [safeWord]);

useModalFocusTrap(modalRef, closeModal);

return (
<div className="project-edition-modal">
<div className="project-edition-modal" ref={modalRef}>
<ModalHeaderSNCF withCloseButton withBorderBottom>
<h1 className="project-edition-modal-title">
{editionMode ? t('projectModificationTitle') : t('projectCreationTitle')}
Expand All @@ -242,6 +247,7 @@ export default function AddOrEditProjectModal({
type="text"
name="projectInputName"
data-testid="projectInputName"
focus
label={
<div className="d-flex align-items-center">
<span className="mr-2">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import React, { useEffect, useMemo, useRef, useState } from 'react';
import React, { useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import Grid from 'common/Grid/Grid';
import InputSNCF from 'common/BootstrapSNCF/InputSNCF';
import { ModalBodySNCF, ModalHeaderSNCF, useModal } from 'common/BootstrapSNCF/ModalSNCF';
import { splitArrayByFirstLetter } from 'utils/array';
import useModalFocusTrap from 'utils/hooks/useModalFocusTrap';

type PowerRestrictionGridModalProps = {
powerRestrictionsList: string[];
Expand Down Expand Up @@ -35,50 +36,7 @@ const PowerRestrictionGridModal = ({
closeModal();
};

// Allow the user to escape the modal by pressing escape and to trap the focus inside it
useEffect(() => {
const modalElement = modalRef.current;

const focusableElements = modalElement?.querySelectorAll(
// last declaration stands for all elements not natively focusable like li
'input, button, [tabindex]:not([tabindex="-1"])'
) as NodeListOf<Element>;

const firstElement = focusableElements[0] as HTMLElement;
const lastElement = focusableElements[focusableElements?.length - 1] as HTMLElement;

/**
*
* Prevent the tab event and set focus on :
* - last element if we are pressing on "shift" in addition to "tab" and are on the first element
* - first element if we are only pressing "tab" and are on the last element
*/
const handleTabKeyPress = (event: KeyboardEvent) => {
if (event.key === 'Tab') {
if (event.shiftKey && document.activeElement === firstElement) {
event.preventDefault();
lastElement.focus();
} else if (!event.shiftKey && document.activeElement === lastElement) {
event.preventDefault();
firstElement.focus();
}
}
};

const handleEscapeKeyPress = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
closeModal();
}
};

modalElement?.addEventListener('keydown', handleTabKeyPress);
modalElement?.addEventListener('keydown', handleEscapeKeyPress);

return () => {
modalElement?.removeEventListener('keydown', handleTabKeyPress);
modalElement?.removeEventListener('keydown', handleEscapeKeyPress);
};
}, []);
useModalFocusTrap(modalRef, closeModal);

return (
<div className="p-2" ref={modalRef}>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import cx from 'classnames';
import React, { useContext, useEffect, useMemo, useState } from 'react';
import React, { useContext, useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { GoPencil, GoTrash } from 'react-icons/go';
import { FaPlus } from 'react-icons/fa';
Expand All @@ -25,6 +25,7 @@ import ModalHeaderSNCF from 'common/BootstrapSNCF/ModalSNCF/ModalHeaderSNCF';
import { ModalContext } from 'common/BootstrapSNCF/ModalSNCF/ModalProvider';
import { InfraSelectorModal } from 'modules/infra/components/InfraSelector';
import { setFailure, setSuccess } from 'reducers/main';
import useModalFocusTrap from 'utils/hooks/useModalFocusTrap';

type CreateOrPatchScenarioForm = ScenarioPatchForm & {
id?: number;
Expand Down Expand Up @@ -86,6 +87,7 @@ export default function AddOrEditScenarioModal({
const dispatch = useDispatch();
const navigate = useNavigate();
const infraID = useInfraID();
const modalRef = useRef<HTMLDivElement>(null);

const selectedValue = useMemo(() => {
if (currentScenario.electrical_profile_set_id) {
Expand Down Expand Up @@ -206,8 +208,10 @@ export default function AddOrEditScenarioModal({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [infraID]);

useModalFocusTrap(modalRef, closeModal);

return (
<div className="scenario-edition-modal">
<div className="scenario-edition-modal" ref={modalRef}>
<ModalHeaderSNCF withCloseButton withBorderBottom>
<h1 className="scenario-edition-modal-title">
{editionMode ? t('scenarioModificationTitle') : t('scenarioCreationTitle')}
Expand All @@ -221,6 +225,7 @@ export default function AddOrEditScenarioModal({
id="scenarioInputName"
type="text"
name="scenarioInputName"
focus
label={
<div className="d-flex align-items-center">
<span className="mr-2">
Expand Down
10 changes: 8 additions & 2 deletions front/src/modules/study/components/AddOrEditStudyModal.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useContext, useEffect, useMemo, useState } from 'react';
import React, { useContext, useEffect, useMemo, useRef, useState } from 'react';
import { useDispatch } from 'react-redux';
import { useTranslation } from 'react-i18next';
import { useNavigate, useParams } from 'react-router-dom';
Expand Down Expand Up @@ -28,6 +28,7 @@ import ModalHeaderSNCF from 'common/BootstrapSNCF/ModalSNCF/ModalHeaderSNCF';
import type { StudyCreateForm } from 'common/api/osrdEditoastApi';

import { setFailure, setSuccess } from 'reducers/main';
import useModalFocusTrap from 'utils/hooks/useModalFocusTrap';

export interface StudyForm extends StudyCreateForm {
id?: number;
Expand Down Expand Up @@ -65,6 +66,8 @@ export default function AddOrEditStudyModal({ editionMode, study }: Props) {

const studyCategoriesOptions = createSelectOptions('studyCategories', studyTypes);

const modalRef = useRef<HTMLDivElement>(null);

const removeTag = (idx: number) => {
const newTags = [...(currentStudy.tags || [])];
newTags.splice(idx, 1);
Expand Down Expand Up @@ -158,8 +161,10 @@ export default function AddOrEditStudyModal({ editionMode, study }: Props) {
};
}, [currentStudy?.start_date, currentStudy?.expected_end_date, currentStudy?.actual_end_date]);

useModalFocusTrap(modalRef, closeModal);

return (
<div className="study-edition-modal">
<div className="study-edition-modal" ref={modalRef}>
<ModalHeaderSNCF withCloseButton withBorderBottom>
<h1 className="study-edition-modal-title">
<img src={studyLogo} alt="Study Logo" />
Expand All @@ -172,6 +177,7 @@ export default function AddOrEditStudyModal({ editionMode, study }: Props) {
id="studyInputName"
type="text"
name="studyInputName"
focus
label={
<div className="d-flex align-items-center">
<span className="mr-2">
Expand Down
54 changes: 54 additions & 0 deletions front/src/utils/hooks/useModalFocusTrap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { useEffect } from 'react';

/**
* Allow the user to escape the modal by pressing escape and to trap the focus inside it
* */

export default function useModalFocusTrap(
modalRef: React.RefObject<HTMLDivElement>,
closeModal: () => void
) {
useEffect(() => {
const modalElement = modalRef.current;

const focusableElements = modalElement?.querySelectorAll(
// last declaration stands for all elements not natively focusable like li
'input, button, [tabindex]:not([tabindex="-1"])'
) as NodeListOf<Element>;

const firstElement = focusableElements[0] as HTMLElement;
const lastElement = focusableElements[focusableElements?.length - 1] as HTMLElement;

/**
*
* Prevent the tab event and set focus on :
* - last element if we are pressing on "shift" in addition to "tab" and are on the first element
* - first element if we are only pressing "tab" and are on the last element
*/
const handleTabKeyPress = (event: KeyboardEvent) => {
if (event.key === 'Tab') {
if (event.shiftKey && document.activeElement === firstElement) {
event.preventDefault();
lastElement.focus();
} else if (!event.shiftKey && document.activeElement === lastElement) {
event.preventDefault();
firstElement.focus();
}
}
};

const handleEscapeKeyPress: (event: KeyboardEvent) => void = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
closeModal();
}
};

modalElement?.addEventListener('keydown', handleTabKeyPress);
modalElement?.addEventListener('keydown', handleEscapeKeyPress);

return () => {
modalElement?.removeEventListener('keydown', handleTabKeyPress);
modalElement?.removeEventListener('keydown', handleEscapeKeyPress);
};
}, [modalRef, closeModal]);
}

0 comments on commit d204e34

Please sign in to comment.