@instructure/quiz-interactions
Version:
A React UI component Library for quiz interaction types.
276 lines (248 loc) • 7.29 kB
JavaScript
/** @jsx jsx */
import {Component} from 'react'
import PropTypes from 'prop-types'
import minBy from 'lodash/fp/minBy'
import maxBy from 'lodash/fp/maxBy'
import {jsx} from '@instructure/emotion'
import t from '@instructure/quiz-i18n/format-message'
import {ItemBodyWrapper} from '@instructure/quiz-rce/components/ItemBodyWrapper/index'
import {NoResponse} from '@instructure/quiz-results-feedback'
import Oval from '../common/Oval'
import Square from '../common/Square'
import Polygon from '../common/Polygon'
import TargetContainer from '../common/TargetContainer'
import generateStyle from './styles'
import generateComponentTheme from './theme'
import FeedbackPopover from './FeedbackPopover/index'
import {withStyleOverrides} from '@instructure/quiz-common/util/withStyleOverrides'
const CORRECT = 'correct'
const INCORRECT = 'incorrect'
const UNKNOWN = 'unknown'
const coordinates = PropTypes.shape({
x: PropTypes.number,
y: PropTypes.number,
})
function getDrawingType(type) {
switch (type) {
case 'oval':
return Oval
case 'polygon':
return Polygon
default:
return Square
}
}
/**
---
category: HotSpot
---
HotSpot Result component
```jsx_example
<SettingsSwitcher locales={LOCALES}>
<HotSpotResult
itemBody='Which of these players is David Ferrer'
interactionData={{
imageUrl: 'https://i.ytimg.com/vi/LgkAebhJ7iE/maxresdefault.jpg'
}}
scoredData={{
value: {
type: 'square',
resultScore: 0,
userResponse: { x: 0.99, y: 0.5 },
correctAnswer: [
{ x: 0.1, y: 0.3 },
{ x: 0.2, y: 0.2 }
]
}
}}
/>
</SettingsSwitcher>
```
**/
(generateStyle, generateComponentTheme)
export default class HotSpotResult extends Component {
static displayName = 'HotSpotResult'
static componentId = `Quizzes${this.displayName}`
static propTypes = {
interactionData: PropTypes.shape({
imageUrl: PropTypes.string.isRequired,
}).isRequired,
itemBody: PropTypes.string.isRequired,
scoredData: PropTypes.shape({
value: PropTypes.shape({
resultScore: PropTypes.number,
type: PropTypes.string,
correctAnswer: PropTypes.oneOfType([
PropTypes.shape({
value: PropTypes.arrayOf(
PropTypes.shape({
type: PropTypes.string,
coordinates: PropTypes.arrayOf(coordinates),
}),
),
}),
PropTypes.arrayOf(coordinates),
]),
userResponse: PropTypes.oneOfType([
PropTypes.arrayOf(
PropTypes.shape({
x: PropTypes.number,
y: PropTypes.number,
correct: PropTypes.bool,
}),
),
coordinates,
]),
}),
}).isRequired,
makeStyles: PropTypes.func,
styles: PropTypes.object,
}
componentDidMount() {
this.props.makeStyles()
}
componentDidUpdate() {
this.props.makeStyles()
}
get correctAnswers() {
const {correctAnswer} = this.props.scoredData.value || {}
if (!correctAnswer) {
return []
}
if (Array.isArray(correctAnswer) && !correctAnswer?.value) {
return [
{
type: this.props.scoredData.value.type,
coordinates: correctAnswer,
},
]
}
return correctAnswer?.value && !Array.isArray(correctAnswer.value)
? [correctAnswer?.value]
: correctAnswer?.value
}
get status() {
const {resultScore} = this.props.scoredData.value || {}
if (resultScore === 1) {
return CORRECT
}
if (resultScore <= 0) {
return INCORRECT
}
return UNKNOWN
}
get userResponse() {
const {userResponse, resultScore} = this.props.scoredData.value || {}
const userResponseArray = Array.isArray(userResponse) ? userResponse : [userResponse]
return userResponseArray?.map(response => {
if (response?.correct == null) {
return {
...response,
correct: this.status,
}
}
return {
...response,
correct: resultScore != null ? (response.correct ? CORRECT : INCORRECT) : UNKNOWN,
}
})
}
getFeedbackRendering(hotspot) {
const {type} = hotspot
if (!this.correctAnswers.length) {
return Function.prototype
}
switch (type) {
case 'oval':
return this.renderScoringDataFeedbackOval
case 'polygon':
return this.renderScoringDataFeedbackPolygon
default:
return this.renderScoringDataFeedbackSquare
}
}
renderScoringDataFeedbackPolygon = (currentCoordinates, flipVertically) => (
<FeedbackPopover
coordinates={(flipVertically ? maxBy : minBy)('y', currentCoordinates)}
flipVertically={flipVertically}
/>
)
renderScoringDataFeedbackOval = ([{x: x1, y: y1}, {x: x2, y: y2}], flipVertically) => (
<FeedbackPopover
coordinates={{
x: (x1 + x2) * 0.5,
y: (flipVertically ? Math.max : Math.min)(y1, y2),
}}
flipVertically={flipVertically}
/>
)
renderScoringDataFeedbackSquare = ([{x: x1, y: y1}, {x: x2, y: y2}], flipVertically) => (
<FeedbackPopover
coordinates={{
x: (x1 + x2) * 0.5,
y: (flipVertically ? Math.max : Math.min)(y1, y2),
}}
flipVertically={flipVertically}
/>
)
renderUserResponseFeedback = (coordinates, flipVertically, status) => {
let text
if (!coordinates) {
const {userResponse} = this.props.scoredData.value || {}
const responseHidden = typeof userResponse === 'undefined'
return <NoResponse responseHidden={responseHidden} status={this.status} />
}
switch (status) {
case CORRECT:
text = t('Selected Answer - Correct')
break
case INCORRECT:
text = t('Selected Answer - Incorrect')
break
case UNKNOWN:
text = t('Selected Answer')
}
return (
<FeedbackPopover
coordinates={coordinates}
flipVertically={flipVertically}
text={text}
status={status}
targetContent={
<div css={this.props.styles.userResponseWrapper}>
<div css={this.props.styles.userResponseTarget} />
</div>
}
/>
)
}
prepareHotspotsWithFeedback = () => {
if (!this.correctAnswers?.length) {
return this.userResponse?.map(() => ({
coordinates: [],
drawingType: getDrawingType(),
renderScoringDataFeedback: Function.prototype,
}))
}
return this.correctAnswers?.map(hotspot => ({
coordinates: hotspot.coordinates,
drawingType: getDrawingType(hotspot.type),
renderScoringDataFeedback: this.getFeedbackRendering(hotspot),
}))
}
render() {
const hotspots = this.prepareHotspotsWithFeedback() || []
return (
<ItemBodyWrapper itemBody={this.props.itemBody}>
<TargetContainer
readOnly
results
hotspots={hotspots}
url={this.props.interactionData.imageUrl}
userResponseCoordinate={this.userResponse}
renderUserResponseFeedback={this.renderUserResponseFeedback}
/>
</ItemBodyWrapper>
)
}
}