Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
148 changes: 148 additions & 0 deletions src/app/content/practiceQuestions/components/Answer/Answer.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
/**
* Answer Component Styles
* Migrated from styled-components to plain CSS
*/

/* AnswerWrapper */
.answer-wrapper {
overflow: visible;
}

/* AnswerBlock (label) */
.answer-block {
padding: 1rem 2.4rem;
display: flex;
align-items: center;
cursor: pointer;
}

.answer-block:focus {
outline: none;
}

.answer-block--disabled {
cursor: not-allowed;
}

/* Focus styles for keyboard navigation */
input:focus + .answer-block,
.answer-wrapper:focus .answer-block {
outline: -webkit-focus-ring-color auto 1px;
}

input:not(:focus-visible):focus + .answer-block {
outline: none;
}

/* Mobile responsive padding */
@media screen and (max-width: 75em) {
.answer-block {
padding: 0.5rem 2.4rem;
}
}

/* AnswerIndicator */
.answer-indicator {
min-width: 4rem;
min-height: 4rem;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
border: 1.5px solid;
}

/* AnswerAlignment */
.answer-alignment {
display: flex;
align-items: center;
min-height: 4rem;
flex: 1;
}

/* AnswerContent */
.answer-content {
margin-left: 1.6rem;
overflow: initial;
}

/* AnswerExcerpt */
.answer-excerpt {
font-size: 1.6rem;
line-height: 2.5rem;
width: 100%;
padding: 0;
overflow: auto;
}

.answer-excerpt * {
overflow: initial;
}

/* AnswerResult */
.answer-result {
font-size: 1.6rem;
line-height: 2.5rem;
color: var(--answer-result-color);
}

/* Theme: Unselected */
.answer-block--unselected {
background-color: var(--answer-bg-unselected);
}

.answer-block--unselected .answer-indicator {
color: var(--answer-indicator-fg-unselected);
background-color: var(--answer-indicator-bg-unselected);
border-color: var(--answer-border-unselected);
}

/* Theme: Selected */
.answer-block--selected {
background-color: var(--answer-bg-selected);
}

.answer-block--selected .answer-indicator {
color: var(--answer-indicator-fg-selected);
background-color: var(--answer-indicator-bg-selected);
border-color: var(--answer-border-selected);
}

/* Theme: Correct */
.answer-block--correct {
background-color: var(--answer-bg-correct);
}

.answer-block--correct .answer-indicator {
color: var(--answer-indicator-fg-correct);
background-color: var(--answer-indicator-bg-correct);
border-color: var(--answer-border-correct);
}

/* Theme: Incorrect */
.answer-block--incorrect {
background-color: var(--answer-bg-incorrect);
}

.answer-block--incorrect .answer-indicator {
color: var(--answer-indicator-fg-incorrect);
background-color: var(--answer-indicator-bg-incorrect);
border-color: var(--answer-border-incorrect);
}

/* Hover states - placed after base states to maintain proper specificity */
.answer-block--unselected:hover .answer-indicator {
border-color: var(--answer-border-hover-unselected);
}

.answer-block--selected:hover .answer-indicator {
border-color: var(--answer-border-hover-selected);
}

.answer-block--correct:hover .answer-indicator {
border-color: var(--answer-border-hover-correct);
}

.answer-block--incorrect:hover .answer-indicator {
border-color: var(--answer-border-hover-incorrect);
}
118 changes: 89 additions & 29 deletions src/app/content/practiceQuestions/components/Answer/index.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,12 @@
import { HTMLElement } from '@openstax/types/lib.dom';
import classNames from 'classnames';
import React from 'react';
import { FormattedMessage, useIntl } from 'react-intl';
import { linkColor, linkHover } from '../../../../components/Typography/Links.constants';
import theme, { hiddenButAccessibleClass } from '../../../../theme';
import { LinkedArchiveTreeSection } from '../../../types';
import { PracticeAnswer, PracticeQuestion } from '../../types';
import {
AnswerAlignment,
AnswerBlock,
AnswerContent,
AnswerExcerpt,
AnswerIndicator,
answerInputClass,
AnswerWrapper,
StyledAnswerResult,
} from './styled';
import './Answer.css';

interface AnswerResultProps {
showCorrect: boolean;
Expand All @@ -21,6 +15,42 @@ interface AnswerResultProps {
isCorrect: boolean;
}

// Answer theme definitions (matching styled.tsx)
const answerThemes = {
correct: {
background: theme.color.neutral.base,
border: '#148a00',
borderHovered: '#148a00',
fontColor: theme.color.neutral.base,
fontColorActive: '#148a00',
indicatorBackground: '#148a00',
},
incorrect: {
background: theme.color.neutral.base,
border: theme.color.primary.red.base,
borderHovered: theme.color.primary.red.base,
fontColor: theme.color.neutral.base,
fontColorActive: theme.color.primary.red.base,
indicatorBackground: theme.color.primary.red.base,
},
selected: {
background: '#E3F8FB',
border: linkHover, // linkColor has insufficient contrast with background
borderHovered: linkHover,
fontColor: theme.color.neutral.base,
fontColorActive: theme.color.secondary.lightBlue.base,
indicatorBackground: linkColor,
},
unselected: {
background: theme.color.neutral.base,
border: theme.color.primary.gray.medium,
borderHovered: linkHover,
fontColor: theme.color.text.label,
fontColorActive: '#C6C6C6',
indicatorBackground: theme.color.neutral.base,
},
};

const AnswerResult = ({ showCorrect, isSelected, isSubmitted, isCorrect }: AnswerResultProps) => {
const resultMsgKey = isCorrect
? 'i18n:practice-questions:popup:correct'
Expand All @@ -33,11 +63,18 @@ const AnswerResult = ({ showCorrect, isSelected, isSubmitted, isCorrect }: Answe
return null;
}

return <StyledAnswerResult isCorrect={isCorrect}>
const resultTheme = isCorrect ? answerThemes.correct : answerThemes.incorrect;

return <div
className="answer-result"
style={{
'--answer-result-color': resultTheme.fontColorActive,
} as React.CSSProperties}
>
<FormattedMessage id={resultMsgKey}>
{(msg) => msg}
</FormattedMessage>
</StyledAnswerResult>;
</div>;
};

interface AnswerProps {
Expand Down Expand Up @@ -70,43 +107,66 @@ const Answer = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [showCorrect, isCorrect]);

return <AnswerWrapper tabIndex={-1} ref={answerRef}>
// Determine which theme to apply based on answer state
const getAnswerTheme = () => {
if ((showCorrect && isCorrect) || (isSubmitted && isSelected)) {
return isCorrect ? answerThemes.correct : answerThemes.incorrect;
} else {
return isSelected ? answerThemes.selected : answerThemes.unselected;
}
};

const answerTheme = getAnswerTheme();
const themeKey = (showCorrect && isCorrect) || (isSubmitted && isSelected)
? (isCorrect ? 'correct' : 'incorrect')
: (isSelected ? 'selected' : 'unselected');
const {formatMessage} = useIntl();

return <div className="answer-wrapper" tabIndex={-1} ref={answerRef}>
<input
id={choiceIndicator}
type='radio'
name={question.uid}
checked={isSelected}
disabled={isSubmitted}
onChange={onSelect}
className={answerInputClass}
className={hiddenButAccessibleClass}
/>
<AnswerBlock
<label
tabIndex={-1}
htmlFor={choiceIndicator}
showCorrect={showCorrect}
isCorrect={isCorrect}
isSubmitted={isSubmitted}
isSelected={isSelected}
className={classNames('answer-block', {
'answer-block--disabled': isSubmitted,
[`answer-block--${themeKey}`]: true,
})}
style={{
[`--answer-bg-${themeKey}`]: answerTheme.background,
[`--answer-indicator-fg-${themeKey}`]: answerTheme.fontColor,
[`--answer-indicator-bg-${themeKey}`]: answerTheme.indicatorBackground,
[`--answer-border-${themeKey}`]: answerTheme.border,
[`--answer-border-hover-${themeKey}`]: answerTheme.borderHovered,
} as React.CSSProperties}
>
<AnswerIndicator
aria-label={useIntl().formatMessage(
<span
className="answer-indicator"
aria-label={formatMessage(
{id: 'i18n:practice-questions:popup:answers:choice'},
{choiceIndicator: choiceIndicator.toUpperCase()}
)}
>{choiceIndicator}</AnswerIndicator>
<AnswerAlignment>
<AnswerContent>
<AnswerExcerpt dangerouslySetInnerHTML={{ __html: answer.content_html }} />
>{choiceIndicator}</span>
<div className="answer-alignment">
<div className="answer-content">
<span className="answer-excerpt" dangerouslySetInnerHTML={{ __html: answer.content_html }} />
<AnswerResult
showCorrect={showCorrect}
isSelected={isSelected}
isSubmitted={isSubmitted}
isCorrect={isCorrect}
/>
</AnswerContent>
</AnswerAlignment>
</AnswerBlock>
</AnswerWrapper>;
</div>
</div>
</label>
</div>;
};

export default Answer;
Loading
Loading