UNPKG

@instructure/quiz-interactions

Version:

A React UI component Library for quiz interaction types.

185 lines (159 loc) • 5.88 kB
/* eslint-disable react/jsx-no-bind */ import React, {useState, useEffect, useRef, useContext, useCallback} from 'react' import PropTypes from 'prop-types' import {ScreenReaderContent} from '@instructure/ui-a11y-content' import {ScientificNumberInput} from '@instructure/quiz-number-input/components/ScientificNumberInput' import {Decimal} from '@instructure/quiz-i18n/util/Decimal' import {ItemBodyWrapper} from '@instructure/quiz-rce/components/ItemBodyWrapper/index' import {RichContentRenderer} from '@instructure/quiz-rce/components/RichContentRenderer/index' import {View} from '@instructure/ui-view' import t from '@instructure/quiz-i18n/format-message' import {Text} from '@instructure/ui-text' import {ApplyLocaleContext} from '@instructure/ui-i18n' import {parseSeparators} from '@instructure/quiz-common/util/parseSeparators' import {Alert} from '@instructure/ui-alerts' /** --- category: Numeric --- Numeric Take component ```jsx_example function Example (props) { const exampleProps = { itemBody: 'x is an integer and 9 < x^2 < 99. What\'s the max value of x, minus the minimum value of x', userResponse: { value: '18' } } return ( <NumericTake {...exampleProps} {...props} /> ) } <SettingsSwitcher locales={LOCALES}> <TakeStateProvider> <Example /> </TakeStateProvider> </SettingsSwitcher> ``` **/ const invalidMessage = { text: t('Answer must be a number. Remove any symbols or units.'), type: 'error', } function NumericTake({ displayValidationWarning = false, itemBody, handleResponseUpdate, userResponse = {value: null}, separatorConfig, }) { const context = useContext(ApplyLocaleContext) const [value, setValue] = useState(null) const [normalized, setNormalized] = useState(null) const [messages, setMessages] = useState([]) const [showSeparatorChangeWarning, setShowSeparatorChangeWarning] = useState(false) const [separatorConfigInitialized, setSeparatorConfigInitialized] = useState(false) const [valueInitialized, setValueInitialized] = useState(false) const hasNormalizedAfterInitialization = useRef(false) // Set up decimal delimiters on mount useEffect(() => { Decimal.accountSettingDelimiters = separatorConfig ? parseSeparators(separatorConfig) : null setSeparatorConfigInitialized(true) }, [separatorConfig]) // Update initial value using the updated locale settings useEffect(() => { if (!separatorConfigInitialized || valueInitialized) { return } try { const newValue = Decimal.toLocaleStringIfValid(userResponse.value, context.locale) setValue(newValue) } catch { setValue(userResponse.value) } setValueInitialized(true) }, [context.locale, valueInitialized, separatorConfigInitialized, userResponse.value]) const updateLogs = (currentValue, currentNormalized) => { // If the response isn't a number, call the handler with the raw response const invalid = displayValidationWarning && currentValue && currentNormalized == null const update = currentNormalized == null ? currentValue : currentNormalized if (update !== userResponse.value) { handleResponseUpdate(update, null, invalid) } } const checkInvalid = useCallback( (currentValue, currentNormalized) => { if (displayValidationWarning && currentValue && currentNormalized == null) { setMessages([invalidMessage]) } }, [displayValidationWarning], ) const handleInitialNormalization = currentNormalized => { setNormalized(currentNormalized) if (!hasNormalizedAfterInitialization.current) { checkInvalid(value, currentNormalized) if (displayValidationWarning && value && currentNormalized == null) { const update = currentNormalized == null ? value : currentNormalized handleResponseUpdate(update, null, true) } } hasNormalizedAfterInitialization.current = true } const handleResponseChange = (_event, newValue, newNormalized) => { setValue(newValue) setNormalized(newNormalized) setMessages([]) updateLogs(newValue, newNormalized) } const handleResponseBlur = (_event, hasFormattingChanged) => { checkInvalid(value, normalized) updateLogs(value, normalized) setShowSeparatorChangeWarning(hasFormattingChanged) } if (!separatorConfigInitialized || !valueInitialized) { return null } return ( <ItemBodyWrapper itemBody={itemBody}> <div className="fs-mask"> <ScientificNumberInput inputType="text" messages={messages} renderLabel={ <View> <Text aria-hidden={true}>{t('Answer')}</Text> <ScreenReaderContent> <RichContentRenderer content={itemBody} /> </ScreenReaderContent> </View> } onChange={handleResponseChange} onBlur={handleResponseBlur} value={value} onInitialNormalization={handleInitialNormalization} autoComplete="off" /> {showSeparatorChangeWarning && ( <Alert variant="info" hasShadow={false} transition="none"> {t( 'Decimal separator auto-adjusted. Verify your entry! (correct format: e.g. {example})', {example: Decimal.toLocaleString('1000.12', context.locale)}, )} </Alert> )} </div> </ItemBodyWrapper> ) } NumericTake.propTypes = { displayValidationWarning: PropTypes.bool, itemBody: PropTypes.string.isRequired, handleResponseUpdate: PropTypes.func.isRequired, userResponse: PropTypes.shape({ value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), }), separatorConfig: PropTypes.shape({ decimalSeparator: PropTypes.string, thousandSeparator: PropTypes.string, }), } export default NumericTake