Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Overlay option added in exercise component and new component IncludeRemoveQuestion created #86

Merged
merged 5 commits into from
Jan 27, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
3 changes: 0 additions & 3 deletions src/components/Card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,6 @@ const StepCardHeader = styled.div`
button.ox-icon-angle-left, button.ox-icon-angle-right {
display: none;
}
.separator {
Copy link
Author

Choose a reason for hiding this comment

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

This style was causing an error for lg display

Copy link
Member

@jivey jivey Jan 21, 2025

Choose a reason for hiding this comment

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

Yeah this should be fixed in #85

display: inherit;
}
`}

/*
Expand Down
81 changes: 80 additions & 1 deletion src/components/Exercise.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Exercise, ExerciseWithStepDataProps, ExerciseWithQuestionStatesProps } from './Exercise';
import { Exercise, ExerciseWithStepDataProps, ExerciseWithQuestionStatesProps, OverlayProps } from './Exercise';
import renderer from 'react-test-renderer';
import React from 'react';

Expand Down Expand Up @@ -299,4 +299,83 @@ describe('Exercise', () => {
expect(tree.toJSON()).toMatchSnapshot();
});
});

describe('with overlay rendering', () => {

let props: ExerciseWithStepDataProps & OverlayProps;

beforeEach(() => {
props = {
enableOverlay: true,
overlayChildren: <span>Overlay</span>,
exercise: {
uid: '1@1',
uuid: 'e4e27897-4abc-40d3-8565-5def31795edc',
group_uuid: '20e82bf6-232e-40c8-ba68-2d22c6498f69',
number: 1,
version: 1,
published_at: '2022-09-06T20:32:21.981Z',
context: 'Context',
stimulus_html: '<b>Stimulus HTML</b>',
tags: [],
authors: [{ user_id: 1, name: 'OpenStax' }],
copyright_holders: [{ user_id: 1, name: 'OpenStax' }],
derived_from: [],
is_vocab: false,
solutions_are_public: false,
versions: [1],
questions: [{
id: '1234@5',
collaborator_solutions: [],
formats: ['true-false'],
stimulus_html: '',
stem_html: '',
is_answer_order_important: false,
answers: [{
id: '1',
correctness: undefined,
content_html: 'True',
}, {
id: '2',
correctness: undefined,
content_html: 'False',
}],
}],
},
questionNumber: 1,
hasMultipleAttempts: false,
onAnswerChange: () => null,
onAnswerSave: () => null,
onNextStep: () => null,
canAnswer: false,
needsSaved: false,
apiIsPending: false,
canUpdateCurrentStep: false,
step: {
uid: '1234@4',
id: 1,
available_points: '1.0',
is_completed: false,
answer_id_order: ['1', '2'],
answer_id: '1',
free_response: '',
feedback_html: '',
correct_answer_id: '',
correct_answer_feedback_html: '',
is_feedback_available: true,
attempts_remaining: 0,
attempt_number: 1,
incorrectAnswerId: 0
},
numberOfQuestions: 1
}
});

it('matches snapshot', () => {
const tree = renderer.create(
<Exercise {...props} show_all_feedback />
).toJSON();
expect(tree).toMatchSnapshot();
});
});
});
151 changes: 150 additions & 1 deletion src/components/Exercise.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
ExerciseWithQuestionStatesProps,
} from './Exercise';
import { Answer } from '../types';
import { IncludeRemoveQuestion } from './IncludeRemoveQuestion';
import styled from 'styled-components';

const exerciseWithStepDataProps: ExerciseWithStepDataProps = {
Expand Down Expand Up @@ -144,6 +145,13 @@ const exerciseWithQuestionStatesProps = (): ExerciseWithQuestionStatesProps => {
}
};

const exerciseWithOverlayProps = (overlayChildren: React.ReactNode) => {
return {
enableOverlay: true,
overlayChildren,
};
}

type TextResizerValue = -2 | -1 | 0 | 1 | 2 | 3;
const textResizerScales = [0.75, 0.9, 1, 1.25, 1.5, 2];
const textResizerValues: TextResizerValue[] = [-2, -1, 0, 1, 2, 3];
Expand Down Expand Up @@ -522,7 +530,6 @@ export const MathJax = () => {
const [correctAnswerId, setCorrectAnswerId] = useState<number | undefined>(
undefined,
);

const props1: ExerciseWithQuestionStatesProps = {
...exerciseWithQuestionStatesProps(),
questionStates: {
Expand Down Expand Up @@ -807,3 +814,145 @@ export const PreviewCard = () => {
</TextResizerProvider>
);
};

export const OverlayCard = () => {
const randomlyCorrectAnswer = Math.floor(Math.random() * 3) + 1;
const props1: ExerciseWithQuestionStatesProps = {
...exerciseWithQuestionStatesProps(),
...exerciseWithOverlayProps(<button>Overlay</button>),
questionStates: {
'1': {
available_points: '1.0',
is_completed: true,
answer_id_order: ['1', '2', '3', '4'],
answer_id: randomlyCorrectAnswer,
free_response: '',
feedback_html: '',
correct_answer_id: randomlyCorrectAnswer.toString(),
correct_answer_feedback_html:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.',
attempts_remaining: 0,
attempt_number: 0,
incorrectAnswerId: 0,
canAnswer: false,
needsSaved: false,
apiIsPending: false,
solution: {
content_html:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.',
solution_type: 'detailed',
},
},
},
exercise: {
tags: [
'book:stax-anp',
'context-cnxmod:bbaedbf4-4d78-4b7c-bc94-2a742f0f2f8c',
'lo:stax-anp:22-3-6',
'exid:stax-anp:1786',
'dok:1',
'time:short',
'book-slug:anatomy-and-physiology',
'book-slug:anatomy-and-physiology-2e',
'module-slug:anatomy-and-physiology:22-3-the-process-of-breathing',
'module-slug:anatomy-and-physiology-2e:22-3-the-process-of-breathing',
'assignment-type:reading',
'blooms:1',
'ost-type:concept-coach',
'assessment:preparedness:https://openstax.org/orn/book:page/4fd99458-6fdf-49bc-8688-a6dc17a1268d:11673dd9-55e6-46d9-8b78-b06df85246bd',
],
uuid: '8ae2b252-8943-4a1a-a123-2e6d9eeef4p5',
group_uuid: '19bd7035-d50b-42d8-8f8c-a4d72588a7aa',
number: 3030,
version: 4,
uid: '3030@4',
published_at: '2022-15-05T21:24:12.207Z',
solutions_are_public: false,
authors: [
{
user_id: 1,
name: 'OpenStax Exercises',
},
],
copyright_holders: [
{
user_id: 2,
name: 'Rice University',
},
],
derived_from: [],
is_vocab: false,
stimulus_html: '',
questions: [
{
id: 320733,
is_answer_order_important: true,
stimulus_html: '',
stem_html:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.',
answers: [
{
id: 832312,
content_html: 'Option 1',
},
{
id: 832313,
content_html: 'Option 2',
},
{
id: 832314,
content_html: 'Option 3',
},
{
id: 832315,
content_html: 'Option 4',
},
],
hints: [],
formats: ['free-response', 'multiple-choice'],
combo_choices: [],
},
],

versions: [4, 3, 2, 1],
},
};

const [buttonVariant, setButtonVariant] = React.useState<'include' | 'remove'>('include');

const props2: ExerciseWithQuestionStatesProps = {
...exerciseWithQuestionStatesProps(),
...exerciseWithOverlayProps(
<IncludeRemoveQuestion
buttonVariant={buttonVariant}
onIncludeHandler={() => setButtonVariant('remove')}
onRemoveHandler={() => setButtonVariant('include')}
/>
),
questionStates: {
'1': {
available_points: '1.0',
is_completed: true,
answer_id_order: ['1', '2'],
answer_id: undefined,
free_response: 'Free response',
feedback_html: 'Feedback',
correct_answer_id: '1',
correct_answer_feedback_html: 'Feedback for the correct answer',
attempts_remaining: 0,
attempt_number: 1,
incorrectAnswerId: 0,
canAnswer: false,
needsSaved: false,
apiIsPending: false,
},
},
};

return (
<TextResizerProvider>
<Exercise {...props1} className='preview-card' />
<Exercise {...props2} />
</TextResizerProvider>
);
};
39 changes: 30 additions & 9 deletions src/components/Exercise/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { ExerciseToolbar, StyledToolbar } from '../ExerciseToolbar';
import { breakpoints } from '../../theme';
import { ExerciseHeaderIcons } from '../ExerciseHeaderIcons';
import { TypesetMathContext } from '../../hooks/useTypesetMath';
import { exerciseStyles } from './styles';
import { exerciseStyles, StyledOverlay } from './styles';

const StyledTaskStepCard = styled(TaskStepCard)`
font-size: calc(1.8rem * var(--content-text-scale));
Expand Down Expand Up @@ -61,10 +61,10 @@ const TaskStepCardWithToolbar = (props: React.PropsWithChildren<TaskStepCardProp
mobileToolbarEnabled: boolean;
}
) => <ToolbarWrapper
desktopToolbarEnabled={props.desktopToolbarEnabled}
mobileToolbarEnabled={props.mobileToolbarEnabled}
>
<ExerciseToolbar icons={props.exerciseIcons} />
desktopToolbarEnabled={props.desktopToolbarEnabled}
mobileToolbarEnabled={props.mobileToolbarEnabled}
>
<ExerciseToolbar icons={props.exerciseIcons} />
<StyledTaskStepCard {...props} />
</ToolbarWrapper>;

Expand Down Expand Up @@ -159,13 +159,20 @@ export interface ExerciseWithQuestionStatesProps extends ExerciseBaseProps {
onAnswerChange: (answer: Omit<Answer, 'id'> & { id: number, question_id: number }) => void;
}

export interface OverlayProps {
enableOverlay?: boolean;
overlayChildren?: React.ReactNode;
}

export const Exercise = styled(({
numberOfQuestions, questionNumber, step, exercise, show_all_feedback, scrollToQuestion, exerciseIcons, ...props
}: { className?: string } & (ExerciseWithStepDataProps | ExerciseWithQuestionStatesProps)) => {
numberOfQuestions, questionNumber, step, exercise, show_all_feedback, scrollToQuestion, exerciseIcons, enableOverlay = false, overlayChildren, ...props
}: { className?: string } & (ExerciseWithStepDataProps | ExerciseWithQuestionStatesProps) & OverlayProps) => {
const legacyStepRender = 'feedback_html' in step;
const questionsRef = React.useRef<Array<HTMLDivElement>>([]);
const container = React.useRef<HTMLDivElement>(null);

const [showOverlay, setShowOverlay] = React.useState<boolean>(false);

const typesetExercise = React.useCallback(() => {
if (container.current) {
typesetMath(container.current);
Expand All @@ -179,6 +186,12 @@ export const Exercise = styled(({
}
}, [scrollToQuestion, exercise]);

const handleBlur = (event: React.FocusEvent<HTMLDivElement>) => {
if (container.current && !container.current.contains(event.relatedTarget as Node)) {
setShowOverlay(false);
}
};

const desktopToolbarEnabled = Object.values(exerciseIcons || {}).some(({ location }) => location?.toolbar?.desktop);
const mobileToolbarEnabled = Object.values(exerciseIcons || {}).some(({ location }) => location?.toolbar?.mobile);

Expand All @@ -195,15 +208,23 @@ export const Exercise = styled(({
{...(exerciseIcons ? { exerciseIcons: exerciseIcons } : null)}
className={props.className}
>
<div ref={container}>
<div
ref={container}
tabIndex={enableOverlay ? 0 : -1} // This container is focusable only if enableOverlay is true
Copy link
Member

Choose a reason for hiding this comment

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

Does this cause any side effects for non-overlay exercises in screenreaders? I'm wondering if we should be more conservative and not set the attribute at all for normal use.

{...(enableOverlay ? { onMouseOver: () => setShowOverlay(true), onMouseLeave: () => setShowOverlay(false), onFocus: () => setShowOverlay(true), onBlur: handleBlur} : {})}
>
{(enableOverlay && showOverlay) &&
<StyledOverlay>
{overlayChildren}
</StyledOverlay>}
<Preamble exercise={exercise} />

{exercise.questions.map((q, i) => {
const state = { ...(legacyStepRender ? step : props['questionStates'][q.id]) };
return (
<ExerciseQuestion
{...props}
{...{...state, available_points: undefined}}
{...{ ...state, available_points: undefined }}
ref={(el: HTMLDivElement) => questionsRef.current[questionNumber + i] = el}
exercise_uid={exercise.uid}
key={q.id}
Expand Down
18 changes: 17 additions & 1 deletion src/components/Exercise/styles.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,20 @@
import { css } from "styled-components";
import styled, { css } from "styled-components";

export const StyledOverlay = styled.div`
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 100%;
height: 100%;
background-color: #FFFFFF;
opacity: 0.8;
z-index: 2;
`;

export const exerciseStyles = css`
&.preview-card {
Expand Down
Loading
Loading