UNPKG

@instructure/quiz-interactions

Version:

A React UI component Library for quiz interaction types.

186 lines (158 loc) • 5.44 kB
/** @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