diff --git a/.changeset/many-eagles-promise.md b/.changeset/many-eagles-promise.md deleted file mode 100644 index 22251efe18..0000000000 --- a/.changeset/many-eagles-promise.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@khanacademy/perseus": minor -"@khanacademy/math-input": patch -"@khanacademy/perseus-editor": patch ---- - -Add story for ExpressionEditor diff --git a/.changeset/tough-pears-impress.md b/.changeset/plenty-wombats-attack.md similarity index 100% rename from .changeset/tough-pears-impress.md rename to .changeset/plenty-wombats-attack.md diff --git a/.changeset/sour-bears-fail.md b/.changeset/sour-bears-fail.md deleted file mode 100644 index 536e5e2478..0000000000 --- a/.changeset/sour-bears-fail.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@khanacademy/perseus": minor ---- - -Extend argument object type for APIOptions.trackInteraction callback to support the arbitrary data each widget may add diff --git a/.github/workflows/node-ci.yml b/.github/workflows/node-ci.yml index a9ca08f95a..848998c324 100644 --- a/.github/workflows/node-ci.yml +++ b/.github/workflows/node-ci.yml @@ -97,6 +97,12 @@ jobs: limited-trigger: ${{ steps.js-files.outputs.filtered }} limited: yarn jest --passWithNoTests --findRelatedTests {} + # We use STOPSHIP internally to mark code that's not safe to go live yet. + # We use an if block because we want to return the exact inverse of what + # `git grep` returns (0 on none found, 1 on some found). + - name: Checks that STOPSHIP is not used in any files. + run: ./utils/stopship-check.sh + cypress: name: Cypress Coverage runs-on: ${{ matrix.os }} diff --git a/packages/math-input/CHANGELOG.md b/packages/math-input/CHANGELOG.md index 183be49745..c8ff72a02f 100644 --- a/packages/math-input/CHANGELOG.md +++ b/packages/math-input/CHANGELOG.md @@ -1,5 +1,46 @@ # @khanacademy/math-input +## 8.1.1 + +### Patch Changes + +- Updated dependencies [57f75510] + - @khanacademy/perseus-core@0.1.1 + +## 8.1.0 + +### Minor Changes + +- 5611204a: Adds back the export of the unwrapped keypad for Khanmigo +- b4430dce: Make sendEvent in the Keypad an optional param + +### Patch Changes + +- Updated dependencies [b4c06409] + - @khanacademy/perseus-core@0.1.0 + +## 8.0.0 + +### Major Changes + +- f9ee9d24: Move KeypadContext from Perseus to MathInput +- b18986d3: Replace Legacy/Mobile keypads with a component that switches between them + +## 7.0.0 + +### Major Changes + +- 04e68d1c: Change keypadElement from LegacyKeypad to KeypadAPI + +### Minor Changes + +- acafa72d: Add MobileKeypad to v2 keypad in MathInput + +### Patch Changes + +- d0f28dbd: Add story for ExpressionEditor +- 54590cc7: Add index.ts files to some dirs in MathInput for organization + ## 6.0.3 ### Patch Changes diff --git a/packages/math-input/package.json b/packages/math-input/package.json index 27c8bf9a74..22d12f61cf 100644 --- a/packages/math-input/package.json +++ b/packages/math-input/package.json @@ -3,7 +3,7 @@ "description": "Khan Academy's new expression editor for the mobile web.", "author": "Khan Academy", "license": "MIT", - "version": "6.0.3", + "version": "8.1.1", "publishConfig": { "access": "public" }, @@ -19,7 +19,7 @@ "source": "src/index.ts", "scripts": {}, "dependencies": { - "@khanacademy/perseus-core": "0.0.2", + "@khanacademy/perseus-core": "0.1.1", "mathquill": "git+https://git@github.com/Khan/mathquill.git#32d9f351aaa68537170b3120a52e99b8def3a2c3", "performance-now": "^0.2.0" }, diff --git a/packages/math-input/src/components/input/math-input.tsx b/packages/math-input/src/components/input/math-input.tsx index 38f1d2d751..48fce13df4 100644 --- a/packages/math-input/src/components/input/math-input.tsx +++ b/packages/math-input/src/components/input/math-input.tsx @@ -12,19 +12,18 @@ import { wonderBlocksBlue, offBlack, } from "../common-style"; -import ProvidedKeypad from "../keypad-legacy/provided-keypad"; import CursorHandle from "./cursor-handle"; import DragListener from "./drag-listener"; import MathWrapper from "./math-wrapper"; import {scrollIntoView} from "./scroll-into-view"; -import type {Cursor} from "../../types"; +import type {Cursor, KeypadAPI} from "../../types"; const constrainingFrictionFactor = 0.8; type Props = { - keypadElement: ProvidedKeypad; + keypadElement?: KeypadAPI; onBlur: () => void; onChange: (value: string, callback: any) => void; onFocus: () => void; @@ -267,7 +266,7 @@ class MathInput extends React.Component { /** Gets and cache they bounds of the keypadElement */ _getKeypadBounds: () => any = () => { if (!this._keypadBounds) { - const node = this.props.keypadElement.getDOMNode(); + const node = this.props.keypadElement?.getDOMNode(); this._cacheKeypadBounds(node); } return this._keypadBounds; @@ -341,7 +340,7 @@ class MathInput extends React.Component { focus: () => void = () => { // Pass this component's handleKey method to the keypad so it can call // it whenever it needs to trigger a keypress action. - this.props.keypadElement.setKeyHandler((key) => { + this.props.keypadElement?.setKeyHandler((key) => { const cursor = this.mathField.pressKey(key); // Trigger an `onChange` if the value in the input changed, and hide diff --git a/packages/perseus/src/keypad-context.ts b/packages/math-input/src/components/keypad-context.ts similarity index 95% rename from packages/perseus/src/keypad-context.ts rename to packages/math-input/src/components/keypad-context.ts index 822ddec956..f2309511ad 100644 --- a/packages/perseus/src/keypad-context.ts +++ b/packages/math-input/src/components/keypad-context.ts @@ -8,7 +8,7 @@ */ import * as React from "react"; -import type {RendererInterface} from "./types"; +import type {RendererInterface} from "@khanacademy/perseus-core"; type KeypadContext = { setKeypadElement: (keypadElement?: HTMLElement | null | undefined) => void; diff --git a/packages/math-input/src/components/keypad-legacy/index.ts b/packages/math-input/src/components/keypad-legacy/index.ts new file mode 100644 index 0000000000..50c772e159 --- /dev/null +++ b/packages/math-input/src/components/keypad-legacy/index.ts @@ -0,0 +1 @@ +export {default} from "./provided-keypad"; diff --git a/packages/math-input/src/components/keypad-legacy/provided-keypad.tsx b/packages/math-input/src/components/keypad-legacy/provided-keypad.tsx index 6fe32b2f13..9d8b8b5972 100644 --- a/packages/math-input/src/components/keypad-legacy/provided-keypad.tsx +++ b/packages/math-input/src/components/keypad-legacy/provided-keypad.tsx @@ -12,7 +12,12 @@ import { } from "./store/actions"; import {createStore} from "./store/index"; -import type {Cursor, KeypadConfiguration, KeyHandler} from "../../types"; +import type { + Cursor, + KeypadConfiguration, + KeyHandler, + KeypadAPI, +} from "../../types"; import type {StyleType} from "@khanacademy/wonder-blocks-core"; type Props = { @@ -21,7 +26,7 @@ type Props = { style?: StyleType; }; -class ProvidedKeypad extends React.Component { +class ProvidedKeypad extends React.Component implements KeypadAPI { store: any; constructor(props) { diff --git a/packages/math-input/src/components/keypad-legacy/two-page-keypad.tsx b/packages/math-input/src/components/keypad-legacy/two-page-keypad.tsx index aa3d1c41c2..92247c56a4 100644 --- a/packages/math-input/src/components/keypad-legacy/two-page-keypad.tsx +++ b/packages/math-input/src/components/keypad-legacy/two-page-keypad.tsx @@ -14,13 +14,14 @@ import { innerBorderWidthPx, offBlack16, } from "../common-style"; -import Tabbar from "../tabbar/tabbar"; -import {TabbarItemType} from "../tabbar/types"; +import Tabbar from "../tabbar"; import Keypad from "./keypad"; import {State as ReduxState} from "./store/types"; import Styles from "./styles"; +import type {TabbarItemType} from "../tabbar"; + const {column, row, fullWidth} = Styles; interface ReduxProps { diff --git a/packages/math-input/src/components/keypad-switch.tsx b/packages/math-input/src/components/keypad-switch.tsx new file mode 100644 index 0000000000..a78f884887 --- /dev/null +++ b/packages/math-input/src/components/keypad-switch.tsx @@ -0,0 +1,23 @@ +import {StyleType} from "@khanacademy/wonder-blocks-core"; +import * as React from "react"; + +import {MobileKeypad} from "./keypad"; +import LegacyKeypad from "./keypad-legacy"; + +type Props = { + onElementMounted?: (arg1: any) => void; + onDismiss?: () => void; + style?: StyleType; + + useV2Keypad?: boolean; +}; + +function KeypadSwitch(props: Props) { + const {useV2Keypad = false, ...rest} = props; + + const KeypadComponent = useV2Keypad ? MobileKeypad : LegacyKeypad; + + return ; +} + +export default KeypadSwitch; diff --git a/packages/math-input/src/components/keypad/index.tsx b/packages/math-input/src/components/keypad/index.tsx index 08c8f7cd7c..8ba6ba6454 100644 --- a/packages/math-input/src/components/keypad/index.tsx +++ b/packages/math-input/src/components/keypad/index.tsx @@ -1,173 +1,2 @@ -import Color from "@khanacademy/wonder-blocks-color"; -import {View} from "@khanacademy/wonder-blocks-core"; -import {StyleSheet} from "aphrodite"; -import * as React from "react"; -import {useEffect} from "react"; - -import Key from "../../data/keys"; -import {ClickKeyCallback} from "../../types"; -import {CursorContext} from "../input/cursor-contexts"; -import Tabbar from "../tabbar/tabbar"; -import {TabbarItemType} from "../tabbar/types"; - -import ExtrasPage from "./keypad-pages/extras-page"; -import GeometryPage from "./keypad-pages/geometry-page"; -import NumbersPage from "./keypad-pages/numbers-page"; -import OperatorsPage from "./keypad-pages/operators-page"; -import SharedKeys from "./shared-keys"; - -import type {AnalyticsEventHandlerFn} from "@khanacademy/perseus-core"; - -export type Props = { - extraKeys: ReadonlyArray; - cursorContext?: typeof CursorContext[keyof typeof CursorContext]; - showDismiss?: boolean; - - multiplicationDot?: boolean; - divisionKey?: boolean; - - trigonometry?: boolean; - preAlgebra?: boolean; - logarithms?: boolean; - basicRelations?: boolean; - advancedRelations?: boolean; - - onClickKey: ClickKeyCallback; - onAnalyticsEvent: AnalyticsEventHandlerFn; -}; - -const defaultProps = { - extraKeys: [], -}; - -function allPages(props: Props): ReadonlyArray { - const pages: Array = ["Numbers"]; - - if ( - // OperatorsButtonSets - props.preAlgebra || - props.logarithms || - props.basicRelations || - props.advancedRelations - ) { - pages.push("Operators"); - } - - if (props.trigonometry) { - pages.push("Geometry"); - } - - if (props.extraKeys?.length) { - pages.push("Extras"); - } - - return pages; -} - -// The main (v2) Keypad. Use this component to present an accessible, onscreen -// keypad to learners for entering math expressions. -export default function Keypad(props: Props) { - const [selectedPage, setSelectedPage] = - React.useState("Numbers"); - const [isMounted, setIsMounted] = React.useState(false); - - const availablePages = allPages(props); - - const { - onClickKey, - cursorContext, - extraKeys, - multiplicationDot, - divisionKey, - preAlgebra, - logarithms, - basicRelations, - advancedRelations, - showDismiss, - onAnalyticsEvent, - } = props; - - useEffect(() => { - if (!isMounted) { - onAnalyticsEvent({ - type: "math-input:keypad-opened", - payload: {virtualKeypadVersion: "MATH_INPUT_KEYPAD_V2"}, - }); - setIsMounted(true); - } - return () => { - if (isMounted) { - onAnalyticsEvent({ - type: "math-input:keypad-closed", - payload: {virtualKeypadVersion: "MATH_INPUT_KEYPAD_V2"}, - }); - setIsMounted(false); - } - }; - }, [onAnalyticsEvent, isMounted]); - - return ( - - { - setSelectedPage(tabbarItem); - }} - style={styles.tabbar} - onClickClose={ - showDismiss ? () => onClickKey("DISMISS") : undefined - } - /> - - - {selectedPage === "Numbers" && ( - - )} - {selectedPage === "Extras" && ( - - )} - {selectedPage === "Operators" && ( - - )} - {selectedPage === "Geometry" && ( - - )} - - - - ); -} - -Keypad.defaultProps = defaultProps; - -const styles = StyleSheet.create({ - tabbar: { - background: Color.white, - }, - grid: { - display: "grid", - gridTemplateColumns: "repeat(6, 1fr)", - gridTemplateRows: "repeat(4, 1fr)", - backgroundColor: "#DBDCDD", - maxHeight: 200, - maxWidth: 300, - }, -}); +export {default} from "./keypad"; +export {default as MobileKeypad} from "./mobile-keypad"; diff --git a/packages/math-input/src/components/keypad/keypad.stories.tsx b/packages/math-input/src/components/keypad/keypad.stories.tsx index 8d9fae5645..3b0b2eb99d 100644 --- a/packages/math-input/src/components/keypad/keypad.stories.tsx +++ b/packages/math-input/src/components/keypad/keypad.stories.tsx @@ -3,7 +3,7 @@ import {INITIAL_VIEWPORTS} from "@storybook/addon-viewport"; import {ComponentStory} from "@storybook/react"; import * as React from "react"; -import Keypad, {Props as KeypadProps} from "./index"; +import Keypad, {Props as KeypadProps} from "./keypad"; const opsPage = "Operators Page"; const numsPage = "Numbers Page"; diff --git a/packages/math-input/src/components/keypad/keypad.tsx b/packages/math-input/src/components/keypad/keypad.tsx new file mode 100644 index 0000000000..9e585948d6 --- /dev/null +++ b/packages/math-input/src/components/keypad/keypad.tsx @@ -0,0 +1,171 @@ +import Color from "@khanacademy/wonder-blocks-color"; +import {View} from "@khanacademy/wonder-blocks-core"; +import {StyleSheet} from "aphrodite"; +import * as React from "react"; +import {useEffect} from "react"; + +import Key from "../../data/keys"; +import {ClickKeyCallback} from "../../types"; +import {CursorContext} from "../input/cursor-contexts"; +import Tabbar from "../tabbar"; + +import ExtrasPage from "./keypad-pages/extras-page"; +import GeometryPage from "./keypad-pages/geometry-page"; +import NumbersPage from "./keypad-pages/numbers-page"; +import OperatorsPage from "./keypad-pages/operators-page"; +import SharedKeys from "./shared-keys"; + +import type {TabbarItemType} from "../tabbar"; +import type {AnalyticsEventHandlerFn} from "@khanacademy/perseus-core"; + +export type Props = { + extraKeys: ReadonlyArray; + cursorContext?: typeof CursorContext[keyof typeof CursorContext]; + showDismiss?: boolean; + + multiplicationDot?: boolean; + divisionKey?: boolean; + + trigonometry?: boolean; + preAlgebra?: boolean; + logarithms?: boolean; + basicRelations?: boolean; + advancedRelations?: boolean; + + onClickKey: ClickKeyCallback; + onAnalyticsEvent: AnalyticsEventHandlerFn; +}; + +const defaultProps = { + extraKeys: [], +}; + +function allPages(props: Props): ReadonlyArray { + const pages: Array = ["Numbers"]; + + if ( + // OperatorsButtonSets + props.preAlgebra || + props.logarithms || + props.basicRelations || + props.advancedRelations + ) { + pages.push("Operators"); + } + + if (props.trigonometry) { + pages.push("Geometry"); + } + + if (props.extraKeys?.length) { + pages.push("Extras"); + } + + return pages; +} + +// The main (v2) Keypad. Use this component to present an accessible, onscreen +// keypad to learners for entering math expressions. +export default function Keypad(props: Props) { + const [selectedPage, setSelectedPage] = + React.useState("Numbers"); + const [isMounted, setIsMounted] = React.useState(false); + + const availablePages = allPages(props); + + const { + onClickKey, + cursorContext, + extraKeys, + multiplicationDot, + divisionKey, + preAlgebra, + logarithms, + basicRelations, + advancedRelations, + showDismiss, + onAnalyticsEvent, + } = props; + + useEffect(() => { + if (!isMounted) { + onAnalyticsEvent?.({ + type: "math-input:keypad-opened", + payload: {virtualKeypadVersion: "MATH_INPUT_KEYPAD_V2"}, + }); + setIsMounted(true); + } + return () => { + if (isMounted) { + onAnalyticsEvent?.({ + type: "math-input:keypad-closed", + payload: {virtualKeypadVersion: "MATH_INPUT_KEYPAD_V2"}, + }); + setIsMounted(false); + } + }; + }, [onAnalyticsEvent, isMounted]); + + return ( + + { + setSelectedPage(tabbarItem); + }} + style={styles.tabbar} + onClickClose={ + showDismiss ? () => onClickKey("DISMISS") : undefined + } + /> + + + {selectedPage === "Numbers" && ( + + )} + {selectedPage === "Extras" && ( + + )} + {selectedPage === "Operators" && ( + + )} + {selectedPage === "Geometry" && ( + + )} + + + + ); +} + +Keypad.defaultProps = defaultProps; + +const styles = StyleSheet.create({ + tabbar: { + background: Color.white, + }, + grid: { + display: "grid", + gridTemplateColumns: "repeat(6, 1fr)", + gridTemplateRows: "repeat(4, 1fr)", + backgroundColor: "#DBDCDD", + }, +}); diff --git a/packages/math-input/src/components/keypad/mobile-keypad.tsx b/packages/math-input/src/components/keypad/mobile-keypad.tsx new file mode 100644 index 0000000000..367eeb5f83 --- /dev/null +++ b/packages/math-input/src/components/keypad/mobile-keypad.tsx @@ -0,0 +1,165 @@ +import {StyleType} from "@khanacademy/wonder-blocks-core"; +import {StyleSheet} from "aphrodite"; +import * as React from "react"; +import ReactDOM from "react-dom"; + +import Key from "../../data/keys"; +import {View} from "../../fake-react-native-web/index"; +import {Cursor, KeypadConfiguration, KeyHandler, KeypadAPI} from "../../types"; + +import Keypad from "./index"; + +/** + * This is the v2 equivalent of v1's ProvidedKeypad. It follows the same + * external API so that it can be hot-swapped with the v1 keypad and + * is responsible for connecting the keypad with MathInput and the Renderer. + * + * Ideally this strategy of attaching methods on the class component for + * other components to call will be replaced props/callbacks since React + * doesn't support this type of code anymore (functional components + * can't have methods attached to them). + */ + +type Props = { + onElementMounted?: (arg1: any) => void; + onDismiss?: () => void; + style?: StyleType; +}; + +type State = { + active: boolean; + keypadConfig?: KeypadConfiguration; + keyHandler?: KeyHandler; + cursor?: Cursor; +}; + +class MobileKeypad extends React.Component implements KeypadAPI { + hasMounted = false; + + state: State = { + active: false, + }; + + activate: () => void = () => { + this.setState({active: true}); + }; + + dismiss: () => void = () => { + this.setState({active: false}, () => { + this.props.onDismiss?.(); + }); + }; + + configure: (configuration: KeypadConfiguration, cb: () => void) => void = ( + configuration, + cb, + ) => { + this.setState({keypadConfig: configuration}); + + // TODO(matthewc)[LC-1080]: this was brought in from v1's ProvidedKeypad. + // We need to investigate whether we still need this. + // HACK(charlie): In Perseus, triggering a focus causes the keypad to + // animate into view and re-configure. We'd like to provide the option + // to re-render the re-configured keypad before animating it into view, + // to avoid jank in the animation. As such, we support passing a + // callback into `configureKeypad`. However, implementing this properly + // would require middleware, etc., so we just hack it on with + // `setTimeout` for now. + setTimeout(() => cb && cb()); + }; + + setCursor: (cursor: Cursor) => void = (cursor) => { + this.setState({cursor}); + }; + + setKeyHandler: (keyHandler: KeyHandler) => void = (keyHandler) => { + this.setState({keyHandler}); + }; + + getDOMNode: () => ReturnType = () => { + return ReactDOM.findDOMNode(this); + }; + + _handleClickKey(key: Key) { + if (key === "DISMISS") { + this.dismiss(); + return; + } + + const cursor = this.state.keyHandler?.(key); + this.setState({cursor}); + } + + render(): React.ReactNode { + const {active, cursor, keypadConfig} = this.state; + + const containerStyle = [ + styles.keypadContainer, + active ? styles.activeKeypadContainer : null, + ]; + + const isExpression = keypadConfig?.keypadType === "EXPRESSION"; + + return ( + { + if (!this.hasMounted && element) { + // TODO(matthewc)[LC-1081]: clean up this weird + // object and type the onElementMounted callback + // Append the dispatch methods that we want to expose + // externally to the returned React element. + const elementWithDispatchMethods = { + ...element, + activate: this.activate, + dismiss: this.dismiss, + configure: this.configure, + setCursor: this.setCursor, + setKeyHandler: this.setKeyHandler, + getDOMNode: this.getDOMNode, + } as const; + + this.hasMounted = true; + this.props.onElementMounted?.( + elementWithDispatchMethods, + ); + } + }} + > + {}} + extraKeys={keypadConfig?.extraKeys} + onClickKey={(key) => this._handleClickKey(key)} + cursorContext={cursor?.context} + multiplicationDot={isExpression} + divisionKey={isExpression} + trigonometry={isExpression} + preAlgebra={isExpression} + logarithms={isExpression} + basicRelations={isExpression} + advancedRelations={isExpression} + showDismiss + /> + + ); + } +} + +const styles = StyleSheet.create({ + keypadContainer: { + bottom: 0, + left: 0, + right: 0, + position: "fixed", + transition: `200ms ease-out`, + transitionProperty: "transform", + transform: "translate3d(0, 100%, 0)", + }, + + activeKeypadContainer: { + transform: "translate3d(0, 0, 0)", + }, +}); + +export default MobileKeypad; diff --git a/packages/math-input/src/components/keypad/shared-keys.tsx b/packages/math-input/src/components/keypad/shared-keys.tsx index 1c3f13a64c..a6a290ee95 100644 --- a/packages/math-input/src/components/keypad/shared-keys.tsx +++ b/packages/math-input/src/components/keypad/shared-keys.tsx @@ -3,7 +3,7 @@ import * as React from "react"; import Keys from "../../data/key-configs"; import {ClickKeyCallback} from "../../types"; import {CursorContext} from "../input/cursor-contexts"; -import {TabbarItemType} from "../tabbar/types"; +import {TabbarItemType} from "../tabbar"; import {KeypadButton} from "./keypad-button"; diff --git a/packages/math-input/src/components/prop-types.js b/packages/math-input/src/components/prop-types.js index fef00370eb..d466989b51 100644 --- a/packages/math-input/src/components/prop-types.js +++ b/packages/math-input/src/components/prop-types.js @@ -5,7 +5,6 @@ import PropTypes from "prop-types"; // NOTE(jared): This is no longer guaranteed to be React element -// NOTE(matthewc): only seems to be used in Perseus export const keypadElementPropType = PropTypes.shape({ activate: PropTypes.func.isRequired, dismiss: PropTypes.func.isRequired, diff --git a/packages/math-input/src/components/tabbar/index.ts b/packages/math-input/src/components/tabbar/index.ts new file mode 100644 index 0000000000..411171f4c4 --- /dev/null +++ b/packages/math-input/src/components/tabbar/index.ts @@ -0,0 +1,2 @@ +export {default} from "./tabbar"; +export {TabbarItemType} from "./types"; diff --git a/packages/math-input/src/full-math-input.stories.tsx b/packages/math-input/src/full-math-input.stories.tsx new file mode 100644 index 0000000000..245e52554d --- /dev/null +++ b/packages/math-input/src/full-math-input.stories.tsx @@ -0,0 +1,68 @@ +import * as React from "react"; + +import {KeypadAPI} from "./types"; + +import {KeypadInput, KeypadType, MobileKeypad} from "./index"; + +export default { + title: "Full Mobile MathInput", +}; + +export const Basic = () => { + const [value, setValue] = React.useState(""); + // Reference to the keypad + const [keypadElement, setKeypadElement] = React.useState(); + // Whether to use Expression or Fraction keypad + const [expression, setExpression] = React.useState(true); + // Whether to use v1 or v2 keypad + const [v2Keypad, setV2Keypad] = React.useState(true); + + React.useEffect(() => { + keypadElement?.configure( + { + keypadType: expression + ? KeypadType.EXPRESSION + : KeypadType.FRACTION, + extraKeys: expression ? ["x", "y", "PI", "THETA"] : [], + }, + () => {}, + ); + }, [keypadElement, expression]); + + return ( +
+
+ + +
+ + { + setValue(newValue); + callback(); + }} + onFocus={() => { + keypadElement?.activate(); + }} + onBlur={() => { + keypadElement?.dismiss(); + }} + /> + + { + if (node) { + setKeypadElement(node); + } + }} + useV2Keypad={v2Keypad} + /> +
+ ); +}; diff --git a/packages/math-input/src/index.ts b/packages/math-input/src/index.ts index 8499c41d41..93cd6e6c38 100644 --- a/packages/math-input/src/index.ts +++ b/packages/math-input/src/index.ts @@ -4,20 +4,41 @@ import "../less/main.less"; -export {CursorContext} from "./components/input/cursor-contexts"; +// MathInput input field (MathQuill wrapper) export {default as KeypadInput} from "./components/input/math-input"; + +export { + // Helper to create a MathQuill MathField + createMathField, + // Instance of the MathQuill library + mathQuillInstance, +} from "./components/input/mathquill-instance"; + +// MathQuill MathField type +export {type MathFieldInterface} from "./components/input/mathquill-types"; + +// Cursor context data: where in a forumla the cursor is in +// ex: in numerator, in parenthesis, in subscript +export {CursorContext} from "./components/input/cursor-contexts"; + +// Helper to get cursor context from MathField export {getCursorContext} from "./components/input/mathquill-helpers"; +// Wrapper around v1 and v2 mobile keypads to switch between them +export {default as MobileKeypad} from "./components/keypad-switch"; +// Unwrapped v2 keypad for desktop +export {default as DesktopKeypad} from "./components/keypad"; + +// Context used to pass data/refs around +export {default as KeypadContext} from "./components/keypad-context"; + +// External API of the "Provided" keypad component export {keypadElementPropType} from "./components/prop-types"; -export {default as LegacyKeypad} from "./components/keypad-legacy/provided-keypad"; -export {default as KeyConfigs} from "./data/key-configs"; + +// Key list, configuration map, and types export type {default as Keys} from "./data/keys"; +export {default as KeyConfigs} from "./data/key-configs"; export {type KeyType, KeypadType} from "./enums"; -export {default as Keypad} from "./components/keypad/index"; +// Helper to translate key pressed to MathField update export {default as keyTranslator} from "./components/key-handlers/key-translator"; -export { - createMathField, - mathQuillInstance, -} from "./components/input/mathquill-instance"; -export {type MathFieldInterface} from "./components/input/mathquill-types"; diff --git a/packages/math-input/src/math-input.stories.tsx b/packages/math-input/src/math-input.stories.tsx deleted file mode 100644 index 494ba9010f..0000000000 --- a/packages/math-input/src/math-input.stories.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import * as React from "react"; - -import {LegacyKeypad, KeypadInput, KeypadType} from "./index"; - -export default { - title: "Full MathInput", -}; - -export const Basic = () => { - const [value, setValue] = React.useState(""); - const [keypadElement, setKeypadElement] = React.useState(null); - const [keypadType, setKeypadType] = React.useState( - KeypadType.EXPRESSION, - ); - - React.useEffect(() => { - keypadElement?.configure({ - keypadType: keypadType, - extraKeys: ["x", "y", "PI", "THETA"], - }); - }, [keypadElement, keypadType]); - - function handleChangeKeypadType() { - setKeypadType( - keypadType === KeypadType.FRACTION - ? KeypadType.EXPRESSION - : KeypadType.FRACTION, - ); - } - - return ( -
-
- -
- - { - setValue(newValue); - callback(); - }} - onFocus={() => { - keypadElement?.activate(); - }} - onBlur={() => { - keypadElement?.dismiss(); - }} - /> - - { - if (node && !keypadElement) { - setKeypadElement(node); - } - }} - /> -
- ); -}; diff --git a/packages/math-input/src/types.ts b/packages/math-input/src/types.ts index f0192a85d8..1dff56ae7c 100644 --- a/packages/math-input/src/types.ts +++ b/packages/math-input/src/types.ts @@ -1,3 +1,5 @@ +import ReactDOM from "react-dom"; + import {CursorContext} from "./components/input/cursor-contexts"; import Key from "./data/keys"; import { @@ -85,3 +87,12 @@ export type ActiveNodesObj = { export type LayoutProps = {initialBounds: Bound}; export type ClickKeyCallback = (key: Key) => void; + +export interface KeypadAPI { + activate: () => void; + dismiss: () => void; + configure: (configuration: KeypadConfiguration, cb: () => void) => void; + setCursor: (cursor: Cursor) => void; + setKeyHandler: (keyHandler: KeyHandler) => void; + getDOMNode: () => ReturnType; +} diff --git a/packages/perseus-core/CHANGELOG.md b/packages/perseus-core/CHANGELOG.md index 44d084c566..aa4f81c8ea 100644 --- a/packages/perseus-core/CHANGELOG.md +++ b/packages/perseus-core/CHANGELOG.md @@ -1,5 +1,17 @@ # @khanacademy/perseus-core +## 0.1.1 + +### Patch Changes + +- 57f75510: Commented RendererInterface + +## 0.1.0 + +### Minor Changes + +- b4c06409: Add perseus-core to changeset + ## 0.0.2 ### Patch Changes diff --git a/packages/perseus-core/package.json b/packages/perseus-core/package.json index 593d91bbc2..89c0441417 100644 --- a/packages/perseus-core/package.json +++ b/packages/perseus-core/package.json @@ -3,7 +3,7 @@ "description": "Shared Perseus infrastructure", "author": "Khan Academy", "license": "MIT", - "version": "0.0.2", + "version": "0.1.1", "publishConfig": { "access": "public" }, diff --git a/packages/perseus-core/src/index.ts b/packages/perseus-core/src/index.ts index 79942bfbc7..39ba2ba455 100644 --- a/packages/perseus-core/src/index.ts +++ b/packages/perseus-core/src/index.ts @@ -1 +1,2 @@ export type {PerseusAnalyticsEvent, AnalyticsEventHandlerFn} from "./analytics"; +export type {KEScore, RendererInterface} from "./types"; diff --git a/packages/perseus-core/src/types.ts b/packages/perseus-core/src/types.ts new file mode 100644 index 0000000000..96604d256c --- /dev/null +++ b/packages/perseus-core/src/types.ts @@ -0,0 +1,26 @@ +// Types that can be shared between Perseus packages +// ideally without causing circular dependencies + +// TODO: this should be typed +type State = any; + +// Interfact currently only implemented by +// ServerItemRenderer and used by KeypadContext +// to pass around a renderer reference +export interface RendererInterface { + getSerializedState(): State; + restoreSerializedState(state: State, callback?: () => void): void; + scoreInput(): KEScore; + blur(): void; + focus(): boolean | null | undefined; + props: any; +} + +export type KEScore = { + empty: boolean; + correct: boolean; + message?: string | null | undefined; + suppressAlmostThere?: boolean | null | undefined; + guess: any; + state: any; +}; diff --git a/packages/perseus-editor/CHANGELOG.md b/packages/perseus-editor/CHANGELOG.md index 71e1c7c4b5..dc1f15f1e2 100644 --- a/packages/perseus-editor/CHANGELOG.md +++ b/packages/perseus-editor/CHANGELOG.md @@ -1,5 +1,47 @@ # @khanacademy/perseus-editor +## 2.4.1 + +### Patch Changes + +- @khanacademy/perseus@7.0.2 + +## 2.4.0 + +### Minor Changes + +- 97d02dac: Fixes a bug where images don't get dimensions in the image editor, and adds small preview to the image editor + +### Patch Changes + +- Updated dependencies [5611204a] + - @khanacademy/perseus@7.0.1 + +## 2.3.13 + +### Patch Changes + +- Updated dependencies [f9ee9d24] +- Updated dependencies [b18986d3] +- Updated dependencies [b18986d3] + - @khanacademy/perseus@7.0.0 + +## 2.3.12 + +### Patch Changes + +- Updated dependencies [a0c71567] + - @khanacademy/perseus@6.7.0 + +## 2.3.11 + +### Patch Changes + +- d0f28dbd: Add story for ExpressionEditor +- Updated dependencies [d0f28dbd] +- Updated dependencies [077b125e] + - @khanacademy/perseus@6.6.0 + ## 2.3.10 ### Patch Changes diff --git a/packages/perseus-editor/package.json b/packages/perseus-editor/package.json index 7c32912daf..fa270edff1 100644 --- a/packages/perseus-editor/package.json +++ b/packages/perseus-editor/package.json @@ -3,7 +3,7 @@ "description": "Perseus editors", "author": "Khan Academy", "license": "MIT", - "version": "2.3.10", + "version": "2.4.1", "publishConfig": { "access": "public" }, @@ -23,7 +23,7 @@ "dependencies": { "@khanacademy/kas": "^0.3.1", "@khanacademy/kmath": "^0.1.1", - "@khanacademy/perseus": "^6.5.1" + "@khanacademy/perseus": "^7.0.2" }, "devDependencies": { "@khanacademy/wonder-blocks-button": "^4.0.8", diff --git a/packages/perseus-editor/src/editor-page.tsx b/packages/perseus-editor/src/editor-page.tsx index 5ed2bde928..b55f544efd 100644 --- a/packages/perseus-editor/src/editor-page.tsx +++ b/packages/perseus-editor/src/editor-page.tsx @@ -14,10 +14,9 @@ import type { DeviceType, Hint, ImageUploader, - RendererInterface, Version, - KEScore, } from "@khanacademy/perseus"; +import type {RendererInterface, KEScore} from "@khanacademy/perseus-core"; const {HUD} = components; diff --git a/packages/perseus-editor/src/widgets/image-editor.tsx b/packages/perseus-editor/src/widgets/image-editor.tsx index f89cd43c84..e5c7571137 100644 --- a/packages/perseus-editor/src/widgets/image-editor.tsx +++ b/packages/perseus-editor/src/widgets/image-editor.tsx @@ -97,6 +97,27 @@ const ImageEditor: any = createReactClass({ const imageSettings = (
+
+ +
+
+ +
+