Skip to content

Commit cfbb492

Browse files
jiveyjomcarvajal
andauthored
Preview card (#89)
* card styles * Overlay option added in exercise component and new component IncludeRemoveQuestion created (#86) * overlay option added in exercise component and new component includeremovequestion created * resolve comments * update snapshots * move overlay logic into Card component * remove focus on card when overlay is up and add autoFocus for first interactive element on overlay * Add export for IncludRemoveQuestion (#87) add export of IncludRemoveQuestion * Modify preview-card styles (#88) * modify preview card styles * change color of card when is included * resolve comments - fix style issues * change props in storybook of Exercise * Add prop method for details button * Refactor props of ExercisePreview * export ExercisePreview * remove focus first for screenreaders, style tweaks * Remove aria attributes in order to read card body content (#90) * remove aria attributes in order to read card body content * conditional aria attributes for cardbody * previewMode in storybook * missing previewMode prop * fix some type issues * disable eslint rule for ts-ignore just for this case * Core 675 refactor exercise preview (#92) * remove includeRemove logic from preview exercise card * print stories deleted by mistake * test description fixed * added new story for preview exercise, hide all feedback propertly * revert print delete * remove questionState prop, added feedback inside answers for stories purposes * is_completed added into ExercisePreview component * update snapshots * New prop showCorrectAnswer (#93) * new prop showCorrectAnswer * fix exercise stories * ExercisePreview now shows question detailed (#94) * ExercisePreview now shows question detailed * Restore print * hide footer with new condition * Restore print * fix indentation in ExerciseQuestion * set answer_id as empty * set new margin for preview card * update snaps * resolve test duplicity --------- Co-authored-by: jomcarvajal <[email protected]> Co-authored-by: jomcarvajal <[email protected]>
1 parent df20459 commit cfbb492

26 files changed

+3846
-1364
lines changed

.github/workflows/checks.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,5 @@ jobs:
2727
node-version: '16'
2828
- run: yarn
2929
- run: yarn lint
30+
- run: yarn typecheck
3031
- run: yarn test

src/components/AnswersTable.tsx

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,15 @@ export interface AnswersTableProps {
2323
onKeyPress?: () => void;
2424
contentRenderer?: JSX.Element;
2525
instructions?: JSX.Element;
26+
previewMode?: boolean;
2627
}
2728

2829
export const AnswersTable = (props: AnswersTableProps) => {
2930
let idCounter = 0;
3031

3132
const {
3233
question, hideAnswers, type = defaultAnswerType, answered_count, choicesEnabled, correct_answer_id,
33-
incorrectAnswerId, answer_id, feedback_html, correct_answer_feedback_html,
34+
incorrectAnswerId, answer_id, feedback_html, correct_answer_feedback_html, previewMode,
3435
show_all_feedback = false, tableFeedbackEnabled, hasCorrectAnswer, onChangeAnswer, onKeyPress, answerIdOrder, instructions
3536
} = props;
3637
if (hideAnswers) { return null; }
@@ -53,7 +54,7 @@ export const AnswersTable = (props: AnswersTableProps) => {
5354
onChangeAnswer: onChangeAnswer,
5455
type,
5556
answered_count,
56-
disabled: !choicesEnabled,
57+
disabled: previewMode || !choicesEnabled,
5758
show_all_feedback,
5859
tableFeedbackEnabled,
5960
onKeyPress
@@ -64,10 +65,10 @@ export const AnswersTable = (props: AnswersTableProps) => {
6465
const answersHtml = answers.map((answer, i) => {
6566
const additionalProps: { answer: AnswerType, iter: number, key: string }
6667
= {
67-
answer: {
68-
...answer,
69-
question_id: typeof question.id === 'string' ? parseInt(question.id, 10) : question.id
70-
},
68+
answer: {
69+
...answer,
70+
question_id: typeof question.id === 'string' ? parseInt(question.id, 10) : question.id
71+
},
7172
iter: i,
7273
key: `${questionAnswerProps.qid}-option-${i}`,
7374
};
@@ -103,7 +104,17 @@ export const AnswersTable = (props: AnswersTableProps) => {
103104
});
104105

105106
return (
106-
<div role="radiogroup" aria-label="Answer choices" className="answers-table">
107+
<div
108+
{
109+
...(!previewMode
110+
? {
111+
role: "radiogroup",
112+
'aria-label': "Answer choices"
113+
}
114+
: {})
115+
}
116+
className="answers-table"
117+
>
107118
{instructions}
108119
{answersHtml}
109120
</div>

src/components/Card.stories.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ const props: TaskStepCardProps = {
2020
},
2121
questionNumber: 1,
2222
numberOfQuestions: 1,
23+
showTotalQuestions: false,
2324
};
2425

2526
export const Default = () => <TaskStepCard {...props}><ExerciseQuestion /></TaskStepCard>;

src/components/Card.tsx

Lines changed: 97 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ReactNode } from "react";
1+
import { ReactNode, useState, useRef, useEffect, useCallback } from "react";
22
import { breakpoints, colors, layouts, mixins } from "../theme";
33
import { AvailablePoints, StepBase, StepWithData } from "../types";
44
import styled from "styled-components";
@@ -33,7 +33,7 @@ const StepCardHeader = styled.div`
3333
display: flex;
3434
align-items: center;
3535
justify-content: space-between;
36-
padding: 16px 24px;
36+
padding: 1.6rem 2.4rem;
3737
background: ${colors.card.header.background};
3838
font-size: 1.8rem;
3939
line-height: 3rem;
@@ -88,9 +88,6 @@ const StepCardHeader = styled.div`
8888
button.ox-icon-angle-left, button.ox-icon-angle-right {
8989
display: none;
9090
}
91-
.separator {
92-
display: inherit;
93-
}
9491
`}
9592
9693
/*
@@ -195,6 +192,21 @@ const StepCardQuestion = styled.div<{ unpadded?: boolean }>`
195192
}
196193
`;
197194

195+
export const StyledOverlay = styled.div`
196+
display: flex;
197+
flex-direction: column;
198+
justify-content: center;
199+
align-items: center;
200+
position: absolute;
201+
top: 50%;
202+
left: 50%;
203+
transform: translate(-50%, -50%);
204+
width: 100%;
205+
height: 100%;
206+
background-color: #FFFFFF80;
207+
z-index: 2;
208+
`;
209+
198210
interface SharedProps {
199211
questionNumber: number;
200212
numberOfQuestions: number;
@@ -213,6 +225,7 @@ export interface StepCardProps extends SharedProps {
213225
questionId?: string;
214226
multipartBadge?: ReactNode;
215227
isHomework: boolean;
228+
overlayChildren?: React.ReactNode;
216229
}
217230

218231
const StepCard = ({
@@ -230,35 +243,91 @@ const StepCard = ({
230243
leftHeaderChildren,
231244
rightHeaderChildren,
232245
headerTitleChildren,
246+
overlayChildren,
233247
...otherProps }: StepCardProps) => {
234248

249+
const overlayRef = useRef<HTMLDivElement>(null);
250+
const [showOverlay, setShowOverlay] = useState<boolean>(false);
251+
235252
const formattedQuestionNumber = numberOfQuestions > 1
236253
? `Questions ${questionNumber} - ${questionNumber + numberOfQuestions - 1}`
237254
: `Question ${questionNumber}`;
238255

256+
const handleOverlayBlur = (event: React.FocusEvent<HTMLDivElement>) => {
257+
if (overlayRef.current && !overlayRef.current.contains(event.relatedTarget as Node)) {
258+
setShowOverlay(false);
259+
}
260+
};
261+
262+
const handleOverlayFocus = useCallback(() => {
263+
setShowOverlay(true);
264+
}, []);
265+
266+
const hideFocusableElements = useCallback(() => {
267+
const focusableElements = Array.from(document.getElementById("step-card")?.querySelectorAll(
268+
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
269+
) || []);
270+
271+
focusableElements.forEach((el) => {
272+
(el as HTMLElement).setAttribute('tabindex', '-1');
273+
});
274+
}, []);
275+
276+
useEffect(() => {
277+
const currentOverlayRef = overlayRef.current;
278+
if (currentOverlayRef && overlayChildren) {
279+
currentOverlayRef.addEventListener('focus', handleOverlayFocus);
280+
hideFocusableElements();
281+
}
282+
return () => {
283+
currentOverlayRef?.removeEventListener('focus', handleOverlayFocus);
284+
};
285+
}, [overlayChildren, overlayRef, handleOverlayFocus, hideFocusableElements]);
286+
239287
return (
240288
<OuterStepCard {...otherProps}>
241289
{multipartBadge}
242290
<InnerStepCard className={className}>
243-
{questionNumber && isHomework && stepType === 'exercise' &&
244-
<StepCardHeader>
245-
<div>
246-
{leftHeaderChildren}
247-
<h2 className="question-info">
248-
{headerTitleChildren}
249-
<span>{formattedQuestionNumber}</span>
250-
{showTotalQuestions ? <span className="num-questions">&nbsp;/ {numberOfQuestions}</span> : null}
251-
<span className="separator">|</span>
252-
<span className="question-id">ID: {questionId}</span>
253-
</h2>
254-
</div>
255-
{availablePoints || rightHeaderChildren ? <div>
256-
{availablePoints && <div className="points">{availablePoints} Points</div>}
257-
{rightHeaderChildren}
258-
</div> : null}
259-
</StepCardHeader>
260-
}
261-
<StepCardQuestion unpadded={unpadded}>{children}</StepCardQuestion>
291+
<div
292+
ref={overlayRef}
293+
{
294+
...(overlayChildren
295+
? {
296+
onMouseOver: () => setShowOverlay(true),
297+
onMouseLeave: () => setShowOverlay(false),
298+
onBlur: handleOverlayBlur,
299+
tabIndex: 0,
300+
}
301+
: {})
302+
}
303+
>
304+
{(overlayChildren && showOverlay) &&
305+
<StyledOverlay id="overlay-element">
306+
{overlayChildren}
307+
</StyledOverlay>
308+
}
309+
<div id="step-card">
310+
{questionNumber && isHomework && stepType === 'exercise' &&
311+
<StepCardHeader className="step-card-header">
312+
<div>
313+
{leftHeaderChildren}
314+
<h2 className="question-info">
315+
{headerTitleChildren}
316+
<span>{formattedQuestionNumber}</span>
317+
{showTotalQuestions ? <span className="num-questions">&nbsp;/ {numberOfQuestions}</span> : null}
318+
<span className="separator">|</span>
319+
<span className="question-id">ID: {questionId}</span>
320+
</h2>
321+
</div>
322+
{availablePoints || rightHeaderChildren ? <div>
323+
{availablePoints && <div className="points">{availablePoints} Points</div>}
324+
{rightHeaderChildren}
325+
</div> : null}
326+
</StepCardHeader>
327+
}
328+
<StepCardQuestion unpadded={unpadded}>{children}</StepCardQuestion>
329+
</div>
330+
</div>
262331
</InnerStepCard>
263332
</OuterStepCard>
264333
)
@@ -268,9 +337,11 @@ StepCard.displayName = 'OSStepCard';
268337
export interface TaskStepCardProps extends SharedProps {
269338
className?: string;
270339
children?: ReactNode;
340+
tabIndex?: number;
271341
step: StepBase | StepWithData;
272342
questionNumber: number;
273343
numberOfQuestions: number;
344+
overlayChildren?: React.ReactNode;
274345
}
275346

276347
const TaskStepCard = ({
@@ -279,6 +350,7 @@ const TaskStepCard = ({
279350
numberOfQuestions,
280351
children,
281352
className,
353+
overlayChildren,
282354
...otherProps
283355
}: TaskStepCardProps) =>
284356
(<StepCard {...otherProps}
@@ -292,6 +364,7 @@ const TaskStepCard = ({
292364
// availablePoints={step.available_points}
293365
className={cn(`${('type' in step ? step.type : 'exercise')}-step`, className)}
294366
questionId={step.uid}
367+
overlayChildren={overlayChildren}
295368
>
296369
{children}
297370
</StepCard>);

src/components/Exercise.spec.tsx

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Exercise, ExerciseWithStepDataProps, ExerciseWithQuestionStatesProps } from './Exercise';
1+
import { Exercise, ExerciseWithStepDataProps, ExerciseWithQuestionStatesProps, OverlayProps } from './Exercise';
22
import renderer from 'react-test-renderer';
33
import React from 'react';
44

@@ -299,4 +299,89 @@ describe('Exercise', () => {
299299
expect(tree.toJSON()).toMatchSnapshot();
300300
});
301301
});
302+
303+
describe('with overlay rendering', () => {
304+
305+
let props: ExerciseWithStepDataProps & OverlayProps;
306+
307+
beforeEach(() => {
308+
props = {
309+
overlayChildren: <span>Overlay</span>,
310+
exercise: {
311+
uid: '1@1',
312+
uuid: 'e4e27897-4abc-40d3-8565-5def31795edc',
313+
group_uuid: '20e82bf6-232e-40c8-ba68-2d22c6498f69',
314+
number: 1,
315+
version: 1,
316+
published_at: '2022-09-06T20:32:21.981Z',
317+
context: 'Context',
318+
stimulus_html: '<b>Stimulus HTML</b>',
319+
tags: [],
320+
authors: [{ user_id: 1, name: 'OpenStax' }],
321+
copyright_holders: [{ user_id: 1, name: 'OpenStax' }],
322+
derived_from: [],
323+
is_vocab: false,
324+
solutions_are_public: false,
325+
versions: [1],
326+
questions: [{
327+
id: '1234@5',
328+
collaborator_solutions: [],
329+
formats: ['true-false'],
330+
stimulus_html: '',
331+
stem_html: '',
332+
is_answer_order_important: false,
333+
answers: [{
334+
id: '1',
335+
correctness: undefined,
336+
content_html: 'True',
337+
}, {
338+
id: '2',
339+
correctness: undefined,
340+
content_html: 'False',
341+
}],
342+
}],
343+
},
344+
questionNumber: 1,
345+
hasMultipleAttempts: false,
346+
onAnswerChange: () => null,
347+
onAnswerSave: () => null,
348+
onNextStep: () => null,
349+
canAnswer: false,
350+
needsSaved: false,
351+
apiIsPending: false,
352+
canUpdateCurrentStep: false,
353+
step: {
354+
uid: '1234@4',
355+
id: 1,
356+
available_points: '1.0',
357+
is_completed: false,
358+
answer_id_order: ['1', '2'],
359+
answer_id: '1',
360+
free_response: '',
361+
feedback_html: '',
362+
correct_answer_id: '',
363+
correct_answer_feedback_html: '',
364+
is_feedback_available: true,
365+
attempts_remaining: 0,
366+
attempt_number: 1,
367+
incorrectAnswerId: 0
368+
},
369+
numberOfQuestions: 1
370+
}
371+
});
372+
373+
it('matches snapshot', () => {
374+
const tree = renderer.create(
375+
<Exercise {...props} show_all_feedback />
376+
).toJSON();
377+
expect(tree).toMatchSnapshot();
378+
});
379+
380+
it('matches snapshot with previewMode', () => {
381+
const tree = renderer.create(
382+
<Exercise {...props} show_all_feedback previewMode />
383+
).toJSON();
384+
expect(tree).toMatchSnapshot();
385+
});
386+
});
302387
});

0 commit comments

Comments
 (0)