diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000000..69012c9646 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,42 @@ +# pie-elements + +## What this is + +A monorepo of **PIE (Portable Interactions & Elements)** assessment item-type widgets — multiple-choice, extended-text-entry, graphing, drag-in-the-blank, hotspot, etc. Each item type is a native `HTMLElement` subclass that mounts a React tree internally, so it can be embedded framework-agnostically by a host player (`pie-player-components`). + +Sibling repo: **`pie-lib`** (`../pie-lib`) provides shared UI/logic building blocks, consumed as normal npm dependencies under the `@pie-lib/*` scope (not a local link — see `resolutions` in root `package.json`). + +## Repo structure + +- Lerna (independent versioning) + Yarn workspaces. Workspaces: `packages/*`, `packages/*/configure`, `packages/*/controller`, `packages/*/print`. +- `packages/` — 32 item packages, plus `pie-models` (TS model/schema defs) and `boilerplate-item-type` (scaffold for new elements). +- `old-packages/` — deprecated items, excluded from tests. +- `scripts/` — build/release tooling, wraps `@pie-framework/build-helper`. + +## Package anatomy (per item type) + +Each item type is split into up to three independently-published npm packages: + +- **main** (`packages//src`) — the custom element: `model`/`session`/`options` setters, dispatches `ModelSetEvent`/`SessionChangedEvent`. This is what a host player embeds. +- **configure/** (`@pie-element/-configure`) — authoring UI that edits the model (built on `@pie-lib/config-ui`, MUI). +- **controller/** (`@pie-element/-controller`) — server/pre-render logic: `createDefaultModel`, `normalize`, and `model(question, session, env, updateSession)`. This is where scoring happens and where fields get stripped based on `env.mode` (`gather`/`view`/`evaluate`) and `env.role` (`student`/`instructor`) — the mechanism that keeps answers/rationale hidden from students. Don't weaken this filtering without understanding why it's there. + +Each package builds `src/` (and `configure/src`, `controller/src`) → `lib/` via Babel. Model docs are auto-generated into `docs/pie-schema.json` / `docs/config-schema.json` from `pie-models`. + +## Commands + +- `yarn build` — build all packages (`scripts/build build`) +- `yarn test` — run tests (`scripts/build test`); Jest is configured **only at the repo root** (`jest.config.js`) — don't add per-package Jest config/devDependencies +- `yarn lint` — ESLint over `packages` +- `yarn update-pie-lib` — bump all `@pie-lib/*` deps to latest and sync the root `resolutions` block (`scripts/sync-pie-lib-resolutions.js`) +- `yarn release` — clean + test + build + `lerna publish` + +## Conventions + +- No TypeScript in item source (except `pie-models`) — plain JS/JSX with Babel. +- New item types start from `boilerplate-item-type`. +- Keep `@pie-lib/*` version bumps going through `update-pie-lib` / `sync-pie-lib-resolutions.js` rather than hand-editing versions in individual package.json files, so the root `resolutions` block stays consistent. + +## Working preferences + +- **Do not create git commits unless explicitly asked.** The user commits their own changes — leave the working tree staged/unstaged as appropriate and let them review and commit themselves. diff --git a/package.json b/package.json index 469a289660..a2325f4c18 100644 --- a/package.json +++ b/package.json @@ -89,27 +89,27 @@ "@jest/test-sequencer": "29.7.0", "@types/d3-array": "3.0.3", "@pie-lib/categorize": "2.0.2", - "@pie-lib/charting": "7.0.12", - "@pie-lib/config-ui": "13.0.12", + "@pie-lib/charting": "7.0.13", + "@pie-lib/config-ui": "13.0.13", "@pie-lib/controller-utils": "2.0.3", "@pie-lib/correct-answer-toggle": "4.0.5", "@pie-lib/drag": "4.0.6", - "@pie-lib/editable-html": "13.0.7", - "@pie-lib/editable-html-tip-tap": "2.1.10", + "@pie-lib/editable-html": "13.0.8", + "@pie-lib/editable-html-tip-tap": "2.1.11", "@pie-lib/feedback": "2.0.2", - "@pie-lib/graphing-solution-set": "4.0.12", + "@pie-lib/graphing-solution-set": "4.0.13", "@pie-lib/graphing-utils": "3.0.2", - "@pie-lib/graphing": "4.0.13", + "@pie-lib/graphing": "4.0.14", "@pie-lib/icons": "4.0.3", - "@pie-lib/mask-markup": "3.0.12", + "@pie-lib/mask-markup": "3.0.13", "@pie-lib/math-evaluator": "4.0.2", "@pie-lib/math-input": "8.1.1", "@pie-lib/math-rendering-accessible": "5.0.3", "@pie-lib/math-rendering": "5.1.0", "@pie-lib/math-toolbar": "3.0.6", - "@pie-lib/plot": "4.0.12", + "@pie-lib/plot": "4.0.13", "@pie-lib/render-ui": "6.1.3", - "@pie-lib/rubric": "2.0.12", + "@pie-lib/rubric": "2.0.13", "@pie-lib/scoring-config": "5.0.3", "@pie-lib/style-utils": "2.0.2", "@pie-lib/test-utils": "2.0.2", diff --git a/packages/boilerplate-item-type/configure/package.json b/packages/boilerplate-item-type/configure/package.json index e719424b14..a610f54d0e 100644 --- a/packages/boilerplate-item-type/configure/package.json +++ b/packages/boilerplate-item-type/configure/package.json @@ -11,8 +11,8 @@ "@mui/icons-material": "^7.3.4", "@mui/material": "^7.3.4", "@pie-framework/pie-configure-events": "^1.3.0", - "@pie-lib/config-ui": "13.0.12", - "@pie-lib/editable-html-tip-tap": "2.1.10", + "@pie-lib/config-ui": "13.0.13", + "@pie-lib/editable-html-tip-tap": "2.1.11", "@pie-lib/render-ui": "6.1.3", "debug": "^4.1.1", "lodash-es": "^4.17.23", diff --git a/packages/calculator/configure/package.json b/packages/calculator/configure/package.json index 56ad02c1e3..1007447b39 100644 --- a/packages/calculator/configure/package.json +++ b/packages/calculator/configure/package.json @@ -11,7 +11,7 @@ "@mui/material": "^7.3.4", "@pie-framework/material-ui-calculator": "4.0.0", "@pie-framework/pie-configure-events": "^1.3.0", - "@pie-lib/config-ui": "13.0.12", + "@pie-lib/config-ui": "13.0.13", "react": "18.3.1", "react-dom": "18.3.1" }, diff --git a/packages/categorize/configure/package.json b/packages/categorize/configure/package.json index 9f1c0974b2..e6179d4760 100644 --- a/packages/categorize/configure/package.json +++ b/packages/categorize/configure/package.json @@ -16,9 +16,9 @@ "@mui/material": "^7.3.4", "@pie-framework/pie-configure-events": "^1.3.0", "@pie-lib/categorize": "2.0.2", - "@pie-lib/config-ui": "13.0.12", + "@pie-lib/config-ui": "13.0.13", "@pie-lib/drag": "4.0.6", - "@pie-lib/editable-html-tip-tap": "2.1.10", + "@pie-lib/editable-html-tip-tap": "2.1.11", "@pie-lib/math-rendering": "5.1.0", "@pie-lib/render-ui": "6.1.3", "@pie-lib/translator": "4.0.2", diff --git a/packages/categorize/package.json b/packages/categorize/package.json index 8822976d92..ffd1c2f88b 100644 --- a/packages/categorize/package.json +++ b/packages/categorize/package.json @@ -16,7 +16,7 @@ "@mui/material": "^7.3.4", "@pie-framework/pie-player-events": "^0.1.0", "@pie-lib/categorize": "2.0.2", - "@pie-lib/config-ui": "13.0.12", + "@pie-lib/config-ui": "13.0.13", "@pie-lib/correct-answer-toggle": "4.0.5", "@pie-lib/drag": "4.0.6", "@pie-lib/math-rendering": "5.1.0", diff --git a/packages/charting/configure/package.json b/packages/charting/configure/package.json index 6e333c8b38..e5368206f5 100644 --- a/packages/charting/configure/package.json +++ b/packages/charting/configure/package.json @@ -12,9 +12,9 @@ "@mui/icons-material": "^7.3.4", "@mui/material": "^7.3.4", "@pie-framework/pie-configure-events": "^1.3.0", - "@pie-lib/charting": "7.0.12", - "@pie-lib/config-ui": "13.0.12", - "@pie-lib/editable-html-tip-tap": "2.1.10", + "@pie-lib/charting": "7.0.13", + "@pie-lib/config-ui": "13.0.13", + "@pie-lib/editable-html-tip-tap": "2.1.11", "@pie-lib/math-rendering": "5.1.0", "@pie-lib/render-ui": "6.1.3", "debug": "^4.1.1", diff --git a/packages/charting/package.json b/packages/charting/package.json index 49105abdea..8a2bb54cdf 100644 --- a/packages/charting/package.json +++ b/packages/charting/package.json @@ -15,7 +15,7 @@ "@mui/icons-material": "^7.3.4", "@mui/material": "^7.3.4", "@pie-framework/pie-player-events": "^0.1.0", - "@pie-lib/charting": "7.0.12", + "@pie-lib/charting": "7.0.13", "@pie-lib/correct-answer-toggle": "4.0.5", "@pie-lib/math-rendering": "5.1.0", "@pie-lib/render-ui": "6.1.3", diff --git a/packages/complex-rubric/configure/package.json b/packages/complex-rubric/configure/package.json index ff408897be..75fb7639f6 100644 --- a/packages/complex-rubric/configure/package.json +++ b/packages/complex-rubric/configure/package.json @@ -14,9 +14,9 @@ "@pie-element/multi-trait-rubric": "^8.1.0", "@pie-element/rubric": "^8.1.0", "@pie-framework/pie-configure-events": "^1.3.0", - "@pie-lib/config-ui": "13.0.12", + "@pie-lib/config-ui": "13.0.13", "@pie-lib/render-ui": "6.1.3", - "@pie-lib/rubric": "2.0.12", + "@pie-lib/rubric": "2.0.13", "debug": "^4.1.1", "lodash-es": "^4.17.23", "prop-types": "^15.8.1", diff --git a/packages/complex-rubric/package.json b/packages/complex-rubric/package.json index d0cbee157d..25a3f86d42 100644 --- a/packages/complex-rubric/package.json +++ b/packages/complex-rubric/package.json @@ -13,7 +13,7 @@ "@pie-element/multi-trait-rubric": "^8.1.0", "@pie-element/rubric": "^8.1.0", "@pie-framework/pie-player-events": "^0.1.0", - "@pie-lib/rubric": "2.0.12", + "@pie-lib/rubric": "2.0.13", "classnames": "^2.2.6", "debug": "^4.1.1", "lodash-es": "^4.17.23", diff --git a/packages/drag-in-the-blank/configure/package.json b/packages/drag-in-the-blank/configure/package.json index d65e2d4eb4..5cd9d59fdb 100644 --- a/packages/drag-in-the-blank/configure/package.json +++ b/packages/drag-in-the-blank/configure/package.json @@ -12,9 +12,9 @@ "@mui/icons-material": "^7.3.4", "@mui/material": "^7.3.4", "@pie-framework/pie-configure-events": "^1.3.0", - "@pie-lib/config-ui": "13.0.12", + "@pie-lib/config-ui": "13.0.13", "@pie-lib/drag": "4.0.6", - "@pie-lib/editable-html-tip-tap": "2.1.10", + "@pie-lib/editable-html-tip-tap": "2.1.11", "@pie-lib/math-rendering": "5.1.0", "debug": "^4.1.1", "lodash-es": "^4.17.23", diff --git a/packages/drag-in-the-blank/package.json b/packages/drag-in-the-blank/package.json index 0a653e92e8..43ce59db0f 100644 --- a/packages/drag-in-the-blank/package.json +++ b/packages/drag-in-the-blank/package.json @@ -17,7 +17,7 @@ "@pie-framework/pie-player-events": "^0.1.0", "@pie-lib/correct-answer-toggle": "4.0.5", "@pie-lib/drag": "4.0.6", - "@pie-lib/mask-markup": "3.0.12", + "@pie-lib/mask-markup": "3.0.13", "@pie-lib/math-rendering": "5.1.0", "@pie-lib/render-ui": "6.1.3", "lodash-es": "^4.17.23", diff --git a/packages/drawing-response/configure/package.json b/packages/drawing-response/configure/package.json index 7982a55ccc..7bb55b02b6 100644 --- a/packages/drawing-response/configure/package.json +++ b/packages/drawing-response/configure/package.json @@ -12,8 +12,8 @@ "@mui/icons-material": "^7.3.4", "@mui/material": "^7.3.4", "@pie-framework/pie-configure-events": "^1.3.0", - "@pie-lib/config-ui": "13.0.12", - "@pie-lib/editable-html-tip-tap": "2.1.10", + "@pie-lib/config-ui": "13.0.13", + "@pie-lib/editable-html-tip-tap": "2.1.11", "debug": "^4.1.1", "lodash-es": "^4.17.23", "prop-types": "^15.8.1", diff --git a/packages/ebsr/configure/package.json b/packages/ebsr/configure/package.json index 7e4314ab4e..e76ad8fc9f 100644 --- a/packages/ebsr/configure/package.json +++ b/packages/ebsr/configure/package.json @@ -13,7 +13,7 @@ "@mui/material": "^7.3.4", "@pie-element/multiple-choice": "^13.2.0", "@pie-framework/pie-configure-events": "^1.2.0", - "@pie-lib/config-ui": "13.0.12", + "@pie-lib/config-ui": "13.0.13", "lodash-es": "^4.17.23", "prop-types": "^15.8.1", "react": "18.3.1", diff --git a/packages/explicit-constructed-response/configure/package.json b/packages/explicit-constructed-response/configure/package.json index 24583013bc..d56f519d7d 100644 --- a/packages/explicit-constructed-response/configure/package.json +++ b/packages/explicit-constructed-response/configure/package.json @@ -10,8 +10,8 @@ "@mui/icons-material": "^7.3.4", "@mui/material": "^7.3.4", "@pie-framework/pie-configure-events": "^1.3.0", - "@pie-lib/config-ui": "13.0.12", - "@pie-lib/editable-html-tip-tap": "2.1.10", + "@pie-lib/config-ui": "13.0.13", + "@pie-lib/editable-html-tip-tap": "2.1.11", "classnames": "^2.2.6", "debug": "^4.1.1", "lodash-es": "^4.17.23", diff --git a/packages/explicit-constructed-response/package.json b/packages/explicit-constructed-response/package.json index 2d08189fc0..d6f4870b20 100644 --- a/packages/explicit-constructed-response/package.json +++ b/packages/explicit-constructed-response/package.json @@ -16,7 +16,7 @@ "@mui/material": "^7.3.4", "@pie-framework/pie-player-events": "^0.1.0", "@pie-lib/correct-answer-toggle": "4.0.5", - "@pie-lib/mask-markup": "3.0.12", + "@pie-lib/mask-markup": "3.0.13", "@pie-lib/math-rendering": "5.1.0", "@pie-lib/render-ui": "6.1.3", "@pie-lib/translator": "4.0.2", diff --git a/packages/extended-text-entry/configure/package.json b/packages/extended-text-entry/configure/package.json index 22707c5f8a..4b4a5de452 100644 --- a/packages/extended-text-entry/configure/package.json +++ b/packages/extended-text-entry/configure/package.json @@ -13,8 +13,8 @@ "@mui/icons-material": "^7.3.4", "@mui/material": "^7.3.4", "@pie-framework/pie-configure-events": "^1.3.0", - "@pie-lib/config-ui": "13.0.12", - "@pie-lib/editable-html-tip-tap": "2.1.10", + "@pie-lib/config-ui": "13.0.13", + "@pie-lib/editable-html-tip-tap": "2.1.11", "lodash-es": "^4.17.23", "prop-types": "^15.8.1", "react": "18.3.1", diff --git a/packages/extended-text-entry/package.json b/packages/extended-text-entry/package.json index db6d1d2f1b..4749cd40c4 100644 --- a/packages/extended-text-entry/package.json +++ b/packages/extended-text-entry/package.json @@ -14,8 +14,8 @@ "@mui/icons-material": "^7.3.4", "@mui/material": "^7.3.4", "@pie-framework/pie-player-events": "^0.1.0", - "@pie-lib/config-ui": "13.0.12", - "@pie-lib/editable-html-tip-tap": "2.1.10", + "@pie-lib/config-ui": "13.0.13", + "@pie-lib/editable-html-tip-tap": "2.1.11", "@pie-lib/math-rendering": "5.1.0", "@pie-lib/render-ui": "6.1.3", "classnames": "^2.2.6", diff --git a/packages/fraction-model/configure/package.json b/packages/fraction-model/configure/package.json index a2693f4fea..3eb6d71626 100644 --- a/packages/fraction-model/configure/package.json +++ b/packages/fraction-model/configure/package.json @@ -11,8 +11,8 @@ "@mui/icons-material": "^7.3.4", "@mui/material": "^7.3.4", "@pie-framework/pie-configure-events": "^1.3.0", - "@pie-lib/config-ui": "13.0.12", - "@pie-lib/editable-html-tip-tap": "2.1.10", + "@pie-lib/config-ui": "13.0.13", + "@pie-lib/editable-html-tip-tap": "2.1.11", "debug": "^4.1.1", "lodash-es": "^4.17.23", "prop-types": "^15.8.1", diff --git a/packages/fraction-model/package.json b/packages/fraction-model/package.json index 8bd438b55f..53f0d26564 100644 --- a/packages/fraction-model/package.json +++ b/packages/fraction-model/package.json @@ -12,7 +12,7 @@ "@mui/icons-material": "^7.3.4", "@mui/material": "^7.3.4", "@pie-framework/pie-player-events": "^0.1.0", - "@pie-lib/config-ui": "13.0.12", + "@pie-lib/config-ui": "13.0.13", "@pie-lib/correct-answer-toggle": "4.0.5", "@pie-lib/math-rendering": "5.1.0", "@pie-lib/render-ui": "6.1.3", diff --git a/packages/graphing-solution-set/configure/package.json b/packages/graphing-solution-set/configure/package.json index 14e03a89cb..3951f24f65 100644 --- a/packages/graphing-solution-set/configure/package.json +++ b/packages/graphing-solution-set/configure/package.json @@ -12,9 +12,9 @@ "@mui/icons-material": "^7.3.4", "@mui/material": "^7.3.4", "@pie-framework/pie-configure-events": "^1.3.0", - "@pie-lib/config-ui": "13.0.12", - "@pie-lib/editable-html-tip-tap": "2.1.10", - "@pie-lib/graphing-solution-set": "4.0.12", + "@pie-lib/config-ui": "13.0.13", + "@pie-lib/editable-html-tip-tap": "2.1.11", + "@pie-lib/graphing-solution-set": "4.0.13", "@pie-lib/math-rendering": "5.1.0", "classnames": "^2.2.6", "debug": "^4.1.1", diff --git a/packages/graphing-solution-set/package.json b/packages/graphing-solution-set/package.json index be4400443c..ae0132f14b 100644 --- a/packages/graphing-solution-set/package.json +++ b/packages/graphing-solution-set/package.json @@ -16,9 +16,9 @@ "@mui/icons-material": "^7.3.4", "@mui/material": "^7.3.4", "@pie-framework/pie-player-events": "^0.1.0", - "@pie-lib/config-ui": "13.0.12", + "@pie-lib/config-ui": "13.0.13", "@pie-lib/correct-answer-toggle": "4.0.5", - "@pie-lib/graphing-solution-set": "4.0.12", + "@pie-lib/graphing-solution-set": "4.0.13", "@pie-lib/math-rendering": "5.1.0", "@pie-lib/render-ui": "6.1.3", "classnames": "^2.2.6", diff --git a/packages/graphing/configure/package.json b/packages/graphing/configure/package.json index 71fef0246d..93976ebbc2 100644 --- a/packages/graphing/configure/package.json +++ b/packages/graphing/configure/package.json @@ -12,9 +12,9 @@ "@mui/icons-material": "^7.3.4", "@mui/material": "^7.3.4", "@pie-framework/pie-configure-events": "^1.3.0", - "@pie-lib/config-ui": "13.0.12", - "@pie-lib/editable-html-tip-tap": "2.1.10", - "@pie-lib/graphing": "4.0.13", + "@pie-lib/config-ui": "13.0.13", + "@pie-lib/editable-html-tip-tap": "2.1.11", + "@pie-lib/graphing": "4.0.14", "@pie-lib/math-rendering": "5.1.0", "classnames": "^2.2.6", "debug": "^4.1.1", diff --git a/packages/graphing/package.json b/packages/graphing/package.json index 46856b1aa5..fe9dacf249 100644 --- a/packages/graphing/package.json +++ b/packages/graphing/package.json @@ -16,7 +16,7 @@ "@mui/material": "^7.3.4", "@pie-framework/pie-player-events": "^0.1.0", "@pie-lib/correct-answer-toggle": "4.0.5", - "@pie-lib/graphing": "4.0.13", + "@pie-lib/graphing": "4.0.14", "@pie-lib/math-rendering": "5.1.0", "@pie-lib/render-ui": "6.1.3", "classnames": "^2.2.6", diff --git a/packages/hotspot/configure/package.json b/packages/hotspot/configure/package.json index 246b72034b..6c74a9f4a1 100644 --- a/packages/hotspot/configure/package.json +++ b/packages/hotspot/configure/package.json @@ -12,8 +12,8 @@ "@mui/icons-material": "^7.3.4", "@mui/material": "^7.3.4", "@pie-framework/pie-configure-events": "^1.3.0", - "@pie-lib/config-ui": "13.0.12", - "@pie-lib/editable-html-tip-tap": "2.1.10", + "@pie-lib/config-ui": "13.0.13", + "@pie-lib/editable-html-tip-tap": "2.1.11", "debug": "^4.1.1", "konva": "8.3.0", "lodash-es": "^4.17.23", diff --git a/packages/hotspot/configure/src/root.jsx b/packages/hotspot/configure/src/root.jsx index eb227d406c..c3484e342a 100644 --- a/packages/hotspot/configure/src/root.jsx +++ b/packages/hotspot/configure/src/root.jsx @@ -12,6 +12,8 @@ import { updateImageDimensions, generateValidationMessage, getUpdatedShapes, get const { Panel, toggle, dropdown } = settings; +const checkNullish = (value) => value !== null && value !== undefined; + const DimensionsContainer = styled('div')(({ theme }) => ({ display: 'flex', marginBottom: theme.spacing(1.5), @@ -164,6 +166,7 @@ export class Root extends React.Component { ...props, }); + return ( - - Define Hotspot - - + Define Hotspot + @@ -255,7 +251,7 @@ export class Root extends React.Component { hotspotColor={model.hotspotColor} outlineColor={model.outlineColor} selectedHotspotColor={model.selectedHotspotColor} - hoverOutlineColor={model.hoverOutlineColor} + hoverOutlineColor={checkNullish(model.hoverOutlineColor) ? model.hoverOutlineColor : 'black'} onUpdateImageDimension={onUpdateImageDimension} onUpdateShapes={onUpdateShapes} onImageUpload={onImageUpload} diff --git a/packages/hotspot/controller/src/index.js b/packages/hotspot/controller/src/index.js index 9380b63879..ef1cda9373 100644 --- a/packages/hotspot/controller/src/index.js +++ b/packages/hotspot/controller/src/index.js @@ -8,6 +8,8 @@ import defaults from './defaults'; const log = debug('pie-elements:hotspot:controller'); +const checkNullish = (value) => value !== null && value !== undefined; + export const normalize = (question) => ({ ...defaults, ...question, @@ -44,7 +46,7 @@ export function model(question, session, env) { imageUrl, outlineColor, hotspotColor, - hoverOutlineColor, + hoverOutlineColor: checkNullish(hoverOutlineColor) ? hoverOutlineColor : 'black', selectedHotspotColor, multipleCorrect, partialScoring, @@ -150,25 +152,25 @@ export const getLogTrace = (model, session, env) => { const traceLog = []; const { answers } = session || {}; const { shapes } = model || {}; - + const allShapes = []; if (shapes) { if (shapes.rectangles) allShapes.push(...shapes.rectangles); if (shapes.polygons) allShapes.push(...shapes.polygons); if (shapes.circles) allShapes.push(...shapes.circles); } - + const correctShapes = allShapes.filter(shape => shape.correct); const totalShapes = allShapes.length; - + traceLog.push(`Total of ${totalShapes} hotspot(s) defined, ${correctShapes.length} correct.`); - + if (answers && answers.length > 0) { traceLog.push(`Student selected ${answers.length} hotspot(s).`); - + let correctSelections = 0; let incorrectSelections = 0; - + answers.forEach(answer => { const shape = allShapes.find(s => s.id === answer.id); if (shape && shape.correct) { @@ -177,11 +179,11 @@ export const getLogTrace = (model, session, env) => { incorrectSelections++; } }); - - const missedCorrect = correctShapes.filter(correctShape => + + const missedCorrect = correctShapes.filter(correctShape => !answers.some(answer => answer.id === correctShape.id) ).length; - + if (correctSelections > 0) { traceLog.push(`${correctSelections} correct hotspot(s) selected.`); } @@ -196,14 +198,14 @@ export const getLogTrace = (model, session, env) => { } const partialScoringEnabled = partialScoring.enabled(model, env); - + if (partialScoringEnabled) { traceLog.push(`Score calculated using partial scoring.`); - + if (answers && answers.length > 0) { let correctSelections = 0; let incorrectSelections = 0; - + answers.forEach(answer => { const shape = allShapes.find(s => s.id === answer.id); if (shape && shape.correct) { @@ -212,10 +214,10 @@ export const getLogTrace = (model, session, env) => { incorrectSelections++; } }); - + const totalCorrectAvailable = correctShapes.length; traceLog.push(`Partial scoring calculation: ${correctSelections} correct selections out of ${totalCorrectAvailable} available.`); - + if (incorrectSelections > totalCorrectAvailable) { const extraSelections = incorrectSelections - (totalCorrectAvailable - correctSelections); traceLog.push(`${extraSelections} extra incorrect selection(s) beyond required amount are deducted from score.`); @@ -236,12 +238,12 @@ export function outcome(config, session, env = {}) { log('outcome...'); if (!session || isEmpty(session)) { - resolve({ - score: 0, - empty: true, - traceLog: ['No hotspots selected. Score: 0.'] + resolve({ + score: 0, + empty: true, + traceLog: ['No hotspots selected. Score: 0.'] }); - } + } if (session.answers) { const traceLog = getLogTrace(config, session, env); @@ -249,10 +251,10 @@ export function outcome(config, session, env = {}) { resolve({ score, empty: false, traceLog }); } else { - resolve({ - score: 0, - empty: true, - traceLog: ['No hotspots selected. Score: 0.'] + resolve({ + score: 0, + empty: true, + traceLog: ['No hotspots selected. Score: 0.'] }); } }); diff --git a/packages/hotspot/docs/demo/generate.js b/packages/hotspot/docs/demo/generate.js index a3d993a030..96daa27d54 100644 --- a/packages/hotspot/docs/demo/generate.js +++ b/packages/hotspot/docs/demo/generate.js @@ -13,6 +13,7 @@ exports.model = (id, element) => ({ 'rgba(254, 241, 96, 0.25)', 'rgba(0, 0, 0, 0.1)', ], + // hoverOutlineColor: 'black', outlineColor: 'blue', outlineList: ['blue', 'red', 'yellow'], multipleCorrect: true, @@ -25,7 +26,8 @@ exports.model = (id, element) => ({ x: 261, y: 44, index: 2, - correct: true + correct: true, + // ariaLabel: 'First rectangle', }, { id: '4', @@ -34,8 +36,8 @@ exports.model = (id, element) => ({ x: 270, y: 215.4886474609375, index: 3, - correct: false - } + correct: false, + }, ], polygons: [ { @@ -53,10 +55,11 @@ exports.model = (id, element) => ({ { x: 109, y: 294 }, { x: 106, y: 280 }, { x: 56, y: 193 }, - { x: 68, y: 139 } + { x: 68, y: 139 }, ], index: 1, - correct: false + correct: false, + // ariaLabel: 'First polygon', }, { id: '5', @@ -71,10 +74,10 @@ exports.model = (id, element) => ({ { x: 564, y: 465 }, { x: 575, y: 461 }, { x: 575, y: 438 }, - { x: 544, y: 367 } + { x: 544, y: 367 }, ], index: 4, - correct: false + correct: false, }, { id: '7', @@ -84,11 +87,11 @@ exports.model = (id, element) => ({ { x: 556, y: 287 }, { x: 575, y: 302 }, { x: 601, y: 280 }, - { x: 598, y: 246 } + { x: 598, y: 246 }, ], index: 6, - correct: true - } + correct: true, + }, ], circles: [ { @@ -97,7 +100,8 @@ exports.model = (id, element) => ({ x: 71, y: 420, index: 1, - correct: false + correct: false, + // ariaLabel: 'First circle', }, { id: '6', @@ -105,14 +109,14 @@ exports.model = (id, element) => ({ x: 479, y: 140, index: 5, - correct: false - } - ] + correct: false, + // ariaLabel: 'Second circle', + }, + ], }, rationale: null, teacherInstructions: null, toolbarEditorPosition: 'bottom', - prompt: - '
Which state(s) have names beginning in "North"?
', + prompt: '
Which state(s) have names beginning in "North"?
', rubricEnabled: false, }); diff --git a/packages/hotspot/src/hotspot/__tests__/circle.test.jsx b/packages/hotspot/src/hotspot/__tests__/circle.test.jsx index 8ac04cba30..de0d08f719 100644 --- a/packages/hotspot/src/hotspot/__tests__/circle.test.jsx +++ b/packages/hotspot/src/hotspot/__tests__/circle.test.jsx @@ -70,7 +70,7 @@ describe('CircleComponent', () => { it('should render with correct position and radius', () => { const { getByTestId } = render(); const circle = getByTestId('circle'); - + expect(circle).toHaveAttribute('x', '50'); expect(circle).toHaveAttribute('y', '50'); expect(circle).toHaveAttribute('radius', '30'); @@ -79,21 +79,21 @@ describe('CircleComponent', () => { it('should render with hotspot color when not selected', () => { const { getByTestId } = render(); const circle = getByTestId('circle'); - + expect(circle).toHaveAttribute('fill', '#FF0000'); }); it('should render with selected color when selected', () => { const { getByTestId } = render(); const circle = getByTestId('circle'); - + expect(circle).toHaveAttribute('fill', '#00FF00'); }); it('should apply scale transform', () => { const { getByTestId } = render(); const group = getByTestId('group'); - + expect(group).toHaveAttribute('scaleX', '2'); expect(group).toHaveAttribute('scaleY', '2'); }); @@ -101,7 +101,7 @@ describe('CircleComponent', () => { it('should render with default scale of 1', () => { const { getByTestId } = render(); const group = getByTestId('group'); - + expect(group).toHaveAttribute('scaleX', '1'); expect(group).toHaveAttribute('scaleY', '1'); }); @@ -112,9 +112,9 @@ describe('CircleComponent', () => { const onClick = jest.fn(); const { getByTestId } = render(); const circle = getByTestId('circle'); - + fireEvent.click(circle); - + expect(onClick).toHaveBeenCalledWith({ id: 'circle1', selected: true, @@ -126,20 +126,20 @@ describe('CircleComponent', () => { const onClick = jest.fn(); const { getByTestId, rerender } = render(); const circle = getByTestId('circle'); - + fireEvent.click(circle); - + expect(onClick).toHaveBeenCalledWith({ id: 'circle1', selected: true, selector: 'Mouse', }); - + rerender(); - + const circleAfter = getByTestId('circle'); fireEvent.click(circleAfter); - + expect(onClick).toHaveBeenCalledWith({ id: 'circle1', selected: false, @@ -151,37 +151,37 @@ describe('CircleComponent', () => { const onClick = jest.fn(); const { getByTestId } = render(); const circle = getByTestId('circle'); - + fireEvent.click(circle); - + expect(onClick).not.toHaveBeenCalled(); }); it('should change cursor to pointer on mouse enter when not disabled', () => { const { getByTestId } = render(); const circle = getByTestId('circle'); - + fireEvent.mouseEnter(circle); - + expect(document.body.style.cursor).toBe('pointer'); }); it('should not change cursor when disabled', () => { const { getByTestId } = render(); const circle = getByTestId('circle'); - + fireEvent.mouseEnter(circle); - + expect(document.body.style.cursor).toBe('default'); }); it('should reset cursor to default on mouse leave', () => { const { getByTestId } = render(); const circle = getByTestId('circle'); - + fireEvent.mouseEnter(circle); fireEvent.mouseLeave(circle); - + expect(document.body.style.cursor).toBe('default'); }); }); @@ -190,9 +190,9 @@ describe('CircleComponent', () => { it('should show hover rect when hoverOutlineColor is provided', () => { const { container, getByTestId } = render(); const circle = getByTestId('circle'); - + fireEvent.mouseEnter(circle); - + const rects = container.querySelectorAll('[data-testid="rect"]'); expect(rects.length).toBeGreaterThan(0); }); @@ -208,9 +208,9 @@ describe('CircleComponent', () => { /> ); const circle = getByTestId('circle'); - + fireEvent.mouseEnter(circle); - + const rect = container.querySelector('[data-testid="rect"]'); if (rect) { // Rect should be positioned at (x - radius, y - radius) with width/height = radius * 2 @@ -226,12 +226,12 @@ describe('CircleComponent', () => { ); const circle = getByTestId('circle'); - + fireEvent.mouseEnter(circle); - + const rect = container.querySelector('[data-testid="rect"]'); if (rect) { - expect(rect).toHaveAttribute('stroke', 'transparent'); + expect(rect).toHaveAttribute('stroke', '#FFFF00'); } }); }); @@ -247,7 +247,7 @@ describe('CircleComponent', () => { showCorrectEnabled={false} /> ); - + const icon = getByTestId('icon-image'); expect(icon).toBeInTheDocument(); }); @@ -262,7 +262,7 @@ describe('CircleComponent', () => { showCorrectEnabled={false} /> ); - + const icon = getByTestId('icon-image'); expect(icon).toBeInTheDocument(); }); @@ -277,7 +277,7 @@ describe('CircleComponent', () => { showCorrectEnabled={false} /> ); - + const icon = getByTestId('icon-image'); expect(icon).toBeInTheDocument(); }); @@ -292,7 +292,7 @@ describe('CircleComponent', () => { showCorrectEnabled={false} /> ); - + const icon = queryByTestId('icon-image'); expect(icon).not.toBeInTheDocument(); }); @@ -307,7 +307,7 @@ describe('CircleComponent', () => { showCorrectEnabled={true} /> ); - + const icon = getByTestId('icon-image'); expect(icon).toBeInTheDocument(); }); @@ -322,7 +322,7 @@ describe('CircleComponent', () => { showCorrectEnabled={true} /> ); - + const icon = getByTestId('icon-image'); expect(icon).toBeInTheDocument(); }); @@ -337,7 +337,7 @@ describe('CircleComponent', () => { showCorrectEnabled={true} /> ); - + const icon = queryByTestId('icon-image'); expect(icon).not.toBeInTheDocument(); }); @@ -352,7 +352,7 @@ describe('CircleComponent', () => { showCorrectEnabled={true} /> ); - + const icon = queryByTestId('icon-image'); expect(icon).not.toBeInTheDocument(); }); @@ -365,7 +365,7 @@ describe('CircleComponent', () => { markAsCorrect={true} /> ); - + const circle = getByTestId('circle'); expect(circle).toHaveAttribute('stroke', 'green'); }); @@ -379,7 +379,7 @@ describe('CircleComponent', () => { markAsCorrect={false} /> ); - + const circle = getByTestId('circle'); expect(circle).toHaveAttribute('stroke', 'red'); }); @@ -395,7 +395,7 @@ describe('CircleComponent', () => { showCorrectEnabled={false} /> ); - + const icon = getByTestId('icon-image'); expect(icon).toHaveAttribute('data-tooltip', 'Great job!'); }); @@ -415,7 +415,7 @@ describe('CircleComponent', () => { showCorrectEnabled={false} /> ); - + const icon = getByTestId('icon-image'); // Icon should be at x - 10, y - 10 expect(icon).toHaveAttribute('data-x', '90'); @@ -431,7 +431,7 @@ describe('CircleComponent', () => { radius={5} /> ); - + const circle = getByTestId('circle'); expect(circle).toHaveAttribute('radius', '5'); }); @@ -443,7 +443,7 @@ describe('CircleComponent', () => { radius={200} /> ); - + const circle = getByTestId('circle'); expect(circle).toHaveAttribute('radius', '200'); }); @@ -455,7 +455,7 @@ describe('CircleComponent', () => { scale={0.5} /> ); - + const group = getByTestId('group'); expect(group).toHaveAttribute('scaleX', '0.5'); expect(group).toHaveAttribute('scaleY', '0.5'); diff --git a/packages/hotspot/src/hotspot/__tests__/rectangle.test.jsx b/packages/hotspot/src/hotspot/__tests__/rectangle.test.jsx index e865c9dbbe..183615fb3f 100644 --- a/packages/hotspot/src/hotspot/__tests__/rectangle.test.jsx +++ b/packages/hotspot/src/hotspot/__tests__/rectangle.test.jsx @@ -71,7 +71,7 @@ describe('RectComponent', () => { const { container } = render(); const rects = container.querySelectorAll('[data-testid="rect"]'); const mainRect = rects[rects.length - 1]; - + expect(mainRect).toHaveAttribute('x', '10'); expect(mainRect).toHaveAttribute('y', '20'); expect(mainRect).toHaveAttribute('width', '100'); @@ -82,7 +82,7 @@ describe('RectComponent', () => { const { container } = render(); const rects = container.querySelectorAll('[data-testid="rect"]'); const mainRect = rects[rects.length - 1]; - + expect(mainRect).toHaveAttribute('fill', '#FF0000'); }); @@ -90,14 +90,14 @@ describe('RectComponent', () => { const { container } = render(); const rects = container.querySelectorAll('[data-testid="rect"]'); const mainRect = rects[rects.length - 1]; - + expect(mainRect).toHaveAttribute('fill', '#00FF00'); }); it('should apply scale transform', () => { const { getByTestId } = render(); const group = getByTestId('group'); - + expect(group).toHaveAttribute('scaleX', '1.5'); expect(group).toHaveAttribute('scaleY', '1.5'); }); @@ -105,7 +105,7 @@ describe('RectComponent', () => { it('should render with default scale of 1', () => { const { getByTestId } = render(); const group = getByTestId('group'); - + expect(group).toHaveAttribute('scaleX', '1'); expect(group).toHaveAttribute('scaleY', '1'); }); @@ -117,9 +117,9 @@ describe('RectComponent', () => { const { container } = render(); const rects = container.querySelectorAll('[data-testid="rect"]'); const mainRect = rects[rects.length - 1]; - + fireEvent.click(mainRect); - + expect(onClick).toHaveBeenCalledWith({ id: 'rect1', selected: true, @@ -132,21 +132,21 @@ describe('RectComponent', () => { const { container, rerender } = render(); const rects = container.querySelectorAll('[data-testid="rect"]'); const mainRect = rects[rects.length - 1]; - + fireEvent.click(mainRect); - + expect(onClick).toHaveBeenCalledWith({ id: 'rect1', selected: true, selector: 'Mouse', }); - + rerender(); - + const rectsAfter = container.querySelectorAll('[data-testid="rect"]'); const mainRectAfter = rectsAfter[rectsAfter.length - 1]; fireEvent.click(mainRectAfter); - + expect(onClick).toHaveBeenCalledWith({ id: 'rect1', selected: false, @@ -159,9 +159,9 @@ describe('RectComponent', () => { const { container } = render(); const rects = container.querySelectorAll('[data-testid="rect"]'); const mainRect = rects[rects.length - 1]; - + fireEvent.click(mainRect); - + expect(onClick).not.toHaveBeenCalled(); }); @@ -169,9 +169,9 @@ describe('RectComponent', () => { const { container } = render(); const rects = container.querySelectorAll('[data-testid="rect"]'); const mainRect = rects[rects.length - 1]; - + fireEvent.mouseEnter(mainRect); - + expect(document.body.style.cursor).toBe('pointer'); }); @@ -179,9 +179,9 @@ describe('RectComponent', () => { const { container } = render(); const rects = container.querySelectorAll('[data-testid="rect"]'); const mainRect = rects[rects.length - 1]; - + fireEvent.mouseEnter(mainRect); - + expect(document.body.style.cursor).toBe('default'); }); @@ -189,10 +189,10 @@ describe('RectComponent', () => { const { container } = render(); const rects = container.querySelectorAll('[data-testid="rect"]'); const mainRect = rects[rects.length - 1]; - + fireEvent.mouseEnter(mainRect); fireEvent.mouseLeave(mainRect); - + expect(document.body.style.cursor).toBe('default'); }); }); @@ -202,9 +202,9 @@ describe('RectComponent', () => { const { container } = render(); const rects = container.querySelectorAll('[data-testid="rect"]'); const mainRect = rects[rects.length - 1]; - + fireEvent.mouseEnter(mainRect); - + const rectsAfterHover = container.querySelectorAll('[data-testid="rect"]'); expect(rectsAfterHover.length).toBeGreaterThan(1); }); @@ -215,12 +215,12 @@ describe('RectComponent', () => { ); const rects = container.querySelectorAll('[data-testid="rect"]'); const mainRect = rects[rects.length - 1]; - + fireEvent.mouseEnter(mainRect); - + const hoverRect = container.querySelector('[stroke="#FFFF00"]'); if (hoverRect) { - expect(hoverRect).toHaveAttribute('stroke', 'transparent'); + expect(hoverRect).toHaveAttribute('stroke', '#FFFF00'); } }); }); @@ -236,7 +236,7 @@ describe('RectComponent', () => { showCorrectEnabled={false} /> ); - + const icon = getByTestId('icon-image'); expect(icon).toBeInTheDocument(); expect(icon).toHaveAttribute('data-src'); @@ -252,7 +252,7 @@ describe('RectComponent', () => { showCorrectEnabled={false} /> ); - + const icon = getByTestId('icon-image'); expect(icon).toBeInTheDocument(); }); @@ -267,7 +267,7 @@ describe('RectComponent', () => { showCorrectEnabled={false} /> ); - + const icon = getByTestId('icon-image'); expect(icon).toBeInTheDocument(); }); @@ -282,7 +282,7 @@ describe('RectComponent', () => { showCorrectEnabled={false} /> ); - + const icon = queryByTestId('icon-image'); expect(icon).not.toBeInTheDocument(); }); @@ -297,7 +297,7 @@ describe('RectComponent', () => { showCorrectEnabled={true} /> ); - + const icon = getByTestId('icon-image'); expect(icon).toBeInTheDocument(); }); @@ -312,7 +312,7 @@ describe('RectComponent', () => { showCorrectEnabled={true} /> ); - + const icon = getByTestId('icon-image'); expect(icon).toBeInTheDocument(); }); @@ -327,7 +327,7 @@ describe('RectComponent', () => { showCorrectEnabled={true} /> ); - + const icon = queryByTestId('icon-image'); expect(icon).not.toBeInTheDocument(); }); @@ -342,7 +342,7 @@ describe('RectComponent', () => { showCorrectEnabled={true} /> ); - + const icon = queryByTestId('icon-image'); expect(icon).not.toBeInTheDocument(); }); @@ -355,7 +355,7 @@ describe('RectComponent', () => { markAsCorrect={true} /> ); - + const rects = container.querySelectorAll('[data-testid="rect"]'); const mainRect = rects[rects.length - 1]; expect(mainRect).toHaveAttribute('stroke', 'green'); @@ -370,7 +370,7 @@ describe('RectComponent', () => { markAsCorrect={false} /> ); - + const rects = container.querySelectorAll('[data-testid="rect"]'); const mainRect = rects[rects.length - 1]; expect(mainRect).toHaveAttribute('stroke', 'red'); @@ -387,7 +387,7 @@ describe('RectComponent', () => { showCorrectEnabled={false} /> ); - + const icon = getByTestId('icon-image'); expect(icon).toHaveAttribute('data-tooltip', 'Correct answer!'); }); @@ -408,7 +408,7 @@ describe('RectComponent', () => { showCorrectEnabled={false} /> ); - + const icon = getByTestId('icon-image'); // Icon should be centered: x + width/2 - 10, y + height/2 - 10 expect(icon).toHaveAttribute('data-x', '50'); // 10 + 100/2 - 10 diff --git a/packages/hotspot/src/hotspot/circle.jsx b/packages/hotspot/src/hotspot/circle.jsx index 1856c9f8ae..82e0ca2d9b 100644 --- a/packages/hotspot/src/hotspot/circle.jsx +++ b/packages/hotspot/src/hotspot/circle.jsx @@ -50,6 +50,7 @@ class CircleComponent extends React.Component { hoverOutlineColor, outlineColor, selected, + focused, x, y, evaluateText, @@ -88,20 +89,10 @@ class CircleComponent extends React.Component { } } - const useHoveredStyle = hovered && hoverOutlineColor; + const useHoveredStyle = (hovered || focused) && hoverOutlineColor; return ( - - {useHoveredStyle && ( - - )} + + {useHoveredStyle && ( + + )} {isEvaluateMode && iconSrc ? : null} ); @@ -128,6 +128,7 @@ CircleComponent.propTypes = { isCorrect: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]), isEvaluateMode: PropTypes.bool.isRequired, disabled: PropTypes.bool.isRequired, + focused: PropTypes.bool, hoverOutlineColor: PropTypes.string, onClick: PropTypes.func.isRequired, outlineColor: PropTypes.string.isRequired, @@ -145,6 +146,7 @@ CircleComponent.propTypes = { CircleComponent.defaultProps = { isCorrect: false, evaluateText: null, + focused: false, strokeWidth: 5, scale: 1, }; diff --git a/packages/hotspot/src/hotspot/container.jsx b/packages/hotspot/src/hotspot/container.jsx index cf61929da3..01ed3178bf 100644 --- a/packages/hotspot/src/hotspot/container.jsx +++ b/packages/hotspot/src/hotspot/container.jsx @@ -14,8 +14,19 @@ const BaseContainer = styled('div')(({ theme }) => ({ background: theme.palette.common.white, border: `${theme.spacing(1)} solid ${theme.palette.common.white}`, width: 'fit-content', + maxWidth: '100%', + overflowX: 'auto', })); +const HiddenFocusable = styled('span')({ + position: 'absolute', + width: 1, + height: 1, + overflow: 'hidden', + clip: 'rect(0, 0, 0, 0)', + whiteSpace: 'nowrap', +}); + const ImageContainer = styled('div')({ position: 'relative', width: 'fit-content', @@ -34,6 +45,13 @@ const StyledStage = styled(Stage)({ }); export class Container extends React.Component { + constructor(props) { + super(props); + this.state = { + focusedShapeId: null, + }; + } + isSelected(shape) { const selectedShape = this.props.session.answers.filter((answer) => answer.id === shape.id)[0]; return !!selectedShape; @@ -57,6 +75,44 @@ export class Container extends React.Component { return null; }; + getAllShapesSorted = () => { + const { shapes: { rectangles = [], polygons = [], circles = [] } } = this.props; + const allShapes = [ + ...rectangles.map((s) => ({ ...s, type: 'rectangle' })), + ...polygons.map((s) => ({ ...s, type: 'polygon' })), + ...circles.map((s) => ({ ...s, type: 'circle' })), + ]; + allShapes.sort((a, b) => String(a.id).localeCompare(String(b.id), undefined, { numeric: true })); + + return allShapes; + }; + + handleShapeFocus = (shapeId) => { + this.setState({ focusedShapeId: shapeId }); + }; + + handleShapeBlur = () => { + this.setState({ focusedShapeId: null }); + }; + + handleShapeKeyDown = (e, shapeId) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + + const { onSelectChoice, disabled } = this.props; + + if (!disabled) { + const shape = this.getAllShapesSorted().find((s) => s.id === shapeId); + + if (shape) { + const selected = this.isSelected(shape); + + onSelectChoice({ id: shapeId, selected: !selected, selector: 'Keyboard' }); + } + } + } + }; + render() { const { dimensions: { width: withProp, height: heightProp }, @@ -74,9 +130,13 @@ export class Container extends React.Component { showCorrect, } = this.props; + const { focusedShapeId } = this.state; + const width = withProp * SCALE; const height = heightProp * SCALE; + const sortedShapes = this.getAllShapesSorted(); + return ( {imageUrl ? ( @@ -125,6 +185,7 @@ export class Container extends React.Component { strokeWidth={strokeWidth} markAsCorrect={markAsCorrect} showCorrectEnabled={showCorrect} + focused={focusedShapeId === shape.id} /> ); })} @@ -153,6 +214,7 @@ export class Container extends React.Component { selectedHotspotColor={selectedHotspotColor} hoverOutlineColor={hoverOutlineColor} showCorrectEnabled={showCorrect} + focused={focusedShapeId === polygon.id} /> ); })} @@ -183,11 +245,29 @@ export class Container extends React.Component { selectedHotspotColor={selectedHotspotColor} hoverOutlineColor={hoverOutlineColor} showCorrectEnabled={showCorrect} + focused={focusedShapeId === shape.id} /> ); })} + + {sortedShapes.map((shape) => { + const selected = this.isSelected(shape); + + return ( + this.handleShapeFocus(shape.id)} + onBlur={this.handleShapeBlur} + onKeyDown={(e) => this.handleShapeKeyDown(e, shape.id)} + /> + ); + })} ); } diff --git a/packages/hotspot/src/hotspot/polygon.jsx b/packages/hotspot/src/hotspot/polygon.jsx index 89168583d4..71027f273a 100644 --- a/packages/hotspot/src/hotspot/polygon.jsx +++ b/packages/hotspot/src/hotspot/polygon.jsx @@ -68,6 +68,7 @@ class PolygonComponent extends React.Component { hoverOutlineColor, outlineColor, selected, + focused, points, evaluateText, strokeWidth, @@ -120,7 +121,7 @@ class PolygonComponent extends React.Component { iconSrc = faWrong; } } - const useHoveredStyle = hovered && hoverOutlineColor; + const useHoveredStyle = (hovered || focused) && hoverOutlineColor; const xValues = pointsParsed.filter((_, index) => index % 2 === 0); // Even indices are x-coordinates const yValues = pointsParsed.filter((_, index) => index % 2 !== 0); // Odd indices are y-coordinates @@ -136,31 +137,30 @@ class PolygonComponent extends React.Component { const rectHeight = maxY - minY; return ( - - {useHoveredStyle && ( - - )} + + {useHoveredStyle && ( + + )} {isEvaluateMode && iconSrc ? : null} ); @@ -174,6 +174,7 @@ PolygonComponent.propTypes = { isEvaluateMode: PropTypes.bool.isRequired, hoverOutlineColor: PropTypes.string, disabled: PropTypes.bool.isRequired, + focused: PropTypes.bool, onClick: PropTypes.func.isRequired, outlineColor: PropTypes.string.isRequired, points: PropTypes.array.isRequired, @@ -188,6 +189,7 @@ PolygonComponent.propTypes = { PolygonComponent.defaultProps = { evaluateText: null, + focused: false, strokeWidth: 5, scale: 1, }; diff --git a/packages/hotspot/src/hotspot/rectangle.jsx b/packages/hotspot/src/hotspot/rectangle.jsx index 43dc26f58e..313dd31687 100644 --- a/packages/hotspot/src/hotspot/rectangle.jsx +++ b/packages/hotspot/src/hotspot/rectangle.jsx @@ -51,6 +51,7 @@ class RectComponent extends React.Component { isEvaluateMode, outlineColor, selected, + focused, width, x, y, @@ -60,6 +61,7 @@ class RectComponent extends React.Component { markAsCorrect, showCorrectEnabled, } = this.props; + const { hovered } = this.state; const outlineColorParsed = isEvaluateMode ? this.getEvaluateOutlineColor(isCorrect, markAsCorrect, outlineColor) @@ -102,22 +104,10 @@ class RectComponent extends React.Component { } } - const { hovered } = this.state; - const useHoveredStyle = hovered && hoverOutlineColor; + const useHoveredStyle = (hovered || focused) && hoverOutlineColor; return ( - - {useHoveredStyle && ( - - )} + + {useHoveredStyle && ( + + )} {isEvaluateMode && iconSrc ? : null} ); @@ -147,6 +146,7 @@ RectComponent.propTypes = { isEvaluateMode: PropTypes.bool.isRequired, hoverOutlineColor: PropTypes.string, disabled: PropTypes.bool.isRequired, + focused: PropTypes.bool, onClick: PropTypes.func.isRequired, outlineColor: PropTypes.string.isRequired, selected: PropTypes.bool.isRequired, @@ -164,6 +164,7 @@ RectComponent.propTypes = { RectComponent.defaultProps = { isCorrect: false, evaluateText: null, + focused: false, strokeWidth: 5, scale: 1, }; diff --git a/packages/image-cloze-association/configure/package.json b/packages/image-cloze-association/configure/package.json index 11531f14d0..7a79e83755 100644 --- a/packages/image-cloze-association/configure/package.json +++ b/packages/image-cloze-association/configure/package.json @@ -12,8 +12,8 @@ "@mui/icons-material": "^7.3.4", "@mui/material": "^7.3.4", "@pie-framework/pie-configure-events": "^1.3.0", - "@pie-lib/config-ui": "13.0.12", - "@pie-lib/editable-html-tip-tap": "2.1.10", + "@pie-lib/config-ui": "13.0.13", + "@pie-lib/editable-html-tip-tap": "2.1.11", "debug": "^4.1.1", "prop-types": "^15.8.1", "react": "18.3.1", diff --git a/packages/inline-dropdown/configure/package.json b/packages/inline-dropdown/configure/package.json index 82eafbad65..3f07b2adbf 100644 --- a/packages/inline-dropdown/configure/package.json +++ b/packages/inline-dropdown/configure/package.json @@ -10,8 +10,8 @@ "@mui/icons-material": "^7.3.4", "@mui/material": "^7.3.4", "@pie-framework/pie-configure-events": "^1.3.0", - "@pie-lib/config-ui": "13.0.12", - "@pie-lib/editable-html-tip-tap": "2.1.10", + "@pie-lib/config-ui": "13.0.13", + "@pie-lib/editable-html-tip-tap": "2.1.11", "@pie-lib/math-rendering": "5.1.0", "@pie-lib/render-ui": "6.1.3", "classnames": "^2.2.6", diff --git a/packages/inline-dropdown/package.json b/packages/inline-dropdown/package.json index 14e678dd0b..aafa0f3158 100644 --- a/packages/inline-dropdown/package.json +++ b/packages/inline-dropdown/package.json @@ -16,7 +16,7 @@ "@mui/material": "^7.3.4", "@pie-framework/pie-player-events": "^0.1.0", "@pie-lib/correct-answer-toggle": "4.0.5", - "@pie-lib/mask-markup": "3.0.12", + "@pie-lib/mask-markup": "3.0.13", "@pie-lib/math-rendering": "5.1.0", "@pie-lib/render-ui": "6.1.3", "lodash-es": "^4.17.23", diff --git a/packages/likert/configure/package.json b/packages/likert/configure/package.json index 09986f67ef..fa1c74acf0 100644 --- a/packages/likert/configure/package.json +++ b/packages/likert/configure/package.json @@ -12,8 +12,8 @@ "@mui/icons-material": "^7.3.4", "@mui/material": "^7.3.4", "@pie-framework/pie-configure-events": "^1.3.0", - "@pie-lib/config-ui": "13.0.12", - "@pie-lib/editable-html-tip-tap": "2.1.10", + "@pie-lib/config-ui": "13.0.13", + "@pie-lib/editable-html-tip-tap": "2.1.11", "@pie-lib/render-ui": "6.1.3", "debug": "^4.1.1", "prop-types": "^15.8.1", diff --git a/packages/match/configure/package.json b/packages/match/configure/package.json index 123be9ca3f..c2eb4f4ac3 100644 --- a/packages/match/configure/package.json +++ b/packages/match/configure/package.json @@ -13,9 +13,9 @@ "@mui/icons-material": "^7.3.4", "@mui/material": "^7.3.4", "@pie-framework/pie-configure-events": "^1.3.0", - "@pie-lib/config-ui": "13.0.12", + "@pie-lib/config-ui": "13.0.13", "@pie-lib/drag": "4.0.6", - "@pie-lib/editable-html-tip-tap": "2.1.10", + "@pie-lib/editable-html-tip-tap": "2.1.11", "@pie-lib/render-ui": "6.1.3", "@pie-lib/test-utils": "2.0.2", "classnames": "^2.2.6", diff --git a/packages/math-inline/configure/package.json b/packages/math-inline/configure/package.json index dcec365954..bc1159a48b 100644 --- a/packages/math-inline/configure/package.json +++ b/packages/math-inline/configure/package.json @@ -12,8 +12,8 @@ "@mui/icons-material": "^7.3.4", "@mui/material": "^7.3.4", "@pie-framework/pie-configure-events": "^1.3.0", - "@pie-lib/config-ui": "13.0.12", - "@pie-lib/editable-html-tip-tap": "2.1.10", + "@pie-lib/config-ui": "13.0.13", + "@pie-lib/editable-html-tip-tap": "2.1.11", "@pie-lib/math-input": "8.1.1", "@pie-lib/math-rendering": "5.1.0", "@pie-lib/math-toolbar": "3.0.6", diff --git a/packages/math-templated/configure/package.json b/packages/math-templated/configure/package.json index c640ba2ae9..1bafdca92e 100644 --- a/packages/math-templated/configure/package.json +++ b/packages/math-templated/configure/package.json @@ -11,8 +11,8 @@ "@mui/icons-material": "^7.3.4", "@mui/material": "^7.3.4", "@pie-framework/pie-configure-events": "^1.3.0", - "@pie-lib/config-ui": "13.0.12", - "@pie-lib/editable-html-tip-tap": "2.1.10", + "@pie-lib/config-ui": "13.0.13", + "@pie-lib/editable-html-tip-tap": "2.1.11", "@pie-lib/math-toolbar": "3.0.6", "@pie-lib/render-ui": "6.1.3", "debug": "^4.1.1", diff --git a/packages/math-templated/package.json b/packages/math-templated/package.json index f755585b43..765867762b 100644 --- a/packages/math-templated/package.json +++ b/packages/math-templated/package.json @@ -13,7 +13,7 @@ "@mui/material": "^7.3.4", "@pie-framework/pie-player-events": "^0.1.0", "@pie-lib/correct-answer-toggle": "4.0.5", - "@pie-lib/mask-markup": "3.0.12", + "@pie-lib/mask-markup": "3.0.13", "@pie-lib/math-input": "8.1.1", "@pie-lib/math-rendering": "5.1.0", "@pie-lib/render-ui": "6.1.3", diff --git a/packages/matrix/configure/package.json b/packages/matrix/configure/package.json index 25b19220c6..4ba6838642 100644 --- a/packages/matrix/configure/package.json +++ b/packages/matrix/configure/package.json @@ -12,8 +12,8 @@ "@mui/icons-material": "^7.3.4", "@mui/material": "^7.3.4", "@pie-framework/pie-configure-events": "^1.3.0", - "@pie-lib/config-ui": "13.0.12", - "@pie-lib/editable-html-tip-tap": "2.1.10", + "@pie-lib/config-ui": "13.0.13", + "@pie-lib/editable-html-tip-tap": "2.1.11", "@pie-lib/render-ui": "6.1.3", "debug": "^4.1.1", "prop-types": "^15.8.1", diff --git a/packages/multi-trait-rubric/configure/package.json b/packages/multi-trait-rubric/configure/package.json index d99cfb3693..92de1d7768 100644 --- a/packages/multi-trait-rubric/configure/package.json +++ b/packages/multi-trait-rubric/configure/package.json @@ -11,9 +11,9 @@ "@mui/icons-material": "^7.3.4", "@mui/material": "^7.3.4", "@pie-framework/pie-configure-events": "^1.3.0", - "@pie-lib/config-ui": "13.0.12", + "@pie-lib/config-ui": "13.0.13", "@pie-lib/drag": "4.0.6", - "@pie-lib/editable-html-tip-tap": "2.1.10", + "@pie-lib/editable-html-tip-tap": "2.1.11", "@pie-lib/render-ui": "6.1.3", "debug": "^4.1.1", "lodash-es": "^4.17.23", diff --git a/packages/multiple-choice/configure/package.json b/packages/multiple-choice/configure/package.json index d2f9ccefcb..362dfeb559 100644 --- a/packages/multiple-choice/configure/package.json +++ b/packages/multiple-choice/configure/package.json @@ -10,8 +10,8 @@ "@mui/icons-material": "^7.3.4", "@mui/material": "^7.3.4", "@pie-framework/pie-configure-events": "^1.3.0", - "@pie-lib/config-ui": "13.0.12", - "@pie-lib/editable-html-tip-tap": "2.1.10", + "@pie-lib/config-ui": "13.0.13", + "@pie-lib/editable-html-tip-tap": "2.1.11", "@pie-lib/render-ui": "6.1.3", "debug": "^4.1.1", "lodash-es": "^4.17.23", diff --git a/packages/multiple-choice/controller/src/__tests__/index.test.js b/packages/multiple-choice/controller/src/__tests__/index.test.js index 57f70bd1ce..b80f3557eb 100644 --- a/packages/multiple-choice/controller/src/__tests__/index.test.js +++ b/packages/multiple-choice/controller/src/__tests__/index.test.js @@ -1,4 +1,4 @@ -import { model, outcome, getScore, createCorrectResponseSession, normalize } from '../index'; +import { model, outcome, getScore, createCorrectResponseSession, normalize, validate } from '../index'; import { isResponseCorrect } from '../utils'; import defaults from '../defaults'; @@ -478,4 +478,45 @@ describe('controller', () => { expect(sess).toEqual({ ...defaults, ...question, choicesLayout: 'vertical' }); }); }); + + describe('validate', () => { + const makeChoice = (value, correct = false) => ({ value, label: value, correct }); + const config = { minAnswerChoices: 2, maxAnswerChoices: 5 }; + + it('returns no error when maxSelections >= correctCount', () => { + const m = { + choiceMode: 'checkbox', + maxSelections: 2, + choices: [makeChoice('A', true), makeChoice('B', true), makeChoice('C')], + }; + expect(validate(m, config).correctResponse).toBeUndefined(); + }); + + it('returns error when maxSelections < correctCount', () => { + const m = { + choiceMode: 'checkbox', + maxSelections: 1, + choices: [makeChoice('A', true), makeChoice('B', true), makeChoice('C')], + }; + expect(validate(m, config).correctResponse).toMatch(/exceeds max selections/); + }); + + it('does not error in radio mode even if maxSelections < correctCount', () => { + const m = { + choiceMode: 'radio', + maxSelections: 1, + choices: [makeChoice('A', true), makeChoice('B', true), makeChoice('C')], + }; + expect(validate(m, config).correctResponse).toBeUndefined(); + }); + + it('does not error when maxSelections is null', () => { + const m = { + choiceMode: 'checkbox', + maxSelections: null, + choices: [makeChoice('A', true), makeChoice('B', true), makeChoice('C')], + }; + expect(validate(m, config).correctResponse).toBeUndefined(); + }); + }); }); diff --git a/packages/multiple-choice/controller/src/index.js b/packages/multiple-choice/controller/src/index.js index f22840c303..8ffdf41116 100644 --- a/packages/multiple-choice/controller/src/index.js +++ b/packages/multiple-choice/controller/src/index.js @@ -245,12 +245,14 @@ export const validate = (model = {}, config = {}) => { }); let hasCorrectResponse = false; + let correctCount = 0; reversedChoices.forEach((choice, index) => { const { correct, value, label, rationale } = choice; if (correct) { hasCorrectResponse = true; + correctCount++; } if (!getContent(label)) { @@ -278,6 +280,12 @@ export const validate = (model = {}, config = {}) => { if (!hasCorrectResponse) { errors.correctResponse = 'No correct response defined.'; + } else { + const { maxSelections, choiceMode } = model; + + if (choiceMode !== 'radio' && maxSelections != null && correctCount > maxSelections) { + errors.correctResponse = `The number of correct answers (${correctCount}) exceeds max selections (${maxSelections}). Students won't be able to select all correct answers.`; + } } if (!isEmpty(choicesErrors)) { diff --git a/packages/multiple-choice/src/__tests__/choice-input-test.jsx b/packages/multiple-choice/src/__tests__/choice-input.test.jsx similarity index 100% rename from packages/multiple-choice/src/__tests__/choice-input-test.jsx rename to packages/multiple-choice/src/__tests__/choice-input.test.jsx diff --git a/packages/multiple-choice/src/__tests__/index-test.jsx b/packages/multiple-choice/src/__tests__/index.test.jsx similarity index 81% rename from packages/multiple-choice/src/__tests__/index-test.jsx rename to packages/multiple-choice/src/__tests__/index.test.jsx index 87b54e34a0..20ae26a56d 100644 --- a/packages/multiple-choice/src/__tests__/index-test.jsx +++ b/packages/multiple-choice/src/__tests__/index.test.jsx @@ -7,6 +7,18 @@ import { isComplete } from '../index'; jest.useFakeTimers(); jest.mock('@pie-lib/math-rendering', () => ({ renderMath: jest.fn() })); +jest.mock('@pie-lib/correct-answer-toggle', () => () => null); +jest.mock('@pie-lib/translator', () => ({ + __esModule: true, + default: { translator: { t: (key) => key } }, +})); +jest.mock('../choice', () => { + const React = require('react'); + return { + __esModule: true, + default: ({ choice }) =>
{choice.label}
, + }; +}); jest.mock('lodash-es', () => { const lodash = require('lodash'); return { @@ -48,6 +60,16 @@ describe('isComplete', () => { }); }); +beforeAll(() => { + customElements.define('pie-multiple-choice', MultipleChoice); +}); + +const makeEl = () => { + const el = new MultipleChoice(); + el.dispatchEvent = jest.fn(); + return el; +}; + describe('multiple-choice', () => { describe('rendering', () => { const renderComponent = (modelOverrides = {}) => { @@ -74,11 +96,6 @@ describe('multiple-choice', () => { expect(screen.getByTestId('preview-layout')).toBeInTheDocument(); }); - it('renders with rationale', () => { - renderComponent({ rationale: 'This is rationale' }); - expect(screen.getByText('This is rationale')).toBeInTheDocument(); - }); - it('renders with teacherInstructions', () => { renderComponent({ teacherInstructions: 'These are teacher instructions' }); expect(screen.getByText('These are teacher instructions')).toBeInTheDocument(); @@ -99,56 +116,51 @@ describe('multiple-choice', () => { describe('events', () => { describe('model', () => { it('dispatches model set event', () => { - const el = new MultipleChoice(); - el.tagName = 'mc-el'; + const el = makeEl(); el.model = {}; - expect(el.dispatchEvent).toBeCalledWith(new ModelSetEvent('mc-el', false, true)); + expect(el.dispatchEvent).toBeCalledWith(new ModelSetEvent(el.tagName.toLowerCase(), false, true)); }); }); describe('onChange', () => { it('dispatches session changed event - add answer (checkbox)', () => { - const el = new MultipleChoice(); - el.tagName = 'mc-el'; + const el = makeEl(); el.model = { choiceMode: 'checkbox' }; el.session = { value: [] }; el._onChange({ value: 'a', selected: true }); - expect(el.dispatchEvent).toBeCalledWith(new SessionChangedEvent('mc-el', true)); + expect(el.dispatchEvent).toBeCalledWith(new SessionChangedEvent(el.tagName.toLowerCase(), true)); }); it('dispatches session changed event - remove answer (checkbox)', () => { - const el = new MultipleChoice(); - el.tagName = 'mc-el'; + const el = makeEl(); el.model = { choiceMode: 'checkbox' }; el.session = { value: ['a'] }; el._onChange({ value: 'a', selected: false }); - expect(el.dispatchEvent).toBeCalledWith(new SessionChangedEvent('mc-el', false)); + expect(el.dispatchEvent).toBeCalledWith(new SessionChangedEvent(el.tagName.toLowerCase(), false)); }); it('dispatches session changed event - add/remove answer (checkbox)', () => { - const el = new MultipleChoice(); - el.tagName = 'mc-el'; + const el = makeEl(); el.model = { choiceMode: 'checkbox' }; el.session = { value: ['1'] }; el._onChange({ id: '2', selected: true }); - expect(el.dispatchEvent).toBeCalledWith(new SessionChangedEvent('mc-el', true)); + expect(el.dispatchEvent).toBeCalledWith(new SessionChangedEvent(el.tagName.toLowerCase(), true)); el._onChange({ id: '1', selected: false }); - expect(el.dispatchEvent).toBeCalledWith(new SessionChangedEvent('mc-el', true)); + expect(el.dispatchEvent).toBeCalledWith(new SessionChangedEvent(el.tagName.toLowerCase(), true)); el._onChange({ id: '2', selected: false }); - expect(el.dispatchEvent).toBeCalledWith(new SessionChangedEvent('mc-el', false)); + expect(el.dispatchEvent).toBeCalledWith(new SessionChangedEvent(el.tagName.toLowerCase(), false)); }); it('dispatches session changed event - add/change answer (radio)', () => { - const el = new MultipleChoice(); - el.tagName = 'mc-el'; + const el = makeEl(); el.model = { choiceMode: 'radio' }; el.session = { value: [] }; el._onChange({ value: 'a', selected: true }); - expect(el.dispatchEvent).toBeCalledWith(new SessionChangedEvent('mc-el', true)); + expect(el.dispatchEvent).toBeCalledWith(new SessionChangedEvent(el.tagName.toLowerCase(), true)); el._onChange({ value: 'b', selected: true }); - expect(el.dispatchEvent).toBeCalledWith(new SessionChangedEvent('mc-el', true)); + expect(el.dispatchEvent).toBeCalledWith(new SessionChangedEvent(el.tagName.toLowerCase(), true)); }); }); }); diff --git a/packages/multiple-choice/src/__tests__/key-events-test.jsx b/packages/multiple-choice/src/__tests__/key-events.test.jsx similarity index 86% rename from packages/multiple-choice/src/__tests__/key-events-test.jsx rename to packages/multiple-choice/src/__tests__/key-events.test.jsx index 9e65fbcb21..a34e8698e2 100644 --- a/packages/multiple-choice/src/__tests__/key-events-test.jsx +++ b/packages/multiple-choice/src/__tests__/key-events.test.jsx @@ -1,4 +1,10 @@ -import MultipleChoice from '../multiple-choice'; +import MultipleChoice from '../index'; + +beforeAll(() => { + if (!customElements.get('pie-multiple-choice-key-events')) { + customElements.define('pie-multiple-choice-key-events', MultipleChoice); + } +}); describe('MultipleChoice', () => { let instance; @@ -6,6 +12,7 @@ describe('MultipleChoice', () => { beforeEach(() => { instance = new MultipleChoice(); instance._model = { + mode: 'gather', choices: [ { value: '1' }, { value: '2' }, @@ -28,6 +35,7 @@ describe('MultipleChoice', () => { expect(instance._onChange).toHaveBeenCalledWith({ value: '1', selected: true, + selector: 'Keyboard', }); }); @@ -37,6 +45,7 @@ describe('MultipleChoice', () => { expect(instance._onChange).toHaveBeenCalledWith({ value: '1', selected: false, + selector: 'Keyboard', }); }); @@ -45,6 +54,7 @@ describe('MultipleChoice', () => { expect(instance._onChange).toHaveBeenCalledWith({ value: '1', selected: true, + selector: 'Keyboard', }); }); @@ -59,6 +69,7 @@ describe('MultipleChoice', () => { expect(instance._onChange).toHaveBeenCalledWith({ value: '2', selected: true, + selector: 'Keyboard', }); }); @@ -69,6 +80,7 @@ describe('MultipleChoice', () => { expect(instance._onChange).toHaveBeenCalledWith({ value: '3', selected: true, + selector: 'Keyboard', }); }); @@ -79,6 +91,7 @@ describe('MultipleChoice', () => { expect(instance._onChange).toHaveBeenCalledWith({ value: '1', selected: true, + selector: 'Keyboard', }); }); diff --git a/packages/multiple-choice/src/__tests__/multiple-choice-test.jsx b/packages/multiple-choice/src/__tests__/multiple-choice-test.jsx deleted file mode 100644 index d4c9a5b3fa..0000000000 --- a/packages/multiple-choice/src/__tests__/multiple-choice-test.jsx +++ /dev/null @@ -1,97 +0,0 @@ -import React from 'react'; -import { render, screen } from '@testing-library/react'; -import { MultipleChoice, Choice } from '../multiple-choice'; -import CorrectAnswerToggle from '@pie-lib/correct-answer-toggle'; - -// Mock the CorrectAnswerToggle component -jest.mock('@pie-lib/correct-answer-toggle', () => { - const MockToggle = (props) => ( -
- Mock Toggle -
- ); - MockToggle.displayName = 'CorrectAnswerToggle'; - return MockToggle; -}); - -describe('Choice', () => { - let onChoiceChanged; - - beforeEach(() => { - onChoiceChanged = jest.fn(); - }); - - const renderChoice = (extras = {}) => { - const props = { - classes: {}, - choice: {}, - index: 0, - choicesLength: 1, - showCorrect: false, - isEvaluateMode: false, - choiceMode: 'radio', - disabled: true, - onChoiceChanged, - checked: true, - correctness: 'correct', - displayKey: '0', - choicesLayout: 'grid', - gridColumns: 2, - ...extras, - }; - return render(); - }; - - describe('logic', () => { - it('does not call onChoiceChanged if disabled is true', () => { - const { container } = renderChoice({ disabled: true }); - const input = container.querySelector('input'); - if (input) { - input.click(); - } - // When disabled, onChange should not be called - expect(onChoiceChanged).not.toHaveBeenCalled(); - }); - }); -}); - -describe('CorespringChoice', () => { - const mkWrapper = (opts = {}) => { - const defaultProps = { - classes: {}, - choices: [], - disabled: false, - keyMode: 'letters', - onChoiceChanged: jest.fn(), - mode: 'gather', - ...opts, - }; - - return render(); - }; - - describe('Toggle', () => { - it('toggle is rendered', () => { - mkWrapper(); - expect(screen.getByTestId('correct-answer-toggle')).toBeInTheDocument(); - }); - - it('toggle show is set to false by default', () => { - mkWrapper(); - const toggle = screen.getByTestId('correct-answer-toggle'); - expect(toggle).toHaveAttribute('data-show', 'false'); - }); - - it('shows toggle if mode is evaluate, feedback is enabled and responseCorrect is false', () => { - mkWrapper({ mode: 'evaluate', responseCorrect: false, feedbackEnabled: true }); - const toggle = screen.getByTestId('correct-answer-toggle'); - expect(toggle).toHaveAttribute('data-show', 'true'); - }); - - it('hides toggle if mode is evaluate and responseCorrect is true', () => { - mkWrapper({ mode: 'evaluate', responseCorrect: true }); - const toggle = screen.getByTestId('correct-answer-toggle'); - expect(toggle).toHaveAttribute('data-show', 'false'); - }); - }); -}); diff --git a/packages/multiple-choice/src/__tests__/multiple-choice.test.jsx b/packages/multiple-choice/src/__tests__/multiple-choice.test.jsx new file mode 100644 index 0000000000..9fa42401ea --- /dev/null +++ b/packages/multiple-choice/src/__tests__/multiple-choice.test.jsx @@ -0,0 +1,226 @@ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import { MultipleChoice } from '../multiple-choice'; + +// Mock the CorrectAnswerToggle component +jest.mock('@pie-lib/correct-answer-toggle', () => { + const MockToggle = (props) => ( +
+ Mock Toggle +
+ ); + MockToggle.displayName = 'CorrectAnswerToggle'; + return MockToggle; +}); + +jest.mock('@pie-lib/render-ui', () => ({ + color: { + text: () => '#000', + background: () => '#fff', + incorrect: () => '#f00', + correct: () => '#0f0', + primaryLight: () => '#eee', + primary: () => '#333', + }, + Collapsible: ({ children }) =>
{children}
, + PreviewPrompt: ({ prompt, tagName: Tag = 'div' }) => {prompt}, + transformDataHeadings: (html) => html, +})); + +jest.mock('@pie-lib/translator', () => ({ + __esModule: true, + default: { translator: { t: (key) => key } }, +})); + +// Mock Choice with separate select/deselect buttons so fireEvent.click +// reliably triggers handleChange with the correct checked value. +jest.mock('../choice', () => { + const React = require('react'); + return { + __esModule: true, + default: ({ choice, onChoiceChanged }) => ( + <> +