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);
|