From 6dae9c439302ff10a688b81fc9acec73b5409d83 Mon Sep 17 00:00:00 2001 From: Pierre Ecarlat <pecarlat@grandlyon.com> Date: Thu, 12 Sep 2024 12:51:39 +0000 Subject: [PATCH] feat(a11y): Optimize keyboard navigation between pages --- .../Action/ActionBegin/ActionBegin.spec.tsx | 3 + .../Action/ActionBegin/ActionBegin.tsx | 3 + .../Action/ActionCard/ActionCard.spec.tsx | 2 + .../Action/ActionCard/ActionCard.tsx | 5 +- .../Action/ActionChoose/ActionChoose.scss | 5 + .../Action/ActionChoose/ActionChoose.spec.tsx | 10 +- .../Action/ActionChoose/ActionChoose.tsx | 15 +- .../__snapshots__/ActionChoose.spec.tsx.snap | 204 +++++++++--------- .../Action/ActionList/ActionList.spec.tsx | 6 +- .../Action/ActionList/ActionList.tsx | 8 +- .../Action/ActionModal/ActionModal.spec.tsx | 2 + .../Action/ActionModal/ActionModal.tsx | 5 +- src/components/Action/ActionView.tsx | 27 ++- .../__snapshots__/ActionView.spec.tsx.snap | 44 ++-- .../GRDFConnect/GrdfConnectView.tsx | 11 +- .../Connection/SGEConnect/SgeConnectView.tsx | 11 +- .../SgeConnectView.spec.tsx.snap | 1 + src/components/Duel/DuelView.tsx | 2 +- .../Exploration/ExplorationView.tsx | 2 +- .../Quiz/QuizQuestion/QuizQuestion.tsx | 12 +- .../QuizQuestion/QuizQuestionContent.spec.tsx | 2 + .../Quiz/QuizQuestion/QuizQuestionContent.tsx | 6 +- .../Quiz/QuizQuestion/quizQuestion.scss | 152 ++++++------- src/components/Quiz/QuizView.tsx | 2 +- src/locales/fr.json | 6 +- 25 files changed, 332 insertions(+), 214 deletions(-) create mode 100644 src/components/Action/ActionChoose/ActionChoose.scss diff --git a/src/components/Action/ActionBegin/ActionBegin.spec.tsx b/src/components/Action/ActionBegin/ActionBegin.spec.tsx index 99bab9320..1b9a39cd6 100644 --- a/src/components/Action/ActionBegin/ActionBegin.spec.tsx +++ b/src/components/Action/ActionBegin/ActionBegin.spec.tsx @@ -36,6 +36,7 @@ describe('ActionBegin component', () => { action={defaultEcogestureData[1]} setShowList={jest.fn()} userChallenge={userChallengeData[1]} + setFocus={jest.fn()} /> </Provider> ) @@ -58,6 +59,7 @@ describe('ActionBegin component', () => { action={mockedEcogesturesData[1]} setShowList={jest.fn()} userChallenge={userChallengeData[1]} + setFocus={jest.fn()} /> </Provider> ) @@ -76,6 +78,7 @@ describe('ActionBegin component', () => { action={defaultEcogestureData[1]} setShowList={jest.fn()} userChallenge={userChallengeData[1]} + setFocus={jest.fn()} /> </Provider> ) diff --git a/src/components/Action/ActionBegin/ActionBegin.tsx b/src/components/Action/ActionBegin/ActionBegin.tsx index 880b904f8..c2b0d6a3b 100644 --- a/src/components/Action/ActionBegin/ActionBegin.tsx +++ b/src/components/Action/ActionBegin/ActionBegin.tsx @@ -16,12 +16,14 @@ interface ActionBeginProps { action?: Ecogesture setShowList: React.Dispatch<React.SetStateAction<boolean>> userChallenge: UserChallenge + setFocus: () => void } const ActionBegin = ({ action, setShowList, userChallenge, + setFocus, }: ActionBeginProps) => { const { t } = useI18n() const client = useClient() @@ -107,6 +109,7 @@ const ActionBegin = ({ action={currentAction} handleCloseClick={() => setOpenLaunchModal(false)} userChallenge={userChallenge} + setFocus={setFocus} /> </div> )} diff --git a/src/components/Action/ActionCard/ActionCard.spec.tsx b/src/components/Action/ActionCard/ActionCard.spec.tsx index 9d451bd17..ff72792f0 100644 --- a/src/components/Action/ActionCard/ActionCard.spec.tsx +++ b/src/components/Action/ActionCard/ActionCard.spec.tsx @@ -24,6 +24,7 @@ describe('ActionCard component', () => { setShowList={jest.fn()} setSelectedAction={jest.fn()} action={defaultEcogestureData[1]} + setFocus={jest.fn()} /> </Provider> ) @@ -37,6 +38,7 @@ describe('ActionCard component', () => { setShowList={jest.fn()} setSelectedAction={jest.fn()} action={defaultEcogestureData[1]} + setFocus={jest.fn()} /> </Provider> ) diff --git a/src/components/Action/ActionCard/ActionCard.tsx b/src/components/Action/ActionCard/ActionCard.tsx index 75c04ea08..a4aea07a2 100644 --- a/src/components/Action/ActionCard/ActionCard.tsx +++ b/src/components/Action/ActionCard/ActionCard.tsx @@ -11,12 +11,14 @@ interface ActionCardProps { action: Ecogesture setSelectedAction: React.Dispatch<React.SetStateAction<Ecogesture | null>> setShowList: React.Dispatch<React.SetStateAction<boolean>> + setFocus: () => void } const ActionCard = ({ action, setSelectedAction, setShowList, + setFocus, }: ActionCardProps) => { const [actionIcon, setActionIcon] = useState<string>('') const [openEcogestureModal, setOpenEcogestureModal] = useState<boolean>(false) @@ -25,7 +27,8 @@ const ActionCard = ({ setSelectedAction(action) setShowList(false) setOpenEcogestureModal(false) - }, [setSelectedAction, setShowList, action]) + setFocus() + }, [setSelectedAction, action, setShowList, setFocus]) useEffect(() => { async function handleEcogestureIcon() { diff --git a/src/components/Action/ActionChoose/ActionChoose.scss b/src/components/Action/ActionChoose/ActionChoose.scss new file mode 100644 index 000000000..a7ab9a15a --- /dev/null +++ b/src/components/Action/ActionChoose/ActionChoose.scss @@ -0,0 +1,5 @@ +.action-content-view { + width: 100%; + margin: auto; + outline: none; +} diff --git a/src/components/Action/ActionChoose/ActionChoose.spec.tsx b/src/components/Action/ActionChoose/ActionChoose.spec.tsx index a2c2d2e70..4f556e86b 100644 --- a/src/components/Action/ActionChoose/ActionChoose.spec.tsx +++ b/src/components/Action/ActionChoose/ActionChoose.spec.tsx @@ -42,7 +42,10 @@ describe('ActionChoose component', () => { ]) const { container } = render( <Provider store={store}> - <ActionChoose userChallenge={userChallengeData[1]} /> + <ActionChoose + userChallenge={userChallengeData[1]} + setFocus={jest.fn()} + /> </Provider> ) await waitFor(() => null, { container }) @@ -64,7 +67,10 @@ describe('ActionChoose component', () => { const { container } = render( <Provider store={store}> - <ActionChoose userChallenge={userChallengeData[1]} /> + <ActionChoose + userChallenge={userChallengeData[1]} + setFocus={jest.fn()} + /> </Provider> ) await waitFor(() => null, { container }) diff --git a/src/components/Action/ActionChoose/ActionChoose.tsx b/src/components/Action/ActionChoose/ActionChoose.tsx index b1e7d3ad7..d6fb6cb23 100644 --- a/src/components/Action/ActionChoose/ActionChoose.tsx +++ b/src/components/Action/ActionChoose/ActionChoose.tsx @@ -2,26 +2,35 @@ import { Ecogesture, UserChallenge } from 'models' import React, { useState } from 'react' import ActionBegin from '../ActionBegin/ActionBegin' import ActionList from '../ActionList/ActionList' +import './ActionChoose.scss' -const ActionChoose = ({ userChallenge }: { userChallenge: UserChallenge }) => { +const ActionChoose = ({ + userChallenge, + setFocus, +}: { + userChallenge: UserChallenge + setFocus: () => void +}) => { const [selectedAction, setSelectedAction] = useState<Ecogesture | null>(null) const [showList, setShowList] = useState<boolean>(false) return ( - <> + <div className="action-content-view"> {!showList ? ( <ActionBegin action={selectedAction ?? undefined} setShowList={setShowList} userChallenge={userChallenge} + setFocus={setFocus} /> ) : ( <ActionList setSelectedAction={setSelectedAction} setShowList={setShowList} + setFocus={setFocus} /> )} - </> + </div> ) } diff --git a/src/components/Action/ActionChoose/__snapshots__/ActionChoose.spec.tsx.snap b/src/components/Action/ActionChoose/__snapshots__/ActionChoose.spec.tsx.snap index 972120fc6..8d568d329 100644 --- a/src/components/Action/ActionChoose/__snapshots__/ActionChoose.spec.tsx.snap +++ b/src/components/Action/ActionChoose/__snapshots__/ActionChoose.spec.tsx.snap @@ -3,120 +3,124 @@ exports[`ActionChoose component should render correctly 1`] = ` <div> <div - class="action-begin" + class="action-content-view" > <div - class="action-container" + class="action-begin" > <div - class="action-begin-container" + class="action-container" > <div - class="icon-container" + class="action-begin-container" > - <svg - aria-hidden="true" - class="action-icon styles__icon___23x3R" - height="100" - width="100" + <div + class="icon-container" > - <use - xlink:href="#test-file-stub" - /> - </svg> - </div> - <div - class="stars" - > - <svg - aria-hidden="true" - class="star styles__icon___23x3R" - height="25" - width="25" - > - <use - xlink:href="#test-file-stub" - /> - </svg> - <svg - aria-hidden="true" - class="star styles__icon___23x3R" - height="25" - width="25" - > - <use - xlink:href="#test-file-stub" - /> - </svg> - <svg - aria-hidden="true" - class="star styles__icon___23x3R" - height="25" - width="25" + <svg + aria-hidden="true" + class="action-icon styles__icon___23x3R" + height="100" + width="100" + > + <use + xlink:href="#test-file-stub" + /> + </svg> + </div> + <div + class="stars" > - <use - xlink:href="#test-file-stub" - /> - </svg> - <svg - aria-hidden="true" - class="star styles__icon___23x3R" - height="25" - width="25" + <svg + aria-hidden="true" + class="star styles__icon___23x3R" + height="25" + width="25" + > + <use + xlink:href="#test-file-stub" + /> + </svg> + <svg + aria-hidden="true" + class="star styles__icon___23x3R" + height="25" + width="25" + > + <use + xlink:href="#test-file-stub" + /> + </svg> + <svg + aria-hidden="true" + class="star styles__icon___23x3R" + height="25" + width="25" + > + <use + xlink:href="#test-file-stub" + /> + </svg> + <svg + aria-hidden="true" + class="star styles__icon___23x3R" + height="25" + width="25" + > + <use + xlink:href="#test-file-stub" + /> + </svg> + <svg + aria-hidden="true" + class="star styles__icon___23x3R" + height="25" + width="25" + > + <use + xlink:href="#test-file-stub" + /> + </svg> + </div> + <h1 + class="text-20-bold" > - <use - xlink:href="#test-file-stub" - /> - </svg> - <svg - aria-hidden="true" - class="star styles__icon___23x3R" - height="25" - width="25" + Bonhomme de neige + </h1> + <div + class="action-duration text-18" > - <use - xlink:href="#test-file-stub" - /> - </svg> - </div> - <h1 - class="text-20-bold" - > - Bonhomme de neige - </h1> - <div - class="action-duration text-18" - > - action.duration - </div> - <div - class="action-text text-18-bold" - /> - <div - class="action-buttons" - > - <button - class="MuiButtonBase-root MuiButton-root MuiButton-text btnSecondary" - tabindex="0" - type="button" + action.duration + </div> + <div + class="action-text text-18-bold" + /> + <div + class="action-buttons" > - <span - class="MuiButton-label" + <button + class="MuiButtonBase-root MuiButton-root MuiButton-text btnSecondary" + tabindex="0" + type="button" > - action.apply - </span> - </button> - <button - class="MuiButtonBase-root MuiButton-root MuiButton-text btnSecondary" - tabindex="0" - type="button" - > - <span - class="MuiButton-label" + <span + class="MuiButton-label" + > + action.apply + </span> + </button> + <button + class="MuiButtonBase-root MuiButton-root MuiButton-text btnSecondary" + tabindex="0" + type="button" > - action.other - </span> - </button> + <span + class="MuiButton-label" + > + action.other + </span> + </button> + </div> </div> </div> </div> diff --git a/src/components/Action/ActionList/ActionList.spec.tsx b/src/components/Action/ActionList/ActionList.spec.tsx index 54329b779..3fd43aeb1 100644 --- a/src/components/Action/ActionList/ActionList.spec.tsx +++ b/src/components/Action/ActionList/ActionList.spec.tsx @@ -28,7 +28,11 @@ describe('ActionList component', () => { }) const { container } = render( <Provider store={store}> - <ActionList setSelectedAction={jest.fn()} setShowList={jest.fn()} /> + <ActionList + setSelectedAction={jest.fn()} + setShowList={jest.fn()} + setFocus={jest.fn()} + /> </Provider> ) await waitFor(() => null, { container }) diff --git a/src/components/Action/ActionList/ActionList.tsx b/src/components/Action/ActionList/ActionList.tsx index c2bcdf72c..13317e4ca 100644 --- a/src/components/Action/ActionList/ActionList.tsx +++ b/src/components/Action/ActionList/ActionList.tsx @@ -9,9 +9,14 @@ import './actionList.scss' interface ActionListProps { setSelectedAction: React.Dispatch<React.SetStateAction<Ecogesture | null>> setShowList: React.Dispatch<React.SetStateAction<boolean>> + setFocus: () => void } -const ActionList = ({ setSelectedAction, setShowList }: ActionListProps) => { +const ActionList = ({ + setSelectedAction, + setShowList, + setFocus, +}: ActionListProps) => { const client = useClient() const { global: { fluidTypes }, @@ -47,6 +52,7 @@ const ActionList = ({ setSelectedAction, setShowList }: ActionListProps) => { action={action} setSelectedAction={setSelectedAction} setShowList={setShowList} + setFocus={setFocus} /> ))} </div> diff --git a/src/components/Action/ActionModal/ActionModal.spec.tsx b/src/components/Action/ActionModal/ActionModal.spec.tsx index 138d005c3..2e8f36c8d 100644 --- a/src/components/Action/ActionModal/ActionModal.spec.tsx +++ b/src/components/Action/ActionModal/ActionModal.spec.tsx @@ -25,6 +25,7 @@ describe('ActionModal component', () => { handleCloseClick={jest.fn()} action={defaultEcogestureData[1]} userChallenge={userChallengeData[1]} + setFocus={jest.fn()} /> </Provider> ) @@ -44,6 +45,7 @@ describe('ActionModal component', () => { handleCloseClick={jest.fn()} action={defaultEcogestureData[1]} userChallenge={userChallengeData[1]} + setFocus={jest.fn()} /> </Provider> ) diff --git a/src/components/Action/ActionModal/ActionModal.tsx b/src/components/Action/ActionModal/ActionModal.tsx index a83e38729..4f9a84260 100644 --- a/src/components/Action/ActionModal/ActionModal.tsx +++ b/src/components/Action/ActionModal/ActionModal.tsx @@ -17,6 +17,7 @@ interface ActionModalProps { action: Ecogesture handleCloseClick: () => void userChallenge: UserChallenge + setFocus: () => void } const ActionModal = ({ @@ -24,6 +25,7 @@ const ActionModal = ({ action, handleCloseClick, userChallenge, + setFocus, }: ActionModalProps) => { const client = useClient() const { t } = useI18n() @@ -38,7 +40,8 @@ const ActionModal = ({ action ) dispatch(updateUserChallengeList(updatedChallenge)) - }, [action, client, dispatch, userChallenge]) + setFocus() + }, [action, client, dispatch, setFocus, userChallenge]) return ( <Dialog diff --git a/src/components/Action/ActionView.tsx b/src/components/Action/ActionView.tsx index 091cdba3b..cec6bf739 100644 --- a/src/components/Action/ActionView.tsx +++ b/src/components/Action/ActionView.tsx @@ -3,28 +3,37 @@ import CozyBar from 'components/Header/CozyBar' import Header from 'components/Header/Header' import { UserActionState } from 'enums' import { UserChallenge } from 'models' -import React from 'react' +import React, { useRef } from 'react' import { useAppSelector } from 'store/hooks' import ActionChoose from './ActionChoose/ActionChoose' import ActionDone from './ActionDone/ActionDone' import ActionOnGoing from './ActionOnGoing/ActionOnGoing' /** - * http://ecolyo.cozy.tools:8080/#/challenges/ + * http://ecolyo.cozy.tools:8080/#/challenges/action */ const ActionView = () => { const { currentChallenge } = useAppSelector(state => state.ecolyo.challenge) + const mainContentRef = useRef<HTMLDivElement>(null) + const focusMainContent = () => { + setTimeout(() => mainContentRef.current?.focus(), 0) + } + const renderAction = (challenge: UserChallenge) => { switch (challenge.action.state) { case UserActionState.UNSTARTED: - return <ActionChoose userChallenge={challenge} /> + return ( + <ActionChoose userChallenge={challenge} setFocus={focusMainContent} /> + ) case UserActionState.ONGOING: return <ActionOnGoing userAction={challenge.action} /> case UserActionState.NOTIFICATION: return <ActionDone currentChallenge={challenge} /> default: - return <ActionChoose userChallenge={challenge} /> + return ( + <ActionChoose userChallenge={challenge} setFocus={focusMainContent} /> + ) } } @@ -32,7 +41,15 @@ const ActionView = () => { <> <CozyBar titleKey="common.title_action" displayBackArrow={true} /> <Header desktopTitleKey="common.title_action" displayBackArrow={true} /> - <Content>{currentChallenge && renderAction(currentChallenge)}</Content> + <Content> + <div + ref={mainContentRef} + style={{ outline: 'none', margin: 'auto' }} + tabIndex={-1} + > + {currentChallenge && renderAction(currentChallenge)} + </div> + </Content> </> ) } diff --git a/src/components/Action/__snapshots__/ActionView.spec.tsx.snap b/src/components/Action/__snapshots__/ActionView.spec.tsx.snap index cd7ed71bd..d36bcbeeb 100644 --- a/src/components/Action/__snapshots__/ActionView.spec.tsx.snap +++ b/src/components/Action/__snapshots__/ActionView.spec.tsx.snap @@ -11,9 +11,14 @@ exports[`ActionView component should render match snapshot with "Notification" s displaybackarrow="true" /> <mock-content> - <mock-action-done - currentchallenge="[object Object]" - /> + <div + style="outline: none; margin: auto;" + tabindex="-1" + > + <mock-action-done + currentchallenge="[object Object]" + /> + </div> </mock-content> </div> `; @@ -29,9 +34,14 @@ exports[`ActionView component should render match snapshot with "Unstarted" stat displaybackarrow="true" /> <mock-content> - <mock-action-choose - userchallenge="[object Object]" - /> + <div + style="outline: none; margin: auto;" + tabindex="-1" + > + <mock-action-choose + userchallenge="[object Object]" + /> + </div> </mock-content> </div> `; @@ -47,9 +57,14 @@ exports[`ActionView component should render match snapshot with "onGoing" state displaybackarrow="true" /> <mock-content> - <mock-action-ongoing - useraction="[object Object]" - /> + <div + style="outline: none; margin: auto;" + tabindex="-1" + > + <mock-action-ongoing + useraction="[object Object]" + /> + </div> </mock-content> </div> `; @@ -65,9 +80,14 @@ exports[`ActionView component should render match snapshot with default case 1`] displaybackarrow="true" /> <mock-content> - <mock-action-choose - userchallenge="[object Object]" - /> + <div + style="outline: none; margin: auto;" + tabindex="-1" + > + <mock-action-choose + userchallenge="[object Object]" + /> + </div> </mock-content> </div> `; diff --git a/src/components/Connection/GRDFConnect/GrdfConnectView.tsx b/src/components/Connection/GRDFConnect/GrdfConnectView.tsx index 75770f05a..9661de4e1 100644 --- a/src/components/Connection/GRDFConnect/GrdfConnectView.tsx +++ b/src/components/Connection/GRDFConnect/GrdfConnectView.tsx @@ -7,7 +7,7 @@ import useKonnectorAuth from 'components/Hooks/useKonnectorAuth' import useUserInstanceSettings from 'components/Hooks/useUserInstanceSettings' import { FluidType } from 'enums' import { AccountGRDFData } from 'models' -import React, { useCallback, useEffect, useState } from 'react' +import React, { useCallback, useEffect, useRef, useState } from 'react' import { useNavigate } from 'react-router-dom' import { useAppSelector } from 'store/hooks' import '../connection.scss' @@ -42,6 +42,11 @@ export const GrdfConnectView = () => { pceConfirm: false, }) + const mainContentRef = useRef<HTMLDivElement>(null) + const focusMainContent = () => { + setTimeout(() => mainContentRef.current?.focus(), 0) + } + const [connect, update] = useKonnectorAuth(FluidType.GAS, { grdfAuthData: formData, }) @@ -98,10 +103,12 @@ export const GrdfConnectView = () => { if (currentStep === GrdfStep.Consent) { setLaunchConnection(true) } + focusMainContent() }, [currentStep, isNextValid]) const handlePrev = () => { setCurrentStep(prev => prev - 1) + focusMainContent() } const renderStep = (step: GrdfStep) => { @@ -125,7 +132,7 @@ export const GrdfConnectView = () => { displayBackArrow={true} /> <Content> - <div className="connectView"> + <div ref={mainContentRef} className="connectView" tabIndex={-1}> <div className="stepContainer"> <FormProgress currentStep={currentStep} diff --git a/src/components/Connection/SGEConnect/SgeConnectView.tsx b/src/components/Connection/SGEConnect/SgeConnectView.tsx index 6c8305bf5..8f0a00d20 100644 --- a/src/components/Connection/SGEConnect/SgeConnectView.tsx +++ b/src/components/Connection/SGEConnect/SgeConnectView.tsx @@ -6,7 +6,7 @@ import Header from 'components/Header/Header' import useKonnectorAuth from 'components/Hooks/useKonnectorAuth' import { FluidType, SgeStep } from 'enums' import { SgeStore } from 'models' -import React, { useCallback, useEffect, useState } from 'react' +import React, { useCallback, useEffect, useRef, useState } from 'react' import { useNavigate } from 'react-router-dom' import { setShouldRefreshConsent, @@ -44,6 +44,11 @@ const SgeConnectView = () => { const currentFluidStatus = fluidStatus[FluidType.ELECTRICITY] const account = currentFluidStatus.connection.account + const mainContentRef = useRef<HTMLDivElement>(null) + const focusMainContent = () => { + setTimeout(() => mainContentRef.current?.focus(), 0) + } + const [connect, update] = useKonnectorAuth(FluidType.ELECTRICITY, { sgeAuthData: sgeConnect, }) @@ -112,6 +117,7 @@ const SgeConnectView = () => { setCurrentSgeState(updatedState) dispatch(updateSgeStore(updatedState)) } + focusMainContent() }, [currentStep, isLoading, dispatch, currentSgeState]) const handlePrev = useCallback(() => { @@ -119,6 +125,7 @@ const SgeConnectView = () => { setCurrentStep(prev => prev - 1) } dispatch(updateSgeStore(currentSgeState)) + focusMainContent() }, [currentSgeState, currentStep, dispatch]) const onChange = useCallback( @@ -160,7 +167,7 @@ const SgeConnectView = () => { displayBackArrow={true} /> <Content> - <div className="connectView"> + <div ref={mainContentRef} className="connectView" tabIndex={-1}> <div className="stepContainer"> <FormProgress currentStep={currentStep} diff --git a/src/components/Connection/SGEConnect/__snapshots__/SgeConnectView.spec.tsx.snap b/src/components/Connection/SGEConnect/__snapshots__/SgeConnectView.spec.tsx.snap index 40c5b3007..8da493fcf 100644 --- a/src/components/Connection/SGEConnect/__snapshots__/SgeConnectView.spec.tsx.snap +++ b/src/components/Connection/SGEConnect/__snapshots__/SgeConnectView.spec.tsx.snap @@ -57,6 +57,7 @@ exports[`SgeConnectView component should be rendered correctly 1`] = ` <mock-content> <div class="connectView" + tabindex="-1" > <div class="stepContainer" diff --git a/src/components/Duel/DuelView.tsx b/src/components/Duel/DuelView.tsx index 3da01109c..99b938ed0 100644 --- a/src/components/Duel/DuelView.tsx +++ b/src/components/Duel/DuelView.tsx @@ -12,7 +12,7 @@ import DuelOngoing from './DuelOngoing/DuelOngoing' import DuelUnlocked from './DuelUnlocked/DuelUnlocked' /** - * http://ecolyo.cozy.tools:8080/#/challenges/ + * http://ecolyo.cozy.tools:8080/#/challenges/duel */ const DuelView = () => { const navigate = useNavigate() diff --git a/src/components/Exploration/ExplorationView.tsx b/src/components/Exploration/ExplorationView.tsx index 14f8737b6..73e74e0c8 100644 --- a/src/components/Exploration/ExplorationView.tsx +++ b/src/components/Exploration/ExplorationView.tsx @@ -10,7 +10,7 @@ import ExplorationFinished from './ExplorationFinished' import ExplorationOngoing from './ExplorationOngoing' /** - * http://ecolyo.cozy.tools:8080/#/challenges/ + * http://ecolyo.cozy.tools:8080/#/challenges/exploration */ const ExplorationView = () => { const { currentChallenge } = useAppSelector(state => state.ecolyo.challenge) diff --git a/src/components/Quiz/QuizQuestion/QuizQuestion.tsx b/src/components/Quiz/QuizQuestion/QuizQuestion.tsx index 8702fd98a..c75a4404f 100644 --- a/src/components/Quiz/QuizQuestion/QuizQuestion.tsx +++ b/src/components/Quiz/QuizQuestion/QuizQuestion.tsx @@ -1,7 +1,7 @@ import Loader from 'components/Loader/Loader' import { useClient } from 'cozy-client' import { QuestionEntity, UserChallenge } from 'models' -import React, { useEffect, useState } from 'react' +import React, { useEffect, useRef, useState } from 'react' import { useNavigate } from 'react-router-dom' import QuizService from 'services/quiz.service' import { useAppSelector } from 'store/hooks' @@ -20,6 +20,11 @@ const QuizQuestion = ({ userChallenge }: { userChallenge: UserChallenge }) => { const [customQuestion, setCustomQuestion] = useState<QuestionEntity>() const [isCustomQuest, setIsCustomQuest] = useState(!questionsIsLocked) + const mainContentRef = useRef<HTMLDivElement>(null) + const focusMainContent = () => { + setTimeout(() => mainContentRef.current?.focus(), 0) + } + const goBack = () => { navigate('/challenges') } @@ -43,7 +48,7 @@ const QuizQuestion = ({ userChallenge }: { userChallenge: UserChallenge }) => { }, [client, fluidTypes, isCustomQuest, userChallenge.quiz.customQuestion]) return ( - <> + <div ref={mainContentRef} className="quiz-content" tabIndex={-1}> {isCustomQuest ? ( <> {!customQuestion ? ( @@ -63,9 +68,10 @@ const QuizQuestion = ({ userChallenge }: { userChallenge: UserChallenge }) => { userChallenge={userChallenge} setIsCustomQuest={setIsCustomQuest} goBack={goBack} + focusCallback={focusMainContent} /> )} - </> + </div> ) } diff --git a/src/components/Quiz/QuizQuestion/QuizQuestionContent.spec.tsx b/src/components/Quiz/QuizQuestion/QuizQuestionContent.spec.tsx index 707db20c9..246131418 100644 --- a/src/components/Quiz/QuizQuestion/QuizQuestionContent.spec.tsx +++ b/src/components/Quiz/QuizQuestion/QuizQuestionContent.spec.tsx @@ -33,6 +33,7 @@ describe('QuizQuestionContent component', () => { userChallenge={userChallengeData[0]} setIsCustomQuest={() => false} goBack={mockedNavigate('/challenges')} + focusCallback={jest.fn()} /> </Provider> ) @@ -48,6 +49,7 @@ describe('QuizQuestionContent component', () => { userChallenge={userChallengeData[0]} setIsCustomQuest={() => false} goBack={mockedNavigate('/challenges')} + focusCallback={jest.fn()} /> </Provider> ) diff --git a/src/components/Quiz/QuizQuestion/QuizQuestionContent.tsx b/src/components/Quiz/QuizQuestion/QuizQuestionContent.tsx index 5fc440334..ae813372a 100644 --- a/src/components/Quiz/QuizQuestion/QuizQuestionContent.tsx +++ b/src/components/Quiz/QuizQuestion/QuizQuestionContent.tsx @@ -17,12 +17,14 @@ interface QuizQuestionContent { userChallenge: UserChallenge setIsCustomQuest: Dispatch<SetStateAction<boolean>> goBack: () => void + focusCallback: () => void } const QuizQuestionContent = ({ userChallenge, setIsCustomQuest, goBack, + focusCallback, }: QuizQuestionContent) => { const { t } = useI18n() const client = useClient() @@ -73,13 +75,15 @@ const QuizQuestionContent = ({ if (questionIndex !== userChallenge.quiz.questions.length - 1) { setQuestionIndex(questionIndex + 1) } + focusCallback() }, [ questionIndex, + userChallenge.quiz.questions.length, + focusCallback, setIsCustomQuest, setQuestionIndex, setUserChoice, setOpenModal, - userChallenge.quiz.questions.length, ]) return ( diff --git a/src/components/Quiz/QuizQuestion/quizQuestion.scss b/src/components/Quiz/QuizQuestion/quizQuestion.scss index cbab0ba2f..035243a5a 100644 --- a/src/components/Quiz/QuizQuestion/quizQuestion.scss +++ b/src/components/Quiz/QuizQuestion/quizQuestion.scss @@ -1,89 +1,93 @@ @import 'src/styles/base/color'; @import 'src/styles/base/breakpoint'; -.quiz-container { - .question-container { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - padding: 1.5rem; - box-shadow: 0px 4px 16px rgba(0, 0, 0, 0.55); - border-radius: 4px; - transition: all 300ms ease; - color: $white; - background: $grey-linear-gradient-background; - position: relative; - @media (min-width: $width-large-phone) { - height: 45vh; - } - - .question-loading { - min-height: 13.875rem; +.quiz-content { + outline: none; + margin: auto; + .quiz-container { + .question-container { display: flex; + flex-direction: column; justify-content: center; align-items: center; + padding: 1.5rem; + box-shadow: 0px 4px 16px rgba(0, 0, 0, 0.55); + border-radius: 4px; + transition: all 300ms ease; + color: $white; + background: $grey-linear-gradient-background; + position: relative; + @media (min-width: $width-large-phone) { + height: 45vh; + } + + .question-loading { + min-height: 13.875rem; + display: flex; + justify-content: center; + align-items: center; + } + .question { + color: $grey-bright; + text-align: center; + margin-bottom: 2rem; + @media (min-width: $width-large-phone) { + padding: 0 1rem; + } + } + .question-title { + color: $blue-light; + margin-bottom: 0.5rem; + } + .btn-back { + color: $white; + background: none; + border: none; + padding: 0; + font-size: 1.3rem; + position: absolute; + top: 1rem; + right: 1rem; + } } - .question { - color: $grey-bright; + .answer { text-align: center; - margin-bottom: 2rem; + width: 100%; @media (min-width: $width-large-phone) { - padding: 0 1rem; + max-width: 80%; + } + input[type='radio'] { + position: fixed; + opacity: 0; + pointer-events: none; + } + input[type='radio']:focus + label { + background: $blue-light; + color: $dark-light-2; + border-color: $blue-light; + } + label { + display: block; + border: 1px solid $grey-bright; + padding: 0.5rem; + margin-bottom: 1rem; + cursor: pointer; + } + input[type='radio']:checked + label, + label:hover { + background: $blue-radial-gradient; + color: $dark-light-2; + border-color: $blue-light; } } - .question-title { - color: $blue-light; - margin-bottom: 0.5rem; - } - .btn-back { - color: $white; - background: none; - border: none; - padding: 0; - font-size: 1.3rem; - position: absolute; - top: 1rem; - right: 1rem; - } - } - .answer { - text-align: center; - width: 100%; - @media (min-width: $width-large-phone) { - max-width: 80%; - } - input[type='radio'] { - position: fixed; - opacity: 0; - pointer-events: none; - } - input[type='radio']:focus + label { - background: $blue-light; - color: $dark-light-2; - border-color: $blue-light; - } - label { - display: block; - border: 1px solid $grey-bright; - padding: 0.5rem; - margin-bottom: 1rem; - cursor: pointer; - } - input[type='radio']:checked + label, - label:hover { - background: $blue-radial-gradient; - color: $dark-light-2; - border-color: $blue-light; + button.validate { + margin-top: 1rem; + width: auto; + padding: 0.5rem 3rem; } - } - button.validate { - margin-top: 1rem; - width: auto; - padding: 0.5rem 3rem; - } - .index-question { - margin: 2rem 0 1rem; + .index-question { + margin: 2rem 0 1rem; + } } } diff --git a/src/components/Quiz/QuizView.tsx b/src/components/Quiz/QuizView.tsx index f6b2a39c6..33abffc04 100644 --- a/src/components/Quiz/QuizView.tsx +++ b/src/components/Quiz/QuizView.tsx @@ -10,7 +10,7 @@ import QuizFinish from './QuizFinish/QuizFinish' import QuizQuestion from './QuizQuestion/QuizQuestion' /** - * http://ecolyo.cozy.tools:8080/#/challenges/ + * http://ecolyo.cozy.tools:8080/#/challenges/quiz */ const QuizView = () => { const { currentChallenge } = useAppSelector(state => state.ecolyo.challenge) diff --git a/src/locales/fr.json b/src/locales/fr.json index 727e662ac..b6ed8bf44 100644 --- a/src/locales/fr.json +++ b/src/locales/fr.json @@ -686,8 +686,8 @@ "later": "Plus tard", "lets_go": "J'y vais", "accessibility": { - "window_title": "Fenêtre de partage de retours", - "button_close": "Fermer la fenêtre de partage de retours" + "window_title": "Fenêtre de service assistance aux utilisateurs", + "button_close": "Fermer la fenêtre de service assistance aux utilisateurs" } }, "dataShare": { @@ -793,7 +793,7 @@ "header": { "accessibility": { "button_back": "Retour à la page précédente", - "button_open_feedbacks": "Ouvrir le partage de retours" + "button_open_feedbacks": "Ouvrir le service assistance aux utilisateurs" } }, "konnector_form": { -- GitLab