From e3b30e454e0f19d7bd6fe10cdbb97423856ea7d3 Mon Sep 17 00:00:00 2001 From: jomcarvajal Date: Mon, 20 Jan 2025 09:53:48 -0600 Subject: [PATCH 1/5] overlay option added in exercise component and new component includeremovequestion created --- src/components/Card.tsx | 3 - src/components/Exercise.spec.tsx | 81 +++++- src/components/Exercise.stories.tsx | 151 ++++++++++- src/components/Exercise/index.tsx | 39 ++- src/components/Exercise/styles.ts | 18 +- src/components/IncludeRemoveQuestion.spec.tsx | 23 ++ .../IncludeRemoveQuestion.stories.tsx | 13 + .../IncludeRemoveQuestion/index.tsx | 46 ++++ .../IncludeRemoveQuestion/styles.ts | 64 +++++ .../__snapshots__/Card.spec.tsx.snap | 10 +- .../__snapshots__/Exercise.spec.tsx.snap | 245 ++++++++++++++++-- .../IncludeRemoveQuestion.spec.tsx.snap | 119 +++++++++ 12 files changed, 773 insertions(+), 39 deletions(-) create mode 100644 src/components/IncludeRemoveQuestion.spec.tsx create mode 100644 src/components/IncludeRemoveQuestion.stories.tsx create mode 100644 src/components/IncludeRemoveQuestion/index.tsx create mode 100644 src/components/IncludeRemoveQuestion/styles.ts create mode 100644 src/components/__snapshots__/IncludeRemoveQuestion.spec.tsx.snap diff --git a/src/components/Card.tsx b/src/components/Card.tsx index 4c8afd54..5ef3e491 100644 --- a/src/components/Card.tsx +++ b/src/components/Card.tsx @@ -87,9 +87,6 @@ const StepCardHeader = styled.div` button.ox-icon-angle-left, button.ox-icon-angle-right { display: none; } - .separator { - display: inherit; - } `} /* diff --git a/src/components/Exercise.spec.tsx b/src/components/Exercise.spec.tsx index aff0bcdd..df81ee74 100644 --- a/src/components/Exercise.spec.tsx +++ b/src/components/Exercise.spec.tsx @@ -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'; @@ -299,4 +299,83 @@ describe('Exercise', () => { expect(tree.toJSON()).toMatchSnapshot(); }); }); + + describe('with overlay rendering', () => { + + let props: ExerciseWithStepDataProps & OverlayProps; + + beforeEach(() => { + props = { + enableOverlay: true, + overlayChildren: Overlay, + 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: 'Stimulus HTML', + 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( + + ).toJSON(); + expect(tree).toMatchSnapshot(); + }); + }); }); diff --git a/src/components/Exercise.stories.tsx b/src/components/Exercise.stories.tsx index 06281507..9a00cde6 100644 --- a/src/components/Exercise.stories.tsx +++ b/src/components/Exercise.stories.tsx @@ -5,6 +5,7 @@ import { ExerciseWithQuestionStatesProps, } from './Exercise'; import { Answer } from '../types'; +import { IncludeRemoveQuestion } from './IncludeRemoveQuestion'; import styled from 'styled-components'; const exerciseWithStepDataProps: ExerciseWithStepDataProps = { @@ -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]; @@ -522,7 +530,6 @@ export const MathJax = () => { const [correctAnswerId, setCorrectAnswerId] = useState( undefined, ); - const props1: ExerciseWithQuestionStatesProps = { ...exerciseWithQuestionStatesProps(), questionStates: { @@ -807,3 +814,145 @@ export const PreviewCard = () => { ); }; + +export const OverlayCard = () => { + const randomlyCorrectAnswer = Math.floor(Math.random() * 3) + 1; + const props1: ExerciseWithQuestionStatesProps = { + ...exerciseWithQuestionStatesProps(), + ...exerciseWithOverlayProps(), + 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( + 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 ( + + + + + ); +}; diff --git a/src/components/Exercise/index.tsx b/src/components/Exercise/index.tsx index afa3ea8f..99b39d0b 100644 --- a/src/components/Exercise/index.tsx +++ b/src/components/Exercise/index.tsx @@ -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)); @@ -61,10 +61,10 @@ const TaskStepCardWithToolbar = (props: React.PropsWithChildren - + desktopToolbarEnabled={props.desktopToolbarEnabled} + mobileToolbarEnabled={props.mobileToolbarEnabled} +> + ; @@ -159,13 +159,20 @@ export interface ExerciseWithQuestionStatesProps extends ExerciseBaseProps { onAnswerChange: (answer: Omit & { 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>([]); const container = React.useRef(null); + const [showOverlay, setShowOverlay] = React.useState(false); + const typesetExercise = React.useCallback(() => { if (container.current) { typesetMath(container.current); @@ -179,6 +186,12 @@ export const Exercise = styled(({ } }, [scrollToQuestion, exercise]); + const handleBlur = (event: React.FocusEvent) => { + 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); @@ -195,7 +208,15 @@ export const Exercise = styled(({ {...(exerciseIcons ? { exerciseIcons: exerciseIcons } : null)} className={props.className} > -
+
setShowOverlay(true), onMouseLeave: () => setShowOverlay(false), onFocus: () => setShowOverlay(true), onBlur: handleBlur} : {})} + > + {(enableOverlay && showOverlay) && + + {overlayChildren} + } {exercise.questions.map((q, i) => { @@ -203,7 +224,7 @@ export const Exercise = styled(({ return ( questionsRef.current[questionNumber + i] = el} exercise_uid={exercise.uid} key={q.id} diff --git a/src/components/Exercise/styles.ts b/src/components/Exercise/styles.ts index 48e17bb4..565f2941 100644 --- a/src/components/Exercise/styles.ts +++ b/src/components/Exercise/styles.ts @@ -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 { diff --git a/src/components/IncludeRemoveQuestion.spec.tsx b/src/components/IncludeRemoveQuestion.spec.tsx new file mode 100644 index 00000000..878e54f7 --- /dev/null +++ b/src/components/IncludeRemoveQuestion.spec.tsx @@ -0,0 +1,23 @@ +import renderer from 'react-test-renderer'; +import { IncludeRemoveQuestion } from './IncludeRemoveQuestion/index'; + +describe('IncludeRemoveQuestion', () => { + it.each` + buttonVariant + ${'include'} + ${'remove'} + `('matches snapshot', ({ buttonVariant }: { buttonVariant: 'include' | 'remove' }) => { + const mockIncludeHandlre = jest.fn(); + const mockRemoveHandler = jest.fn(); + const component = renderer.create( + + ); + + renderer.act(() => { + component.root.findAllByType('button')[0].props.onClick(); + }); + + expect(buttonVariant === 'include' ? mockIncludeHandlre : mockRemoveHandler).toHaveBeenCalled(); + expect(component).toMatchSnapshot(); + }); +}); diff --git a/src/components/IncludeRemoveQuestion.stories.tsx b/src/components/IncludeRemoveQuestion.stories.tsx new file mode 100644 index 00000000..af8bfe21 --- /dev/null +++ b/src/components/IncludeRemoveQuestion.stories.tsx @@ -0,0 +1,13 @@ +import React from "react"; +import { IncludeRemoveQuestion } from "./IncludeRemoveQuestion"; + +export const Default = () => { + const [buttonVariant, setButtonVariant] = React.useState<'include' | 'remove'>('include'); + return ( + setButtonVariant('remove')} + onRemoveHandler={() => setButtonVariant('include')} + /> + ); +}; \ No newline at end of file diff --git a/src/components/IncludeRemoveQuestion/index.tsx b/src/components/IncludeRemoveQuestion/index.tsx new file mode 100644 index 00000000..6bae82c1 --- /dev/null +++ b/src/components/IncludeRemoveQuestion/index.tsx @@ -0,0 +1,46 @@ +import React from "react"; +import { faPlus, faMinus, faEllipsisH } from "@fortawesome/free-solid-svg-icons"; +import { StyledContainer, StyledButton, StyledIcon } from "./styles"; + +export interface IncludeRemoveQuestionProps { + // Prop that defines the variant of the button + buttonVariant: 'include' | 'remove'; + // Method invoked when the include button is clicked + onIncludeHandler: any; + // Method invoked when the remove button is clicked + onRemoveHandler: any; +} + +export const IncludeRemoveQuestion = ({buttonVariant, onIncludeHandler, onRemoveHandler }: IncludeRemoveQuestionProps) => { + + const buttonIcon = React.useMemo(() => buttonVariant === 'include' ? faPlus : faMinus, [buttonVariant]); + const onClickHandler = (variant: 'include' | 'remove') => { + switch (variant) { + case 'include': + onIncludeHandler(); + break; + case 'remove': + onRemoveHandler(); + break; + default: + break; + } + }; + + const generateButtonText = (string: string) => { + return string.charAt(0).toUpperCase() + string.slice(1) + ' question'; + }; + + return ( + + onClickHandler(buttonVariant)} aria-label="details"> + + {generateButtonText(buttonVariant)} + + + + Details + + + ); +}; \ No newline at end of file diff --git a/src/components/IncludeRemoveQuestion/styles.ts b/src/components/IncludeRemoveQuestion/styles.ts new file mode 100644 index 00000000..54337333 --- /dev/null +++ b/src/components/IncludeRemoveQuestion/styles.ts @@ -0,0 +1,64 @@ +import { colors } from '../../theme'; +import styled from "styled-components"; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; + + +export const StyledContainer = styled.div` + display: flex; + width: fit-content; +`; + +export const StyledButton = styled.button` + width: 74px; + height: 87px; + color: white; + border: none; + + &:hover { + box-shadow: 0px 1px 4px 0px #00000066; + } + + span { + font-family: "HelveticaNeue"; + line-height: 13px; + font-size: 14px; + font-weight: bold; + } + + &.include { + background-color: ${colors.palette.mediumBlue}; + padding-top: 14px; + } + + &.remove { + background-color: ${colors.palette.orange}; + padding-top: 14px; + } + + &.details { + background-color: ${colors.palette.neutralDarker}; + } +`; + +export const StyledIcon = styled(FontAwesomeIcon)` + border-radius: 50%; + background-color: ${colors.palette.white}; + vertical-align: middle; + position: relative; + bottom: 5px; + font-weight: 900; + line-height: 13.8px; + + + &.include { + color: ${colors.palette.mediumBlue}; + } + + &.remove { + color: ${colors.palette.orange}; + } + + &.details { + color: ${colors.palette.neutralDarker}; + } +`; diff --git a/src/components/__snapshots__/Card.spec.tsx.snap b/src/components/__snapshots__/Card.spec.tsx.snap index c2731f0f..aa5723cc 100644 --- a/src/components/__snapshots__/Card.spec.tsx.snap +++ b/src/components/__snapshots__/Card.spec.tsx.snap @@ -8,7 +8,7 @@ exports[`StepCard matches snapshot 1`] = ` className="sc-bczRLJ eAXREJ" >

+
+
+
Stimulus HTML", + } + } + /> +
+
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+
+
+
+ +
+
+ +
+
+
+
+
+
+
+
+

+`; + +exports[`Exercise with overlay rendering matches snapshot 1`] = ` +
+
+
+
+

+ + Question 1 + + +  / + 1 + + + | + + + ID: + 1234@4 + +

+
+
+
+

-
+

-
+

-
+

-
+
+ + +
+`; + +exports[`IncludeRemoveQuestion matches snapshot 2`] = ` +
+ + +
+`; From dbee6c6b6fcdfe24f14e15e69e57fa2b75b67119 Mon Sep 17 00:00:00 2001 From: jomcarvajal Date: Tue, 21 Jan 2025 12:01:30 -0600 Subject: [PATCH 2/5] resolve comments --- src/components/Card.tsx | 4 + src/components/Exercise/index.tsx | 40 +++++--- src/components/IncludeRemoveQuestion.spec.tsx | 6 +- .../IncludeRemoveQuestion/index.tsx | 4 +- .../IncludeRemoveQuestion/styles.ts | 19 ++-- src/components/Print.stories.tsx | 99 ------------------- 6 files changed, 45 insertions(+), 127 deletions(-) diff --git a/src/components/Card.tsx b/src/components/Card.tsx index 5ef3e491..d2e9c4ad 100644 --- a/src/components/Card.tsx +++ b/src/components/Card.tsx @@ -87,6 +87,9 @@ const StepCardHeader = styled.div` button.ox-icon-angle-left, button.ox-icon-angle-right { display: none; } + .separator { + display: inherit; + } `} /* @@ -264,6 +267,7 @@ StepCard.displayName = 'OSStepCard'; export interface TaskStepCardProps extends SharedProps { className?: string; children?: ReactNode; + tabIndex?: number; step: StepBase | StepWithData; questionNumber: number; numberOfQuestions: number; diff --git a/src/components/Exercise/index.tsx b/src/components/Exercise/index.tsx index 99b39d0b..b9a3ca6f 100644 --- a/src/components/Exercise/index.tsx +++ b/src/components/Exercise/index.tsx @@ -55,18 +55,21 @@ const ToolbarWrapper = styled.div<{ `} `; -const TaskStepCardWithToolbar = (props: React.PropsWithChildren & +const TaskStepCardWithToolbar = React.forwardRef & Pick & { desktopToolbarEnabled: boolean; mobileToolbarEnabled: boolean; } -) => +>((props, ref) => ( + - ; + +)); const Preamble = ({ exercise }: { exercise: ExerciseData }) => { return ( @@ -170,12 +173,13 @@ export const Exercise = styled(({ const legacyStepRender = 'feedback_html' in step; const questionsRef = React.useRef>([]); const container = React.useRef(null); + const hoverRef = React.useRef(null); const [showOverlay, setShowOverlay] = React.useState(false); const typesetExercise = React.useCallback(() => { - if (container.current) { - typesetMath(container.current); + if (hoverRef.current) { + typesetMath(hoverRef.current); } }, []); @@ -187,7 +191,7 @@ export const Exercise = styled(({ }, [scrollToQuestion, exercise]); const handleBlur = (event: React.FocusEvent) => { - if (container.current && !container.current.contains(event.relatedTarget as Node)) { + if (hoverRef.current && !hoverRef.current.contains(event.relatedTarget as Node)) { setShowOverlay(false); } }; @@ -198,6 +202,7 @@ export const Exercise = styled(({ return -
setShowOverlay(true), onMouseLeave: () => setShowOverlay(false), onFocus: () => setShowOverlay(true), onBlur: handleBlur} : {})} + { + ...(enableOverlay + ? { + onMouseOver: () => setShowOverlay(true), + onMouseLeave: () => setShowOverlay(false), + onFocus: () => setShowOverlay(true), + onBlur: handleBlur + } + : {}) + } > - {(enableOverlay && showOverlay) && + {(enableOverlay && showOverlay) && {overlayChildren} } diff --git a/src/components/IncludeRemoveQuestion.spec.tsx b/src/components/IncludeRemoveQuestion.spec.tsx index 878e54f7..40c539ba 100644 --- a/src/components/IncludeRemoveQuestion.spec.tsx +++ b/src/components/IncludeRemoveQuestion.spec.tsx @@ -7,17 +7,17 @@ describe('IncludeRemoveQuestion', () => { ${'include'} ${'remove'} `('matches snapshot', ({ buttonVariant }: { buttonVariant: 'include' | 'remove' }) => { - const mockIncludeHandlre = jest.fn(); + const mockIncludeHandler = jest.fn(); const mockRemoveHandler = jest.fn(); const component = renderer.create( - + ); renderer.act(() => { component.root.findAllByType('button')[0].props.onClick(); }); - expect(buttonVariant === 'include' ? mockIncludeHandlre : mockRemoveHandler).toHaveBeenCalled(); + expect(buttonVariant === 'include' ? mockIncludeHandler : mockRemoveHandler).toHaveBeenCalled(); expect(component).toMatchSnapshot(); }); }); diff --git a/src/components/IncludeRemoveQuestion/index.tsx b/src/components/IncludeRemoveQuestion/index.tsx index 6bae82c1..ff2acbc3 100644 --- a/src/components/IncludeRemoveQuestion/index.tsx +++ b/src/components/IncludeRemoveQuestion/index.tsx @@ -6,9 +6,9 @@ export interface IncludeRemoveQuestionProps { // Prop that defines the variant of the button buttonVariant: 'include' | 'remove'; // Method invoked when the include button is clicked - onIncludeHandler: any; + onIncludeHandler: () => void; // Method invoked when the remove button is clicked - onRemoveHandler: any; + onRemoveHandler: () => void; } export const IncludeRemoveQuestion = ({buttonVariant, onIncludeHandler, onRemoveHandler }: IncludeRemoveQuestionProps) => { diff --git a/src/components/IncludeRemoveQuestion/styles.ts b/src/components/IncludeRemoveQuestion/styles.ts index 54337333..7038e278 100644 --- a/src/components/IncludeRemoveQuestion/styles.ts +++ b/src/components/IncludeRemoveQuestion/styles.ts @@ -9,30 +9,29 @@ export const StyledContainer = styled.div` `; export const StyledButton = styled.button` - width: 74px; - height: 87px; + width: 7.4rem; + height: 8.7rem; color: white; border: none; &:hover { - box-shadow: 0px 1px 4px 0px #00000066; + box-shadow: 0 0.1rem 0.4rem 0 #00000066; } span { - font-family: "HelveticaNeue"; - line-height: 13px; - font-size: 14px; + line-height: 1.3rem; + font-size: 1.4rem; font-weight: bold; } &.include { background-color: ${colors.palette.mediumBlue}; - padding-top: 14px; + padding-top: 1.4rem; } &.remove { background-color: ${colors.palette.orange}; - padding-top: 14px; + padding-top: 1.4rem; } &.details { @@ -45,9 +44,9 @@ export const StyledIcon = styled(FontAwesomeIcon)` background-color: ${colors.palette.white}; vertical-align: middle; position: relative; - bottom: 5px; + bottom: 0.5rem; font-weight: 900; - line-height: 13.8px; + line-height: 1.4rem; &.include { diff --git a/src/components/Print.stories.tsx b/src/components/Print.stories.tsx index 8d2a3ada..e69de29b 100644 --- a/src/components/Print.stories.tsx +++ b/src/components/Print.stories.tsx @@ -1,99 +0,0 @@ -import { ExerciseData, ExerciseQueryData, ExerciseQuestionData, StepBase } from '../../src/types'; -import data from '../../exercises.json'; -import styled from 'styled-components'; -import { Exercise } from './Exercise'; - -const ExerciseWrapper = styled.div` - break-inside: avoid; - - .step-card-body { - padding: 24px 48px !important; - } - - .step-card-footer { - display: none; - } - - .exercise-id { - height: auto; - } - - .exercise-step { - min-height: auto; - } - - .question-feedback { - box-shadow: none !important; - } - - .openstax-answer { - break-inside: avoid; - - .answer-letter-wrapper::after { - content: '' !important; - } - } -`; - -const exercises = (data as ExerciseQueryData).exercises as ExerciseData[]; - -const firstQuestionNumByExercise = exercises.reduce((acc, ex) => ({ - ...acc, - [ex.uuid]: acc.questionCounter + 1, - questionCounter: acc.questionCounter + ex.questions.length - }), {questionCounter: 0}); - -// placeholder until exercise data contains correct answer IDs -const formatAnswerData = (questions: ExerciseQuestionData[]) => questions.map((q) => ( - {id: q.id, correct_answer_id: (q.answers.find((a) => a.correctness === '1.0')?.id || '')})); - -const questionStateFields = { - available_points: '1.0', - is_completed: true, - answer_id: '1', - free_response: '', - feedback_html: '', - correct_answer_feedback_html: '', - attempts_remaining: 0, - attempt_number: 1, - incorrectAnswerId: 0 -} - -export const Default = () => ( - <> - {data.title &&

Exercises for {data.title}

} - {exercises.map(((exercise) => { - - const step: StepBase = { - id: 1, - uid: exercise.uid, - available_points: '1.0', - }; - - const questionStates = formatAnswerData(exercise.questions).reduce((acc, answer) => { - const {id, correct_answer_id} = answer; - return {...acc, [id]: {...questionStateFields, correct_answer_id}}; - }, {}); - - return ( - - undefined} - onAnswerSave={() => undefined} - onNextStep={() => undefined} - apiIsPending={false} - canUpdateCurrentStep={false} - exercise={exercise} - step={step} - questionNumber={firstQuestionNumByExercise[exercise.uuid]} - numberOfQuestions={exercises.length} - questionStates={questionStates} - show_all_feedback={true} /> - - ) - }))} - ); From a3282e54c0f6f390555bb92ec8cbf726aa3e98e2 Mon Sep 17 00:00:00 2001 From: jomcarvajal Date: Tue, 21 Jan 2025 12:18:32 -0600 Subject: [PATCH 3/5] update snapshots --- src/components/__snapshots__/Card.spec.tsx.snap | 10 +++++----- .../__snapshots__/Exercise.spec.tsx.snap | 12 ++++++------ .../IncludeRemoveQuestion.spec.tsx.snap | 16 ++++++++-------- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/components/__snapshots__/Card.spec.tsx.snap b/src/components/__snapshots__/Card.spec.tsx.snap index aa5723cc..ced55555 100644 --- a/src/components/__snapshots__/Card.spec.tsx.snap +++ b/src/components/__snapshots__/Card.spec.tsx.snap @@ -8,7 +8,7 @@ exports[`StepCard matches snapshot 1`] = ` className="sc-bczRLJ eAXREJ" >

@@ -108,33 +112,35 @@ exports[`TaskStepCard can optionally provide task 1`] = `
-
-
-

- - Question 1 - - - | - - +
+
+

- ID: - 1234@1 - -

+ + Question 1 + + + | + + + ID: + 1234@1 + +

+
+
-
`; @@ -147,33 +153,35 @@ exports[`TaskStepCard can optionally provide type 1`] = `
-
-
-

- - Question 1 - - - | - - +
+
+

- ID: - 1234@1 - -

+ + Question 1 + + + | + + + ID: + 1234@1 + +

+
+
-
`; @@ -186,33 +194,35 @@ exports[`TaskStepCard matches snapshot 1`] = `
-
-
-

- - Question 1 - - - | - - +
+
+

- ID: - 1234@1 - -

+ + Question 1 + + + | + + + ID: + 1234@1 + +

+
+
-
`; diff --git a/src/components/__snapshots__/CompletionStatus.spec.tsx.snap b/src/components/__snapshots__/CompletionStatus.spec.tsx.snap index 00c3d301..2f19115d 100644 --- a/src/components/__snapshots__/CompletionStatus.spec.tsx.snap +++ b/src/components/__snapshots__/CompletionStatus.spec.tsx.snap @@ -2,10 +2,10 @@ exports[`CompletionStatus matches snapshot 1`] = `

No questions have been answered.

@@ -13,7 +13,7 @@ exports[`CompletionStatus matches snapshot 1`] = ` Begin working on the quiz.

+
- Next - + +

@@ -205,180 +205,182 @@ exports[`Exercise with overlay rendering matches snapshot 1`] = ` className="sc-bczRLJ eAXREJ exercise-step sc-breuTD dSaRVj sc-hAZoDl RyOjk" >
-
-

- - Question 1 - - -  / - 1 - - - | - - +
+

- ID: - 1234@4 - -

+ + Question 1 + + +  / + 1 + + + | + + + ID: + 1234@4 + +

+
-
-
-
+
-
Stimulus HTML", + /> +
Stimulus HTML", + } } - } - /> -
+ />
-
- - -
-
-
-
+ +
+ + +
+
- - - +
+ +
+ + +
-
-
- -
-
-
+
- Next - + +
@@ -401,171 +403,171 @@ exports[`Exercise with question state data matches snapshot 1`] = `
-
-
-

- - Question 1 - - - | - - +
+
+

- ID: - 1234@5 - -

+ + Question 1 + + + | + + + ID: + 1234@5 + +

+
-
-
-
+
-
Stimulus HTML", + /> +
Stimulus HTML", + } } - } - /> -
+ />
-
- - -
-
-
-
+ +
+ + +
+
- - - +
+ +
+ + +
-
-
- -
-
-
+
- Next - + +
@@ -582,7 +584,7 @@ exports[`Exercise with question state data renders header icons with multiple ch className="sc-ksZaOG gLzA-Dq" >
-
- +
+
- -
-
+
-
Stimulus HTML", + /> +
Stimulus HTML", + } } - } - /> -
+ />
-
- - -
-
-
-
+ +
+ + +
+
- - - +
+ +
+ + +
-
-
- -
-
-
+
- Next - + +
@@ -909,7 +911,7 @@ exports[`Exercise with question state data renders header icons with two-step ex className="sc-ksZaOG gLzA-Dq" >
-
- +
+
-
-
+
-
- View topic in textbook +
+
+ View topic in textbook +
-
-
- -
+ -
+
-
- Suggest a correction +
+
+ Suggest a correction +
-
-
-
+
-
+
-
- In a two-step question, OpenStax asks for your own answer first, then gives multiple-choice options to help you assess your learnings. Recalling the answer to a question from memory helps you to retain things longer. +
+
+ In a two-step question, OpenStax asks for your own answer first, then gives multiple-choice options to help you assess your learnings. Recalling the answer to a question from memory helps you to retain things longer. +
-
-
-
+
-
Stimulus HTML", + /> +
Stimulus HTML", + } } - } - /> -
+ />
-
- - -
-
-
-
+ +
+ + +
+
- - - +
+ +
+ + +
-
-
- -
-
-
+
- Next - + +
@@ -1282,184 +1284,184 @@ exports[`Exercise with question state data shows a detailed solution 1`] = `
-
-
-

- - Question 1 - - - | - - +
+
+

- ID: - 1234@5 - -

+ + Question 1 + + + | + + + ID: + 1234@5 + +

+
-
-
-
+
-
Stimulus HTML", + /> +
Stimulus HTML", + } } - } - /> -
+ />
-
- - -
-
-
-
+ +
+ + +
+
- - - +
+ +
+ + +
-
-
- -
- - Detailed solution: - - +
+
+ + Detailed solution: + + + +
-
-
- + +
From d7bb31c5a2d49ff6e26a49b8adedd84db9162b6e Mon Sep 17 00:00:00 2001 From: jomcarvajal Date: Fri, 24 Jan 2025 17:15:42 -0600 Subject: [PATCH 5/5] remove focus on card when overlay is up and add autoFocus for first interactive element on overlay --- src/components/Card.tsx | 87 +- src/components/Exercise.stories.tsx | 2 +- .../IncludeRemoveQuestion/index.tsx | 4 +- .../__snapshots__/Card.spec.tsx.snap | 280 +-- .../__snapshots__/Exercise.spec.tsx.snap | 1993 +++++++++-------- .../IncludeRemoveQuestion.spec.tsx.snap | 8 +- 6 files changed, 1228 insertions(+), 1146 deletions(-) diff --git a/src/components/Card.tsx b/src/components/Card.tsx index 5a07c2fa..e1d9641a 100644 --- a/src/components/Card.tsx +++ b/src/components/Card.tsx @@ -1,4 +1,4 @@ -import { ReactNode, useState, useRef } from "react"; +import { ReactNode, useState, useRef, useEffect, useCallback } from "react"; import { breakpoints, colors, layouts, mixins } from "../theme"; import { AvailablePoints, StepBase, StepWithData } from "../types"; import styled from "styled-components"; @@ -205,8 +205,7 @@ export const StyledOverlay = styled.div` transform: translate(-50%, -50%); width: 100%; height: 100%; - background-color: #FFFFFF; - opacity: 0.8; + background-color: #FFFFFF80; z-index: 2; `; @@ -249,6 +248,8 @@ const StepCard = ({ overlayChildren, ...otherProps }: StepCardProps) => { + // Helps to stop focusing first child when is already focused + const [previousFocusedElement, setPreviousFocusedElement] = useState(null); const overlayRef = useRef(null); const [showOverlay, setShowOverlay] = useState(false); @@ -262,6 +263,42 @@ const StepCard = ({ } }; + const handleOverlayFocus = useCallback((event: FocusEvent) => { + setShowOverlay(true); + const firstOverlayFocusableElement = document.getElementById('overlay-element')?.querySelector( + 'button, [href], input, select, textarea' + ) as HTMLElement; + + if ( + (firstOverlayFocusableElement !== previousFocusedElement) && + (event.target === overlayRef.current) + ) { + setPreviousFocusedElement(firstOverlayFocusableElement); + firstOverlayFocusableElement.focus(); + } + }, [overlayRef, previousFocusedElement]); + + const hideFocusableElements = useCallback(() => { + const focusableElements = Array.from(document.getElementById("step-card")?.querySelectorAll( + 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' + ) || []); + + focusableElements.forEach((el) => { + (el as HTMLElement).setAttribute('tabindex', '-1'); + }); + }, []); + + useEffect(() => { + const currentOverlayRef = overlayRef.current; + if (currentOverlayRef && overlayChildren) { + currentOverlayRef.addEventListener('focus', handleOverlayFocus); + hideFocusableElements(); + } + return () => { + currentOverlayRef?.removeEventListener('focus', handleOverlayFocus); + }; + }, [overlayChildren, overlayRef, handleOverlayFocus, hideFocusableElements]); + return ( {multipartBadge} @@ -273,7 +310,6 @@ const StepCard = ({ ? { onMouseOver: () => setShowOverlay(true), onMouseLeave: () => setShowOverlay(false), - onFocus: () => setShowOverlay(true), onBlur: handleOverlayBlur, tabIndex: 0, } @@ -281,28 +317,31 @@ const StepCard = ({ } > {(overlayChildren && showOverlay) && - + {overlayChildren} - } - {questionNumber && isHomework && stepType === 'exercise' && - -
- {leftHeaderChildren} -

- {headerTitleChildren} - {formattedQuestionNumber} - {showTotalQuestions ?  / {numberOfQuestions} : null} - | - ID: {questionId} -

-
- {availablePoints || rightHeaderChildren ?
- {availablePoints &&
{availablePoints} Points
} - {rightHeaderChildren} -
: null} -
+
} - {children} +
+ {questionNumber && isHomework && stepType === 'exercise' && + +
+ {leftHeaderChildren} +

+ {headerTitleChildren} + {formattedQuestionNumber} + {showTotalQuestions ?  / {numberOfQuestions} : null} + | + ID: {questionId} +

+
+ {availablePoints || rightHeaderChildren ?
+ {availablePoints &&
{availablePoints} Points
} + {rightHeaderChildren} +
: null} +
+ } + {children} +
diff --git a/src/components/Exercise.stories.tsx b/src/components/Exercise.stories.tsx index ac9f7c5e..fd0592dc 100644 --- a/src/components/Exercise.stories.tsx +++ b/src/components/Exercise.stories.tsx @@ -951,7 +951,7 @@ export const OverlayCard = () => { return ( - + ); }; diff --git a/src/components/IncludeRemoveQuestion/index.tsx b/src/components/IncludeRemoveQuestion/index.tsx index ff2acbc3..3a7b104b 100644 --- a/src/components/IncludeRemoveQuestion/index.tsx +++ b/src/components/IncludeRemoveQuestion/index.tsx @@ -33,11 +33,11 @@ export const IncludeRemoveQuestion = ({buttonVariant, onIncludeHandler, onRemove return ( - onClickHandler(buttonVariant)} aria-label="details"> + onClickHandler(buttonVariant)} aria-label={buttonVariant}> {generateButtonText(buttonVariant)} - + Details diff --git a/src/components/__snapshots__/Card.spec.tsx.snap b/src/components/__snapshots__/Card.spec.tsx.snap index f7e6616c..1882d238 100644 --- a/src/components/__snapshots__/Card.spec.tsx.snap +++ b/src/components/__snapshots__/Card.spec.tsx.snap @@ -9,40 +9,44 @@ exports[`StepCard matches snapshot 1`] = ` >
-
-

- - Question 1 - - +
+

- | - - + Question 1 + + + | + + + ID: + +

+
+
+
- ID: - -

-
-
-
- 1.0 - Points + 1.0 + Points +
-
-
- Question content +
+ Question content +
@@ -58,46 +62,50 @@ exports[`StepCard matches snapshot with more than one question 1`] = ` >
-
-

- - Questions 1 - 3 - - +
+

-  / - 3 - - - | - - + Questions 1 - 3 + + +  / + 3 + + + | + + + ID: + +

+
+
+
- ID: - -

-
-
-
- 1.0 - Points + 1.0 + Points +
-
-
- Question content +
+ Question content +
@@ -114,32 +122,36 @@ exports[`TaskStepCard can optionally provide task 1`] = ` >
-
-

- - Question 1 - - - | - - +
+

- ID: - 1234@1 - -

+ + Question 1 + + + | + + + ID: + 1234@1 + +

+
+
-
@@ -155,32 +167,36 @@ exports[`TaskStepCard can optionally provide type 1`] = ` >
-
-

- - Question 1 - - - | - - +
+

- ID: - 1234@1 - -

+ + Question 1 + + + | + + + ID: + 1234@1 + +

+
+
-
@@ -196,32 +212,36 @@ exports[`TaskStepCard matches snapshot 1`] = ` >
-
-

- - Question 1 - - - | - - +
+

- ID: - 1234@1 - -

+ + Question 1 + + + | + + + ID: + 1234@1 + +

+
+
-
diff --git a/src/components/__snapshots__/Exercise.spec.tsx.snap b/src/components/__snapshots__/Exercise.spec.tsx.snap index 9c4547d9..6214b6af 100644 --- a/src/components/__snapshots__/Exercise.spec.tsx.snap +++ b/src/components/__snapshots__/Exercise.spec.tsx.snap @@ -13,174 +13,178 @@ exports[`Exercise using step data matches snapshot 1`] = ` >
-
-

- - Question 1 - - +
+

-  / - 1 - - - | - - - ID: - 1234@4 - -

+ + Question 1 + + +  / + 1 + + + | + + + ID: + 1234@4 + +

+
-
-
-
-
+
+
-
Stimulus HTML", + /> +
Stimulus HTML", + } } - } - /> -
+ />
-
- - -
-
-
-
+ +
+ + +
+
- - - +
+ +
+ + +
-
-
- -
-
-
+
- Next - + +
@@ -206,180 +210,183 @@ exports[`Exercise with overlay rendering matches snapshot 1`] = ` >
-
-

- - Question 1 - - -  / - 1 - - +
+

- | - - - ID: - 1234@4 - -

+ + Question 1 + + +  / + 1 + + + | + + + ID: + 1234@4 + +

+
-
-
-
-
+
+
-
Stimulus HTML", + /> +
Stimulus HTML", + } } - } - /> -
+ />
-
- - -
-
-
-
+ +
+ + +
+
- - - +
+ +
+ + +
-
-
- -
-
-
+
- Next - + +
@@ -405,168 +412,172 @@ exports[`Exercise with question state data matches snapshot 1`] = ` >
-
-

- - Question 1 - - +
+

- | - - - ID: - 1234@5 - -

+ + Question 1 + + + | + + + ID: + 1234@5 + +

+
-
-
-
-
+
+
-
Stimulus HTML", + /> +
Stimulus HTML", + } } - } - /> -
+ />
-
- - -
-
-
-
+ +
+ + +
+
- - - +
+ +
+ + +
-
-
- -
-
-
+
- Next - + +
@@ -646,254 +657,258 @@ exports[`Exercise with question state data renders header icons with multiple ch >
-
-

- - Question 1 - - +
+

- | - - - ID: - 1234@5 - -

-
-

+
+
+
- -
-
-
+
+
-
Stimulus HTML", + /> +
Stimulus HTML", + } } - } - /> -
+ />
-
- - -
-
-
-
+ +
+ + +
+
- - - +
+ +
+ + +
-
-
- -
-
- + +
+
+ +
@@ -973,294 +988,298 @@ exports[`Exercise with question state data renders header icons with two-step ex >
-
-

- - Question 1 - - +
+

- | - - - ID: - 1234@5 - -

-
-

+
+
+
-
-
+
-
- View topic in textbook +
+
+ View topic in textbook +
-
-
- -
+ -
+
-
- Suggest a correction +
+
+ Suggest a correction +
-
-
-
+
-
+
-
- In a two-step question, OpenStax asks for your own answer first, then gives multiple-choice options to help you assess your learnings. Recalling the answer to a question from memory helps you to retain things longer. +
+
+ In a two-step question, OpenStax asks for your own answer first, then gives multiple-choice options to help you assess your learnings. Recalling the answer to a question from memory helps you to retain things longer. +
-
-
-
-
+
+
-
Stimulus HTML", + /> +
Stimulus HTML", + } } - } - /> -
+ />
-
- - -
-
-
-
+ +
+ + +
+
- - - +
+ +
+ + +
-
-
- -
-
- + +
+
+ +
@@ -1286,181 +1305,185 @@ exports[`Exercise with question state data shows a detailed solution 1`] = ` >
-
-

- - Question 1 - - - | - - +
+

- ID: - 1234@5 - -

+ + Question 1 + + + | + + + ID: + 1234@5 + +

+
-
-
-
-
+
+
-
Stimulus HTML", + /> +
Stimulus HTML", + } } - } - /> -
+ />
-
- - -
-
-
-
+ +
+ + +
+
- - - +
+ +
+ + +
-
-
- -
- - Detailed solution: - - +
+
+ + Detailed solution: + + + +
-
-
- + +
diff --git a/src/components/__snapshots__/IncludeRemoveQuestion.spec.tsx.snap b/src/components/__snapshots__/IncludeRemoveQuestion.spec.tsx.snap index 96cb6f64..e1d396ac 100644 --- a/src/components/__snapshots__/IncludeRemoveQuestion.spec.tsx.snap +++ b/src/components/__snapshots__/IncludeRemoveQuestion.spec.tsx.snap @@ -5,7 +5,7 @@ exports[`IncludeRemoveQuestion matches snapshot 1`] = ` className="sc-bczRLJ fXHYVE" >