@instructure/quiz-interactions
Version:
A React UI component Library for quiz interaction types.
185 lines (159 loc) • 5.88 kB
JavaScript
/* 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