diff --git a/.changeset/20260501125058-pie-element-multiple-choice.md b/.changeset/20260501125058-pie-element-multiple-choice.md new file mode 100644 index 00000000..2c4f487b --- /dev/null +++ b/.changeset/20260501125058-pie-element-multiple-choice.md @@ -0,0 +1,5 @@ +--- + "@pie-element/multiple-choice": patch +--- + +multiple-choice release flow test diff --git a/.changeset/common-cougars-hang.md b/.changeset/common-cougars-hang.md new file mode 100644 index 00000000..a14ac250 --- /dev/null +++ b/.changeset/common-cougars-hang.md @@ -0,0 +1,5 @@ +--- +"@pie-element/multiple-choice": patch +--- + +test multiple-choice release flow diff --git a/.changeset/floppy-sites-watch.md b/.changeset/floppy-sites-watch.md new file mode 100644 index 00000000..1aeb2440 --- /dev/null +++ b/.changeset/floppy-sites-watch.md @@ -0,0 +1,5 @@ +--- +"@pie-element/multiple-choice": patch +--- + +test release flow diff --git a/.changeset/orange-toes-dream.md b/.changeset/orange-toes-dream.md new file mode 100644 index 00000000..a14ac250 --- /dev/null +++ b/.changeset/orange-toes-dream.md @@ -0,0 +1,5 @@ +--- +"@pie-element/multiple-choice": patch +--- + +test multiple-choice release flow diff --git a/.changeset/pre.json b/.changeset/pre.json new file mode 100644 index 00000000..ad02edb1 --- /dev/null +++ b/.changeset/pre.json @@ -0,0 +1,88 @@ +{ + "mode": "pre", + "tag": "next", + "initialVersions": { + "@pie-element/element-demo": "0.1.1", + "esm-player-test": "1.0.0", + "@pie-element/core": "0.1.0", + "@pie-element/element-player": "0.1.1", + "@pie-element/element-theme": "0.1.0", + "@pie-element/element-theme-daisyui": "0.1.0", + "@pie-element/categorize": "13.0.1", + "@pie-element/charting": "12.0.1", + "@pie-element/complex-rubric": "7.0.1", + "@pie-element/drag-in-the-blank": "10.0.1", + "@pie-element/drawing-response": "12.0.1", + "@pie-element/ebsr": "14.1.0", + "@pie-element/explicit-constructed-response": "11.0.1", + "@pie-element/extended-text-entry": "15.0.1", + "@pie-element/fraction-model": "6.0.1", + "@pie-element/graphing": "10.0.1", + "@pie-element/graphing-solution-set": "6.0.1", + "@pie-element/hotspot": "11.0.1", + "@pie-element/image-cloze-association": "10.0.1", + "@pie-element/inline-dropdown": "10.0.1", + "@pie-element/likert": "4.0.1", + "@pie-element/match": "12.0.1", + "@pie-element/match-list": "7.0.1", + "@pie-element/math-inline": "0.1.0", + "@pie-element/math-templated": "0.1.0", + "@pie-element/matrix": "4.0.1", + "@pie-element/multi-trait-rubric": "8.0.1", + "@pie-element/multiple-choice": "13.1.0", + "@pie-element/number-line": "13.0.1", + "@pie-element/passage": "7.0.1", + "@pie-element/placement-ordering": "14.0.1", + "@pie-element/rubric": "8.0.1", + "@pie-element/select-text": "13.0.1", + "@pie-element/mc-populated-blank": "0.2.10", + "@pie-element/simple-cloze": "0.1.3", + "@pie-element/venn-classification": "0.1.0", + "@pie-lib/categorize": "2.0.1", + "@pie-lib/charting": "7.0.1", + "@pie-lib/config-ui": "13.0.1", + "@pie-lib/controller-utils": "2.0.1", + "@pie-lib/correct-answer-toggle": "4.0.1", + "@pie-lib/drag": "4.0.1", + "@pie-lib/editable-html-tip-tap": "2.0.1", + "@pie-lib/graphing": "4.0.2", + "@pie-lib/graphing-solution-set": "4.0.1", + "@pie-lib/graphing-utils": "3.0.1", + "@pie-lib/icons": "4.0.1", + "@pie-lib/mask-markup": "3.0.1", + "@pie-lib/math-input": "0.1.0", + "@pie-lib/math-rendering": "0.1.0", + "@pie-lib/math-toolbar": "3.0.1", + "@pie-lib/plot": "4.0.1", + "@pie-lib/render-ui": "6.0.1", + "@pie-lib/rubric": "2.0.1", + "@pie-lib/style-utils": "2.0.1", + "@pie-lib/test-utils": "2.0.1", + "@pie-lib/text-select": "3.0.1", + "@pie-lib/tools": "2.0.1", + "@pie-lib/translator": "4.0.1", + "@pie-lib/delivery-events-svelte": "0.1.0", + "@pie-lib/editable-html-tiptap-svelte": "0.1.2", + "@pie-lib/math-input-svelte": "0.1.0", + "@pie-lib/styling-svelte": "0.1.2", + "@pie-element/print-player": "1.0.1", + "@pie-element/element-bundler": "0.1.1", + "@pie-element/bundler-shared": "0.1.1", + "@pie-element/shared-configure-events": "0.1.0", + "@pie-element/shared-controller-utils": "0.1.0", + "@pie-element/shared-feedback": "0.1.0", + "@pie-element/shared-math-rendering-mathjax": "0.1.0", + "@pie-element/shared-player-events": "0.1.0", + "@pie-element/shared-test-utils": "0.1.0", + "@pie-element/shared-theming": "0.1.0", + "@pie-element/shared-theming-mui": "0.1.0", + "@pie-element/shared-types": "0.1.0", + "@pie-element/shared-utils": "0.1.0", + "@pie-element/cli": "0.1.1" + }, + "changesets": [ + "20260501125058-pie-element-multiple-choice", + "common-cougars-hang", + "floppy-sites-watch" + ] +} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0f1ba7c9..a0782236 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -74,7 +74,7 @@ jobs: PUBLISH_NPM_TOKEN: "" PUBLISH_NODE_AUTH_TOKEN: "" # Flip to 'true' and restore `publish: bun run release:publish` on changesets/action to re-enable CI publish. - ENABLE_CICD_NPM_PUBLISH: "false" + ENABLE_CICD_NPM_PUBLISH: "true" permissions: contents: write pull-requests: write @@ -312,7 +312,7 @@ jobs: with: # Omit `publish` so the action never runs `bun run release:publish` from CI. # Re-enable publishing by setting ENABLE_CICD_NPM_PUBLISH=true and adding: - # publish: bun run release:publish + publish: bun run release:publish createGithubReleases: false version: bun run version title: 'chore(release): version packages [skip-heavy-ci]' diff --git a/README.md b/README.md index 272172c0..7bcccd48 100644 --- a/README.md +++ b/README.md @@ -60,8 +60,8 @@ If you're a maintainer and need to sync changes from the upstream `pie-elements` ```bash # 1. Clone upstream repositories as siblings (one-time setup) cd .. -git clone https://github.com/PieLabs/pie-elements.git -git clone https://github.com/PieLabs/pie-lib.git +git clone https://github.com/pie-framework/pie-elements.git +git clone https://github.com/pie-framework/pie-lib.git cd pie-elements-ng # 2. Pull latest upstream changes diff --git a/bun.lock b/bun.lock index 89b77535..2fbd3570 100644 --- a/bun.lock +++ b/bun.lock @@ -30,7 +30,7 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "svelte": "^5.54.0", - "turbo": "^2.9.6", + "turbo": "^2.9.9", "typescript": "^5.9.3", "vite": "^8.0.1", "vitest": "^4.1.0", @@ -310,7 +310,7 @@ }, "packages/elements-react/ebsr": { "name": "@pie-element/ebsr", - "version": "14.0.1", + "version": "14.1.1-next.1", "dependencies": { "@mui/material": "^7.3.4", "@pie-element/multiple-choice": "workspace:*", @@ -858,7 +858,7 @@ }, "packages/elements-react/multiple-choice": { "name": "@pie-element/multiple-choice", - "version": "13.0.1", + "version": "13.1.1-next.1", "dependencies": { "@emotion/react": "^11.14.0", "@emotion/style": "^0.8.0", @@ -1340,7 +1340,7 @@ }, "packages/lib-react/graphing": { "name": "@pie-lib/graphing", - "version": "4.0.1", + "version": "4.0.2", "dependencies": { "@dnd-kit/core": "^6.3.0", "@dnd-kit/sortable": "10.0.0", @@ -2657,17 +2657,17 @@ "@tufjs/models": ["@tufjs/models@4.1.0", "", { "dependencies": { "@tufjs/canonical-json": "2.0.0", "minimatch": "^10.1.1" } }, "sha512-Y8cK9aggNRsqJVaKUlEYs4s7CvQ1b1ta2DVPyAimb0I2qhzjNk+A+mxvll/klL0RlfuIUei8BF7YWiua4kQqww=="], - "@turbo/darwin-64": ["@turbo/darwin-64@2.9.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-X/56SnVXIQZBLKwniGTwEQTGmtE5brSACnKMBWpY3YafuxVYefrC2acamfjgxP7BG5w3I+6jf0UrLoSzgPcSJg=="], + "@turbo/darwin-64": ["@turbo/darwin-64@2.9.9", "", { "os": "darwin", "cpu": "x64" }, "sha512-hTEiNu2ABZZOO1qbjnKASI8eF3BdOOzU6iKv5w5uGOK65DDMc10cS40N1kqM99YT0uSAGUwNu6GdFctRPeEeVA=="], - "@turbo/darwin-arm64": ["@turbo/darwin-arm64@2.9.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-aalBeSl4agT/QtYGDyf/XLajedWzUC9Vg/pm/YO6QQ93vkQ91Vz5uK1ta5RbVRDozQSz4njxUNqRNmOXDzW+qw=="], + "@turbo/darwin-arm64": ["@turbo/darwin-arm64@2.9.9", "", { "os": "darwin", "cpu": "arm64" }, "sha512-MinO40EEcP5mJiTVpfjtEulsEBhVeryfq21QhYtJZ8hQJLHGgy459rcmDVAY8/JERe4dkVU4KW+zoLF22o01EA=="], - "@turbo/linux-64": ["@turbo/linux-64@2.9.6", "", { "os": "linux", "cpu": "x64" }, "sha512-YKi05jnNHaD7vevgYwahpzGwbsNNTwzU2c7VZdmdFm7+cGDP4oREUWSsainiMfRqjRuolQxBwRn8wf1jmu+YZA=="], + "@turbo/linux-64": ["@turbo/linux-64@2.9.9", "", { "os": "linux", "cpu": "x64" }, "sha512-7JNLw88Isk+gMlbsC8pulLDkrqe2B827ZsKFEHilb17AC6Xn/62pzH7afjY7fEU6Ayp4XP/vGhlRWOzqBvBvIQ=="], - "@turbo/linux-arm64": ["@turbo/linux-arm64@2.9.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-02o/ZS69cOYEDczXvOB2xmyrtzjQ2hVFtWZK1iqxXUfzMmTjZK4UumrfNnjckSg+gqeBfnPRHa0NstA173Ik3g=="], + "@turbo/linux-arm64": ["@turbo/linux-arm64@2.9.9", "", { "os": "linux", "cpu": "arm64" }, "sha512-0pnXDwPw1rHii98JZPRg7SvsjIzy7jrhkwGU9Jy5fVYoMdYd3P2vbtLfII+OJ0Mm4Ar5yykdHDTz3RWiRI1o9g=="], - "@turbo/windows-64": ["@turbo/windows-64@2.9.6", "", { "os": "win32", "cpu": "x64" }, "sha512-wVdQjvnBI15wB6JrA+43CtUtagjIMmX6XYO758oZHAsCNSxqRlJtdyujih0D8OCnwCRWiGWGI63zAxR0hO6s9g=="], + "@turbo/windows-64": ["@turbo/windows-64@2.9.9", "", { "os": "win32", "cpu": "x64" }, "sha512-vjDQycz4gQVvIq4n2rPtiiIESwJlAc406qtkiZlqyL+fHZEd9SxYNlBIFYtc5cuMuwrk+sIKrhN7XvwjmvS9YQ=="], - "@turbo/windows-arm64": ["@turbo/windows-arm64@2.9.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-1XUUyWW0W6FTSqGEhU8RHVqb2wP1SPkr7hIvBlMEwH9jr+sJQK5kqeosLJ/QaUv4ecSAd1ZhIrLoW7qslAzT4A=="], + "@turbo/windows-arm64": ["@turbo/windows-arm64@2.9.9", "", { "os": "win32", "cpu": "arm64" }, "sha512-V6NiH43oCctepbOdQFp7UjqLyK8p6Tt824QA+G4TE+B1BBHu80A0W8OCL+H7uBJ3XZjAj/hvPDw3k3l65DoDGw=="], "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], @@ -3971,7 +3971,7 @@ "tuf-js": ["tuf-js@4.1.0", "", { "dependencies": { "@tufjs/models": "4.1.0", "debug": "^4.4.3", "make-fetch-happen": "^15.0.1" } }, "sha512-50QV99kCKH5P/Vs4E2Gzp7BopNV+KzTXqWeaxrfu5IQJBOULRsTIS9seSsOVT8ZnGXzCyx55nYWAi4qJzpZKEQ=="], - "turbo": ["turbo@2.9.6", "", { "optionalDependencies": { "@turbo/darwin-64": "2.9.6", "@turbo/darwin-arm64": "2.9.6", "@turbo/linux-64": "2.9.6", "@turbo/linux-arm64": "2.9.6", "@turbo/windows-64": "2.9.6", "@turbo/windows-arm64": "2.9.6" }, "bin": { "turbo": "bin/turbo" } }, "sha512-+v2QJey7ZUeUiuigkU+uFfklvNUyPI2VO2vBpMYJA+a1hKFLFiKtUYlRHdb3P9CrAvMzi0upbjI4WT+zKtqkBg=="], + "turbo": ["turbo@2.9.9", "", { "optionalDependencies": { "@turbo/darwin-64": "2.9.9", "@turbo/darwin-arm64": "2.9.9", "@turbo/linux-64": "2.9.9", "@turbo/linux-arm64": "2.9.9", "@turbo/windows-64": "2.9.9", "@turbo/windows-arm64": "2.9.9" }, "bin": { "turbo": "bin/turbo" } }, "sha512-3xfzXE/yTjhh0S5dIWlE+3E+J9A09REpLI1ZqVh2+HrNZoVzZn0pkvjiRgVK/Ev3PF9XnaTwCntTx+CADWXcyA=="], "type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="], diff --git a/package.json b/package.json index 7b91c9d6..77c2d01f 100644 --- a/package.json +++ b/package.json @@ -102,7 +102,7 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "svelte": "^5.54.0", - "turbo": "^2.9.6", + "turbo": "^2.9.9", "typescript": "^5.9.3", "vite": "^8.0.1", "vitest": "^4.1.0" diff --git a/packages/elements-react/ebsr/CHANGELOG.md b/packages/elements-react/ebsr/CHANGELOG.md new file mode 100644 index 00000000..a0caa146 --- /dev/null +++ b/packages/elements-react/ebsr/CHANGELOG.md @@ -0,0 +1,16 @@ +# @pie-element/ebsr + +## 14.1.1-next.1 + +### Patch Changes + +- Updated dependencies [e32415a] + - @pie-element/multiple-choice@13.1.1-next.1 + +## 14.1.1-next.0 + +### Patch Changes + +- Updated dependencies [259eb4d] +- Updated dependencies [7bd4a51] + - @pie-element/multiple-choice@13.1.1-next.0 diff --git a/packages/elements-react/ebsr/package.json b/packages/elements-react/ebsr/package.json index 717252a4..d09490be 100644 --- a/packages/elements-react/ebsr/package.json +++ b/packages/elements-react/ebsr/package.json @@ -1,6 +1,6 @@ { "name": "@pie-element/ebsr", - "version": "14.0.1", + "version": "14.1.1-next.1", "description": "", "dependencies": { "@pie-element/multiple-choice": "workspace:*", diff --git a/packages/elements-react/ebsr/src/delivery/index.tsx b/packages/elements-react/ebsr/src/delivery/index.tsx index b2d500e2..e7d33697 100644 --- a/packages/elements-react/ebsr/src/delivery/index.tsx +++ b/packages/elements-react/ebsr/src/delivery/index.tsx @@ -36,6 +36,38 @@ export const isSessionComplete = (session) => { return isNonEmptyArray(a) && isNonEmptyArray(b); }; +function getPlayerAttributes(element) { + const player = + element.closest('pie-player') || + element.closest('pie-item-player'); + + if (!player) { + return { baseHeadingLevel: undefined, includeSrHeading: true }; + } + + const getRaw = (camelCaseName, hyphenatedName, allLowerName) => { + let raw = player[camelCaseName]; + + // fallback in case someone sets via HTML attribute manually + if (raw == null) { + raw = + player.getAttribute(hyphenatedName) ?? + player.getAttribute(allLowerName); + } + + return raw; + }; + + const levelRaw = getRaw('baseHeadingLevel', 'base-heading-level', 'baseheadinglevel'); + const level = parseInt(levelRaw, 10); + const baseHeadingLevel = Number.isFinite(level) && level >= 1 && level <= 6 ? level : undefined; + + const srRaw = getRaw('includeSrHeading', 'include-sr-heading', 'includesrheading'); + const includeSrHeading = srRaw == null ? true : srRaw !== false && srRaw !== 'false'; + + return { baseHeadingLevel, includeSrHeading }; +} + export default class Ebsr extends HTMLElement { constructor() { super(); @@ -95,6 +127,12 @@ export default class Ebsr extends HTMLElement { mode, keyMode: this._model[key].choicePrefix, }; + + // Parts of an EBSR item should not render their own SR headings — + // the EBSR element itself provides the item-level heading. + const { includeSrHeading, baseHeadingLevel } = getPlayerAttributes(this); + part.includeSrHeading = includeSrHeading; + part.baseHeadingLevel = baseHeadingLevel !== undefined ? Math.min(6, baseHeadingLevel + (includeSrHeading ? 1 : 0)) : undefined; } } @@ -126,16 +164,43 @@ export default class Ebsr extends HTMLElement { connectedCallback() { this._render(); + this._initPlayerObserver(); this.addEventListener(SESSION_CHANGED, this.onSessionUpdated); } disconnectedCallback() { + this._disconnectPlayerObserver(); this.removeEventListener(SESSION_CHANGED, this.onSessionUpdated); } + _initPlayerObserver() { + const player = this.closest('pie-player') || this.closest('pie-item-player'); + if (!player) return; + + this._playerObserver = new MutationObserver(() => { + this._render(); + }); + this._playerObserver.observe(player, { + attributes: true, + attributeFilter: ['base-heading-level', 'baseheadinglevel', 'include-sr-heading', 'includesrheading'], + }); + } + + _disconnectPlayerObserver() { + if (this._playerObserver) { + this._playerObserver.disconnect(); + this._playerObserver = null; + } + } + _render() { this.ariaLabel = 'Two-Part Question'; this.role = 'region'; + + const { baseHeadingLevel: ebsrLevel, includeSrHeading } = getPlayerAttributes(this); + const headingTag = ebsrLevel ? `h${Math.min(6, ebsrLevel)}` : 'h2'; + const srHeading = includeSrHeading ? `<${headingTag} class="srOnly">Two-Part Question` : ''; + this.innerHTML = ` -

Two-Part Question

+ ${srHeading} <${MC_TAG_NAME} id="a"> <${MC_TAG_NAME} id="b"> `; diff --git a/packages/elements-react/multiple-choice/CHANGELOG.md b/packages/elements-react/multiple-choice/CHANGELOG.md new file mode 100644 index 00000000..32c70a04 --- /dev/null +++ b/packages/elements-react/multiple-choice/CHANGELOG.md @@ -0,0 +1,14 @@ +# @pie-element/multiple-choice + +## 13.1.1-next.1 + +### Patch Changes + +- e32415a: test release flow + +## 13.1.1-next.0 + +### Patch Changes + +- 259eb4d: multiple-choice release flow test +- 7bd4a51: test multiple-choice release flow diff --git a/packages/elements-react/multiple-choice/package.json b/packages/elements-react/multiple-choice/package.json index 89db7430..3bd231af 100644 --- a/packages/elements-react/multiple-choice/package.json +++ b/packages/elements-react/multiple-choice/package.json @@ -1,6 +1,6 @@ { "name": "@pie-element/multiple-choice", - "version": "13.0.1", + "version": "13.1.0", "description": "React implementation of multiple-choice element synced from pie-elements", "dependencies": { "@emotion/react": "^11.14.0", diff --git a/packages/elements-react/multiple-choice/src/delivery/index.ts b/packages/elements-react/multiple-choice/src/delivery/index.ts index 119da07f..bf0f16c8 100644 --- a/packages/elements-react/multiple-choice/src/delivery/index.ts +++ b/packages/elements-react/multiple-choice/src/delivery/index.ts @@ -82,6 +82,51 @@ export const isComplete = (session, model, audioComplete, elementContext) => { return true; }; +function getPlayerAttributes(element) { + const player = + element.closest('pie-player') || + element.closest('pie-item-player'); + + if (!player) { + return { baseHeadingLevel: undefined, includeSrHeading: true }; + } + + const getRaw = (camelCaseName, hyphenatedName, allLowerName) => { + let raw = player[camelCaseName]; + + // fallback in case someone sets via HTML attribute manually + if (raw == null) { + raw = + player.getAttribute(hyphenatedName) ?? + player.getAttribute(allLowerName); + } + + return raw; + }; + + const levelRaw = getRaw('baseHeadingLevel', 'base-heading-level', 'baseheadinglevel'); + const level = parseInt(levelRaw, 10); + const baseHeadingLevel = Number.isFinite(level) && level >= 1 && level <= 6 ? level : undefined; + + const srRaw = getRaw('includeSrHeading', 'include-sr-heading', 'includesrheading'); + const includeSrHeading = srRaw == null ? true : srRaw !== false && srRaw !== 'false'; + + console.log('getPlayerAttributes', { baseHeadingLevel, includeSrHeading }); + return { baseHeadingLevel, includeSrHeading }; +} + +// Resolves heading attributes for a custom element, preferring explicit instance +// properties (set by a parent element such as EBSR) over player-level attributes. +function resolveHeadingProps(element) { + const fromPlayer = getPlayerAttributes(element); + + console.log('element._baseHeadingLevel', element._baseHeadingLevel, 'element._includeSrHeading', element._includeSrHeading); + return { + baseHeadingLevel: element._baseHeadingLevel !== undefined ? element._baseHeadingLevel : fromPlayer.baseHeadingLevel, + includeSrHeading: element._includeSrHeading !== undefined ? element._includeSrHeading : fromPlayer.includeSrHeading, + }; +} + export default class MultipleChoice extends HTMLElement { constructor() { super(); @@ -105,6 +150,7 @@ export default class MultipleChoice extends HTMLElement { options: this._options, onChoiceChanged: this._onChange.bind(this), onShowCorrectToggle: this.onShowCorrectToggle.bind(this), + ...resolveHeadingProps(this), }); //TODO: aria-label is set in the _rerender because we need to change it when the model.choiceMode is updated. Consider revisiting the placement of the aria-label setting in the _rerender @@ -220,6 +266,16 @@ export default class MultipleChoice extends HTMLElement { this._rerender(); } + set baseHeadingLevel(level) { + this._baseHeadingLevel = level; + this._rerender(); + } + + set includeSrHeading(value) { + this._includeSrHeading = value; + this._rerender(); + } + set session(s) { this._session = s; this._rerender(); @@ -262,6 +318,7 @@ export default class MultipleChoice extends HTMLElement { connectedCallback() { this._initMathObserver(); + this._initPlayerObserver(); this._rerender(); // Observation: audio in Chrome will have the autoplay attribute, @@ -350,8 +407,29 @@ export default class MultipleChoice extends HTMLElement { } } + _initPlayerObserver() { + const player = this.closest('pie-player') || this.closest('pie-item-player'); + if (!player) return; + + this._playerObserver = new MutationObserver(() => { + this._rerender(); + }); + this._playerObserver.observe(player, { + attributes: true, + attributeFilter: ['base-heading-level', 'baseheadinglevel', 'include-sr-heading', 'includesrheading'], + }); + } + + _disconnectPlayerObserver() { + if (this._playerObserver) { + this._playerObserver.disconnect(); + this._playerObserver = null; + } + } + disconnectedCallback() { this._disconnectMathObserver(); + this._disconnectPlayerObserver(); if (this._keyboardEventsEnabled) { window.removeEventListener('keydown', this._boundHandleKeyDown); this._keyboardEventsEnabled = false; diff --git a/packages/elements-react/multiple-choice/src/delivery/main.tsx b/packages/elements-react/multiple-choice/src/delivery/main.tsx index a3ffe622..867d2079 100644 --- a/packages/elements-react/multiple-choice/src/delivery/main.tsx +++ b/packages/elements-react/multiple-choice/src/delivery/main.tsx @@ -52,6 +52,8 @@ class Main extends React.Component { names: PropTypes.arrayOf(PropTypes.string), rules: PropTypes.string, }), + baseHeadingLevel: PropTypes.number, + includeSrHeading: PropTypes.bool, }; static defaultProps = { @@ -60,7 +62,7 @@ class Main extends React.Component { }; render() { - const { model, onChoiceChanged, session, onShowCorrectToggle, options } = this.props; + const { model, onChoiceChanged, session, onShowCorrectToggle, options, baseHeadingLevel, includeSrHeading } = this.props; const { extraCSSRules, fontSizeFactor } = model; // model.partLabel is a property used for ebsr @@ -72,6 +74,8 @@ class Main extends React.Component { session={session} onChoiceChanged={onChoiceChanged} onShowCorrectToggle={onShowCorrectToggle} + baseHeadingLevel={baseHeadingLevel} + includeSrHeading={includeSrHeading} /> ); diff --git a/packages/elements-react/multiple-choice/src/delivery/multiple-choice.tsx b/packages/elements-react/multiple-choice/src/delivery/multiple-choice.tsx index 0c47e8d2..433b89e9 100644 --- a/packages/elements-react/multiple-choice/src/delivery/multiple-choice.tsx +++ b/packages/elements-react/multiple-choice/src/delivery/multiple-choice.tsx @@ -14,7 +14,7 @@ import CorrectAnswerToggle from '@pie-lib/correct-answer-toggle'; import classNames from 'classnames'; import { styled } from '@mui/material/styles'; import Box from '@mui/material/Box'; -import { color, Collapsible as CollapsibleImport, PreviewPrompt as PreviewPromptImport } from '@pie-lib/render-ui'; +import { color, Collapsible as CollapsibleImport, PreviewPrompt as PreviewPromptImport, transformDataHeadings } from '@pie-lib/render-ui'; function isRenderableReactInteropType(value: any) { return ( @@ -146,6 +146,8 @@ export class MultipleChoice extends React.Component { pauseImage: PropTypes.string, }, options: PropTypes.object, + baseHeadingLevel: PropTypes.number, + includeSrHeading: PropTypes.bool, }; constructor(props) { @@ -272,17 +274,21 @@ export class MultipleChoice extends React.Component { // renderHeading function was added for accessibility. renderHeading() { - const { mode, choiceMode } = this.props; + const { mode, choiceMode, includeSrHeading, baseHeadingLevel, partLabel } = this.props; - if (mode !== 'gather') { + // When a part label is present the item is an EBSR part — the SR heading + // is provided by the EBSR element, not here. + const shouldRenderSrHeading = !partLabel && includeSrHeading !== false; + + if (!shouldRenderSrHeading || mode !== 'gather') { return null; } - return choiceMode === 'radio' ? ( - Multiple Choice Question - ) : ( - Multiple Select Question - ); + const clampedLevel = baseHeadingLevel ? Math.min(6, baseHeadingLevel) : 2; + const HeadingTag = SrOnly.withComponent(`h${clampedLevel}`); + const label = choiceMode === 'radio' ? 'Multiple Choice Question' : 'Multiple Select Question'; + + return {label}; } handleGroupFocus: any = (e) => { @@ -323,6 +329,7 @@ export class MultipleChoice extends React.Component { session, customAudioButton, options, + baseHeadingLevel, } = this.props; const { showCorrect, maxSelectionsErrorState } = this.state; const isEvaluateMode = mode === 'evaluate'; @@ -330,6 +337,18 @@ export class MultipleChoice extends React.Component { const columnsStyle = gridColumns > 1 ? { gridTemplateColumns: `repeat(${gridColumns}, 1fr)` } : undefined; const selections = (session.value && session.value.length) || 0; + // Heading levels are optional and only applied when baseHeadingLevel is provided. + const getContentHeadingLevel = () => { + if (!baseHeadingLevel) return undefined; + // SR heading (rendered or external) sits at baseHeadingLevel. + // Content is always one below that; part label (EBSR) sits between them. + let offset = 1; // content default: baseHeadingLevel + 1 + if (partLabel) offset += 1; // part label at base + 1, content pushed to base + 2 + return Math.min(6, baseHeadingLevel + offset); + }; + const contentHeadingLevel = getContentHeadingLevel(); + const transformPrompt = (html) => (html && contentHeadingLevel) ? transformDataHeadings(html, contentHeadingLevel) : html; + const teacherInstructionsDiv = ( - {partLabel && {partLabel}} + {partLabel && {partLabel}} {this.renderHeading()} @@ -394,7 +413,7 @@ export class MultipleChoice extends React.Component { { + if (this.input[index]) { + this.input[index].focus(); + } + }, 0); return; } diff --git a/packages/shared/math-rendering-mathjax/src/adapter.ts b/packages/shared/math-rendering-mathjax/src/adapter.ts index eff49da9..f38795e7 100644 --- a/packages/shared/math-rendering-mathjax/src/adapter.ts +++ b/packages/shared/math-rendering-mathjax/src/adapter.ts @@ -16,30 +16,43 @@ interface MathJaxTexConfig { } interface MathJaxConfig { + loader?: { + load?: string[]; + }; tex?: MathJaxTexConfig; options?: { enableMenu?: boolean; enableExplorer?: boolean; + enableAssistiveMml?: boolean; }; startup?: { ready?: () => void; defaultReady?: () => void; + document?: MathJaxDocument; }; chtml?: { fontURL?: string; }; } +interface MathJaxDocument { + assistiveMml?: () => { + updateDocument?: () => unknown; + }; +} + interface MathJaxInstance { version?: string; tex?: MathJaxTexConfig; options?: { enableMenu?: boolean; enableExplorer?: boolean; + enableAssistiveMml?: boolean; }; startup: { ready?: () => void; defaultReady: () => void; + document?: MathJaxDocument; }; typesetPromise?: (elements?: Element[]) => Promise; } @@ -71,6 +84,9 @@ function ensureMathjaxLoaded(options: MathjaxOptions): Promise { const { useSingleDollar = false, accessibility = true, loadFonts = true, srcUrl } = options; const config: MathJaxConfig = { + loader: { + load: accessibility ? ['a11y/assistive-mml'] : [], + }, tex: { packages: ['base', 'ams', 'autoload'], macros: { @@ -83,6 +99,7 @@ function ensureMathjaxLoaded(options: MathjaxOptions): Promise { options: { enableMenu: accessibility, enableExplorer: accessibility, + enableAssistiveMml: accessibility, }, startup: { ready: () => { @@ -124,6 +141,20 @@ function ensureMathjaxLoaded(options: MathjaxOptions): Promise { return mathjaxLoading; } +function attachAssistiveMml(): void { + const mathDocument = window.MathJax?.startup?.document; + + if (typeof mathDocument?.assistiveMml !== 'function') { + return; + } + + try { + mathDocument.assistiveMml().updateDocument?.(); + } catch (error) { + console.warn('[mathjax-renderer] Failed to attach assistive MathML:', error); + } +} + /** * Create a MathJax-based math renderer * @@ -155,6 +186,7 @@ export function createMathjaxRenderer( } await window.MathJax.typesetPromise([element]); + attachAssistiveMml(); }; } diff --git a/packages/shared/math-rendering-mathjax/tests/adapter.test.ts b/packages/shared/math-rendering-mathjax/tests/adapter.test.ts new file mode 100644 index 00000000..f32aec12 --- /dev/null +++ b/packages/shared/math-rendering-mathjax/tests/adapter.test.ts @@ -0,0 +1,47 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { createMathjaxRenderer } from '../src/adapter.js'; + +describe('createMathjaxRenderer', () => { + afterEach(() => { + vi.restoreAllMocks(); + document.body.innerHTML = ''; + delete window.MathJax; + }); + + it('attaches assistive MathML for screen readers after typesetting', async () => { + const target = document.createElement('div'); + target.innerHTML = '\\(x^2 + 1\\)'; + document.body.append(target); + + const updatedDocument = { + assistiveMml: vi.fn(() => { + const container = target.querySelector('mjx-container'); + const assistiveMath = document.createElement('mjx-assistive-mml'); + assistiveMath.innerHTML = + 'x2+1'; + container?.append(assistiveMath); + return updatedDocument; + }), + updateDocument: vi.fn(() => updatedDocument), + }; + const mathDocument = { + assistiveMml: vi.fn(() => updatedDocument.assistiveMml()), + }; + + window.MathJax = { + version: '4.0.0', + startup: { + defaultReady: vi.fn(), + document: mathDocument, + }, + typesetPromise: vi.fn(async () => { + target.innerHTML = 'x squared plus 1'; + }), + }; + + await createMathjaxRenderer()(target); + + expect(mathDocument.assistiveMml).toHaveBeenCalled(); + expect(target.querySelector('mjx-assistive-mml math')).not.toBeNull(); + }); +}); diff --git a/turbo.json b/turbo.json index a614d6c6..fea30e38 100644 --- a/turbo.json +++ b/turbo.json @@ -1,6 +1,7 @@ { "$schema": "https://turbo.build/schema.json", "ui": "tui", + "concurrency": "76", "tasks": { "build": { "dependsOn": ["^build"],