All files / src/utils/components/question-bank/question-display/grid-question GridQuestionViewOnly.jsx

100% Statements 16/16
95.23% Branches 20/21
100% Functions 7/7
100% Lines 13/13

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142                                      11x                     8x 120x               8x 11x 11x         14x   11x                                                                       20x                               20x 20x                           40x 40x                                                  
import React, { memo } from "react";
import { Box, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Typography } from "@mui/material";
import { alpha, useTheme } from "@mui/material/styles";
import CheckCircleIcon from "@mui/icons-material/CheckCircle";
import TinyMCETextEditorReadOnly from "@/utils/components/TinyMCETextEditorReadOnly";
import RichTextContent from "@/utils/components/question-bank/question-display/utils/RichTextContent";
import {
    choiceRowSurfaceBg,
    questionCardSurfaceBg,
    questionItemNumberSx,
    questionStemTypographySx,
} from "@/utils/components/question-bank/question-display/utils/questionCardStyles";
 
// The app's global theme overrides (src/theme/ui/overrides/components/table.jsx) fight a fully
// outlined grid on two fronts: MuiTableCell gives every header cell a dashed bottom border and a
// `background.neutral` fill, and MuiTableRow clears the LAST row's cell borders entirely
// (`borderColor: transparent`, at higher specificity than a plain cell-level override) — both
// reasonable for an ordinary data table, but this is an answer grid, where every cell in every
// row, including the last, needs a visible boundary and the header shouldn't read as a tinted band.
const gridTableSx = (theme) => ({
    "& .MuiTableCell-root": { border: `1px solid ${theme.palette.divider}` },
    "& .MuiTableCell-head": { backgroundColor: "transparent" },
    "& .MuiTableRow-root:last-of-type .MuiTableCell-root": { borderColor: theme.palette.divider },
});
 
// The first (statement) column always gets exactly half the table, capped at
// that share even for long text — it never yields width to the option
// columns. The option columns split the OTHER half evenly between however
// many of them there are, so the two always sum to 100% regardless of column
// count.
const ROW_HEADER_COLUMN_WIDTH = "50%";
const optionColumnWidth = (columnCount) => `${50 / Math.max(columnCount, 1)}%`;
 
/**
 * Read-only rendering of a Grid Question — rows × columns table, marking the
 * correct column per row when `showAnswers` is true. `correctAnswer.answers`
 * is `[{id: row id, answer: correct column's TEXT}]`, the same {id, answer}
 * shape Drag-and-Drop uses (see docs/features/grid-question.md).
 */
const GridQuestionViewOnly = ({ question, showAnswers, itemNumber }) => {
    const theme = useTheme();
    const cardBg = questionCardSurfaceBg(theme);
 
    // Optional chaining throughout: a question saved without rows/choices/a
    // correct answer must lose its table, not the list it renders inside —
    // same defensive convention DragDropViewOnly uses.
    const correctTextByRowId = new Map((question.correctAnswer?.answers ?? []).map((a) => [a.id, a.answer]));
 
    return (
        <Box sx={{ display: "flex", flexDirection: "column", gap: 3, width: "100%" }}>
            <Box sx={{ display: "flex", alignItems: "flex-start", gap: 1 }}>
                {itemNumber != null && itemNumber !== false && (
                    <Typography component="span" sx={{ ...questionItemNumberSx, flexShrink: 0 }}>
                        {itemNumber}.
                    </Typography>
                )}
                <Box sx={{ flex: 1, ...questionStemTypographySx }}>
                    <TinyMCETextEditorReadOnly content={question.question} backgroundColor={cardBg} />
                </Box>
            </Box>
 
            <TableContainer
                sx={{
                    ml: itemNumber ? 3.5 : 0,
                    border: "1px solid",
                    borderColor: alpha(theme.palette.grey[500], 0.18),
                    borderRadius: 1.5,
                }}
            >
                <Table size="small" sx={{ ...gridTableSx(theme), tableLayout: "fixed" }}>
                    <TableHead>
                        <TableRow>
                            <TableCell
                                sx={{
                                    width: ROW_HEADER_COLUMN_WIDTH,
                                    maxWidth: ROW_HEADER_COLUMN_WIDTH,
                                    fontWeight: 600,
                                }}
                            >
                                <Box component="span" sx={{ ml: 1 }}>
                                    {question.rowHeaderLabel || "Statement"}
                                </Box>
                            </TableCell>
                            {(question.choices ?? []).map((column) => (
                                <TableCell
                                    key={column.id}
                                    align="center"
                                    sx={{
                                        width: optionColumnWidth(question.choices?.length),
                                        maxWidth: optionColumnWidth(question.choices?.length),
                                        fontWeight: 600,
                                    }}
                                >
                                    <RichTextContent content={column.text} />
                                </TableCell>
                            ))}
                        </TableRow>
                    </TableHead>
                    <TableBody>
                        {(question.rows ?? []).map((row) => {
                            const correctText = correctTextByRowId.get(row.id);
                            return (
                                <TableRow key={row.id}>
                                    <TableCell
                                        sx={{
                                            width: ROW_HEADER_COLUMN_WIDTH,
                                            maxWidth: ROW_HEADER_COLUMN_WIDTH,
                                        }}
                                    >
                                        <RichTextContent
                                            content={row.text}
                                            sx={{ ml: 1, overflowWrap: "break-word", wordBreak: "break-word" }}
                                        />
                                    </TableCell>
                                    {(question.choices ?? []).map((column) => {
                                        const isCorrect = showAnswers && correctText === column.text;
                                        return (
                                            <TableCell
                                                key={column.id}
                                                align="center"
                                                sx={{
                                                    width: optionColumnWidth(question.choices?.length),
                                                    maxWidth: optionColumnWidth(question.choices?.length),
                                                    bgcolor: isCorrect ? choiceRowSurfaceBg(theme, true) : undefined,
                                                }}
                                            >
                                                {isCorrect && <CheckCircleIcon fontSize="small" color="success" />}
                                            </TableCell>
                                        );
                                    })}
                                </TableRow>
                            );
                        })}
                    </TableBody>
                </Table>
            </TableContainer>
        </Box>
    );
};
 
export default memo(GridQuestionViewOnly);