Skip to content
Closed
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions src/components/CioQuiz/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export enum QuestionTypes {
Reset = 'reset',
Hydrate = 'hydrate',
Complete = 'complete',
JumpToQuestion = 'jump_to_question',
}

export interface QuestionAnswer<Value> {
Expand Down Expand Up @@ -54,6 +55,7 @@ export type ActionAnswerQuestion =
| Action<QuestionTypes.Back, CurrentQuestion>
| Action<QuestionTypes.Reset>
| Action<QuestionTypes.Complete>
| Action<QuestionTypes.JumpToQuestion, { questionId: number }>
| Action<QuestionTypes.Hydrate, Partial<QuizLocalReducerState>>;

// API actions
Expand All @@ -63,6 +65,7 @@ export enum QuizAPIActionTypes {
SET_QUIZ_RESULTS,
SET_CURRENT_QUESTION,
RESET_QUIZ,
JUMP_TO_QUESTION,
SET_QUIZ_SHARED_RESULTS,
SET_QUIZ_RESULTS_CONFIG,
SET_QUIZ_RESULTS_CONFIG_ERROR,
Expand All @@ -85,7 +88,10 @@ export type ActionSetCurrentQuestion = Action<
QuizAPIActionTypes.SET_CURRENT_QUESTION,
{ quizCurrentQuestion: NextQuestionResponse; quizSessionId?: string; quizVersionId?: string }
>;

export type ActionJumpToQuestion = Action<
QuizAPIActionTypes.JUMP_TO_QUESTION,
{ questionId: number }
>;
export type ActionResetQuiz = Action<QuizAPIActionTypes.RESET_QUIZ>;
export type ActionSetQuizResultsConfig =
| Action<
Expand All @@ -100,4 +106,5 @@ export type ActionQuizAPI =
| ActionSetCurrentQuestion
| ActionResetQuiz
| ActionSetQuizSharedResults
| ActionSetQuizResultsConfig;
| ActionSetQuizResultsConfig
| ActionJumpToQuestion;
2 changes: 2 additions & 0 deletions src/components/CioQuiz/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
PrimaryColorStyles,
QuizReturnState,
GetShareResultsButtonProps,
GetJumpToQuestionButtonProps,
} from '../../types';

export interface QuizContextValue {
Expand All @@ -36,6 +37,7 @@ export interface QuizContextValue {
getAddToFavoritesButtonProps: GetAddToFavoritesButtonProps;
getQuizResultButtonProps: GetQuizResultButtonProps;
getQuizResultLinkProps: GetQuizResultLinkProps;
getJumpToQuestionButtonProps: GetJumpToQuestionButtonProps;
primaryColorStyles: PrimaryColorStyles;
customClickItemCallback: boolean;
customAddToFavoritesCallback: boolean;
Expand Down
2 changes: 2 additions & 0 deletions src/components/CioQuiz/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export default function CioQuiz(props: IQuizProps) {
getHydrateQuizButtonProps,
getNextQuestionButtonProps,
getSkipQuestionButtonProps,
getJumpToQuestionButtonProps,
getOpenTextInputProps,
getPreviousQuestionButtonProps,
getQuizImageProps,
Expand Down Expand Up @@ -69,6 +70,7 @@ export default function CioQuiz(props: IQuizProps) {
getHydrateQuizButtonProps,
getNextQuestionButtonProps,
getSkipQuestionButtonProps,
getJumpToQuestionButtonProps,
getOpenTextInputProps,
getPreviousQuestionButtonProps,
getQuizImageProps,
Expand Down
8 changes: 8 additions & 0 deletions src/components/CioQuiz/quizApiReducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,14 @@ export default function apiReducer(
selectedOptionsWithAttributes: action.payload?.quizResults.attributes,
};
}
case QuizAPIActionTypes.JUMP_TO_QUESTION: {
return {
...state,
quizResults: undefined,
selectedOptionsWithAttributes: undefined,
matchedOptions: undefined,
};
}
case QuizAPIActionTypes.SET_QUIZ_RESULTS_CONFIG: {
return {
...state,
Expand Down
77 changes: 37 additions & 40 deletions src/components/CioQuiz/quizLocalReducer.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable complexity */
import { AnswerInputState, QuestionOption } from '../../types';
import { ActionAnswerQuestion, QuestionTypes, ActionAnswerInputQuestion } from './actions';

Expand All @@ -18,16 +19,6 @@ export const initialState: QuizLocalReducerState = {
isQuizCompleted: false,
};

function answerInputReducer(state: AnswerInputState, action: ActionAnswerInputQuestion) {
return {
...state,
[String(action.payload!.questionId)]: {
type: action.type,
value: action.payload!.input,
},
};
}

function handleNextQuestion(state: QuizLocalReducerState) {
const { answers, answerInputs } = state;
const newAnswers = [...answers];
Expand Down Expand Up @@ -63,47 +54,30 @@ function handleNextQuestion(state: QuizLocalReducerState) {
};
}

const handleAnswerInput = (state: QuizLocalReducerState, action: ActionAnswerInputQuestion) => ({
...state,
answerInputs: {
...state.answerInputs,
[String(action.payload!.questionId)]: {
type: action.type,
value: action.payload!.input,
},
},
isQuizCompleted: false,
});

export default function quizLocalReducer(
state: QuizLocalReducerState,
action: ActionAnswerQuestion
): QuizLocalReducerState {
switch (action.type) {
case QuestionTypes.OpenText:
return {
...state,
answerInputs: answerInputReducer(state.answerInputs, action),
isQuizCompleted: false,
};
case QuestionTypes.Cover:
return {
...state,
answerInputs: answerInputReducer(state.answerInputs, action),
isQuizCompleted: false,
};
case QuestionTypes.SingleSelect:
return {
...state,
answerInputs: answerInputReducer(state.answerInputs, action),
isQuizCompleted: false,
};
case QuestionTypes.MultipleSelect:
return {
...state,
answerInputs: answerInputReducer(state.answerInputs, action),
isQuizCompleted: false,
};
case QuestionTypes.SingleFilterValue:
return {
...state,
answerInputs: answerInputReducer(state.answerInputs, action),
isQuizCompleted: false,
};
case QuestionTypes.MultipleFilterValues:
return {
...state,
answerInputs: answerInputReducer(state.answerInputs, action),
isQuizCompleted: false,
};
return handleAnswerInput(state, action);
case QuestionTypes.Next: {
return handleNextQuestion(state);
}
Expand Down Expand Up @@ -145,6 +119,29 @@ export default function quizLocalReducer(
};
}

case QuestionTypes.JumpToQuestion: {
const questionId = action.payload?.questionId;
if (questionId === undefined) return state;
const prevAnswerInputs = { ...state.prevAnswerInputs };

// Remove all keys greater than questionId from answerInputs
const filteredAnswerInputs: AnswerInputState = {};
Object.keys(prevAnswerInputs).forEach((key) => {
if (parseInt(key, 10) > questionId) return;
filteredAnswerInputs[key] = prevAnswerInputs[key];
});

// Calculate the number of questions to keep (questions <= questionId)
const questionsToKeep = Object.keys(filteredAnswerInputs).length;

return {
...state,
answerInputs: filteredAnswerInputs,
answers: state.answers.slice(0, questionsToKeep),
isQuizCompleted: false,
};
}

case QuestionTypes.Reset:
return {
...initialState,
Expand Down
9 changes: 9 additions & 0 deletions src/hooks/usePropsGetters/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
GetAddToFavoritesButtonProps,
GetSkipQuestionButtonProps,
GetShareResultsButtonProps,
GetJumpToQuestionButtonProps,
} from '../../types';
import { QuizAPIReducerState } from '../../components/CioQuiz/quizApiReducer';
import { QuizLocalReducerState } from '../../components/CioQuiz/quizLocalReducer';
Expand All @@ -27,6 +28,7 @@ import useNextQuestionButtonProps from './useNextQuestionButtonProps';
import usePreviousQuestionButtonProps from './usePreviousQuestionButtonProps';
import useAddToFavoritesButtonProps from './useAddToFavoritesButtonProps';
import useSkipQuestionButtonProps from './useSkipQuestionButtonProps';
import useJumpToQuestionButtonProps from './useJumpToQuestionButtonProps';

const usePropsGetters = (
quizEvents: QuizEventsReturn,
Expand All @@ -44,6 +46,7 @@ const usePropsGetters = (
addToCart,
addToFavorites,
resultClick,
jumpToQuestion,
} = quizEvents;

const getOpenTextInputProps: GetOpenTextInputProps = useOpenTextInputProps(
Expand Down Expand Up @@ -76,6 +79,11 @@ const usePropsGetters = (
quizApiState
);

const getJumpToQuestionButtonProps: GetJumpToQuestionButtonProps = useJumpToQuestionButtonProps(
jumpToQuestion,
quizApiState
);

const getPreviousQuestionButtonProps: GetPreviousQuestionButtonProps =
usePreviousQuestionButtonProps(quizApiState, previousQuestion);

Expand Down Expand Up @@ -182,6 +190,7 @@ const usePropsGetters = (
getQuizResultButtonProps,
getQuizResultLinkProps,
getSkipQuestionButtonProps,
getJumpToQuestionButtonProps,
};
};

Expand Down
32 changes: 32 additions & 0 deletions src/hooks/usePropsGetters/useJumpToQuestionButtonProps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { useCallback } from 'react';
import { QuizAPIReducerState } from '../../components/CioQuiz/quizApiReducer';
import { GetJumpToQuestionButtonProps, QuizEventsReturn } from '../../types';

export default function useNextQuestionButtonProps(
jumpToQuestion: QuizEventsReturn.JumpToQuestion,
quizApiState: QuizAPIReducerState
): GetJumpToQuestionButtonProps {
const getJumpToQuestionButtonProps: GetJumpToQuestionButtonProps = useCallback(
(id: number) => {
const currentQuestionId = quizApiState.quizCurrentQuestion?.next_question?.id;
let buttonDisabled;
if (!currentQuestionId || (currentQuestionId && id >= currentQuestionId)) {
buttonDisabled = true;
}

return {
className: buttonDisabled ? 'cio-question-cta-button disabled' : 'cio-question-cta-button',
tabIndex: buttonDisabled ? -1 : 0,
'aria-disabled': buttonDisabled ? 'true' : 'false',
'aria-describedby': buttonDisabled ? 'jump-to-button-help' : '',
type: 'button',
Comment on lines 19 to 22
Copy link

Copilot AI Sep 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The return object includes accessibility attributes (tabIndex, aria-disabled, aria-describedby, type) but the JumpToQuestionButtonProps interface only defines className, onClick, and style. Either update the interface to include these properties or remove them from the return object.

Copilot uses AI. Check for mistakes.
onClick: () => {
jumpToQuestion(id);
},
};
},
[quizApiState.quizCurrentQuestion, jumpToQuestion]
);

return getJumpToQuestionButtonProps;
}
9 changes: 9 additions & 0 deletions src/hooks/useQuizEvents/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import useQuizState from '../useQuizState';
import { resetQuizSessionStorageState } from '../../utils';
import useQuizAddToFavorites from './useQuizAddToFavorites';
import useQuizSkipClick from './useQuizSkipClick';
import useJumpToQuestion from './useJumpToQuestion';

type UseQuizEvents = (
quizOptions: IQuizProps,
Expand Down Expand Up @@ -82,6 +83,13 @@ const useQuizEvents: UseQuizEvents = (quizOptions, cioClient, quizState) => {
quizResults: quizApiState.quizResults,
});

const jumpToQuestion = useJumpToQuestion({
dispatchLocalState,
dispatchApiState,
quizApiState,
quizLocalState,
});

// Quiz rehydrate
const hydrateQuizLocalState = useHydrateQuizLocalState(
quizOptions.quizId,
Expand All @@ -98,6 +106,7 @@ const useQuizEvents: UseQuizEvents = (quizOptions, cioClient, quizState) => {
nextQuestion,
skipQuestion,
resetQuiz,
jumpToQuestion,
hydrateQuiz: hydrateQuizLocalState,
resetSessionStorageState,
};
Expand Down
49 changes: 49 additions & 0 deletions src/hooks/useQuizEvents/useJumpToQuestion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { useCallback } from 'react';
import {
ActionAnswerQuestion,
ActionQuizAPI,
QuestionTypes,
QuizAPIActionTypes,
} from '../../components/CioQuiz/actions';
import { QuizEventsReturn } from '../../types';
import { QuizAPIReducerState } from '../../components/CioQuiz/quizApiReducer';
import { QuizLocalReducerState } from '../../components/CioQuiz/quizLocalReducer';

type IUseJumpToQuestionProps = {
dispatchLocalState: React.Dispatch<ActionAnswerQuestion>;
dispatchApiState: React.Dispatch<ActionQuizAPI>;
quizApiState: QuizAPIReducerState;
quizLocalState: QuizLocalReducerState;
};

const useJumpToQuestion = (props: IUseJumpToQuestionProps): QuizEventsReturn.JumpToQuestion => {
const { dispatchLocalState, dispatchApiState, quizApiState, quizLocalState } = props;
const quizResetClickHandler = useCallback(
(questionId: number) => {
const isComplete = quizLocalState.isQuizCompleted;
const currentQuestionId = quizApiState.quizCurrentQuestion?.id;

if (isComplete || questionId >= currentQuestionId) {
return;
}
dispatchLocalState({
type: QuestionTypes.JumpToQuestion,
payload: { questionId },
});
dispatchApiState({
type: QuizAPIActionTypes.JUMP_TO_QUESTION,
payload: { questionId },
});
},
[
quizLocalState.isQuizCompleted,
quizApiState.quizCurrentQuestion?.id,
dispatchLocalState,
dispatchApiState,
]
);

return quizResetClickHandler;
};

export default useJumpToQuestion;
2 changes: 1 addition & 1 deletion src/hooks/useQuizEvents/useQuizResetClick.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ type IUseQuizResetClickProps = {
quizResults?: QuizResultsResponse | QuizSharedResultsData;
};

const useQuizResetClick = (props: IUseQuizResetClickProps): QuizEventsReturn.NextQuestion => {
const useQuizResetClick = (props: IUseQuizResetClickProps): QuizEventsReturn.ResetQuiz => {
const { resetQuizSessionStorageState, dispatchLocalState, dispatchApiState, quizResults } = props;
const { removeSharedResultsQueryParams, isSharedResultsQuery } = useQueryParams();
const quizResetClickHandler = useCallback(() => {
Expand Down
2 changes: 1 addition & 1 deletion src/stories/Quiz/Hooks/Docs/markdown/EventsDocs.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,4 @@
| addToCart | `function(e: React.MouseEvent<HTMLElement>, item, price) => void` | Action event to trigger add to cart click events |
| addToFavorites | `function(e: React.MouseEvent<HTMLElement>, item, price) => void` | Action event trigger add to favorites click events |
| hydrateQuiz | `function() => void` | Action event to hydrate the quiz with saved state in session storage on reload |
| quizAnswerChanged | `function(payload: string \| string[] ) => void` | Action event to trigger add to cart click events |
| quizAnswerChanged | `function(payload: string \| options[] ) => void` | Action event to change an answer to a question |
Loading
Loading