diff --git a/.changeset/blue-kings-repeat.md b/.changeset/blue-kings-repeat.md new file mode 100644 index 00000000000..b616f9c8082 --- /dev/null +++ b/.changeset/blue-kings-repeat.md @@ -0,0 +1,6 @@ +--- +"@khanacademy/math-input": patch +"@khanacademy/perseus": patch +--- + +Convert hardcoded colors and fonts in math-input components to semantic tokens (cursor-handle, keypad-button, navigation-button) diff --git a/packages/math-input/src/__docs__/math-input-initial-state-regression.stories.tsx b/packages/math-input/src/__docs__/math-input-initial-state-regression.stories.tsx new file mode 100644 index 00000000000..6944ce0720c --- /dev/null +++ b/packages/math-input/src/__docs__/math-input-initial-state-regression.stories.tsx @@ -0,0 +1,47 @@ +import * as React from "react"; + +import {themeModes} from "../../../../.storybook/modes"; +import CursorHandle from "../components/input/cursor-handle"; + +import type {Meta, StoryObj} from "@storybook/react-vite"; + +const meta: Meta = { + title: "Math Input/Components/Visual Regression Tests/Initial State", + component: CursorHandle, + tags: ["!autodocs", "!manifest"], + parameters: { + docs: { + description: { + component: + "Regression tests for math-input components that do NOT " + + "need any interactions to test.", + }, + }, + chromatic: {disableSnapshot: false, modes: themeModes}, + }, +}; +export default meta; + +type Story = StoryObj; + +// This component is touch-only in production and never reached via mouse +// interactions, so it is tested here in isolation. +export const CursorHandleVisible: Story = { + decorators: [ + (Story) => ( +
+ +
+ ), + ], + args: { + x: 50, + y: 20, + animateIntoPosition: false, + visible: true, + onTouchStart: () => {}, + onTouchMove: () => {}, + onTouchEnd: () => {}, + onTouchCancel: () => {}, + }, +}; diff --git a/packages/math-input/src/__docs__/math-input-interactions-regression.stories.tsx b/packages/math-input/src/__docs__/math-input-interactions-regression.stories.tsx new file mode 100644 index 00000000000..2f406210e5f --- /dev/null +++ b/packages/math-input/src/__docs__/math-input-interactions-regression.stories.tsx @@ -0,0 +1,47 @@ +import * as React from "react"; +import {action} from "storybook/actions"; + +import {themeModes} from "../../../../.storybook/modes"; +import NavigationPad from "../components/keypad/navigation-pad"; + +import TintedBackgroundDecorator from "./tinted-background-decorator"; + +import type {Meta, StoryObj} from "@storybook/react-vite"; + +const meta: Meta = { + title: "Math Input/Components/Visual Regression Tests/Interactions", + component: NavigationPad, + tags: ["!autodocs", "!manifest"], + parameters: { + docs: { + description: { + component: + "Regression tests for math-input components that DO need " + + "interactions to test.", + }, + }, + chromatic: {disableSnapshot: false, modes: themeModes}, + }, + decorators: [ + (Story) => ( +
+ +
+ ), + TintedBackgroundDecorator, + ], + args: { + onClickKey: action("onClickKey"), + }, +}; +export default meta; + +type Story = StoryObj; + +// NavigationPad renders inline (not a portal) so canvas queries work directly. +export const NavigationButtonPressed: Story = { + play: async ({canvas, userEvent}) => { + const button = canvas.getByRole("button", {name: "Left arrow"}); + await userEvent.pointer({target: button, keys: "[MouseLeft>]"}); + }, +}; diff --git a/packages/math-input/src/components/input/cursor-handle.tsx b/packages/math-input/src/components/input/cursor-handle.tsx index d53bc47f2a7..998357104ef 100644 --- a/packages/math-input/src/components/input/cursor-handle.tsx +++ b/packages/math-input/src/components/input/cursor-handle.tsx @@ -1,7 +1,8 @@ /** - * Renders the green tear-shaped handle under the cursor. + * Renders the blue tear-shaped handle under the cursor. */ +import {semanticColor} from "@khanacademy/wonder-blocks-tokens"; import * as React from "react"; import { @@ -118,12 +119,15 @@ class CursorHandle extends React.Component { diff --git a/packages/math-input/src/components/keypad/__tests__/__snapshots__/keypad.test.tsx.snap b/packages/math-input/src/components/keypad/__tests__/__snapshots__/keypad.test.tsx.snap index 29951917334..56d5aa642ed 100644 --- a/packages/math-input/src/components/keypad/__tests__/__snapshots__/keypad.test.tsx.snap +++ b/packages/math-input/src/components/keypad/__tests__/__snapshots__/keypad.test.tsx.snap @@ -167,7 +167,7 @@ exports[`keypad should snapshot expanded: first render 1`] = ` class="default_7zhre1-o_O-outerBoxBase_3w5jmh" >
{ - const tintColor = secondary ? "#F6F6F7" : action ? "#DBDCDD" : undefined; + // TODO(LEMS-4261): `action` is never passed by any caller — this branch + // is currently unreachable. Revisit when/if the prop is wired up. + const tintColor = secondary + ? semanticColor.core.background.base.subtle + : action + ? semanticColor.core.background.neutral.subtle + : undefined; return ( = { title: "Widgets/Expression/Visual Regression Tests/Interactions", component: Expression, tags: ["!autodocs", "!manifest"], + decorators: [ + (Story) => { + // MathQuill toggles `mq-blink` via setInterval, making snapshots + // non-deterministic. The blink rules in main.css use `!important` + // inside `@layer shared`; CSS layers reverse !important priority + // (layered beats unlayered), so our overrides must be inside the + // same layer. + React.useLayoutEffect(() => { + const style = document.createElement("style"); + style.textContent = ` + @layer shared { + :root .mq-cursor.mq-blink { visibility: visible !important; } + :root .keypad-input .mq-editable-field .mq-cursor { transition: none !important; } + :root .keypad-input .mq-editable-field .mq-cursor.mq-blink { opacity: 1 !important; } + } + `; + document.head.appendChild(style); + return () => style.remove(); + }, []); + return ; + }, + ], parameters: { docs: { description: { @@ -139,6 +162,17 @@ export const WithTextInField: Story = { }, }; +export const KeypadButtonPressed: Story = { + decorators: [expressionRendererDecorator], + args: keypadArgs, + play: async ({canvas, userEvent}) => { + await openKeypad({canvas, userEvent}); + // Keypad renders into a React portal outside the canvas + const button = within(document.body).getByRole("button", {name: "1"}); + await userEvent.pointer({target: button, keys: "[MouseLeft>]"}); + }, +}; + export const MobileInputFocused: Story = { decorators: [expressionRendererDecorator], args: {