From dc879ed09cf6c3454057de214b2fbf1ae2e5f668 Mon Sep 17 00:00:00 2001 From: Ivy Olamit Date: Wed, 10 Jun 2026 15:51:47 -0700 Subject: [PATCH 01/10] [LEMS-4131/poc-improve-editor-controls-contrast] docs(changeset): --- .changeset/shiny-dodos-eat.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .changeset/shiny-dodos-eat.md diff --git a/.changeset/shiny-dodos-eat.md b/.changeset/shiny-dodos-eat.md new file mode 100644 index 00000000000..a845151cc84 --- /dev/null +++ b/.changeset/shiny-dodos-eat.md @@ -0,0 +1,2 @@ +--- +--- From e8bd0d1259d82013fcdbde69fd9adf8ce467a4b2 Mon Sep 17 00:00:00 2001 From: Ivy Olamit Date: Wed, 10 Jun 2026 18:13:47 -0700 Subject: [PATCH 02/10] [LEMS-4131/poc-improve-editor-controls-contrast] POC Improve disabled editor contrast by letting Wonder Blocks own the disabled state --- packages/perseus-editor/package.json | 2 + .../src/components/segmented-control.tsx | 223 ++++++++ .../src/styles/perseus-editor.css | 26 +- .../__tests__/numeric-input-editor.test.tsx | 29 +- .../src/widgets/expression-editor.tsx | 16 +- .../src/widgets/graded-group-editor.tsx | 3 + .../src/widgets/graded-group-set-editor.tsx | 5 +- .../components/interactive-graph-sr-tree.tsx | 35 +- .../locked-figures/locked-figure-select.tsx | 4 +- .../locked-figures/locked-figures-section.tsx | 6 +- .../src/widgets/numeric-input-editor.tsx | 519 ++++++++---------- .../src/widgets/radio/editor.tsx | 6 +- .../radio/radio-option-settings-actions.tsx | 7 + .../widgets/radio/radio-option-settings.tsx | 89 ++- .../src/widgets/radio/radio-status-pill.tsx | 72 ++- .../perseus/src/components/button-group.tsx | 16 + .../src/widgets/label-image/answer-pill.tsx | 7 + 17 files changed, 632 insertions(+), 433 deletions(-) create mode 100644 packages/perseus-editor/src/components/segmented-control.tsx diff --git a/packages/perseus-editor/package.json b/packages/perseus-editor/package.json index ab76181538c..132daa7544e 100644 --- a/packages/perseus-editor/package.json +++ b/packages/perseus-editor/package.json @@ -51,6 +51,7 @@ "devDependencies": { "@khanacademy/mathjax-renderer": "catalog:devDeps", "@khanacademy/wonder-blocks-accordion": "catalog:devDeps", + "@khanacademy/wonder-blocks-badge": "catalog:devDeps", "@khanacademy/wonder-blocks-banner": "catalog:devDeps", "@khanacademy/wonder-blocks-button": "catalog:devDeps", "@khanacademy/wonder-blocks-clickable": "catalog:devDeps", @@ -83,6 +84,7 @@ "peerDependencies": { "@khanacademy/mathjax-renderer": "catalog:peerDeps", "@khanacademy/wonder-blocks-accordion": "catalog:peerDeps", + "@khanacademy/wonder-blocks-badge": "catalog:peerDeps", "@khanacademy/wonder-blocks-banner": "catalog:peerDeps", "@khanacademy/wonder-blocks-button": "catalog:peerDeps", "@khanacademy/wonder-blocks-clickable": "catalog:peerDeps", diff --git a/packages/perseus-editor/src/components/segmented-control.tsx b/packages/perseus-editor/src/components/segmented-control.tsx new file mode 100644 index 00000000000..001244e0c73 --- /dev/null +++ b/packages/perseus-editor/src/components/segmented-control.tsx @@ -0,0 +1,223 @@ +import Clickable from "@khanacademy/wonder-blocks-clickable"; +import {View} from "@khanacademy/wonder-blocks-core"; +import { + border, + font, + semanticColor, + sizing, +} from "@khanacademy/wonder-blocks-tokens"; +import {StyleSheet} from "aphrodite"; +import * as React from "react"; + +import type {StyleType} from "@khanacademy/wonder-blocks-core"; + +/** + * A single selectable segment. Built on Wonder Blocks `Clickable` (the same + * primitive `Pill` uses) so it gets focus handling and a real `disabled` / + * `aria-disabled` state for free, while letting us render arbitrary children + * (math, icons) and set a `radio`/`checkbox` role. + * + * Disabled styling intentionally uses the WB disabled tokens so it reads as + * disabled consistently with the rest of the design system (no opacity hack). + */ +type ToggleButtonProps = { + selected: boolean; + onClick: () => void; + disabled?: boolean; + role?: "radio" | "checkbox"; + "aria-label"?: string; + children: React.ReactNode; + style?: StyleType; +}; + +export function ToggleButton({ + selected, + onClick, + disabled = false, + role = "radio", + children, + style, + "aria-label": ariaLabel, +}: ToggleButtonProps): React.ReactElement { + return ( + + {({hovered, pressed}) => ( + + {children} + + )} + + ); +} + +/** + * A row of mutually-exclusive (single-select) segments — a segmented control. + * Replaces the deprecated `Pill`-as-button pattern. Pass `disabled` to disable + * the whole group (e.g. `editingDisabled`). + */ +type Option = { + value: string; + label: React.ReactNode; + ariaLabel?: string; +}; + +type SegmentedControlProps = { + options: ReadonlyArray