{/* Correct / Incorrect status selection */}
{/* Content and rationale text areas */}
@@ -124,6 +111,7 @@ export const RadioOptionSettings = React.forwardRef<
content={content}
choiceIndex={index}
isNoneOfTheAbove={isNoneOfTheAbove ?? false}
+ editingDisabled={editingDisabled}
onContentChange={onContentChange}
/>
@@ -143,6 +131,7 @@ export const RadioOptionSettings = React.forwardRef<
id={rationaleTextAreaId}
value={rationale ?? ""}
placeholder={`Why is this choice ${correct ? "correct" : "incorrect"}?`}
+ disabled={editingDisabled}
onChange={(value) => {
onRationaleChange(index, value);
}}
@@ -154,6 +143,7 @@ export const RadioOptionSettings = React.forwardRef<
content={content}
showDelete={showDelete}
showMove={showMove}
+ editingDisabled={editingDisabled}
onDelete={onDelete}
onMove={(movement) => onMove(index, movement)}
/>
diff --git a/packages/perseus-editor/src/widgets/radio/radio-status-pill.tsx b/packages/perseus-editor/src/widgets/radio/radio-status-pill.tsx
index a5bf6206bae..640ecd44676 100644
--- a/packages/perseus-editor/src/widgets/radio/radio-status-pill.tsx
+++ b/packages/perseus-editor/src/widgets/radio/radio-status-pill.tsx
@@ -1,60 +1,66 @@
+import {StatusBadge} from "@khanacademy/wonder-blocks-badge";
import {PhosphorIcon} from "@khanacademy/wonder-blocks-icon";
-import Pill from "@khanacademy/wonder-blocks-pill";
-import {semanticColor, sizing, border} from "@khanacademy/wonder-blocks-tokens";
+import {border, semanticColor, sizing} from "@khanacademy/wonder-blocks-tokens";
import checkIcon from "@phosphor-icons/core/bold/check-bold.svg";
import minusCircleIcon from "@phosphor-icons/core/bold/minus-circle-bold.svg";
+import {StyleSheet} from "aphrodite";
import * as React from "react";
interface RadioStatusPillProps {
index: number;
correct?: boolean;
multipleSelect: boolean;
- onClick: () => void;
}
+/**
+ * A read-only status indicator showing whether a choice is correct (a green
+ * check) or incorrect (a red minus), labelled with the choice letter (A, B,
+ * C...). Status is changed via the adjacent segmented control, so this is no
+ * longer interactive — it is a Wonder Blocks `StatusBadge`.
+ */
export function RadioStatusPill({
index,
correct,
multipleSelect,
- onClick,
}: RadioStatusPillProps) {
return (
-
}
+ label={String.fromCharCode(65 + index)}
+ styles={{
+ root: [
+ styles.badge,
+ // Round for single select, square for multiple select.
+ multipleSelect ? styles.square : styles.round,
+ // Correct uses the strong (dark green) fill with knockout
+ // (white) content, matching the runtime choice indicator.
+ // Incorrect keeps StatusBadge's default subtle critical
+ // (light red) styling.
+ correct && styles.correctStrong,
+ ],
+ label: correct ? styles.knockoutForeground : undefined,
+ icon: correct ? styles.knockoutForeground : undefined,
}}
- onClick={onClick}
- >
- <>
-
- {String.fromCharCode(65 + index)}
- >
-
+ />
);
}
+
+const styles = StyleSheet.create({
+ badge: {
+ marginInlineEnd: sizing.size_080,
+ },
+ round: {
+ borderRadius: sizing.size_240,
+ },
+ square: {
+ borderRadius: border.radius.radius_040,
+ },
+ correctStrong: {
+ backgroundColor: semanticColor.core.background.success.strong,
+ borderColor: semanticColor.core.border.success.strong,
+ },
+ knockoutForeground: {
+ color: semanticColor.core.foreground.knockout.default,
+ },
+});
diff --git a/packages/perseus-editor/src/widgets/table-editor.test.tsx b/packages/perseus-editor/src/widgets/table-editor.test.tsx
new file mode 100644
index 00000000000..7e75d84724f
--- /dev/null
+++ b/packages/perseus-editor/src/widgets/table-editor.test.tsx
@@ -0,0 +1,50 @@
+import {render, screen} from "@testing-library/react";
+import * as React from "react";
+
+import TableEditor from "./table-editor";
+
+const baseProps = {
+ rows: 2,
+ columns: 3,
+ headers: ["a", "b", "c"],
+ answers: [
+ ["2", "4", "6"],
+ ["3", "6", "9"],
+ ],
+};
+
+describe("TableEditor", () => {
+ it("renders the answer cells", () => {
+ // Arrange, Act
+ render(
{}} />);
+
+ // Assert (4 and 9 are unique to answer cells; 2/3 also appear in the
+ // row/column size inputs)
+ expect(screen.getByDisplayValue("4")).toBeInTheDocument();
+ expect(screen.getByDisplayValue("9")).toBeInTheDocument();
+ });
+
+ it("leaves the answer cells enabled when editing is allowed", () => {
+ // Arrange, Act
+ render( {}} />);
+
+ // Assert
+ expect(screen.getByDisplayValue("4")).toBeEnabled();
+ expect(screen.getByDisplayValue("9")).toBeEnabled();
+ });
+
+ it("disables the answer cells when editing is disabled", () => {
+ // Arrange, Act
+ render(
+ {}}
+ apiOptions={{editingDisabled: true}}
+ />,
+ );
+
+ // Assert
+ expect(screen.getByDisplayValue("4")).toBeDisabled();
+ expect(screen.getByDisplayValue("9")).toBeDisabled();
+ });
+});
diff --git a/packages/perseus-editor/src/widgets/table-editor.tsx b/packages/perseus-editor/src/widgets/table-editor.tsx
index a05a5221512..c88612fffcb 100644
--- a/packages/perseus-editor/src/widgets/table-editor.tsx
+++ b/packages/perseus-editor/src/widgets/table-editor.tsx
@@ -90,6 +90,8 @@ class TableEditor extends React.Component {
};
render(): React.ReactNode {
+ const editingDisabled = this.props.apiOptions?.editingDisabled ?? false;
+
const tableProps: Partial> = {
headers: this.props.headers,
onChange: this.props.onChange,
@@ -103,7 +105,12 @@ class TableEditor extends React.Component {
// user input is actually editing answers
this.props.onChange({answers: userInput});
},
- apiOptions: this.props.apiOptions,
+ // When editing is disabled, mark the table read-only so its answer
+ // cells render disabled (the widget's inputs key off `readOnly`).
+ apiOptions: {
+ ...this.props.apiOptions,
+ readOnly: editingDisabled || this.props.apiOptions?.readOnly,
+ },
editableHeaders: true,
onFocus: () => {},
onBlur: () => {},
@@ -119,6 +126,7 @@ class TableEditor extends React.Component {
{
if (val) {
this.onSizeInput(this.props.rows, val);
@@ -135,6 +143,7 @@ class TableEditor extends React.Component {
// eslint-disable-next-line react/no-string-refs
ref="numberOfRows"
value={this.props.rows}
+ disabled={editingDisabled}
onChange={(val) => {
if (val) {
this.onSizeInput(val, this.props.columns);
diff --git a/packages/perseus/src/components/button-group.tsx b/packages/perseus/src/components/button-group.tsx
index 4a504d79870..d79746cc328 100644
--- a/packages/perseus/src/components/button-group.tsx
+++ b/packages/perseus/src/components/button-group.tsx
@@ -26,6 +26,11 @@ type Props = {
* Customizes the selected button's styling.
*/
selectedButtonStyle?: CSSProperties;
+
+ /**
+ * When true, all buttons are disabled (non-interactive and visually muted).
+ */
+ disabled?: boolean;
};
type DefaultProps = {
@@ -75,11 +80,13 @@ class ButtonGroup extends React.Component {
type="button"
ref={"button" + i}
key={"" + i}
+ disabled={this.props.disabled}
className={css(
styles.buttonStyle,
button.value === value && styles.selectedStyle,
button.value === value &&
this.props.selectedButtonStyle,
+ this.props.disabled && styles.disabledStyle,
)}
onClick={() => this.toggleSelect(button.value)}
>
@@ -136,6 +143,15 @@ const styles = StyleSheet.create({
selectedStyle: {
backgroundColor: "#ddd",
},
+
+ disabledStyle: {
+ cursor: "not-allowed",
+ backgroundColor: "#f0f0f1",
+ color: "#888d93",
+ ":hover": {
+ backgroundColor: "#f0f0f1",
+ },
+ },
});
export default ButtonGroup;
diff --git a/packages/perseus/src/widgets/label-image/answer-pill.tsx b/packages/perseus/src/widgets/label-image/answer-pill.tsx
index 6c561a039e1..b68e1727e63 100644
--- a/packages/perseus/src/widgets/label-image/answer-pill.tsx
+++ b/packages/perseus/src/widgets/label-image/answer-pill.tsx
@@ -1,3 +1,10 @@
+// TODO(LEMS-4131): `Pill` is deprecated (use `Badge`/`StatusBadge`). This is
+// the only remaining `Pill` after the editor Pill migration. It is intentionally
+// left for now because it is a RUNTIME, learner-facing component (rendered by
+// label-image/marker.tsx), not part of the editor — so it is out of scope for
+// the editor disabled-state work and should be migrated under its own
+// runtime-scoped ticket with its own QA. Note it is interactive (has onClick),
+// so the replacement is an interactive control, not a Badge.
import Pill from "@khanacademy/wonder-blocks-pill";
import {semanticColor} from "@khanacademy/wonder-blocks-tokens";
import {StyleSheet, type CSSProperties} from "aphrodite";
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 6b1a5304d64..8653912bba2 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -15,6 +15,9 @@ catalogs:
'@khanacademy/wonder-blocks-announcer':
specifier: 1.1.1
version: 1.1.1
+ '@khanacademy/wonder-blocks-badge':
+ specifier: 1.1.14
+ version: 1.1.14
'@khanacademy/wonder-blocks-banner':
specifier: 5.1.2
version: 5.1.2
@@ -798,6 +801,9 @@ importers:
'@khanacademy/wonder-blocks-accordion':
specifier: catalog:devDeps
version: 3.1.61(@phosphor-icons/core@2.0.2)(aphrodite@1.2.5)(react-dom@18.2.0(react@18.2.0))(react-router-dom-v5-compat@6.30.0(react-dom@18.2.0(react@18.2.0))(react-router-dom@5.3.4(react@18.2.0))(react@18.2.0))(react-router-dom@5.3.4(react@18.2.0))(react-router@5.3.4(react@18.2.0))(react@18.2.0)
+ '@khanacademy/wonder-blocks-badge':
+ specifier: catalog:devDeps
+ version: 1.1.14(@phosphor-icons/core@2.0.2)(aphrodite@1.2.5)(react-dom@18.2.0(react@18.2.0))(react-router-dom-v5-compat@6.30.0(react-dom@18.2.0(react@18.2.0))(react-router-dom@5.3.4(react@18.2.0))(react@18.2.0))(react-router-dom@5.3.4(react@18.2.0))(react-router@5.3.4(react@18.2.0))(react@18.2.0)
'@khanacademy/wonder-blocks-banner':
specifier: catalog:devDeps
version: 5.1.2(@phosphor-icons/core@2.0.2)(aphrodite@1.2.5)(react-dom@18.2.0(react@18.2.0))(react-router-dom-v5-compat@6.30.0(react-dom@18.2.0(react@18.2.0))(react-router-dom@5.3.4(react@18.2.0))(react@18.2.0))(react-router-dom@5.3.4(react@18.2.0))(react-router@5.3.4(react@18.2.0))(react@18.2.0)
@@ -2385,6 +2391,13 @@ packages:
aphrodite: ^1.2.5
react: 18.2.0
+ '@khanacademy/wonder-blocks-badge@1.1.14':
+ resolution: {integrity: sha512-DU00dSY2hUVI1dwuv+HylrFHb9dgcSAwgLKt6MAVxZEQviO5J0a97PYU1ERWzuq47FYSL4jN7Iv9H89AbXMMiw==}
+ peerDependencies:
+ '@phosphor-icons/core': ^2.0.2
+ aphrodite: ^1.2.5
+ react: 18.2.0
+
'@khanacademy/wonder-blocks-banner@5.1.2':
resolution: {integrity: sha512-B1SVwdF6YV+N4efLDTBDY6fKNM5MdpgXy/i8ZSNWQnYeoSVnQK4pVzs+tZpWejnpbu5Mz2sJj8pfeiS1Tfl/hw==}
peerDependencies:
@@ -11437,6 +11450,22 @@ snapshots:
- react-router-dom
- react-router-dom-v5-compat
+ '@khanacademy/wonder-blocks-badge@1.1.14(@phosphor-icons/core@2.0.2)(aphrodite@1.2.5)(react-dom@18.2.0(react@18.2.0))(react-router-dom-v5-compat@6.30.0(react-dom@18.2.0(react@18.2.0))(react-router-dom@5.3.4(react@18.2.0))(react@18.2.0))(react-router-dom@5.3.4(react@18.2.0))(react-router@5.3.4(react@18.2.0))(react@18.2.0)':
+ dependencies:
+ '@khanacademy/wonder-blocks-core': 12.4.4(aphrodite@1.2.5)(react-dom@18.2.0(react@18.2.0))(react-router-dom-v5-compat@6.30.0(react-dom@18.2.0(react@18.2.0))(react-router-dom@5.3.4(react@18.2.0))(react@18.2.0))(react-router-dom@5.3.4(react@18.2.0))(react-router@5.3.4(react@18.2.0))(react@18.2.0)
+ '@khanacademy/wonder-blocks-icon': 5.3.16(@phosphor-icons/core@2.0.2)(aphrodite@1.2.5)(react-dom@18.2.0(react@18.2.0))(react-router-dom-v5-compat@6.30.0(react-dom@18.2.0(react@18.2.0))(react-router-dom@5.3.4(react@18.2.0))(react@18.2.0))(react-router-dom@5.3.4(react@18.2.0))(react-router@5.3.4(react@18.2.0))(react@18.2.0)
+ '@khanacademy/wonder-blocks-styles': 0.2.46(aphrodite@1.2.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
+ '@khanacademy/wonder-blocks-tokens': 16.6.0(aphrodite@1.2.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
+ '@khanacademy/wonder-blocks-typography': 4.3.6(aphrodite@1.2.5)(react-dom@18.2.0(react@18.2.0))(react-router-dom-v5-compat@6.30.0(react-dom@18.2.0(react@18.2.0))(react-router-dom@5.3.4(react@18.2.0))(react@18.2.0))(react-router-dom@5.3.4(react@18.2.0))(react-router@5.3.4(react@18.2.0))(react@18.2.0)
+ '@phosphor-icons/core': 2.0.2
+ aphrodite: 1.2.5
+ react: 18.2.0
+ transitivePeerDependencies:
+ - react-dom
+ - react-router
+ - react-router-dom
+ - react-router-dom-v5-compat
+
'@khanacademy/wonder-blocks-banner@5.1.2(@phosphor-icons/core@2.0.2)(aphrodite@1.2.5)(react-dom@18.2.0(react@18.2.0))(react-router-dom-v5-compat@6.30.0(react-dom@18.2.0(react@18.2.0))(react-router-dom@5.3.4(react@18.2.0))(react@18.2.0))(react-router-dom@5.3.4(react@18.2.0))(react-router@5.3.4(react@18.2.0))(react@18.2.0)':
dependencies:
'@khanacademy/wonder-blocks-button': 11.6.2(@phosphor-icons/core@2.0.2)(aphrodite@1.2.5)(react-dom@18.2.0(react@18.2.0))(react-router-dom-v5-compat@6.30.0(react-dom@18.2.0(react@18.2.0))(react-router-dom@5.3.4(react@18.2.0))(react@18.2.0))(react-router-dom@5.3.4(react@18.2.0))(react-router@5.3.4(react@18.2.0))(react@18.2.0)
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index bb73f7840ad..33360f1609c 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -65,6 +65,7 @@ catalogs:
"@khanacademy/mathjax-renderer": ^3.0.0
"@khanacademy/wonder-blocks-accordion": ^3.1.61
"@khanacademy/wonder-blocks-announcer": ^1.1.1
+ "@khanacademy/wonder-blocks-badge": ^1.1.14
"@khanacademy/wonder-blocks-banner": ^5.1.2
"@khanacademy/wonder-blocks-button": ^11.6.2
"@khanacademy/wonder-blocks-clickable": ^8.2.1
@@ -106,6 +107,7 @@ catalogs:
"@khanacademy/mathjax-renderer": 3.0.0
"@khanacademy/wonder-blocks-accordion": 3.1.61
"@khanacademy/wonder-blocks-announcer": 1.1.1
+ "@khanacademy/wonder-blocks-badge": 1.1.14
"@khanacademy/wonder-blocks-banner": 5.1.2
"@khanacademy/wonder-blocks-button": 11.6.2
"@khanacademy/wonder-blocks-clickable": 8.2.1