UNPKG

@instructure/quiz-grading

Version:

The Quiz React SDK by Instructure Inc.

469 lines (416 loc) • 14.2 kB
/** @jsx jsx */ import {Component} from 'react' import PropTypes from 'prop-types' import ImmutablePropTypes from 'react-immutable-proptypes' import {Map} from 'immutable' import debounce from 'lodash/debounce' import { IconArrowOpenEndLine, IconArrowOpenStartLine, IconCheckLine, IconDiscussionLine, IconDiscussionSolid, IconXLine, } from '@instructure/ui-icons' import {Button} from '@instructure/ui-buttons' import NumberInput from '@instructure/quiz-number-input/components/NumberInput/index' import {Text} from '@instructure/ui-text' import {View} from '@instructure/ui-view' import {PresentationContent, ScreenReaderContent} from '@instructure/ui-a11y-content' import canvas from '@instructure/ui-themes' import {jsx} from '@instructure/emotion' import t from '@instructure/quiz-i18n/format-message' import {Flex} from '@instructure/quiz-common/components/Flex/index' import {HideableButton} from '@instructure/quiz-common/components/HideableButton/index' import { GRADE_BY_QUESTION_PREVIOUS, GRADE_BY_QUESTION_NEXT, } from '@instructure/quiz-common/constants' import {withStyleOverrides} from '@instructure/quiz-common/util/withStyleOverrides' import {RichContentInput} from '@instructure/quiz-rce/components/RichContentInput/index' import generateStyle from './styles' import generateComponentTheme from './theme' export class BaseGradingBar extends Component { static displayName = 'GradingBar' static componentId = `Quizzes${this.displayName}` static propTypes = { activeItemId: PropTypes.string, gradeByQuestionEnabled: PropTypes.bool, graderFeedback: ImmutablePropTypes.map.isRequired, handleChangeGradeStatus: PropTypes.func.isRequired, handleChangePoints: PropTypes.func.isRequired, isAutogradeable: PropTypes.bool.isRequired, isEditing: PropTypes.bool.isRequired, isSubjective: PropTypes.bool.isRequired, itemResultsModifications: ImmutablePropTypes.map.isRequired, postToParent: PropTypes.func.isRequired, regradeToggleAction: PropTypes.func.isRequired, renderUpdateButton: PropTypes.func.isRequired, sessionItem: ImmutablePropTypes.record.isRequired, sessionItemResult: ImmutablePropTypes.record.isRequired, startFeedbackEdit: PropTypes.func.isRequired, submitFeedbackEdit: PropTypes.func.isRequired, updateFeedbackEdit: PropTypes.func.isRequired, cancelFeedbackEdit: PropTypes.func.isRequired, styles: PropTypes.object, regradingFromSpeedGrader: PropTypes.bool, isSurvey: PropTypes.bool.isRequired, } static defaultProps = { activeItemId: null, gradeByQuestionEnabled: false, } regradeButtonRef = null feedbackButtonRef = null _timeouts = [] constructor(props) { super(props) let points = this.props.sessionItemResult.getWorkingInstance().score if (typeof points === 'number' || typeof points === 'string') { points = Number(points) } else { points = null } this.state = { points, normalizedPoints: points, } } // ============= // EXTERNALLY USED UTILS // ============= focusRegradeButton = () => { this.regradeButtonRef && this.regradeButtonRef.focus() } focusFeedbackButton = () => { this.feedbackButtonRef && this.feedbackButtonRef.focus() } // ============= // INTERNAL UTILS // ============= get regradeInfo() { return this.props.sessionItemResult.regradeInfo } hasRegradeInfo() { return this.regradeInfo && this.regradeInfo.size > 1 } pointsPossibleDescription(pointsPossible) { return t( `{ points, plural, one {# point} other {# points} }`, {points: pointsPossible}, ) } getGradeStatus() { const resultMods = this.props.itemResultsModifications || Map() const workingGradeStatus = resultMods.getIn(['scoredData', 'gradeStatus']) if (workingGradeStatus) { return workingGradeStatus } return this.props.sessionItemResult.getIn(['scoredData', 'gradeStatus']) } // ============= // HANDLERS // ============= updatePoints = debounce( () => { this.props.handleChangePoints(this.state.normalizedPoints) }, 250, {leading: true, trailing: true}, ) handleChangePoints = (event, points, normalizedPoints) => { const parsedPoints = parseFloat(normalizedPoints) || 0 this.setState({points, normalizedPoints: parsedPoints}, this.updatePoints) const gradeStatus = this.getGradeStatus() if (!gradeStatus || gradeStatus === 'waiting') { this.props.handleChangeGradeStatus('graded') } } handleBlurPoints = () => { this.updatePoints.flush() this.setState(({normalizedPoints}) => { return {points: normalizedPoints || 0} }) } handleStatusCorrectToggle = () => { const newPoints = +this.props.sessionItemResult.pointsPossible this.props.handleChangePoints(newPoints) this.setState({ points: `${newPoints}`, normalizedPoints: `${newPoints}`, }) const newStatus = this.getGradeStatus() === 'correct' ? 'graded' : 'correct' this.props.handleChangeGradeStatus(newStatus, newStatus === 'graded') } handleStatusIncorrectToggle = () => { this.props.handleChangePoints(0) this.setState({ points: '0', normalizedPoints: '0', }) const newStatus = this.getGradeStatus() === 'incorrect' ? 'graded' : 'incorrect' this.props.handleChangeGradeStatus(newStatus, newStatus === 'graded') } previousStudent = e => { e.preventDefault() this.props.postToParent({subject: GRADE_BY_QUESTION_PREVIOUS}) } nextStudent = e => { e.preventDefault() this.props.postToParent({subject: GRADE_BY_QUESTION_NEXT}) } jumpToSidebar = e => { e.preventDefault() const sidebarWrapper = document.getElementById('sidebar-wrapper') if (sidebarWrapper && sidebarWrapper.focus) { sidebarWrapper.focus() } } handleRegradeButtonRef = node => { this.regradeButtonRef = node } handleFeedbackButtonRef = node => { this.feedbackButtonRef = node } // ============= // RENDERING // ============= renderToSidebarButton() { return ( <Flex justifyItems="end" margin="small medium"> <HideableButton href="#" onClick={this.jumpToSidebar}> {t('Jump to Question Navigator')} </HideableButton> </Flex> ) } renderSubjectiveButtons() { if (this.props.isSubjective) { const correctVariant = this.getGradeStatus() === 'correct' ? 'success' : 'primary-inverse' const correctText = this.getGradeStatus() === 'correct' ? t('toggle correct off') : t('toggle correct on') const incorrectVariant = this.getGradeStatus() === 'incorrect' ? 'danger' : 'primary-inverse' const incorrectText = this.getGradeStatus() === 'incorrect' ? t('toggle incorrect off') : t('toggle incorrect on') return ( <div css={this.props.styles.subjectiveButtonsWrapper}> <Button color={correctVariant} onClick={this.handleStatusCorrectToggle} renderIcon={IconCheckLine} > <ScreenReaderContent>{correctText}</ScreenReaderContent> </Button> <Button color={incorrectVariant} margin="0 xx-small" onClick={this.handleStatusIncorrectToggle} renderIcon={IconXLine} > <ScreenReaderContent>{incorrectText}</ScreenReaderContent> </Button> </div> ) } } renderFeedbackSection() { const workingSessionItemResult = this.props.sessionItemResult.getWorkingInstance() const graderFeedbackContent = workingSessionItemResult.getIn( ['feedback', 'graderFeedback', 'content'], '', ) let icon, text if (graderFeedbackContent) { icon = IconDiscussionSolid text = t('Edit Grader Level Feedback (present)') } else { icon = IconDiscussionLine text = t('Edit Grader Level Feedback') } return ( <Button ref={this.handleFeedbackButtonRef} onClick={this.props.startFeedbackEdit} renderIcon={icon} data-automation="sdk-grading-edit-feedback-button" > <ScreenReaderContent>{text}</ScreenReaderContent> </Button> ) } renderRegradeInfo() { return ( <div css={this.props.styles.regradeParent}> <div css={this.props.styles.originalScore}> <Text color="secondary"> {t( `Previous score { possible, plural, one {{points, number} / # point} other {{points, number} / # points} }`, { points: this.regradeInfo.get('originalScore'), possible: this.regradeInfo.get('originalPointsPossible'), }, )} </Text> <PresentationContent> <div css={this.props.styles.divider}> <Text color="secondary">{'|' /* eslint-disable-line react/jsx-no-literals */}</Text> </div> </PresentationContent> </div> <div css={this.props.styles.regradeText}> <Text color="alert">{t('Regrade score:')}</Text> </div> {this.renderNumberInput()} <Text color="alert">{ // eslint-disable-next-line react/jsx-no-literals `/ ${this.pointsPossibleDescription(this.regradeInfo.get('newPointsPossible'))}` }</Text> </div> ) } renderNumberInput() { return ( <div css={this.props.styles.points}> <NumberInput placeholder="--" renderLabel={<ScreenReaderContent>{t('Edit Score')}</ScreenReaderContent>} onChange={this.handleChangePoints} onBlur={this.handleBlurPoints} value={this.state.points} width="6rem" data-automation="sdk-grading-edit-score-input" /> </div> ) } renderDefaultNumberInput() { return ( <div css={this.props.styles.defaultNumberInput}> {this.renderNumberInput()} <span css={this.props.styles.pointsPossible}>{ // eslint-disable-next-line react/jsx-no-literals `/ ${this.pointsPossibleDescription(this.props.sessionItemResult.pointsPossible)}` }</span> </div> ) } renderRegradeButton() { if ( !this.props.isAutogradeable || !this.props.regradingFromSpeedGrader || this.props.isSurvey ) { return null } return ( <div css={this.props.styles.regradeButtonWrapper}> <Button margin="0 small" onClick={this.props.regradeToggleAction} ref={this.handleRegradeButtonRef} data-automation="sdk-regrade-request-button" > {t.rich('Regrade <0>question {position, number}</0>', { 0: ({children}) => <ScreenReaderContent key="0">{children}</ScreenReaderContent>, position: this.props.sessionItem.position, })} </Button> </div> ) } renderEditGraderFeedback() { if (!this.props.isEditing) { return } const textareaId = `edit_grader_feedback_${this.props.sessionItem.id}` return ( <div css={this.props.styles.editFeedback}> <label htmlFor="edit_grader_feedback">{t('Additional Comments')}</label> <RichContentInput label="" defaultContent={this.props.graderFeedback.get('content')} onKeyUp={this.props.updateFeedbackEdit} onChange={this.props.updateFeedbackEdit} textareaId={textareaId} /> <div className="editFeedbackButtons" css={this.props.styles.editFeedbackButtons}> <Button size="small" margin="x-small" onClick={this.props.cancelFeedbackEdit} data-automation="sdk-grading-edit-feedback-cancel" > {t('Cancel')} </Button> <Button color="primary" size="small" margin="x-small" onClick={this.props.submitFeedbackEdit} data-automation="sdk-grading-edit-feedback-submit" > {t('Done')} </Button> </div> </div> ) } renderGradeByQuestionControls() { const {activeItemId, gradeByQuestionEnabled, sessionItem} = this.props if (gradeByQuestionEnabled && sessionItem.itemRecord.id === activeItemId) { return ( <Flex justifyItems="end" margin="small medium"> {this.props.renderUpdateButton({margin: '0 xx-small'})} <Button margin="0 xx-small" href="#" onClick={this.previousStudent}> <Flex alignItems="center"> <IconArrowOpenStartLine /> <View margin="0 0 0 x-small">{t('Previous Student')}</View> </Flex> </Button> <Button margin="0 xx-small" href="#" onClick={this.nextStudent}> <Flex alignItems="center"> <View margin="0 x-small 0 0">{t('Next Student')}</View> <IconArrowOpenEndLine /> </Flex> </Button> </Flex> ) } } render() { const {isSurvey} = this.props return ( <div> {this.renderEditGraderFeedback()} <View as="div" borderWidth="small none none none" margin="0 medium" borderColor={canvas.colors.contrasts.grey1111} > <Flex as="div" alignItems="center" wrap="wrap"> <div css={this.props.styles.feedbackButtonWrapper}>{this.renderFeedbackSection()}</div> {this.renderRegradeButton()} {!isSurvey && ( <div className="fs-mask" css={this.props.styles.pointsSection}> {this.renderSubjectiveButtons()} {this.hasRegradeInfo() ? this.renderRegradeInfo() : this.renderDefaultNumberInput()} </div> )} </Flex> </View> {this.renderGradeByQuestionControls()} {this.renderToSidebarButton()} </div> ) } } export const GradingBar = withStyleOverrides(generateStyle, generateComponentTheme)(BaseGradingBar) export default GradingBar