Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
611ff51
[poc/accessibility-checker-in-editor] Add axe-core functionality to e…
mark-fitzgerald Nov 5, 2025
ed6ceab
[poc/accessibility-checker-in-editor] Add "Show me" button to highlig…
mark-fitzgerald Nov 6, 2025
08fca80
[poc/accessibility-checker-in-editor] Add issues list that indicates …
mark-fitzgerald Nov 6, 2025
a945cfa
[poc/accessibility-checker-in-editor] Change issues list to a JS file…
mark-fitzgerald Nov 6, 2025
c044518
[poc/accessibility-checker-in-editor] Fix TSC error.
mark-fitzgerald Nov 6, 2025
fca43cb
[poc/accessibility-checker-in-editor] Restore problemarea for exercis…
mark-fitzgerald Nov 6, 2025
f72d46d
[poc/accessibility-checker-in-editor] Restore isMobile definition.
mark-fitzgerald Nov 6, 2025
2360acf
[poc/accessibility-checker-in-editor] Hide problemarea when in Storyb…
mark-fitzgerald Nov 6, 2025
b7c9349
[poc/accessibility-checker-in-editor] Adjust CSS to ensure that the l…
mark-fitzgerald Nov 7, 2025
e6e5311
[poc/accessibility-checker-in-editor] Adjust include/exclude configur…
mark-fitzgerald Nov 7, 2025
d68babf
[poc/accessibility-checker-in-editor] Add logic to wait until preview…
mark-fitzgerald Nov 7, 2025
ce025ac
[poc/accessibility-checker-in-editor] Adjust delay when waiting for i…
mark-fitzgerald Nov 7, 2025
635f477
[poc/accessibility-checker-in-editor] Add logic to handle iFrames tha…
mark-fitzgerald Nov 7, 2025
4f8e87b
[poc/accessibility-checker-in-editor] Add debugging messages.
mark-fitzgerald Nov 7, 2025
32d5dce
[poc/accessibility-checker-in-editor] Add update function for retry c…
mark-fitzgerald Nov 7, 2025
83fd8a6
[poc/accessibility-checker-in-editor] Add update function to callback…
mark-fitzgerald Nov 7, 2025
b5ac29d
[poc/accessibility-checker-in-editor] Remove console messages.
mark-fitzgerald Nov 8, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/perseus-editor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"@khanacademy/perseus-linter": "workspace:*",
"@khanacademy/perseus-score": "workspace:*",
"@khanacademy/perseus-utils": "workspace:*",
"axe-core": "^4.11.0",
"katex": "0.11.1",
"mafs": "^0.19.0",
"tiny-invariant": "catalog:prodDeps"
Expand Down
4 changes: 3 additions & 1 deletion packages/perseus-editor/src/__docs__/preview-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,9 @@ function PreviewPanel({
/>
</div>

<View className={styles.innerPanel}>{children}</View>
<View id="preview-panel" className={styles.innerPanel}>
{children}
</View>
</div>
)}
</View>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const makeIssue = (id: string, impact: IssueImpact = "medium") => ({
help: "Example help",
impact,
message: "Example message",
type: "Warning" as const,
});

describe("IssuesPanel", () => {
Expand Down
15 changes: 12 additions & 3 deletions packages/perseus-editor/src/components/issue-details.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
// WidgetIssueDetails.tsx
import {isFeatureOn} from "@khanacademy/perseus-core";
import {color} from "@khanacademy/wonder-blocks-tokens";
import {semanticColor} from "@khanacademy/wonder-blocks-tokens";
import {LabelLarge, LabelSmall} from "@khanacademy/wonder-blocks-typography";
import * as React from "react";

import IssueCta from "./issue-cta";
import PerseusEditorAccordion from "./perseus-editor-accordion";
import ShowMe from "./show-me-issue";

import type {Issue} from "./issues-panel";
import type {APIOptions} from "@khanacademy/perseus";
Expand All @@ -19,6 +20,11 @@ const IssueDetails = ({apiOptions, issue}: IssueProps) => {
const [expanded, setExpanded] = React.useState(false);
const toggleVisibility = () => setExpanded(!expanded);

const accordionColor =
issue.type === "Alert"
? semanticColor.feedback.critical.subtle.background
: semanticColor.feedback.warning.subtle.background;

// TODO(LEMS-3520): Remove this once the "image-widget-upgrade" feature
// flag is has been fully rolled out. Also remove the `apiOptions` prop.
const imageUpgradeFF = isFeatureOn({apiOptions}, "image-widget-upgrade");
Expand All @@ -28,7 +34,9 @@ const IssueDetails = ({apiOptions, issue}: IssueProps) => {
animated={true}
expanded={expanded}
onToggle={toggleVisibility}
containerStyle={{backgroundColor: color.fadedGold8}}
containerStyle={{
backgroundColor: accordionColor,
}}
panelStyle={{backgroundColor: "white"}}
header={
<LabelLarge
Expand All @@ -39,7 +47,7 @@ const IssueDetails = ({apiOptions, issue}: IssueProps) => {
whiteSpace: "nowrap",
}}
>
{`Warning: ${issue.id}`}
{`${issue.type}: ${issue.id}`}
</LabelLarge>
}
>
Expand All @@ -56,6 +64,7 @@ const IssueDetails = ({apiOptions, issue}: IssueProps) => {
Issue:
</LabelSmall>
<span>{issue.message}</span>
<ShowMe elements={issue.elements} />
{imageUpgradeFF && <IssueCta issue={issue} />}
</PerseusEditorAccordion>
);
Expand Down
28 changes: 24 additions & 4 deletions packages/perseus-editor/src/components/issues-panel.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import {View} from "@khanacademy/wonder-blocks-core";
import {PhosphorIcon} from "@khanacademy/wonder-blocks-icon";
import {color as wbColor} from "@khanacademy/wonder-blocks-tokens";
import {semanticColor} from "@khanacademy/wonder-blocks-tokens";
import iconPass from "@phosphor-icons/core/fill/check-circle-fill.svg";
import iconWarning from "@phosphor-icons/core/fill/warning-fill.svg";
import iconAlert from "@phosphor-icons/core/fill/warning-octagon-fill.svg";
import * as React from "react";
import {useState} from "react";

Expand All @@ -12,13 +13,16 @@ import ToggleableCaret from "./toggleable-caret";
import type {APIOptions} from "@khanacademy/perseus";

export type IssueImpact = "low" | "medium" | "high";
export type IssueType = "Warning" | "Alert";
export type Issue = {
id: string;
description: string;
elements?: Element[];
helpUrl: string;
help: string;
impact: IssueImpact;
message: string;
type: IssueType;
};

type IssuesPanelProps = {
Expand All @@ -32,19 +36,35 @@ const IssuesPanel = ({apiOptions, issues = []}: IssuesPanelProps) => {
const [showPanel, setShowPanel] = useState(false);

const hasWarnings = issues.length > 0;
const hasAlerts = issues.some((issue) => issue.type === "Alert");
const issuesCount = `${issues.length} issue${
issues.length === 1 ? "" : "s"
}`;

const icon = hasWarnings ? iconWarning : iconPass;
const iconColor = hasWarnings ? wbColor.gold : wbColor.green;
const icon = hasAlerts ? iconAlert : hasWarnings ? iconWarning : iconPass;
const iconColor = hasAlerts
? semanticColor.feedback.critical.strong.icon
: hasWarnings
? semanticColor.feedback.warning.strong.icon
: semanticColor.feedback.success.strong.icon;

const togglePanel = () => {
if (hasWarnings) {
setShowPanel(!showPanel);
}
};

const impactOrder = {high: 3, medium: 2, low: 1};
const sortedIssues = issues.sort((a, b) => {
if (a.type !== b.type) {
return a.type === "Alert" ? -1 : 1;
}
if (impactOrder[b.impact] !== impactOrder[a.impact]) {
return impactOrder[b.impact] - impactOrder[a.impact];
}
return a.id.localeCompare(b.id);
});

return (
<div className="perseus-widget-editor">
<div className="perseus-widget-editor-title">
Expand Down Expand Up @@ -74,7 +94,7 @@ const IssuesPanel = ({apiOptions, issues = []}: IssuesPanelProps) => {
{showPanel && (
<div className="perseus-widget-editor-panel">
<div className="perseus-widget-editor-content">
{issues.map((issue) => (
{sortedIssues.map((issue) => (
<IssueDetails
apiOptions={apiOptions}
key={issue.id}
Expand Down
72 changes: 72 additions & 0 deletions packages/perseus-editor/src/components/show-me-issue.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import Switch from "@khanacademy/wonder-blocks-switch";
import {LabelSmall} from "@khanacademy/wonder-blocks-typography";
import * as React from "react";
import {useState} from "react";

import type {CSSProperties} from "react";

type BoundaryRect = {
top: number;
left: number;
height: number;
width: number;
};

const ShowMe = ({elements}: {elements?: Element[]}) => {
const [showMe, setShowMe] = useState(false);

if (!elements || elements.length === 0) {
return null;
}
const issueBoundary = elements?.reduce(
(boundary: BoundaryRect, element: Element, index: number) => {
const elementBoundary = element.getBoundingClientRect();
boundary.top += elementBoundary.top;
boundary.left += elementBoundary.left;
if (index === elements.length - 1) {
boundary.height = elementBoundary.height;
boundary.width = elementBoundary.width;
}
return boundary;
},
{top: 0, left: 0, height: 0, width: 0},
);
const showMeStyle = {
marginTop: "1em",
fontWeight: "bold",
display: "flex",
alignItems: "center",
};
const showMeOutlineStyle: CSSProperties =
showMe && issueBoundary.width !== 0
? {
display: "block",
border: "2px solid red",
borderRadius: "4px",
position: "fixed",
height: issueBoundary.height + 8,
width: issueBoundary.width + 8,
top: issueBoundary.top - 4,
left: issueBoundary.left - 4,
}
: {display: "none"};

const showMeToggle = (
<LabelSmall style={showMeStyle}>
<span style={{marginInlineEnd: "1em"}}>Show Me</span>
<Switch checked={showMe} onChange={setShowMe} />
<div style={showMeOutlineStyle} />
</LabelSmall>
);
const showMeUnavailable = (
<div>
Unable to find the offending element. Please ask a developer for
help fixing this.
</div>
);

// eslint-disable-next-line
return issueBoundary ? showMeToggle : showMeUnavailable;
};

export default ShowMe;
14 changes: 14 additions & 0 deletions packages/perseus-editor/src/iframe-content-renderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,20 @@ class IframeContentRenderer extends React.Component<Props> {
frame.style.width = "100%";
frame.style.height = "100%";
frame.src = this.props.url;
// Add axe-core library to the iFrame
frame.onload = () => {
const iframeDoc =
frame.contentDocument || frame.contentWindow?.document;
if (iframeDoc) {
const axeCoreScriptElement = iframeDoc.createElement("script");
axeCoreScriptElement.src =
"https://unpkg.com/[email protected]/axe.js";
iframeDoc.body.appendChild(axeCoreScriptElement);
} else {
// eslint-disable-next-line no-console
console.warn("Unable to add axe-core to iframe document");
}
};

if (this.props.datasetKey) {
// If the user has specified a data-* attribute to place on the
Expand Down
51 changes: 33 additions & 18 deletions packages/perseus-editor/src/item-editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import Editor from "./editor";
import IframeContentRenderer from "./iframe-content-renderer";
import ItemExtrasEditor from "./item-extras-editor";
import {WARNINGS} from "./messages";
import {runAxeCoreOnUpdate} from "./util/a11y-checker";
import {ItemEditorContext} from "./util/item-editor-context";

import type {Issue} from "./components/issues-panel";
Expand Down Expand Up @@ -49,6 +50,7 @@ type Props = {

type State = {
issues: Issue[];
axeCoreIssues: Issue[];
};

class ItemEditor extends React.Component<Props, State> {
Expand All @@ -63,38 +65,48 @@ class ItemEditor extends React.Component<Props, State> {
};
static prevContent: string | undefined;
static prevWidgets: PerseusWidgetsMap | undefined;
a11yCheckerTimeoutId: any;

frame = React.createRef<IframeContentRenderer>();
questionEditor = React.createRef<Editor>();
itemExtrasEditor = React.createRef<ItemExtrasEditor>();

state = {
issues: [],
axeCoreIssues: [],
};

static getDerivedStateFromProps(props: Props): Partial<State> | null {
componentDidUpdate(prevProps: Props) {
// Short-circuit if nothing changed
if (
props.question?.content === ItemEditor.prevContent &&
props.question?.widgets === ItemEditor.prevWidgets
this.props.question?.content === prevProps.question?.content &&
this.props.question?.widgets === prevProps.question?.widgets
) {
return null;
return;
}

// Update cached values
ItemEditor.prevContent = props.question?.content;
ItemEditor.prevWidgets = props.question?.widgets;

const parsed = PerseusMarkdown.parse(props.question?.content ?? "", {});
const parsed = PerseusMarkdown.parse(
this.props.question?.content ?? "",
{},
);
const linterContext = {
content: props.question?.content,
widgets: props.question?.widgets,
content: this.props.question?.content,
widgets: this.props.question?.widgets,
stack: [],
};

return {
issues: [
...(props.issues ?? []),
this.a11yCheckerTimeoutId = runAxeCoreOnUpdate(
this.a11yCheckerTimeoutId,
(issues) => {
this.setState({
axeCoreIssues: issues,
});
},
);

const gatherIssues = () => {
return [
...(this.props.issues ?? []),
...(PerseusLinter.runLinter(parsed, linterContext, false)?.map(
(linterWarning) => {
if (linterWarning.rule === "inaccessible-widget") {
Expand All @@ -109,8 +121,12 @@ class ItemEditor extends React.Component<Props, State> {
);
},
) ?? []),
],
];
};

this.setState({
issues: gatherIssues(),
});
}

// Notify the parent that the question or answer area has been updated.
Expand Down Expand Up @@ -155,6 +171,7 @@ class ItemEditor extends React.Component<Props, State> {
this.props.deviceType === "phone" ||
this.props.deviceType === "tablet";
const editingDisabled = this.props.apiOptions?.editingDisabled ?? false;
const allIssues = this.state.issues.concat(this.state.axeCoreIssues);

return (
<ItemEditorContext.Provider
Expand All @@ -168,7 +185,7 @@ class ItemEditor extends React.Component<Props, State> {
<div className="perseus-editor-left-cell">
<IssuesPanel
apiOptions={this.props.apiOptions}
issues={this.state.issues}
issues={allIssues}
/>
<div className="pod-title">Question</div>
<fieldset disabled={editingDisabled}>
Expand All @@ -193,7 +210,6 @@ class ItemEditor extends React.Component<Props, State> {
/>
</fieldset>
</div>

<div className="perseus-editor-right-cell">
<div id="problemarea">
<DeviceFramer
Expand All @@ -217,7 +233,6 @@ class ItemEditor extends React.Component<Props, State> {
</div>
</div>
</div>

<div className="perseus-editor-row perseus-answer-container">
<div className="perseus-editor-left-cell">
<div className="pod-title">Question extras</div>
Expand Down
2 changes: 2 additions & 0 deletions packages/perseus-editor/src/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export const WARNINGS = {
impact: "medium",
message:
"Selecting inaccessible widgets for a practice item will result in this exercise being hidden from users with 'Hide visually dependant content' setting set to true. Please select another widget or create an alternative practice item.",
type: "Warning",
}),

genericLinterWarning: (rule: string, message: string): Issue => ({
Expand All @@ -21,5 +22,6 @@ export const WARNINGS = {
"https://docs.google.com/document/d/1N13f4sY-7EXWDwQ04ivA9vJBVvPPd60qjBT73B4NHuM/edit?tab=t.0",
impact: "low",
message: message,
type: "Warning",
}),
};
Loading
Loading