Skip to content

Commit 855a76b

Browse files
Perseus Editor updates for PEICH (#2514)
PR to merge several changes from PEICH project thus far and expose new editor functionality for our content creators. All of the changes included have been approved through the PR process. This is simply a formal commit to add to Perseus main now that we're ready to release to content. - Issues Panel (#2377) - Adds a warning message in the Issues Panel when a content creator uses a widget marked as inaccessible (#2474) - Migrate accessibility logic from perseus to perseus-core (#2485) - Add a basic FreeResponse widget (#2273) - Add scoring criteria to Free Response Widget (#2286) - [Free Response Widget] Add ability to customize placeholder (#2297) - [Free Response Widget] Add optional character limit support (#2332) - Render TeX in Free Response Widget questions (#2453) - Update Free Response styles and accessibility (#2486) - Use LabeledField in FreeResponseEditor (#2493) - Add Free Response scoring and validation functions (#2498) - Changeset: Release the new Free Response Widget (#2501) - Version Packages (#2500) - Add story for radio widget single select with images and scroll (#2494) - Update to latest WB packages (#2455) - Wonder Blocks: Updates WB Button instances to match new API (color -> actionType) (#2491) - Version Packages (#2506) - Ensure that our parser can handle empty strings for labelLocation for Interactive Graphs (#2509) - Add tab navigation to new radio widget (#2510) - Add typesafe parser for Categorizer's user input type (#2516) - Update label image widget to handle answerless data and do some cleanup (#2495) - Remove preferred popover direction from Label Image editor (#2512) - [feature/peich] Revert bad commits. :( - Removing manual traversal. (#2517) - [feature/peich] Exporting violatingWidgets. - Pass data between EditorPage and ItemEditor via new props (#2526) - Add isItemAccessible function and enhance types (#2532) - docs(changeset): Move traversal tests from perseus-editor and perseus into perseus-core (#2544) - [feature/peich] A few fixes from the merge with main. - Adding partially inaccessible functions to InteractiveGraph and LabelImage. (#2553) - [feature/peich] Reverting pnpm lock file changes. Author: catandthemachines Reviewers: mark-fitzgerald, ivyolamit, SonicScrewdriver, anakaren-rojas Required Reviewers: Approved By: mark-fitzgerald Checks: ✅ 14 checks were successful Pull Request URL: #2514
2 parents cec57bb + d13916d commit 855a76b

File tree

88 files changed

+1534
-1191
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

88 files changed

+1534
-1191
lines changed

.changeset/chilly-jokes-teach.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
"@khanacademy/perseus": minor
3+
"@khanacademy/perseus-core": minor
4+
"@khanacademy/perseus-editor": minor
5+
"@khanacademy/perseus-linter": minor
6+
---
7+
8+
Adding partically accessible widget function to interactive graph and label image.

.changeset/evil-needles-clap.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@khanacademy/perseus-core": major
3+
"@khanacademy/perseus": major
4+
---
5+
6+
Migrate accessibility logic from perseus to perseus-core

.changeset/green-pumas-know.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@khanacademy/perseus-core": patch
3+
---
4+
5+
Move deleted traversal tests into perseus-core

.changeset/light-pandas-happen.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@khanacademy/perseus-core": minor
3+
---
4+
5+
Add isItemAccessible helper which simplifies common usages of the violatingWidgets() function

.changeset/old-needles-sit.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@khanacademy/perseus-editor": major
3+
"@khanacademy/perseus-linter": major
4+
"@khanacademy/perseus-core": major
5+
---
6+
7+
Moved checkAccessibilityAndWarn logic into a custom linter rule

.changeset/quick-pants-glow.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@khanacademy/perseus-editor": major
3+
---
4+
5+
This change introduces the Issues Panel container (LEMS-2925), which displays a list of accessibility warnings in the editor. Each warning includes a title, description, impact, and message. The panel also uses icons to indicate whether there are issues or if all checks have passed, improving visibility and clarity for content creators.

.changeset/rich-lamps-bake.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@khanacademy/perseus-editor": major
3+
"@khanacademy/perseus-core": minor
4+
---
5+
6+
Pass data between EditorPage and ItemEditor via new props

.changeset/thick-weeks-kneel.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@khanacademy/perseus": patch
3+
---
4+
5+
Removed unnecessary logic.
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
import {isItemAccessible} from "./accessibility";
2+
import {registerCoreWidgets} from "./widgets/core-widget-registry";
3+
4+
import type {PerseusItem} from "./data-schema";
5+
6+
describe("isItemAccessible", () => {
7+
beforeEach(() => {
8+
registerCoreWidgets();
9+
});
10+
11+
it("should return false if the item contains any inaccessible widgets", () => {
12+
const itemData: PerseusItem = {
13+
question: {
14+
content:
15+
"Match the following items: [[☃ matcher 1]]\n\nHere's an explanation: [[☃ explanation 1]]\n\nAnd a graph: [[☃ interactive-graph 1]]",
16+
widgets: {
17+
"explanation 1": {
18+
type: "explanation",
19+
options: {
20+
showPrompt: "Show",
21+
hidePrompt: "Hide",
22+
explanation: "Test explanation",
23+
widgets: {},
24+
static: false,
25+
},
26+
},
27+
"interactive-graph 1": {
28+
type: "interactive-graph",
29+
options: {
30+
graph: {
31+
type: "point",
32+
numPoints: 1,
33+
},
34+
range: [
35+
[-10, 10],
36+
[-10, 10],
37+
],
38+
step: [1, 1],
39+
markings: "graph",
40+
showProtractor: false,
41+
correct: {
42+
type: "point",
43+
coords: [[0, 0]],
44+
},
45+
lockedFigures: [],
46+
},
47+
},
48+
"matcher 1": {
49+
type: "matcher",
50+
options: {
51+
labels: ["Concepts", "Definitions"],
52+
left: ["A", "B", "C"],
53+
right: ["1", "2", "3"],
54+
orderMatters: false,
55+
padding: true,
56+
},
57+
},
58+
},
59+
images: {},
60+
},
61+
hints: [],
62+
answerArea: null,
63+
};
64+
65+
expect(isItemAccessible(itemData)).toBe(false);
66+
});
67+
68+
it("should return true if all widgets are accessible", () => {
69+
const itemData: PerseusItem = {
70+
question: {
71+
content:
72+
"Here's an explanation: [[☃ explanation 1]]\n\nChoose an option: [[☃ radio 1]]",
73+
widgets: {
74+
"explanation 1": {
75+
type: "explanation",
76+
options: {
77+
showPrompt: "Show",
78+
hidePrompt: "Hide",
79+
explanation: "Test explanation",
80+
widgets: {},
81+
static: false,
82+
},
83+
},
84+
"radio 1": {
85+
type: "radio",
86+
options: {
87+
choices: [
88+
{content: "Option 1", correct: true},
89+
{content: "Option 2", correct: false},
90+
],
91+
},
92+
},
93+
},
94+
images: {},
95+
},
96+
hints: [],
97+
answerArea: null,
98+
};
99+
100+
expect(isItemAccessible(itemData)).toBe(true);
101+
});
102+
103+
it("should mark item as inaccessible if widget options are inaccessible", () => {
104+
const itemData: PerseusItem = {
105+
question: {
106+
content: "Here's an image: [[☃ image 1]]",
107+
widgets: {
108+
"image 1": {
109+
type: "image",
110+
options: {
111+
backgroundImage: {
112+
url: "https://example.com/image.png",
113+
width: 400,
114+
height: 300,
115+
},
116+
// No alt text makes this image inaccessible
117+
alt: "",
118+
},
119+
},
120+
},
121+
images: {},
122+
},
123+
hints: [],
124+
answerArea: null,
125+
};
126+
127+
expect(isItemAccessible(itemData)).toBe(false);
128+
});
129+
130+
it("should mark item as accessible if widget options are accessible", () => {
131+
const itemData: PerseusItem = {
132+
question: {
133+
content: "Here's an image: [[☃ image 1]]",
134+
widgets: {
135+
"image 1": {
136+
type: "image",
137+
options: {
138+
backgroundImage: {
139+
url: "https://example.com/image.png",
140+
width: 400,
141+
height: 300,
142+
},
143+
// Alt text makes this image accessible
144+
alt: "A descriptive alt text for the image",
145+
},
146+
},
147+
},
148+
images: {},
149+
},
150+
hints: [],
151+
answerArea: null,
152+
};
153+
154+
expect(isItemAccessible(itemData)).toBe(true);
155+
});
156+
157+
it("should handle empty items", () => {
158+
const itemData: PerseusItem = {
159+
question: {
160+
content: "",
161+
widgets: {},
162+
images: {},
163+
},
164+
hints: [],
165+
answerArea: null,
166+
};
167+
168+
expect(isItemAccessible(itemData)).toBe(true);
169+
});
170+
171+
it("should handle items with no widgets", () => {
172+
const itemData: PerseusItem = {
173+
question: {
174+
content: "Just some text",
175+
widgets: {},
176+
images: {},
177+
},
178+
hints: [],
179+
answerArea: null,
180+
};
181+
182+
expect(isItemAccessible(itemData)).toBe(true);
183+
});
184+
});
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/**
2+
* Identifies whether or not a given perseus item requires the use of a mouse
3+
* or screen, based on the widgets it contains.
4+
*/
5+
6+
import {traverse} from "./traversal";
7+
import * as Widgets from "./widgets/core-widget-registry";
8+
9+
import type {PerseusItem} from "./data-schema";
10+
11+
/**
12+
* Returns a list of widget types that cause a given Perseus item to require
13+
* the use of a screen or mouse.
14+
*
15+
* For now we'll just check the `accessible` field on each of the widgets
16+
* in the item data, but in the future we may specify accessibility on
17+
* each widget with higher granularity.
18+
*
19+
* @deprecated This function returns a list of widget _types_ that violate our
20+
* accessibility requirements which is not very accurate given that some
21+
* instances _could_ be accessible and some not based on their widget options.
22+
* In most cases, you should use {@link isItemAccessible} instead.
23+
*
24+
* @todo Inline this into {@link isItemAccessible}!
25+
*/
26+
export function violatingWidgets(itemData: PerseusItem): Array<string> {
27+
// TODO(jordan): Hints as well
28+
const widgetTypes: Array<string> = [];
29+
30+
traverse(itemData.question, null, function (info) {
31+
if (info.type && !Widgets.isAccessible(info.type, info.options)) {
32+
widgetTypes.push(info.type);
33+
}
34+
});
35+
36+
// Uniquify the list of widgets (by type)
37+
return [...new Set(widgetTypes)];
38+
}
39+
40+
/**
41+
* Returns true if the given Perseus item is accessible (i.e., does not contain
42+
* any widgets that violate our accessibility requirements).
43+
*/
44+
export function isItemAccessible(itemData: PerseusItem): boolean {
45+
return violatingWidgets(itemData).length === 0;
46+
}

0 commit comments

Comments
 (0)