diff --git a/.changeset/twelve-rockets-taste.md b/.changeset/twelve-rockets-taste.md new file mode 100644 index 0000000000..4a4c34e6a9 --- /dev/null +++ b/.changeset/twelve-rockets-taste.md @@ -0,0 +1,7 @@ +--- +"@khanacademy/math-input": minor +"@khanacademy/perseus": minor +"@khanacademy/perseus-editor": minor +--- + +add scientific notation button / toggle to basic keypad diff --git a/packages/math-input/src/components/keypad/__tests__/keypad.test.tsx b/packages/math-input/src/components/keypad/__tests__/keypad.test.tsx index 2602b94368..a923a61010 100644 --- a/packages/math-input/src/components/keypad/__tests__/keypad.test.tsx +++ b/packages/math-input/src/components/keypad/__tests__/keypad.test.tsx @@ -329,4 +329,34 @@ describe("keypad", () => { // Assert expect(screen.getByTestId("period-decimal")).toBeInTheDocument(); }); + + it("shows the exponent button for scientific keypad", () => { + //Arrange + //Act + render( + {}} + scientific + onAnalyticsEvent={async () => {}} + />, + ); + + //Assert + expect( + screen.getByRole("button", {name: "Custom exponent"}), + ).toBeInTheDocument(); + }); + + it("does not show the exponent button for non-scientific keypad", () => { + //Arrange + //Act + render( + {}} onAnalyticsEvent={async () => {}} />, + ); + + //Assert + expect( + screen.queryByRole("button", {name: "Custom exponent"}), + ).not.toBeInTheDocument(); + }); }); diff --git a/packages/math-input/src/components/keypad/keypad.tsx b/packages/math-input/src/components/keypad/keypad.tsx index 4e21539b2e..05a90a956c 100644 --- a/packages/math-input/src/components/keypad/keypad.tsx +++ b/packages/math-input/src/components/keypad/keypad.tsx @@ -34,6 +34,7 @@ export type Props = { basicRelations?: boolean; advancedRelations?: boolean; fractionsOnly?: boolean; + scientific?: boolean; onClickKey: ClickKeyCallback; onAnalyticsEvent: AnalyticsEventHandlerFn; @@ -94,6 +95,7 @@ export default function Keypad(props: Props) { logarithms, basicRelations, advancedRelations, + scientific, showDismiss, onAnalyticsEvent, fractionsOnly, @@ -187,6 +189,7 @@ export default function Keypad(props: Props) { convertDotToTimes={convertDotToTimes} divisionKey={divisionKey} selectedPage={selectedPage} + scientific={scientific} /> )} diff --git a/packages/math-input/src/components/keypad/mobile-keypad-internals.tsx b/packages/math-input/src/components/keypad/mobile-keypad-internals.tsx index 139077db3c..402b2b2d1c 100644 --- a/packages/math-input/src/components/keypad/mobile-keypad-internals.tsx +++ b/packages/math-input/src/components/keypad/mobile-keypad-internals.tsx @@ -220,6 +220,7 @@ class MobileKeypadInternals containerWidth > expandedViewThreshold } showDismiss + scientific={keypadConfig?.scientific} /> ) : null} diff --git a/packages/math-input/src/components/keypad/shared-keys.tsx b/packages/math-input/src/components/keypad/shared-keys.tsx index 77512e966d..1d159f83c0 100644 --- a/packages/math-input/src/components/keypad/shared-keys.tsx +++ b/packages/math-input/src/components/keypad/shared-keys.tsx @@ -16,6 +16,7 @@ type Props = { cursorContext?: (typeof CursorContext)[keyof typeof CursorContext]; convertDotToTimes?: boolean; divisionKey?: boolean; + scientific?: boolean; }; export default function SharedKeys(props: Props) { @@ -25,6 +26,7 @@ export default function SharedKeys(props: Props) { divisionKey, convertDotToTimes, selectedPage, + scientific, } = props; const {strings, locale} = useMathInputI18n(); const cursorKeyConfig = getCursorContextConfig(strings, cursorContext); @@ -56,7 +58,14 @@ export default function SharedKeys(props: Props) { coord={[5, 0]} secondary /> - + {scientific && ( + + )} {/* Row 2 */} ; times?: boolean; + scientific?: boolean; }; export type KeyHandler = (key: Key) => Cursor; diff --git a/packages/perseus-editor/src/widgets/__tests__/expression-editor.test.tsx b/packages/perseus-editor/src/widgets/__tests__/expression-editor.test.tsx index 1cda1663d7..b6cc2f96e2 100644 --- a/packages/perseus-editor/src/widgets/__tests__/expression-editor.test.tsx +++ b/packages/perseus-editor/src/widgets/__tests__/expression-editor.test.tsx @@ -209,6 +209,23 @@ describe("expression-editor", () => { }); }); + it("should toggle scientific checkbox", async () => { + const onChangeMock = jest.fn(); + + render(); + act(() => jest.runOnlyPendingTimers()); + + await userEvent.click( + screen.getByRole("checkbox", { + name: "scientific", + }), + ); + + expect(onChangeMock).toBeCalledWith({ + buttonSets: ["basic", "scientific"], + }); + }); + it("should be possible to add an answer", async () => { const onChangeMock = jest.fn(); diff --git a/packages/perseus-editor/src/widgets/expression-editor.tsx b/packages/perseus-editor/src/widgets/expression-editor.tsx index 2d749c551a..4ee6eeffdd 100644 --- a/packages/perseus-editor/src/widgets/expression-editor.tsx +++ b/packages/perseus-editor/src/widgets/expression-editor.tsx @@ -50,6 +50,7 @@ const buttonSetsList: LegacyButtonSets = [ "basic", "trig", "prealgebra", + "scientific", "logarithms", "basic relations", "advanced relations", diff --git a/packages/perseus/src/components/__tests__/math-input.test.tsx b/packages/perseus/src/components/__tests__/math-input.test.tsx index 7ec7dd9aa4..8ed6bed9b4 100644 --- a/packages/perseus/src/components/__tests__/math-input.test.tsx +++ b/packages/perseus/src/components/__tests__/math-input.test.tsx @@ -6,15 +6,17 @@ import {testDependencies} from "../../../../../testing/test-dependencies"; import * as Dependencies from "../../dependencies"; import MathInput from "../math-input"; +import type {KeypadButtonSets} from "../math-input"; import type {UserEvent} from "@testing-library/user-event"; -const allButtonSets = { +const allButtonSets: KeypadButtonSets = { advancedRelations: true, basicRelations: true, divisionKey: true, logarithms: true, preAlgebra: true, trigonometry: true, + scientific: true, }; describe("Perseus' MathInput", () => { @@ -31,7 +33,7 @@ describe("Perseus' MathInput", () => { }); it("renders", () => { - // Assemble + // Arrange render( {}} @@ -50,7 +52,7 @@ describe("Perseus' MathInput", () => { }); it("provides a default aria label", () => { - // Assemble + // Arrange render( {}} @@ -69,7 +71,7 @@ describe("Perseus' MathInput", () => { }); it("is possible to overwrite the aria label", () => { - // Assemble + // Arrange render( {}} @@ -89,7 +91,7 @@ describe("Perseus' MathInput", () => { }); it("is possible to type in the input", async () => { - // Assemble + // Arrange const mockOnChange = jest.fn(); render( { }); it("is possible to use buttons", async () => { - // Assemble + // Arrange const mockOnChange = jest.fn(); render( { expect(mockOnChange).toHaveBeenLastCalledWith("1+2-3"); }); + it("is possible to use the scientific keypad", async () => { + // Arrange + const mockOnChange = jest.fn(); + render( + Promise.resolve()}} + convertDotToTimes={false} + value="" + />, + ); + act(() => jest.runOnlyPendingTimers()); + + // Act + await userEvent.click( + screen.getByRole("button", {name: /open math keypad/}), + ); + await userEvent.click(screen.getByRole("button", {name: "2"})); + await userEvent.click( + screen.getByRole("button", {name: "Custom exponent"}), + ); + await userEvent.click(screen.getByRole("button", {name: "2"})); + act(() => jest.runOnlyPendingTimers()); + + // Assert + expect(mockOnChange).toHaveBeenLastCalledWith("2^{2}"); + }); + it("is possible to use buttons with legacy props", async () => { - // Assemble + // Arrange const mockOnChange = jest.fn(); render( { }); it("returns focus to input after button click", async () => { - // Assemble + // Arrange render( {}} @@ -197,7 +228,7 @@ describe("Perseus' MathInput", () => { }); it("does not return focus to input after button press via keyboard", async () => { - // Assemble + // Arrange render( {}} @@ -226,7 +257,7 @@ describe("Perseus' MathInput", () => { }); it("does not focus on the keypad button when it is clicked with the mouse", async () => { - // Assemble + // Arrange render( {}} diff --git a/packages/perseus/src/components/math-input.tsx b/packages/perseus/src/components/math-input.tsx index 995273ae3e..d0792c94d3 100644 --- a/packages/perseus/src/components/math-input.tsx +++ b/packages/perseus/src/components/math-input.tsx @@ -32,13 +32,14 @@ import type {Keys, MathFieldInterface} from "@khanacademy/math-input"; type ButtonsVisibleType = "always" | "never" | "focused"; -type KeypadButtonSets = { +export type KeypadButtonSets = { advancedRelations?: boolean; basicRelations?: boolean; divisionKey?: boolean; logarithms?: boolean; preAlgebra?: boolean; trigonometry?: boolean; + scientific?: boolean; }; type Props = { @@ -495,6 +496,9 @@ const mapButtonSets = (buttonSets?: LegacyButtonSets) => { case "trig": keypadButtonSets.trigonometry = true; break; + case "scientific": + keypadButtonSets.scientific = true; + break; case "basic": default: break; diff --git a/packages/perseus/src/perseus-types.ts b/packages/perseus/src/perseus-types.ts index b91cbe3b67..08102d7a53 100644 --- a/packages/perseus/src/perseus-types.ts +++ b/packages/perseus/src/perseus-types.ts @@ -397,6 +397,7 @@ export type LegacyButtonSets = ReadonlyArray< | "basic" | "basic+div" | "trig" + | "scientific" | "prealgebra" | "logarithms" | "basic relations" @@ -406,7 +407,6 @@ export type LegacyButtonSets = ReadonlyArray< export type PerseusExpressionWidgetOptions = { // The expression forms the answer may come in answerForms: ReadonlyArray; - // Different buttons sets that can show in the expression. Options are "basic", "basic+div", "trig", "prealgebra", "logarithms", "basic relations", "advanced relations" buttonSets: LegacyButtonSets; // Variables that can be used as functions. Default: ["f", "g", "h"] functions: ReadonlyArray; diff --git a/packages/perseus/src/widgets/expression/expression.stories.tsx b/packages/perseus/src/widgets/expression/expression.stories.tsx index 6bb70aa05b..3cc18d2ae0 100644 --- a/packages/perseus/src/widgets/expression/expression.stories.tsx +++ b/packages/perseus/src/widgets/expression/expression.stories.tsx @@ -68,6 +68,7 @@ export const DesktopKitchenSink = (args: StoryArgs): React.ReactElement => { "logarithms", "basic relations", "advanced relations", + "scientific", ] as LegacyButtonSets, }; diff --git a/packages/perseus/src/widgets/expression/expression.test.tsx b/packages/perseus/src/widgets/expression/expression.test.tsx index 1c25d9ff45..0346752551 100644 --- a/packages/perseus/src/widgets/expression/expression.test.tsx +++ b/packages/perseus/src/widgets/expression/expression.test.tsx @@ -1,4 +1,5 @@ import {it, describe, beforeEach} from "@jest/globals"; +import {KeypadType} from "@khanacademy/math-input"; import {act, screen, waitFor} from "@testing-library/react"; import {userEvent as userEventLib} from "@testing-library/user-event"; @@ -9,7 +10,7 @@ import { import * as Dependencies from "../../dependencies"; import {renderQuestion} from "../__testutils__/renderQuestion"; -import {Expression} from "./expression"; +import {Expression, keypadConfigurationForProps} from "./expression"; import { expressionItem2, expressionItem3, @@ -17,8 +18,12 @@ import { expressionItemWithLabels, } from "./expression.testdata"; -import type {PerseusItem} from "../../perseus-types"; +import type { + PerseusExpressionWidgetOptions, + PerseusItem, +} from "../../perseus-types"; import type {APIOptions} from "../../types"; +import type {KeypadConfiguration} from "@khanacademy/math-input"; import type {UserEvent} from "@testing-library/user-event"; const renderAndAnswer = async ( @@ -586,3 +591,77 @@ describe("Expression Widget", function () { }); }); }); + +describe("Keypad configuration", () => { + it("should handle basic button set", async () => { + // Arrange + const widgetOptions: PerseusExpressionWidgetOptions = { + answerForms: [], + buttonSets: ["basic"], + times: false, + functions: [], + }; + + const expected: KeypadConfiguration = { + keypadType: KeypadType.EXPRESSION, + times: false, + extraKeys: ["PI"], + }; + + // Act + const result = keypadConfigurationForProps(widgetOptions); + + // Assert + expect(result).toEqual(expected); + }); + + it("should handle basic+div button set", async () => { + // Arrange + // Act + // Assert + expect( + keypadConfigurationForProps({ + answerForms: [], + buttonSets: ["basic+div"], + times: false, + functions: [], + }), + ).toEqual({ + keypadType: KeypadType.EXPRESSION, + times: false, + extraKeys: ["PI"], + }); + }); + + it("should return expression keypad configuration by default", async () => { + // Arrange + // Act + const result = keypadConfigurationForProps({ + answerForms: [], + buttonSets: [], + times: false, + functions: [], + }); + + // Assert + expect(result.keypadType).toEqual(KeypadType.EXPRESSION); + }); + + it("should handle scientific button set", async () => { + // Arrange + // Act + // Assert + expect( + keypadConfigurationForProps({ + answerForms: [], + buttonSets: ["scientific"], + times: false, + functions: [], + }), + ).toEqual({ + keypadType: KeypadType.EXPRESSION, + times: false, + extraKeys: ["PI"], + }); + }); +}); diff --git a/packages/perseus/src/widgets/expression/expression.tsx b/packages/perseus/src/widgets/expression/expression.tsx index 86df657b79..e1460d0d13 100644 --- a/packages/perseus/src/widgets/expression/expression.tsx +++ b/packages/perseus/src/widgets/expression/expression.tsx @@ -490,7 +490,7 @@ const styles = StyleSheet.create({ * to be included as keys on the keypad. These are scraped from the answer * forms. */ -const keypadConfigurationForProps = ( +export const keypadConfigurationForProps = ( widgetOptions: PerseusExpressionWidgetOptions, ): KeypadConfiguration => { // Always use the Expression keypad, regardless of the button sets that have @@ -547,7 +547,12 @@ const keypadConfigurationForProps = ( extraKeys = ["PI"]; } - return {keypadType, extraKeys, times: widgetOptions.times}; + return { + keypadType, + extraKeys, + times: widgetOptions.times, + // scientific: widgetOptions.buttonSets.includes("scientific"), // POC note: this line may or may not be needed ~ doesn't seem to impact output + }; }; const propUpgrades = {