UNPKG

@instructure/quiz-interactions

Version:

A React UI component Library for quiz interaction types.

264 lines (263 loc) • 12 kB
/** @jsx jsx */ import React, { useEffect, useImperativeHandle, useLayoutEffect, useRef } from 'react'; import PropTypes from 'prop-types'; import { jsx } from '@instructure/emotion'; import { Flex } from '@instructure/quiz-common/components/Flex/index'; import { withStyleOverrides } from '@instructure/quiz-common/util/withStyleOverrides'; import { FormFieldGroup } from '@instructure/quiz-common/components/FormFieldGroup/index'; import { Text } from '@instructure/ui-text'; import ChoiceInput from '../../../common/edit/components/ChoiceInput'; import AnswerInput from '../../../common/edit/components/AnswerInput'; import RemoveChoiceButton from '../../../common/edit/components/RemoveChoiceButton'; import Footer from '../../../common/edit/components/Footer'; import generateStyle from './styles'; import generateComponentTheme from './theme'; import t from '@instructure/quiz-i18n/format-message'; import { normalizeErrors } from '../../../../util/normalizeErrors'; var BaseCategoryForm = /*#__PURE__*/ React.forwardRef(function(props, ref) { var categoriesErrors = props.categoriesErrors, getCategoryErrors = props.getCategoryErrors, categoryScoringData = props.categoryScoringData, disabled = props.disabled, distractors = props.distractors, distractorErrors = props.distractorErrors, notifyScreenreader = props.notifyScreenreader, onCategoryInputChange = props.onCategoryInputChange, onCreateAnswer = props.onCreateAnswer, onCreateCategory = props.onCreateCategory, onDistractorInputChange = props.onDistractorInputChange, onModalClose = props.onModalClose, onModalOpen = props.onModalOpen, onRemoveAnswer = props.onRemoveAnswer, onRemoveCategory = props.onRemoveCategory, sortedCategories = props.sortedCategories, styles = props.styles; var answerRefsRef = useRef({}); var categoryRefsRef = useRef({}); var categoryAnswerInputRefsRef = useRef({}); var answerRemoveHandlersRef = useRef({}); var categoryRemoveHandlersRef = useRef({}); var createAnswerHandlersRef = useRef({}); var isMountedRef = useRef(false); var footerRef = useRef(null); var prevPropsRef = useRef({ onCreateAnswer: onCreateAnswer, onRemoveAnswer: onRemoveAnswer, onRemoveCategory: onRemoveCategory }); useEffect(function() { // focusOnMount is conditional on this component being mounted so that focus // is not stolen on initial render when creating a new categorization question isMountedRef.current = true; }, []); useLayoutEffect(function() { if (prevPropsRef.current.onCreateAnswer !== onCreateAnswer) { createAnswerHandlersRef.current = {}; } if (prevPropsRef.current.onRemoveAnswer !== onRemoveAnswer) { answerRemoveHandlersRef.current = {}; } if (prevPropsRef.current.onRemoveCategory !== onRemoveCategory) { categoryRemoveHandlersRef.current = {}; } prevPropsRef.current = { onCreateAnswer: onCreateAnswer, onRemoveAnswer: onRemoveAnswer, onRemoveCategory: onRemoveCategory }; }, [ onCreateAnswer, onRemoveAnswer, onRemoveCategory ]); // Expose refs to parent component useImperativeHandle(ref, function() { return { footerRef: footerRef.current, categoryRefs: categoryRefsRef.current, categoryAnswerInputRefs: categoryAnswerInputRefsRef.current, answerRefs: answerRefsRef.current }; }); var getCreateAnswerHandler = function(categoryId) { var createAnswerHandler = createAnswerHandlersRef.current[categoryId]; if (createAnswerHandler) return createAnswerHandler; createAnswerHandler = onCreateAnswer.bind(null, categoryId); createAnswerHandlersRef.current[categoryId] = createAnswerHandler; return createAnswerHandler; }; var getAnswerRemoveHandler = function(answerId, categoryId) { var answerRemoveHandler = answerRemoveHandlersRef.current[categoryId]; if (answerRemoveHandler) return answerRemoveHandler; answerRemoveHandler = onRemoveAnswer.bind(null, answerId, categoryId); answerRemoveHandlersRef.current[answerId] = answerRemoveHandler; return answerRemoveHandler; }; var getCategoryRemoveHandler = function(categoryId) { var categoryRemoveHandler = categoryRemoveHandlersRef.current[categoryId]; if (categoryRemoveHandler) return categoryRemoveHandler; categoryRemoveHandler = onRemoveCategory.bind(null, categoryId); categoryRemoveHandlersRef.current[categoryId] = categoryRemoveHandler; return categoryRemoveHandler; }; var handleFooterRef = function(node) { footerRef.current = node; }; // =========== // RENDER // =========== var renderAnswer = function(answer, categoryId, focusOnMount, index) { var onRemoveAnswer = getAnswerRemoveHandler(answer.id, categoryId); return /*#__PURE__*/ jsx(ChoiceInput, { key: "answerInput".concat(answer.id), ref: function(node) { answerRefsRef.current[answer.id] = node; }, disabledFields: disabled ? [ 'answerInput' ] : [], id: answer.id, itemBody: answer.itemBody, errors: distractorErrors(answer.id), onInputChange: onDistractorInputChange, onModalClose: onModalClose, onModalOpen: onModalOpen, onRemoveChoice: onRemoveAnswer, shouldRenderRemoveChoice: !disabled, focusOnMount: focusOnMount && isMountedRef.current, noRCE: true, notifyScreenreader: notifyScreenreader, isRequired: true, automationData: "sdk-category-".concat(index + 1, "-answer") }); }; var renderRemoveButton = function(category, index) { var onRemoveCategory = getCategoryRemoveHandler(category.id); var singleCategory = sortedCategories.length === 1; if (!singleCategory && !disabled) { return /*#__PURE__*/ jsx(RemoveChoiceButton, { choiceId: category.id, screenReaderText: t('Remove Category {index}: {category}', { index: index, category: category.itemBody }), onRemoveChoice: onRemoveCategory, automationData: "sdk-remove-category-".concat(category.id) }); } }; var renderCategory = function(category, index, focusOnMount) { var onCreateAnswer = getCreateAnswerHandler(category.id); var categoryAnswerInputRefs = function(node) { categoryAnswerInputRefsRef.current[category.id] = node; }; var categoryRef = function(node) { categoryRefsRef.current[category.id] = node; }; return /*#__PURE__*/ jsx("div", { key: category.id, className: "categoryContainer", css: styles.categoryContainer }, /*#__PURE__*/ jsx("div", { css: styles.categoryContainerWrapper }, /*#__PURE__*/ jsx("div", { css: styles.itemContainer }, /*#__PURE__*/ jsx("div", { css: styles.itemContainerBody }, /*#__PURE__*/ jsx(Text, { color: "primary" }, t('Category {position, number}', { position: index + 1 }))), renderRemoveButton(category, index + 1)), /*#__PURE__*/ jsx("div", { css: styles.categoryBody }, /*#__PURE__*/ jsx("div", { css: styles.categoryBodyDescriptionInput }, /*#__PURE__*/ jsx(AnswerInput, { ref: categoryAnswerInputRefs, disabled: disabled, onChangeHandler: onCategoryInputChange, onModalClose: onModalClose, onModalOpen: onModalOpen, id: category.id, itemBody: category.itemBody, errors: getCategoryErrors(category.id), renderLabel: t('Description'), screenReaderText: t('Category Description'), focusOnMount: focusOnMount && isMountedRef.current, noRCE: true, automationData: "sdk-category-".concat(index + 1, "-description"), isRequired: true })), /*#__PURE__*/ jsx(Flex, { padding: "x-small", direction: "column", gap: "small" }, categoryScoringData(category.id).map(function(answerId, answerIndex, source) { var answer = distractors[answerId]; var answerFocusOnMount = answerIndex + 1 === source.length && !answer.itemBody; return /*#__PURE__*/ jsx(Flex.Item, { key: "answerInput".concat(answer.id) }, renderAnswer(answer, category.id, answerFocusOnMount, answerIndex)); })), /*#__PURE__*/ jsx("div", { css: styles.categoryBodyAnswers }), !disabled && /*#__PURE__*/ jsx(Footer, { onCreateChoice: onCreateAnswer, ref: categoryRef, screenReaderText: t('Add answer to category {position, number}', { position: index + 1 }), notifyScreenreader: notifyScreenreader, automationData: "sdk-add-answer-to-category-".concat(index + 1, "-button") })))); }; return /*#__PURE__*/ jsx("div", { css: styles.categoriesWrapper }, /*#__PURE__*/ jsx(FormFieldGroup, { description: t('Categories'), messages: normalizeErrors(categoriesErrors) }, /*#__PURE__*/ jsx("div", { css: styles.categoriesContainer }, sortedCategories.map(function(category, index, source) { var focusOnMount = index + 1 === source.length && !category.itemBody; return renderCategory(category, index, focusOnMount); }))), !disabled && /*#__PURE__*/ jsx(Footer, { ref: handleFooterRef, onCreateChoice: onCreateCategory, buttonText: t('Category'), screenReaderText: t('Add Category'), notifyScreenreader: notifyScreenreader, automationData: "sdk-add-category-button" })); }); BaseCategoryForm.displayName = 'CategoryForm'; BaseCategoryForm.componentId = 'QuizzesCategoryForm'; BaseCategoryForm.propTypes = { categoriesErrors: PropTypes.arrayOf(PropTypes.shape({ text: PropTypes.string, type: PropTypes.string })), getCategoryErrors: PropTypes.func, categoryScoringData: PropTypes.func, disabled: PropTypes.bool, distractors: PropTypes.objectOf(PropTypes.shape({ id: PropTypes.string.isRequired, itemBody: PropTypes.string.isRequired })).isRequired, distractorErrors: PropTypes.func, notifyScreenreader: PropTypes.func.isRequired, onCategoryInputChange: PropTypes.func, onCreateAnswer: PropTypes.func, onCreateCategory: PropTypes.func, onDistractorInputChange: PropTypes.func, onModalClose: PropTypes.func, onModalOpen: PropTypes.func, onRemoveAnswer: PropTypes.func, onRemoveCategory: PropTypes.func, sortedCategories: PropTypes.arrayOf(PropTypes.shape({ id: PropTypes.string.isRequired, itemBody: PropTypes.string })).isRequired, styles: PropTypes.object }; BaseCategoryForm.defaultProps = { categoriesErrors: [], getCategoryErrors: void 0, categoryScoringData: void 0, disabled: void 0, distractorErrors: void 0, onCategoryInputChange: void 0, onCreateAnswer: void 0, onCreateCategory: void 0, onDistractorInputChange: void 0, onModalClose: void 0, onModalOpen: void 0, onRemoveAnswer: void 0, onRemoveCategory: void 0 }; var CategoryForm = withStyleOverrides(generateStyle, generateComponentTheme)(BaseCategoryForm); CategoryForm.displayName = 'CategoryForm'; CategoryForm.componentId = 'QuizzesCategoryForm'; export { BaseCategoryForm }; export default CategoryForm;