@instructure/quiz-interactions
Version:
A React UI component Library for quiz interaction types.
173 lines (143 loc) • 4.2 kB
JavaScript
/** @jsx jsx */
import {useCallback, useEffect, useRef} from 'react'
import PropTypes from 'prop-types'
import {useDrop} from 'react-dnd'
import {Menu} from '@instructure/ui-menu'
import {Text} from '@instructure/ui-text'
import {jsx} from '@instructure/emotion'
import {processNewMathInElem} from '@instructure/quiz-rce/components/RichContentRenderer/mathRenderer'
import BlankAnswer from '../BlankAnswer'
import generateStyle from './styles'
import generateComponentTheme from './theme'
import t from '@instructure/quiz-i18n/format-message'
import {withStyleOverrides} from '@instructure/quiz-common/util/withStyleOverrides'
const ENTER_KEY = 'Enter'
const SPACE_KEY = ' '
const hasAnswerOf = choice => Boolean(choice && choice.id)
function BlankDropTarget(props) {
const {choices, choice, blankId, readOnly, onChange, onDrop, returnChoiceToWordbank, styles} =
props
const blankAnswerRef = useRef(null)
const dragTargetRef = useRef(null)
const parentNodeRef = useRef(null)
const popoverRef = useRef(null)
const hasAnswer = hasAnswerOf(choice)
const prevHasAnswerRef = useRef(hasAnswer)
const [, drop] = useDrop(
() => ({
accept: 'fitb',
drop(item) {
if (onDrop) onDrop(item)
},
}),
[onDrop],
)
useEffect(() => {
const prevHasAnswer = prevHasAnswerRef.current
if (hasAnswer && !prevHasAnswer) {
blankAnswerRef.current?.focus()
} else if (!hasAnswer && prevHasAnswer) {
dragTargetRef.current?.focus()
}
processNewMathInElem(parentNodeRef.current)
prevHasAnswerRef.current = hasAnswer
})
const setParentRef = useCallback(
node => {
parentNodeRef.current = node
drop(node)
},
[drop],
)
const setPopoverRef = useCallback(node => {
popoverRef.current = node
}, [])
function toggleDragDropdown() {
setTimeout(() => {
if (popoverRef.current?._contentElement) {
processNewMathInElem(popoverRef.current._contentElement)
}
})
}
function handleRemoveAnswer() {
returnChoiceToWordbank(choice.id)
}
function handleDragTargetKeyDown(event) {
if (event.key === ENTER_KEY || event.key === SPACE_KEY) {
event.preventDefault()
event.target.click()
}
}
function makeOptionSelectHandler(selectedChoice) {
return function onOptionSelect() {
onChange(selectedChoice)
}
}
function renderAnswer() {
return (
<BlankAnswer
ref={blankAnswerRef}
choiceId={choice.id}
blankId={blankId}
itemBody={choice.itemBody}
onRemoveAnswer={handleRemoveAnswer}
readOnly={readOnly}
/>
)
}
function renderDragTarget() {
return (
<div
css={styles.dragTarget}
onKeyDown={handleDragTargetKeyDown}
ref={dragTargetRef}
role="button"
tabIndex="0"
>
<Text color="secondary">{t('Answer')}</Text>
</div>
)
}
function renderOptions() {
return choices.map(c => (
<Menu.Item key={c.id} value={c.id} onSelect={makeOptionSelectHandler(c)}>
{c.itemBody}
</Menu.Item>
))
}
function renderDragDropdown() {
return (
<Menu trigger={renderDragTarget()} onToggle={toggleDragDropdown} popoverRef={setPopoverRef}>
{renderOptions()}
</Menu>
)
}
return (
<div ref={setParentRef} role="presentation" tabIndex="-1">
{hasAnswer ? renderAnswer() : renderDragDropdown()}
</div>
)
}
BlankDropTarget.displayName = 'BlankDropTarget'
BlankDropTarget.componentId = `Quizzes${BlankDropTarget.displayName}`
BlankDropTarget.propTypes = {
choices: PropTypes.array,
choice: PropTypes.object,
blankId: PropTypes.string.isRequired,
value: PropTypes.string,
readOnly: PropTypes.bool,
onChange: PropTypes.func,
onDrop: PropTypes.func,
returnChoiceToWordbank: PropTypes.func,
styles: PropTypes.object,
}
BlankDropTarget.defaultProps = {
choices: undefined,
choice: undefined,
value: undefined,
readOnly: false,
onChange: undefined,
onDrop: undefined,
returnChoiceToWordbank: undefined,
}
export default withStyleOverrides(generateStyle, generateComponentTheme)(BlankDropTarget)