@instructure/quiz-interactions
Version:
A React UI component Library for quiz interaction types.
195 lines (175 loc) • 6.07 kB
JavaScript
/** @jsx jsx */
import {Component} from 'react'
import PropTypes from 'prop-types'
import {camelize} from 'humps'
import {ScreenReaderContent} from '@instructure/ui-a11y-content'
import {RadioInput} from '@instructure/ui-radio-input'
import {jsx} from '@instructure/emotion'
import t from '@instructure/quiz-i18n/format-message'
import {ItemBodyWrapper} from '@instructure/quiz-rce/components/ItemBodyWrapper/index'
import {RichContentRenderer} from '@instructure/quiz-rce/components/RichContentRenderer/index'
import {FeedbackWrapper, NoResponse} from '@instructure/quiz-results-feedback'
import generateStyle from './styles'
import generateComponentTheme from './theme'
import {withStyleOverrides} from '@instructure/quiz-common/util/withStyleOverrides'
export const correctChoice = function ({interactionData, scoredData}) {
return (
interactionData.choices.find(item => {
const choiceData = (scoredData.value && scoredData.value[item.id]) || {}
return choiceData.hasOwnProperty('correct') ? choiceData.correct : choiceData.resultScore
}) || {}
)
}
export const getChoiceData = (scoredData, id) => {
if (!scoredData?.value) return {}
// Some imported quizzes have non-standard IDs (e.g., "ans-6-4") that get
// transformed by camelizeHelpers (e.g., "ans64" or "ans6_4"). To make sure
// student selections still match correctly, we check both the original,
// dash-stripped, and camelized versions of the ID.
return (
scoredData.value[id] ||
scoredData.value[id.replace(/-/g, '')] ||
scoredData.value[camelize(id)] ||
{}
)
}
/**
---
category: MultipleChoice
---
Multiple Choice Result component
```jsx_example
<SettingsSwitcher locales={LOCALES}>
<MultipleChoiceResult
itemBody="<p>Who was the first <strong>President</strong> of the United States?</p>"
itemId="44"
interactionData={{
choices: [{
id: 'uuid1',
position: 1,
itemBody: '<p>George Washington</p><p>was the first</p><p>president</p>'
},
{ id: 'uuid2', position: 2, itemBody: '<p>Alexander Hamilton</p>' },
{ id: 'uuid3', position: 3, itemBody: '<p>John Adams</p>' },
{ id: 'uuid4', position: 4, itemBody: '<p>Thomas Jefferson</p>' }
]
}}
scoredData={{
correct: false,
value: {
uuid1: { resultScore: 1, correct: true },
uuid2: { resultScore: 0, userResponded: true }
}
}}
/>
</SettingsSwitcher>
```
**/
@withStyleOverrides(generateStyle, generateComponentTheme)
export default class MultipleChoiceResult extends Component {
static displayName = 'MultipleChoiceResult'
static componentId = `Quizzes${this.displayName}`
static propTypes = {
interactionData: PropTypes.shape({
choices: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.string,
itemBody: PropTypes.string,
position: PropTypes.number,
}),
),
}).isRequired,
itemBody: PropTypes.string.isRequired,
itemId: PropTypes.string,
scoredData: PropTypes.shape({
correct: PropTypes.bool,
value: PropTypes.objectOf(
PropTypes.shape({
correct: PropTypes.bool,
resultScore: PropTypes.number,
userResponded: PropTypes.bool,
}),
),
}).isRequired,
styles: PropTypes.object,
}
static defaultProps = {
itemId: void 0,
}
renderFeedbackChoice = ({id, itemBody}, status) => (
<div key={id} css={this.props.styles.responseWrapper}>
<FeedbackWrapper
correctAnswer={correctChoice(this.props).itemBody}
hiddenCorrectAnswerText={t('Correct answer: ')}
hiddenIncorrectAnswerText={t('Incorrect answer: ')}
status={status}
richContent
>
<RadioInput
value={id}
name={`interaction_${this.props.itemId}`}
label={<RichContentRenderer content={itemBody} />}
checked
readOnly
/>
</FeedbackWrapper>
</div>
)
renderUngradedChoice = ({id, itemBody}, checked) => (
<div key={id} css={[this.props.styles.responseWrapper, this.props.styles.radioWrapper]}>
<RadioInput
value={id}
name={`interaction_${this.props.itemId}`}
label={
<div>
<RichContentRenderer content={itemBody} />
{!checked && <ScreenReaderContent>{t(', Not Selected')}</ScreenReaderContent>}
</div>
}
checked={checked}
readOnly
/>
</div>
)
render() {
const {scoredData} = this.props
const choices = this.props?.interactionData?.choices?.map(choice => {
const choiceData = getChoiceData(scoredData, choice?.id)
const correct = choiceData.hasOwnProperty('correct')
? choiceData.correct
: choiceData.resultScore
const checked = choiceData.userResponded
return {choice, correct, checked}
})
const noResponse = choices.every(({checked}) => !checked)
const correctnessKnown = typeof scoredData.correct !== 'undefined'
return (
<ItemBodyWrapper itemBody={this.props.itemBody}>
<div>
{choices.map(({choice, correct, checked}) => {
let status
if (correct === true || correct > 0) {
status = 'correct'
} else if (correctnessKnown && checked) {
status = 'incorrect'
}
if (!status || !checked) {
// Choice has no feedback
return this.renderUngradedChoice(choice, checked)
}
return this.renderFeedbackChoice(choice, status)
})}
</div>
{noResponse && (
<NoResponse
css={this.props.styles.noResponseWrapper}
correctAnswerLabel={correctChoice(this.props).itemBody}
responseHidden={!scoredData.value}
richContent
status={correctnessKnown ? 'incorrect' : 'unknown'}
/>
)}
</ItemBodyWrapper>
)
}
}