@instructure/quiz-interactions
Version:
A React UI component Library for quiz interaction types.
186 lines (158 loc) • 5.44 kB
JavaScript
/** @jsx jsx */
import {useLayoutEffect, useRef} from 'react'
import PropTypes from 'prop-types'
import striptags from 'striptags'
import {htmlDecode} from 'htmlencode'
import {Text} from '@instructure/ui-text'
import {jsx} from '@instructure/emotion'
import XRegExp from 'xregexp'
import t from '@instructure/quiz-i18n/format-message'
import {IconWarningSolid} from '@instructure/ui-icons'
import {View} from '@instructure/ui-view'
// XRegExp supports unicode so we can use the codepoints for all {L}etters and {N}umbers
// See "Unicode Categories" here https://www.regular-expressions.info/unicode.html
const WORD_REGEX = XRegExp("[-_'\\p{L}\\p{N}]+", 'gu')
export const countWords = content => {
if (!content) {
return 0
}
const matches = content.match(WORD_REGEX)
return matches ? matches.length : 0
}
export const getRceEssayContent = essay => htmlDecode(striptags(essay))
export const showWordCount = (wordCount, rce, isEditing) => {
if (!wordCount) return false
if (rce && isEditing) return false
return true
}
function WordCountComponent({
essay,
rce,
isEditing,
wordCount,
wordLimitEnabled,
wordLimitMin,
wordLimitMax,
notifyScreenreader,
}) {
const isFirstRender = useRef(true)
const prevPropsRef = useRef({essay, rce, wordLimitMin, wordLimitMax, wordLimitEnabled})
const count = (props = {essay, rce}) =>
countWords(props.rce ? getRceEssayContent(props.essay) : props.essay)
const isOverWordLimit = (props = {wordLimitEnabled, wordLimitMax, essay, rce}) => {
return (
props.wordLimitEnabled && props.wordLimitMax !== null && count(props) > props.wordLimitMax
)
}
const getErrorMessage = (props = {wordLimitMin, wordLimitEnabled, wordLimitMax, essay, rce}) => {
const minError = props.wordLimitMin && count(props) < props.wordLimitMin
const maxError = isOverWordLimit(props)
const countError = props.wordLimitEnabled && (minError || maxError)
const minErrorText = t(
`{wordLimitMin, plural,
=1 {Must use at least # word}
other {Must use at least # words}}`,
{wordLimitMin: props.wordLimitMin},
)
const maxErrorText = t('Maximum words exceeded!')
const errorText = count(props) < props.wordLimitMin ? minErrorText : maxErrorText
return countError ? errorText : ''
}
useLayoutEffect(() => {
if (isFirstRender.current) {
isFirstRender.current = false
prevPropsRef.current = {essay, rce, wordLimitMin, wordLimitMax, wordLimitEnabled}
return
}
const currentProps = {essay, rce, wordLimitMin, wordLimitMax, wordLimitEnabled}
const prevProps = prevPropsRef.current
const errorMessage = getErrorMessage(prevProps)
const nextErrorMessage = getErrorMessage(currentProps)
const errorMessageChanged = errorMessage !== nextErrorMessage
const overLimit = nextErrorMessage && isOverWordLimit(currentProps)
const wordCountChangedAndOverLimit = overLimit && count(prevProps) !== count(currentProps)
if (
notifyScreenreader &&
(errorMessageChanged || wordCountChangedAndOverLimit) &&
nextErrorMessage
) {
notifyScreenreader(nextErrorMessage)
}
prevPropsRef.current = currentProps
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [essay, wordLimitMin, wordLimitMax, wordLimitEnabled, rce])
const getLimitsText = () => {
if (wordLimitEnabled) {
const limits = []
if (wordLimitMin) {
limits.push(t('{wordLimitMin} min', {wordLimitMin: wordLimitMin}))
}
if (wordLimitMax) {
limits.push(t('{wordLimitMax} max', {wordLimitMax: wordLimitMax}))
}
const limit = limits.join(' / ')
return (
<Text color="secondary" size="small">
{limit}{' '}
<Text color="primary" size="small">
{t('(word limit)')}
</Text>
</Text>
)
}
}
const renderWordCount = () => {
const maxError = isOverWordLimit()
return (
<View as="span" padding="0 space12 0 0">
<Text
size="small"
color={wordLimitEnabled && maxError ? 'danger' : 'primary'}
weight="bold"
>
{t('{wordcount, plural, =1 {# word} other {# words}}', {
wordcount: count(),
})}
</Text>
</View>
)
}
const maxError = isOverWordLimit()
return (
<View className="fs-mask" textAlign="end">
{showWordCount(wordCount, rce, isEditing) && renderWordCount()}
{getLimitsText()}
<View>
<Text color={maxError ? 'danger' : null} size="small">
{maxError && <IconWarningSolid />}{' '}
<View display="inline-block" margin="0 0 0 xxx-small">
{getErrorMessage()}
</View>
</Text>
</View>
</View>
)
}
WordCountComponent.displayName = 'WordCount'
WordCountComponent.componentId = 'QuizzesWordCount'
WordCountComponent.propTypes = {
essay: PropTypes.string,
rce: PropTypes.bool,
isEditing: PropTypes.bool,
wordCount: PropTypes.bool,
wordLimitEnabled: PropTypes.bool,
wordLimitMin: PropTypes.number,
wordLimitMax: PropTypes.number,
notifyScreenreader: PropTypes.func,
}
WordCountComponent.defaultProps = {
essay: void 0,
rce: void 0,
isEditing: true,
wordCount: void 0,
wordLimitEnabled: void 0,
wordLimitMin: void 0,
wordLimitMax: void 0,
notifyScreenreader: void 0,
}
export const WordCount = WordCountComponent