@instructure/quiz-interactions
Version:
A React UI component Library for quiz interaction types.
382 lines (338 loc) • 12.8 kB
JavaScript
/** @jsx jsx */
import {Component} from 'react'
import PropTypes from 'prop-types'
import {Table} from '@instructure/ui-table'
import {Text} from '@instructure/ui-text'
import {Spinner} from '@instructure/ui-spinner'
import {Checkbox} from '@instructure/ui-checkbox'
import {PresentationContent, ScreenReaderContent} from '@instructure/ui-a11y-content'
import {Button} from '@instructure/ui-buttons'
import {jsx} from '@instructure/emotion'
import {View} from '@instructure/ui-view'
import {Grid} from '@instructure/ui-grid'
import {Decimal} from '@instructure/quiz-i18n/util/Decimal'
import {isScientificNotation} from '@instructure/quiz-scientific-notation'
import {toErrors} from '../../../util/instUIMessages'
import * as util from './util'
import generateStyle from './styles'
import generateComponentTheme from './theme'
import t from '@instructure/quiz-i18n/format-message'
import {NumberInput} from '@instructure/quiz-number-input/components/NumberInput/index'
import {SimpleSelect} from '@instructure/quiz-common/components/SimpleSelect/index'
import {TextArea} from '@instructure/quiz-common/components/TextArea/index'
import {withStyleOverrides} from '@instructure/quiz-common/util/withStyleOverrides'
import {Alert} from '@instructure/ui-alerts'
(generateStyle, generateComponentTheme)
export default class FormulaSection extends Component {
static displayName = 'FormulaSection'
static componentId = `Quizzes${this.displayName}`
static propTypes = {
formulaErrors: PropTypes.arrayOf(PropTypes.string),
generatedSolutionsErrors: PropTypes.arrayOf(PropTypes.string),
handleAnswerCountChange: PropTypes.func.isRequired,
handleAnswerPrecisionChange: PropTypes.func.isRequired,
handleFormulaChange: PropTypes.func.isRequired,
handleGenerateSolutions: PropTypes.func.isRequired,
handleMarginOfErrorChange: PropTypes.func.isRequired,
handleMarginOfErrorTypeChange: PropTypes.func.isRequired,
handleScientificNotationChange: PropTypes.func.isRequired,
locale: PropTypes.string.isRequired,
overrideEditableForRegrading: PropTypes.bool.isRequired,
scoringData: PropTypes.object.isRequired,
status: PropTypes.string.isRequired,
styles: PropTypes.object,
}
static defaultProps = {
formulaErrors: void 0,
generatedSolutionsErrors: void 0,
}
static contextTypes = {
disableTextAreaAutoGrow: PropTypes.bool,
}
state = {
marginOfErrorValue: null,
}
handleMarginOfErrorChange = (event, value, normalizedValue) => {
this.setState({marginOfErrorValue: value})
this.props.handleMarginOfErrorChange(event, value, normalizedValue)
}
handleMarginOfErrorBlur = () => {
this.setState({marginOfErrorValue: null})
}
formatNumStr(value) {
return isScientificNotation(value)
? this.localizedScientificNotation(value)
: this.localizedValue(value)
}
localizedScientificNotation = value => {
const [mantissa, exponent] = value?.toString().split('*') || []
return `${this.localizedValue(mantissa)}*${exponent}`
}
localizedValue = value => {
return value ? Decimal.toLocaleString(value.toString(), this.props.locale) : null
}
get variableNames() {
return this.props.scoringData.value.variables.map(variable => variable.name).sort()
}
renderRow = (solution, idx) => {
const inputValues = this.variableNames.map(variableName => {
return solution.inputs.find(v => v.name === variableName)?.value
})
const {margin, marginType} = this.props.scoringData.value.numeric
const parsedMargin = Number.parseFloat(margin)
let marginString
if (margin === '' || parsedMargin === 0 || Number.isNaN(parsedMargin)) {
marginString = null
} else if (marginType === 'absolute') {
marginString = t(' +/- {margin}', {margin: this.formatNumStr(parsedMargin)})
} else {
marginString = t(' +/- {margin}%', {margin: this.formatNumStr(parsedMargin)})
}
return (
<Table.Row key={idx}>
{inputValues.map((value, index) => (
// eslint-disable-next-line react/no-array-index-key
<Table.Cell key={index}>{this.formatNumStr(value)}</Table.Cell>
))}
<Table.Cell>
{this.formatNumStr(solution.output)}
<span css={this.props.styles.marginPlusMinus}>{marginString}</span>
</Table.Cell>
</Table.Row>
)
}
renderGeneratedSolutionsTable() {
if (this.props.status === util.STATUS_RUNNING) {
return (
<div>
<Spinner renderTitle={t('Running')} />
</div>
)
}
const solutions = this.props.scoringData.value.generatedSolutions
let errorMessage = null
if (this.props.status === util.STATUS_FAILED && solutions.length === 0) {
return (
<div role="alert">
<Alert hasShadow={false} variant="warning">
{t('We were not able to find any solutions.')}
</Alert>
</div>
)
}
if (this.props.status === util.STATUS_FAILED) {
errorMessage = util.buildSolutionsGeneratedMessage(this.props.status, solutions.length)
} else if (solutions.length === 0) {
return null
}
const idPrefix = 'generated-results-'
return (
<div>
{errorMessage && (
<div role="alert">
<Alert hasShadow={false} variant="warning">
{errorMessage}
</Alert>
</div>
)}
<div css={this.props.styles.tableWrapper}>
<Table caption={t('Generated Results')} layout="fixed">
<Table.Head>
<Table.Row>
{/* eslint-disable react/no-array-index-key */}
{this.variableNames.map((variableName, idx) => (
<Table.ColHeader id={idPrefix + idx} key={idx}>
{variableName}
</Table.ColHeader>
))}
{/* eslint-enable react/no-array-index-key */}
<Table.ColHeader id="generated-results-result">{t('Result')}</Table.ColHeader>
</Table.Row>
</Table.Head>
<Table.Body>{solutions.map(this.renderRow)}</Table.Body>
</Table>
</div>
</div>
)
}
renderGeneratedSolutionsSection() {
if (this.props.status === util.STATUS_CANCELED) {
return null
}
return (
<div css={this.props.styles.generatedSolutions}>
{this.props.status === util.STATUS_FORMULA_SETUP_INVALID ? (
<Alert liveRegionPoliteness="polite" variant="warning">
{t('Error in formula setup. See above for details.')}
</Alert>
) : (
this.renderGeneratedSolutionsTable()
)}
</div>
)
}
formulaErrors() {
if (this.props.status === util.STATUS_FORMULA_SETUP_INVALID) {
return toErrors(this.props.formulaErrors || [])
}
}
renderNumberOfGeneratedSolutionsInput() {
const currentSolutionsNumber = String(
Number.parseInt(this.props.scoringData.value.answerCount, 10) || 0,
)
return (
<NumberInput
disabled={this.props.overrideEditableForRegrading}
value={currentSolutionsNumber}
onChange={this.props.handleAnswerCountChange}
renderLabel={t('Number of solutions')}
isRequired={true}
messages={toErrors(this.props.generatedSolutionsErrors)}
min="1"
max="200"
showArrows
data-automation="sdk-number-of-solutions-input"
aria-valuetext={`${currentSolutionsNumber} ${t('Solutions possible')}`}
/>
)
}
renderSolutionPrecisionInput() {
const currentDecimalPlaces = this.props.scoringData.value.answerPrecision || 0
return (
<NumberInput
decimalPrecision={0}
disabled={this.props.overrideEditableForRegrading}
value={currentDecimalPlaces}
onChange={this.props.handleAnswerPrecisionChange}
renderLabel={t('Decimal places')}
min="0"
max="16"
showArrows
aria-valuetext={`${currentDecimalPlaces} ${t('Decimal places')}`}
/>
)
}
renderScientificNotationCheckbox() {
return (
<Checkbox
checked={this.props.scoringData.value.scientificNotation || false}
disabled={this.props.overrideEditableForRegrading}
label={t('Display as Scientific Notation')}
onChange={this.props.handleScientificNotationChange}
variant="toggle"
/>
)
}
renderMarginOfErrorTypeSelect() {
return (
<SimpleSelect
onChange={this.props.handleMarginOfErrorTypeChange}
value={this.props.scoringData.value.numeric.marginType}
renderLabel={t('Margin type')}
data-automation="sdk-formula-margin-of-error-type"
>
<SimpleSelect.Option id="formula-section-select-option-absolute" value="absolute">
{t('Absolute')}
</SimpleSelect.Option>
<SimpleSelect.Option id="formula-section-select-option-percent" value="percent">
{t('Percent')}
</SimpleSelect.Option>
</SimpleSelect>
)
}
renderMarginOfErrorInput() {
let value = this.state.marginOfErrorValue
if (value === null) {
value = Number.parseFloat(this.props.scoringData.value.numeric.margin)
}
return (
<NumberInput
value={value}
onChange={this.handleMarginOfErrorChange}
onBlur={this.handleMarginOfErrorBlur}
renderLabel={t('+/- margin of error')}
min="0"
data-automation="sdk-formula-margin-of-error-value"
showArrows
aria-valuetext={`${value} ${t('+/- margin of error')}`}
/>
)
}
renderGenerateSolutionsButton() {
return (
<Button
type="submit"
disabled={this.props.overrideEditableForRegrading}
color="primary"
onClick={this.props.handleGenerateSolutions}
data-automation="sdk-generate-button"
>
<PresentationContent>{t('Generate')}</PresentationContent>
<ScreenReaderContent>{t('Generate Solutions')}</ScreenReaderContent>
</Button>
)
}
render() {
return (
<div>
<div css={this.props.styles.sectionHeading}>
<Text size="large">{t('Formula Definition')}</Text>
</div>
<div css={this.props.styles.instructions}>
<Text color="primary">
{t(
'Next, write the formula or formulas used to compute' +
' the correct answer. Use the same variable names listed above. (e.g., "5 + x")',
)}
</Text>
</div>
<TextArea
disabled={this.props.overrideEditableForRegrading}
value={this.props.scoringData.value.formula}
onChange={this.props.handleFormulaChange}
messages={this.formulaErrors()}
label={<ScreenReaderContent>{t('Formula')}</ScreenReaderContent>}
autoGrow={this.context.disableTextAreaAutoGrow ? false : null}
data-automation="sdk-formula-definition-text-area"
/>
<div>
<div css={this.props.styles.sectionHeading}>
<Text size="large">{t('Generate Possible Solutions')}</Text>
</div>
<div css={this.props.styles.instructions}>
<Text color="primary">
{t(
'Finally, build as many variable-solution combinations as you need for your quiz.',
)}
</Text>
</div>
<div css={this.props.styles.generateSolutionsInput}>
<Grid vAlign="top" startAt="medium">
<Grid.Row>
<Grid.Col width={4}>{this.renderNumberOfGeneratedSolutionsInput()}</Grid.Col>
<Grid.Col width={4}>{this.renderSolutionPrecisionInput()}</Grid.Col>
<Grid.Col width={4}>
{/* HACK: Margin and padding to align the checkbox with inputs on the same row.
Find a better way to do this when INSTUI-1980 lands */}
<View as="div" margin="medium 0 0 0" padding="xx-small 0 0 0">
{this.renderScientificNotationCheckbox()}
</View>
</Grid.Col>
</Grid.Row>
{!this.props.scoringData.value.scientificNotation && (
<Grid.Row>
<Grid.Col width={4}>{this.renderMarginOfErrorTypeSelect()}</Grid.Col>
<Grid.Col width={4}>{this.renderMarginOfErrorInput()}</Grid.Col>
</Grid.Row>
)}
<Grid.Row>
<Grid.Col width={4}>{this.renderGenerateSolutionsButton()}</Grid.Col>
</Grid.Row>
</Grid>
</div>
{this.renderGeneratedSolutionsSection()}
</div>
</div>
)
}
}