UNPKG

@instructure/quiz-interactions

Version:

A React UI component Library for quiz interaction types.

382 lines (338 loc) • 12.8 kB
/** @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' @withStyleOverrides(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> ) } }