diff --git a/.changeset/metal-suits-cheat.md b/.changeset/metal-suits-cheat.md deleted file mode 100644 index a845151cc8..0000000000 --- a/.changeset/metal-suits-cheat.md +++ /dev/null @@ -1,2 +0,0 @@ ---- ---- diff --git a/.changeset/nasty-beans-hang.md b/.changeset/nasty-beans-hang.md deleted file mode 100644 index a845151cc8..0000000000 --- a/.changeset/nasty-beans-hang.md +++ /dev/null @@ -1,2 +0,0 @@ ---- ---- diff --git a/.changeset/new-flowers-study.md b/.changeset/new-flowers-study.md deleted file mode 100644 index 964f73ecf0..0000000000 --- a/.changeset/new-flowers-study.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@khanacademy/math-input": patch -"@khanacademy/perseus": patch ---- - -Add tests for mobile MathInput diff --git a/.changeset/few-teachers-fry.md b/.changeset/selfish-falcons-collect.md similarity index 100% rename from .changeset/few-teachers-fry.md rename to .changeset/selfish-falcons-collect.md diff --git a/.changeset/sour-cooks-invite.md b/.changeset/sour-cooks-invite.md new file mode 100644 index 0000000000..d2bd45aafa --- /dev/null +++ b/.changeset/sour-cooks-invite.md @@ -0,0 +1,5 @@ +--- +"perseus-build-settings": patch +--- + +Add build step to replace **lib_version** with each package's published package version diff --git a/.changeset/tiny-tomatoes-knock.md b/.changeset/tiny-tomatoes-knock.md new file mode 100644 index 0000000000..fe7dbeaa91 --- /dev/null +++ b/.changeset/tiny-tomatoes-knock.md @@ -0,0 +1,14 @@ +--- +"@khanacademy/kas": patch +"@khanacademy/kmath": patch +"@khanacademy/math-input": patch +"@khanacademy/perseus": patch +"@khanacademy/perseus-core": patch +"@khanacademy/perseus-editor": patch +"@khanacademy/perseus-error": patch +"@khanacademy/perseus-linter": patch +"@khanacademy/pure-markdown": patch +"@khanacademy/simple-markdown": patch +--- + +Print package name and version when loaded in the page diff --git a/.changeset/yellow-needles-cover.md b/.changeset/yellow-needles-cover.md deleted file mode 100644 index a845151cc8..0000000000 --- a/.changeset/yellow-needles-cover.md +++ /dev/null @@ -1,2 +0,0 @@ ---- ---- diff --git a/.codecov.yml b/.codecov.yml index 14180321c5..272ef57fb6 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -3,20 +3,12 @@ coverage: round: down range: "70...100" - status: - project: - default: on - patch: - default: off - changes: - default: off - comment: layout: "diff, reach, files, footer" behavior: default - require_changes: no - require_base: no - require_head: yes + require_changes: false + require_base: false + require_head: true # https://docs.codecov.com/docs/ignoring-paths ignore: diff --git a/.github/workflows/node-ci.yml b/.github/workflows/node-ci.yml index 1d341d0122..245412abab 100644 --- a/.github/workflows/node-ci.yml +++ b/.github/workflows/node-ci.yml @@ -31,8 +31,10 @@ jobs: id: match with: changed-files: ${{ steps.changed.outputs.files }} - files: packages/ # Only look for changes in packages - globs: "!(**/__tests__/*), !(**/__stories__/*), !(**/dist/*)" # Ignore test files + files: "packages/, config/build/" # Only look for changes in packages, build + globs: "!(**/__tests__/*), !(**/__testdata__/*), !(**/__stories__/*), !(**/dist/*)" # Ignore test files + matchAllGlobs: true # Default is to match any of the globs, which ends up matching all files + conjunctive: true # Only match files that match all of the above - name: Verify changeset entries uses: Khan/changeset-per-package@v1.0.1 @@ -136,6 +138,14 @@ jobs: - name: Run test with coverage run: yarn cypress:ci + # Upload coverage report as an GitHub artifact so that it can be used + # later in upload_coverage. + - name: Upload Artifact + uses: actions/upload-artifact@v2 + with: + name: cypress-coverage + path: ./.nyc_output/out.json + coverage: name: Jest Coverage runs-on: ${{ matrix.os }} @@ -154,11 +164,50 @@ jobs: - name: Jest with coverage run: yarn coverage + # Upload coverage report as an GitHub artifact so that it can be used + # later in upload_coverage. + - name: Upload Artifact + uses: actions/upload-artifact@v2 + with: + name: jest-coverage + path: ./coverage/coverage-final.json + + upload_coverage: + name: Upload Coverage + runs-on: ubuntu-latest + needs: [cypress, coverage] + steps: + - uses: actions/checkout@v2 + + - name: Use Node.js ${{ matrix.node-version }} & Install & cache node_modules + uses: Khan/actions@shared-node-cache-v0 + with: + node-version: ${{ matrix.node-version }} + + - name: Download Jest Coverage + uses: actions/download-artifact@v2 + with: + name: jest-coverage + # path to decompress the artifact into, decompressed file + # will be ./coverage-final.json + path: ./ + + - name: Download Cypress Coverage + uses: actions/download-artifact@v2 + with: + name: cypress-coverage + # path to decompress the artifact into, decompressed file + # will be ./out.json + path: ./ + + # Upload both coverage files at once. This avoids issues where it Codecov + # shows the results from only one of the reports which would make it appear + # as though coverage dropped a lot. - name: Upload Coverage uses: codecov/codecov-action@v3 with: token: ${{ secrets.CODECOV_TOKEN }} - files: ./coverage/coverage-final.json + files: ./coverage-final.json,./out.json check_builds: name: Check builds for changes in size diff --git a/.nycrc.json b/.nycrc.json new file mode 100644 index 0000000000..8eaba5538e --- /dev/null +++ b/.nycrc.json @@ -0,0 +1,14 @@ +{ + "all": true, + "include": [ + "packages/*/src/**/*.js", + "packages/*/src/**/*.jsx", + "packages/*/src/**/*.ts", + "packages/*/src/**/*.tsx" + ], + "exclude": [ + "**/*.stories.tsx", + "**/*.test.ts", + "**/*.test.tsx" + ] +} \ No newline at end of file diff --git a/.storybook/main.js b/.storybook/main.js index abd144466e..cbead592b6 100644 --- a/.storybook/main.js +++ b/.storybook/main.js @@ -1,7 +1,7 @@ const babelConfig = require("../babel.config"); -const util = require("util"); const path = require("path"); const fs = require("fs"); +const glob = require("fast-glob"); module.exports = { core: { @@ -49,23 +49,17 @@ module.exports = { }); const aliases = {}; - fs.readdirSync(path.join(__dirname, "../packages")).forEach((name) => { - if (name.startsWith(".")) { - return; - } - const stat = fs.statSync(path.join(__dirname, "../packages", name)); - if (stat.isFile()) { - return; - } - const pkgPath = path.join("../packages", name, "package.json"); - const pkgJson = require(pkgPath); - aliases["@khanacademy/" + name] = path.join( - __dirname, - "../packages", - name, - pkgJson.source, - ); - }); + glob.sync(path.join(__dirname, "../packages/*/package.json")).forEach( + (pkgPath) => { + const pkgJson = require(pkgPath); + aliases[pkgJson.name] = path.join( + __dirname, + "../packages", + path.basename(path.dirname(pkgPath)), + pkgJson.source, + ); + }, + ); fs.readdirSync(path.join(__dirname, "../vendor")).forEach((name) => { aliases[name] = path.join(__dirname, "../vendor", name); }); diff --git a/config/build/rollup.config.js b/config/build/rollup.config.js index 281a586eb8..1e18fe14aa 100644 --- a/config/build/rollup.config.js +++ b/config/build/rollup.config.js @@ -35,7 +35,7 @@ const rootDir = ancesdir(__dirname); */ // Kahn's algorithm -// https://en.wikipedia.org/wiki/Topological_sorting#Kahn's_algorithm +// https://en.wikipedia.org/wiki/Topological_sorting#Kahn%27s_algorithm const topoSort = (yarnWorkspacesOutput) => { const sorted = []; // the keys are depended on by the values @@ -160,7 +160,7 @@ const getFormats = ({configFormats}) => */ const createConfig = ( commandLineArgs, - {name, format, platform, inputFile, file, plugins}, + {name, fullName, version, format, platform, inputFile, file, plugins}, ) => { const valueReplacementMappings = { __IS_BROWSER__: platform === "browser", @@ -188,6 +188,16 @@ const createConfig = ( preventAssignment: true, values: valueReplacementMappings, }), + // This replace() plugin instance injects the current package + // version and name into the output bundle. This provides useful + // runtime information anywhere that Perseus is used. + replace({ + preventAssignment: true, + include: [makePackageBasedPath(name, "src/version.ts")], + values: { + __lib_version__: version, + }, + }), alias({ entries: { hubble: path.join(rootDir, "vendor", "hubble"), @@ -266,6 +276,8 @@ const getPackageInfo = (commandLineArgs, pkgName) => { if (formats.has("cjs")) { configs.push({ name: pkgName, + fullName: pkgJson.name, + version: pkgJson.version, format: "cjs", platform: "browser", file: pkgJson.main, @@ -275,6 +287,8 @@ const getPackageInfo = (commandLineArgs, pkgName) => { if (formats.has("esm")) { configs.push({ name: pkgName, + fullName: pkgJson.name, + version: pkgJson.version, format: "esm", platform: "browser", file: pkgJson.module, diff --git a/package.json b/package.json index e180f40f0d..afef9f4c3b 100644 --- a/package.json +++ b/package.json @@ -124,10 +124,11 @@ }, "scripts": { "gen:parsers": "yarn --cwd packages/kas gen:parsers", - "build": "yarn gen:parsers && rollup -c config/build/rollup.config.js", + "prebuild": "yarn gen:parsers", + "build": "rollup -c config/build/rollup.config.js", "build:types": "yarn tsc --build tsconfig-build.json", - "build:prodsizecheck": "yarn gen:parsers && rollup -c config/build/rollup.config.js --configEnvironment='production'", - "watch": "rollup -c config/build/rollup.config.js --watch", + "build:prodsizecheck": "yarn build --configEnvironment='production'", + "watch": "yarn build --watch", "clean": "rm -rf packages/*/dist && rm -rf packages/*/node_modules && rm -rf .nyc_output && rm -f packages/*/*.tsbuildinfo", "coverage": "cross-env NODE_OPTIONS=--max_old_space_size=8192 yarn run jest --coverage", "coverage:types": "cross-env NODE_OPTIONS=--max_old_space_size=8192 typescript-coverage-report", diff --git a/packages/kas/package.json b/packages/kas/package.json index f6fefe595a..67a011599e 100644 --- a/packages/kas/package.json +++ b/packages/kas/package.json @@ -22,7 +22,9 @@ "gen:parsers": "node src/parser-generator.js", "test": "bash -c 'yarn --silent --cwd \"../..\" test ${@:0} $($([[ ${@: -1} = -* ]] || [[ ${@: -1} = bash ]]) && echo $PWD)'" }, - "dependencies": {}, + "dependencies": { + "@khanacademy/perseus-core": "1.1.0" + }, "devDependencies": { "jison": "0.4.15", "perseus-build-settings": "^0.2.0", diff --git a/packages/kas/src/index.js b/packages/kas/src/index.js index b5738e13ed..a89dfbddf8 100644 --- a/packages/kas/src/index.js +++ b/packages/kas/src/index.js @@ -1,2 +1,4 @@ +export {libVersion} from "./version"; + export * from "./nodes"; export {compare} from "./compare"; diff --git a/packages/kas/src/version.ts b/packages/kas/src/version.ts new file mode 100644 index 0000000000..c8b2bf4fd2 --- /dev/null +++ b/packages/kas/src/version.ts @@ -0,0 +1,10 @@ +// This file is processed by a Rollup plugin (replace) to inject the production +// version number during the release build. +// In dev, you'll never see the version number. + +import {addLibraryVersionToPerseusDebug} from "@khanacademy/perseus-core"; + +const libName = "@khanacademy/kas"; +export const libVersion = "__lib_version__"; + +addLibraryVersionToPerseusDebug(libName, libVersion); diff --git a/packages/kas/tsconfig-build.json b/packages/kas/tsconfig-build.json index 6feca45c00..5f2f33e952 100644 --- a/packages/kas/tsconfig-build.json +++ b/packages/kas/tsconfig-build.json @@ -4,5 +4,7 @@ "outDir": "./dist", "rootDir": "src" }, - "references": [] + "references": [ + {"path": "../perseus-core/tsconfig-build.json"} + ] } diff --git a/packages/kmath/package.json b/packages/kmath/package.json index 0c98d04480..50697dcc5c 100644 --- a/packages/kmath/package.json +++ b/packages/kmath/package.json @@ -20,7 +20,9 @@ "scripts": { "test": "bash -c 'yarn --silent --cwd \"../..\" test ${@:0} $($([[ ${@: -1} = -* ]] || [[ ${@: -1} = bash ]]) && echo $PWD)'" }, - "dependencies": {}, + "dependencies": { + "@khanacademy/perseus-core": "1.1.0" + }, "devDependencies": { "perseus-build-settings": "^0.2.0", "underscore": "1.4.4" diff --git a/packages/kmath/src/index.ts b/packages/kmath/src/index.ts index 9cd3960a16..528a142d7f 100644 --- a/packages/kmath/src/index.ts +++ b/packages/kmath/src/index.ts @@ -1,3 +1,5 @@ +export {libVersion} from "./version"; + export * as number from "./number"; export * as vector from "./vector"; export * as point from "./point"; diff --git a/packages/kmath/src/version.ts b/packages/kmath/src/version.ts new file mode 100644 index 0000000000..365038c5f4 --- /dev/null +++ b/packages/kmath/src/version.ts @@ -0,0 +1,10 @@ +// This file is processed by a Rollup plugin (replace) to inject the production +// version number during the release build. +// In dev, you'll never see the version number. + +import {addLibraryVersionToPerseusDebug} from "@khanacademy/perseus-core"; + +const libName = "@khanacademy/kmath"; +export const libVersion = "__lib_version__"; + +addLibraryVersionToPerseusDebug(libName, libVersion); diff --git a/packages/kmath/tsconfig-build.json b/packages/kmath/tsconfig-build.json index 5e7f4bce86..e06eddef57 100644 --- a/packages/kmath/tsconfig-build.json +++ b/packages/kmath/tsconfig-build.json @@ -4,5 +4,7 @@ "outDir": "./dist", "rootDir": "src", }, - "references": [] + "references": [ + {"path": "../perseus-core/tsconfig-build.json"} + ] } diff --git a/packages/math-input/CHANGELOG.md b/packages/math-input/CHANGELOG.md index 8ef98fdd98..0581497d0a 100644 --- a/packages/math-input/CHANGELOG.md +++ b/packages/math-input/CHANGELOG.md @@ -1,5 +1,23 @@ # @khanacademy/math-input +## 14.0.0 + +### Major Changes + +- 14138bb0: Move StatefulKeypadContextProvider into math-input +- 14138bb0: Hoist keypad active state into keypad context + +## 13.1.0 + +### Minor Changes + +- 3b19a1bf: Ensured that we're properly calling componentWillUnmount + +### Patch Changes + +- 7e2ae0ef: Bugfix for fraction button in v2 fraction keypad +- 1dc460c7: Add tests for mobile MathInput + ## 13.0.0 ### Major Changes diff --git a/packages/math-input/package.json b/packages/math-input/package.json index 7ab361785b..35e57141fb 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": "13.0.0", + "version": "14.0.0", "publishConfig": { "access": "public" }, diff --git a/packages/math-input/src/components/__tests__/integration.test.tsx b/packages/math-input/src/components/__tests__/integration.test.tsx index 856cdaa783..a5f7914201 100644 --- a/packages/math-input/src/components/__tests__/integration.test.tsx +++ b/packages/math-input/src/components/__tests__/integration.test.tsx @@ -1,18 +1,29 @@ import "@testing-library/jest-dom"; -import {screen, render, fireEvent, within} from "@testing-library/react"; +import { + screen, + render, + fireEvent, + within, + waitFor, +} from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import MathQuill from "mathquill"; import React, {useState} from "react"; +import {KeypadType} from "../../enums"; import MathInput from "../input/math-input"; -import KeypadContext from "../keypad-context"; +import {KeypadContext, StatefulKeypadContextProvider} from "../keypad-context"; import KeypadSwitch from "../keypad-switch"; -import type {KeypadAPI} from "../../types"; +import type {KeypadConfiguration} from "../../types"; const MQ = MathQuill.getInterface(2); -function InputWithContext() { +const defaultConfiguration: KeypadConfiguration = { + keypadType: KeypadType.FRACTION, +}; + +function InputWithContext({keypadConfiguration}) { const [value, setValue] = useState(""); return ( @@ -27,7 +38,12 @@ function InputWithContext() { cb(); }} onFocus={() => { - keypadElement?.activate(); + keypadElement?.configure( + keypadConfiguration, + () => { + keypadElement?.activate(); + }, + ); }} onBlur={() => { keypadElement?.dismiss(); @@ -56,26 +72,12 @@ function KeypadWithContext() { ); } -function ConnectedMathInput() { - const [keypadElement, setKeypadElement] = useState(); - const [renderer, setRenderer] = useState(null); - const [scrollableElement, setScrollableElement] = - useState(); - +function ConnectedMathInput({keypadConfiguration = defaultConfiguration}) { return ( - - + + - + ); } @@ -107,10 +109,14 @@ describe("math input integration", () => { fireEvent.touchStart(input); + await waitFor(() => { + expect(screen.getByRole("button", {name: "4"})).toBeVisible(); + }); + expect(screen.getByRole("button", {name: "1"})).toBeVisible(); }); - it("updates input when using keypad", () => { + it("updates input when using keypad", async () => { render(); const input = screen.getByLabelText( @@ -118,6 +124,11 @@ describe("math input integration", () => { ); fireEvent.touchStart(input); + + await waitFor(() => { + expect(screen.getByRole("button", {name: "4"})).toBeVisible(); + }); + userEvent.click(screen.getByRole("button", {name: "1"})); // MathQuill is problematic, @@ -130,7 +141,7 @@ describe("math input integration", () => { expect(span1).toBeVisible(); }); - it("updates input when pressing many numbers", () => { + it("updates input when pressing many numbers", async () => { render(); const input = screen.getByLabelText( @@ -139,6 +150,10 @@ describe("math input integration", () => { fireEvent.touchStart(input); + await waitFor(() => { + expect(screen.getByRole("button", {name: "4"})).toBeVisible(); + }); + const testNumbers = [8, 6, 7, 5, 3, 0, 9]; testNumbers.forEach((num) => { userEvent.click(screen.getByRole("button", {name: `${num}`})); @@ -153,7 +168,7 @@ describe("math input integration", () => { expect(mathquillInstance.latex()).toBe("8675309"); }); - it("can handle symbols", () => { + it("can handle symbols", async () => { render(); const input = screen.getByLabelText( @@ -162,6 +177,10 @@ describe("math input integration", () => { fireEvent.touchStart(input); + await waitFor(() => { + expect(screen.getByRole("button", {name: "4"})).toBeVisible(); + }); + userEvent.click(screen.getByRole("button", {name: "4"})); userEvent.click(screen.getByRole("button", {name: "2"})); userEvent.click(screen.getByRole("button", {name: "Percent"})); @@ -174,4 +193,81 @@ describe("math input integration", () => { expect(mathquillInstance.latex()).toBe("42\\%"); }); + + it("handles fractions correctly in expression", async () => { + const keypadConfiguration = { + keypadType: KeypadType.EXPRESSION, + }; + render( + , + ); + + const input = screen.getByLabelText( + "Math input box Tap with one or two fingers to open keyboard", + ); + + fireEvent.touchStart(input); + + await waitFor(() => { + expect(screen.getByRole("button", {name: "4"})).toBeVisible(); + }); + + userEvent.click(screen.getByRole("button", {name: "1"})); + userEvent.click( + screen.getByRole("button", { + name: "Fraction, excluding the current expression", + }), + ); + userEvent.click(screen.getByRole("button", {name: "4"})); + userEvent.click( + screen.getByRole("button", { + name: "Navigate right out of the numerator and into the denominator", + }), + ); + userEvent.click(screen.getByRole("button", {name: "2"})); + + // MathQuill is problematic, + // this is how to get the value of the input directly from MQ + const mathquillInstance = + // eslint-disable-next-line testing-library/no-node-access + MQ(document.getElementsByClassName("mq-editable-field")[0]); + + expect(mathquillInstance.latex()).toBe("1\\frac{4}{2}"); + }); + + it("handles fractions correctly in fraction", async () => { + render(); + + const input = screen.getByLabelText( + "Math input box Tap with one or two fingers to open keyboard", + ); + + fireEvent.touchStart(input); + + await waitFor(() => { + expect(screen.getByRole("button", {name: "4"})).toBeVisible(); + }); + + userEvent.click(screen.getByRole("button", {name: "1"})); + userEvent.click( + screen.getByRole("button", { + name: "Fraction, excluding the current expression", + }), + ); + userEvent.click(screen.getByRole("button", {name: "4"})); + userEvent.click( + screen.getByRole("button", { + name: "Navigate right out of the numerator and into the denominator", + }), + ); + userEvent.click(screen.getByRole("button", {name: "2"})); + + // MathQuill is problematic, + // this is how to get the value of the input directly from MQ + const mathquillInstance = + // eslint-disable-next-line testing-library/no-node-access + MQ(document.getElementsByClassName("mq-editable-field")[0]); + + expect(mathquillInstance.latex()).toBe("1\\frac{4}{2}"); + }); }); diff --git a/packages/math-input/src/components/keypad-context.ts b/packages/math-input/src/components/keypad-context.ts deleted file mode 100644 index c2d7282cbd..0000000000 --- a/packages/math-input/src/components/keypad-context.ts +++ /dev/null @@ -1,37 +0,0 @@ -/** - * KeypadContext provides a way to the Keypad and (Server)ItemRenderer to - * communicate. - * - * The KeypadContext.Provider wraps the ExerciseFooter while KeypadContext.Consumer - * wraps each (Server)ItemRenderer render site and the Keypad rendered in the - * ExerciseFooter. - */ -import * as React from "react"; - -import type {KeypadAPI} from "../types"; -import type {KeypadContextRendererInterface} from "@khanacademy/perseus-core"; - -type KeypadContext = { - setKeypadElement: (keypadElement?: KeypadAPI) => void; - keypadElement: KeypadAPI | null | undefined; - setRenderer: ( - renderer?: KeypadContextRendererInterface | null | undefined, - ) => void; - renderer: KeypadContextRendererInterface | null | undefined; - setScrollableElement: ( - scrollableElement?: HTMLElement | null | undefined, - ) => void; - scrollableElement: HTMLElement | null | undefined; -}; - -// @ts-expect-error - TS2322 - Type 'Context<{ setKeypadElement: (keypadElement: HTMLElement | null | undefined) => void; keypadElement: null; setRenderer: (renderer: RendererInterface | null | undefined) => void; renderer: null; setScrollableElement: (scrollableElement: HTMLElement | ... 1 more ... | undefined) => void; scrollableElement: null; }>' is not assignable to type 'Context'. -const context: React.Context = React.createContext({ - setKeypadElement: (keypadElement) => {}, - keypadElement: null, - setRenderer: (renderer) => {}, - renderer: null, - setScrollableElement: (scrollableElement) => {}, - scrollableElement: null, -}); - -export default context; diff --git a/packages/math-input/src/components/keypad-context.tsx b/packages/math-input/src/components/keypad-context.tsx new file mode 100644 index 0000000000..28519d5a33 --- /dev/null +++ b/packages/math-input/src/components/keypad-context.tsx @@ -0,0 +1,61 @@ +/** + * KeypadContext provides a way to the Keypad and Perseus Renderers to + * communicate. + * + * The StatefulKeypadContextProvider wraps the application + * while KeypadContext.Consumer wraps things that need this state: + * - mobile keypad usages + * - Perseus Renderers (Server/Item/Article) + */ +import * as React from "react"; +import {useState} from "react"; + +import type {KeypadAPI, KeypadContextType} from "../types"; +import type {KeypadContextRendererInterface} from "@khanacademy/perseus-core"; + +// @ts-expect-error - TS2322 - Type 'Context<{ setKeypadElement: (keypadElement: HTMLElement | null | undefined) => void; keypadElement: null; setRenderer: (renderer: RendererInterface | null | undefined) => void; renderer: null; setScrollableElement: (scrollableElement: HTMLElement | ... 1 more ... | undefined) => void; scrollableElement: null; }>' is not assignable to type 'Context'. +export const KeypadContext: React.Context = + React.createContext({ + setKeypadActive: (keypadActive) => {}, + keypadActive: false, + setKeypadElement: (keypadElement) => {}, + keypadElement: null, + setRenderer: (renderer) => {}, + renderer: null, + setScrollableElement: (scrollableElement) => {}, + scrollableElement: null, + }); + +type Props = React.PropsWithChildren; + +export function StatefulKeypadContextProvider(props: Props) { + // whether or not to display the keypad + const [keypadActive, setKeypadActive] = useState(false); + // used to communicate between the keypad and the Renderer + const [keypadElement, setKeypadElement] = useState(); + // this is a KeypadContextRendererInterface from Perseus + const [renderer, setRenderer] = + useState(); + const [scrollableElement, setScrollableElement] = + useState(); + + return ( + + {props.children} + + ); +} 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 752d6d0ae5..94dfe821a4 100644 --- a/packages/math-input/src/components/keypad-legacy/provided-keypad.tsx +++ b/packages/math-input/src/components/keypad-legacy/provided-keypad.tsx @@ -22,6 +22,8 @@ import type {AnalyticsEventHandlerFn} from "@khanacademy/perseus-core"; import type {StyleType} from "@khanacademy/wonder-blocks-core"; type Props = { + setKeypadActive: (keypadActive: boolean) => void; + keypadActive: boolean; onElementMounted?: (arg1: any) => void; onDismiss?: () => void; style?: StyleType; @@ -37,12 +39,22 @@ class ProvidedKeypad extends React.Component implements KeypadAPI { this.store = createStore(); } + componentDidUpdate(prevProps) { + if (this.props.keypadActive && !prevProps.keypadActive) { + this.store.dispatch(activateKeypad()); + } + + if (!this.props.keypadActive && prevProps.keypadActive) { + this.store.dispatch(dismissKeypad()); + } + } + activate: () => void = () => { - this.store.dispatch(activateKeypad()); + this.props.setKeypadActive(true); }; dismiss: () => void = () => { - this.store.dispatch(dismissKeypad()); + this.props.setKeypadActive(false); }; configure: (configuration: KeypadConfiguration, cb: () => void) => void = ( diff --git a/packages/math-input/src/components/keypad-switch.tsx b/packages/math-input/src/components/keypad-switch.tsx index b38a92eeb7..a1ead10df1 100644 --- a/packages/math-input/src/components/keypad-switch.tsx +++ b/packages/math-input/src/components/keypad-switch.tsx @@ -1,6 +1,7 @@ import * as React from "react"; import {MobileKeypad} from "./keypad"; +import {KeypadContext} from "./keypad-context"; import LegacyKeypad from "./keypad-legacy"; import type {AnalyticsEventHandlerFn} from "@khanacademy/perseus-core"; @@ -23,7 +24,19 @@ function KeypadSwitch(props: Props) { // Note: Although we pass the "onAnalyticsEvent" to both keypad components, // only the current one uses it. There's no point in instrumenting the // legacy keypad given that it's on its way out the door. - return ; + return ( + + {({setKeypadActive, keypadActive}) => { + return ( + + ); + }} + + ); } export default KeypadSwitch; diff --git a/packages/math-input/src/components/keypad/keypad-pages/fractions-page.tsx b/packages/math-input/src/components/keypad/keypad-pages/fractions-page.tsx index eec3afd122..d818ceec5f 100644 --- a/packages/math-input/src/components/keypad/keypad-pages/fractions-page.tsx +++ b/packages/math-input/src/components/keypad/keypad-pages/fractions-page.tsx @@ -101,7 +101,7 @@ export default function FractionsPage(props: Props) { secondary /> void; style?: StyleType; onAnalyticsEvent: AnalyticsEventHandlerFn; + setKeypadActive: (keypadActive: boolean) => void; + keypadActive: boolean; }; type State = { - active: boolean; containerWidth: number; hasBeenActivated: boolean; keypadConfig?: KeypadConfiguration; @@ -52,7 +52,6 @@ class MobileKeypad extends React.Component implements KeypadAPI { hasMounted = false; state: State = { - active: false, containerWidth: 0, hasBeenActivated: false, }; @@ -81,7 +80,7 @@ class MobileKeypad extends React.Component implements KeypadAPI { } } - componentWillUnMount() { + componentWillUnmount() { window.removeEventListener("resize", this._throttleResizeHandler); window.removeEventListener( "orientationchange", @@ -109,16 +108,15 @@ class MobileKeypad extends React.Component implements KeypadAPI { }; activate: () => void = () => { + this.props.setKeypadActive(true); this.setState({ - active: true, hasBeenActivated: true, }); }; dismiss: () => void = () => { - this.setState({active: false}, () => { - this.props.onDismiss?.(); - }); + this.props.setKeypadActive(false); + this.props.onDismiss?.(); }; configure: (configuration: KeypadConfiguration, cb: () => void) => void = ( @@ -162,14 +160,14 @@ class MobileKeypad extends React.Component implements KeypadAPI { } render(): React.ReactNode { - const {style} = this.props; - const {active, hasBeenActivated, containerWidth, cursor, keypadConfig} = + const {keypadActive, style} = this.props; + const {hasBeenActivated, containerWidth, cursor, keypadConfig} = this.state; const containerStyle = [ // internal styles styles.keypadContainer, - active && styles.activeKeypadContainer, + keypadActive && styles.activeKeypadContainer, // styles passed as props ...(Array.isArray(style) ? style : [style]), ]; @@ -179,7 +177,7 @@ class MobileKeypad extends React.Component implements KeypadAPI { // during the initial render. // Done inline (dynamicStyle) since stylesheets might not be loaded yet. let dynamicStyle = {}; - if (!active && !hasBeenActivated) { + if (!keypadActive && !hasBeenActivated) { dynamicStyle = {visibility: "hidden"}; } diff --git a/packages/math-input/src/full-mobile-input.stories.tsx b/packages/math-input/src/full-mobile-input.stories.tsx index 9f1aff494c..ab843d08eb 100644 --- a/packages/math-input/src/full-mobile-input.stories.tsx +++ b/packages/math-input/src/full-mobile-input.stories.tsx @@ -1,9 +1,13 @@ import {action} from "@storybook/addon-actions"; import * as React from "react"; -import type {KeypadAPI} from "./types"; - -import {KeypadInput, KeypadType, MobileKeypad} from "./index"; +import { + KeypadInput, + KeypadType, + MobileKeypad, + StatefulKeypadContextProvider, + KeypadContext, +} from "./index"; export default { title: "math-input/Full Mobile MathInput", @@ -19,32 +23,19 @@ export default { }, }; -export const Basic = () => { +const Basic = ({keypadElement, setKeypadElement}) => { 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(false); // Whether to use CDOT or TIMES const [times, setTimes] = React.useState(true); // Whether to use v1 or v2 keypad const [v2Keypad, setV2Keypad] = React.useState(true); - // Whether the keypad is open or not - const [keypadOpen, setKeypadOpen] = React.useState(false); const input = React.useRef(null); const timesLabel = times ? "CDOT" : "TIMES"; - const toggleKeypad = () => { - if (keypadOpen) { - keypadElement?.dismiss(); - } else { - keypadElement?.activate(); - } - setKeypadOpen(!keypadOpen); - }; - React.useEffect(() => { keypadElement?.configure( { @@ -73,9 +64,6 @@ export const Basic = () => { - @@ -111,3 +99,18 @@ export const Basic = () => { ); }; + +export function Wrapped() { + return ( + + + {({keypadElement, setKeypadElement}) => ( + + )} + + + ); +} diff --git a/packages/math-input/src/index.ts b/packages/math-input/src/index.ts index 864e558696..d566b07a0a 100644 --- a/packages/math-input/src/index.ts +++ b/packages/math-input/src/index.ts @@ -4,6 +4,8 @@ import "../less/main.less"; +export {libVersion} from "./version"; + // MathInput input field (MathQuill wrapper) export {default as KeypadInput} from "./components/input/math-input"; @@ -30,7 +32,10 @@ export {default as MobileKeypad} from "./components/keypad-switch"; export {default as DesktopKeypad} from "./components/keypad"; // Context used to pass data/refs around -export {default as KeypadContext} from "./components/keypad-context"; +export { + KeypadContext, + StatefulKeypadContextProvider, +} from "./components/keypad-context"; // External API of the "Provided" keypad component export {keypadElementPropType} from "./components/prop-types"; diff --git a/packages/math-input/src/types.ts b/packages/math-input/src/types.ts index 0c051d2905..83c947b9e0 100644 --- a/packages/math-input/src/types.ts +++ b/packages/math-input/src/types.ts @@ -7,6 +7,7 @@ import type { KeyType, KeypadType, } from "./enums"; +import type {KeypadContextRendererInterface} from "@khanacademy/perseus-core"; import type * as React from "react"; import type ReactDOM from "react-dom"; @@ -105,3 +106,18 @@ export interface KeypadAPI { setKeyHandler: (keyHandler: KeyHandler) => void; getDOMNode: () => ReturnType; } + +export type KeypadContextType = { + setKeypadActive: (keypadActive: boolean) => void; + keypadActive: boolean; + setKeypadElement: (keypadElement?: KeypadAPI) => void; + keypadElement: KeypadAPI | null | undefined; + setRenderer: ( + renderer?: KeypadContextRendererInterface | null | undefined, + ) => void; + renderer: KeypadContextRendererInterface | null | undefined; + setScrollableElement: ( + scrollableElement?: HTMLElement | null | undefined, + ) => void; + scrollableElement: HTMLElement | null | undefined; +}; diff --git a/packages/math-input/src/version.ts b/packages/math-input/src/version.ts new file mode 100644 index 0000000000..b0bf3adb0c --- /dev/null +++ b/packages/math-input/src/version.ts @@ -0,0 +1,10 @@ +// This file is processed by a Rollup plugin (replace) to inject the production +// version number during the release build. +// In dev, you'll never see the version number. + +import {addLibraryVersionToPerseusDebug} from "@khanacademy/perseus-core"; + +const libName = "@khanacademy/math-input"; +export const libVersion = "__lib_version__"; + +addLibraryVersionToPerseusDebug(libName, libVersion); diff --git a/packages/math-input/tsconfig-build.json b/packages/math-input/tsconfig-build.json index 08e1bfcdf9..24f54adc72 100644 --- a/packages/math-input/tsconfig-build.json +++ b/packages/math-input/tsconfig-build.json @@ -6,5 +6,6 @@ }, "references": [ {"path": "../perseus-core/tsconfig-build.json"}, + {"path": "../perseus-core/tsconfig-build.json"}, ] } diff --git a/packages/perseus-core/src/index.ts b/packages/perseus-core/src/index.ts index cefcea17d5..3ae29fd247 100644 --- a/packages/perseus-core/src/index.ts +++ b/packages/perseus-core/src/index.ts @@ -4,3 +4,8 @@ export type { KeypadContextRendererInterface, RendererInterface, } from "./types"; + +// Careful, `version.ts` uses this function so it _must_ be imported above it +export {addLibraryVersionToPerseusDebug} from "./utils/add-library-version-to-perseus-debug"; + +export {libVersion} from "./version"; diff --git a/packages/perseus-core/src/utils/add-library-version-to-perseus-debug.test.ts b/packages/perseus-core/src/utils/add-library-version-to-perseus-debug.test.ts new file mode 100644 index 0000000000..012d424c38 --- /dev/null +++ b/packages/perseus-core/src/utils/add-library-version-to-perseus-debug.test.ts @@ -0,0 +1,109 @@ +import {addLibraryVersionToPerseusDebug} from "./add-library-version-to-perseus-debug"; + +describe("add-library-version-to-perseus-debug", () => { + beforeEach(() => { + delete globalThis.__perseus_debug__; + }); + + it("should add the given library to __perseus_debug__", () => { + // Array + + // Act + addLibraryVersionToPerseusDebug("test-lib", "1.0.0"); + + // Assert + expect(globalThis.__perseus_debug__).toMatchInlineSnapshot(` + { + "test-lib": "v1.0.0", + } + `); + }); + + it("should extend __perseus_debug__ when multiple libraries registered", () => { + // Arrange + + // Act + addLibraryVersionToPerseusDebug("test-lib", "1.0.0"); + addLibraryVersionToPerseusDebug("sample-lib", "2.0.0"); + addLibraryVersionToPerseusDebug("utility-lib", "3.0.0"); + + // Assert + expect(globalThis.__perseus_debug__).toMatchInlineSnapshot(` + { + "sample-lib": "v2.0.0", + "test-lib": "v1.0.0", + "utility-lib": "v3.0.0", + } + `); + }); + + it("should convert library entry to array when multiple versions of the same library registered", () => { + // Arrange + jest.spyOn(console, "warn").mockImplementation(); + + // Act + addLibraryVersionToPerseusDebug("test-lib", "1.0.1"); + addLibraryVersionToPerseusDebug("test-lib", "4.1.8"); + addLibraryVersionToPerseusDebug("test-lib", "1.0.0"); + addLibraryVersionToPerseusDebug("test-lib", "2.0.0"); + + // Assert + expect(globalThis.__perseus_debug__).toMatchInlineSnapshot(` + { + "test-lib": [ + "v1.0.0", + "v1.0.1", + "v2.0.0", + "v4.1.8", + ], + } + `); + }); + + it("should warn when multiple versions of the same library registered", () => { + // Arrange + const warnSpy = jest.spyOn(console, "warn").mockImplementation(); + + // Act + addLibraryVersionToPerseusDebug("test-lib", "1.0.1"); + addLibraryVersionToPerseusDebug("test-lib", "4.1.8"); + addLibraryVersionToPerseusDebug("test-lib", "1.0.0"); + addLibraryVersionToPerseusDebug("test-lib", "2.0.0"); + + // Assert + expect(warnSpy).toHaveBeenCalledWith( + expect.stringMatching( + /Multiple versions of test-lib loaded on this page/, + ), + ); + }); + + it("should not register duplicates for duplicate calls of the same library and version", () => { + // Arrange + + // Act + addLibraryVersionToPerseusDebug("test-lib", "1.0.0"); + addLibraryVersionToPerseusDebug("test-lib", "1.0.0"); + addLibraryVersionToPerseusDebug("test-lib", "1.0.0"); + + // Assert + expect(globalThis.__perseus_debug__).toMatchInlineSnapshot(` + { + "test-lib": "v1.0.0", + } + `); + }); + + it("should not warn for duplicate calls for a library of the same library and version", () => { + // Arrange + const warnSpy = jest.spyOn(console, "warn").mockImplementation(); + + // Act + addLibraryVersionToPerseusDebug("test-lib", "1.0.0"); + addLibraryVersionToPerseusDebug("test-lib", "1.0.0"); + addLibraryVersionToPerseusDebug("test-lib", "1.0.0"); + + // Assert + expect(warnSpy).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/perseus-core/src/utils/add-library-version-to-perseus-debug.ts b/packages/perseus-core/src/utils/add-library-version-to-perseus-debug.ts new file mode 100644 index 0000000000..5ebc22e3df --- /dev/null +++ b/packages/perseus-core/src/utils/add-library-version-to-perseus-debug.ts @@ -0,0 +1,52 @@ +/** + * Adds the given perseus library version information to the __perseus_debug__ + * object and ensures that the object is attached to `globalThis` (`window` in + * browser environments). + * + * This allows each library to provide runtime version information to assist in + * debugging in production environments. + */ +export const addLibraryVersionToPerseusDebug = ( + libraryName: string, + libraryVersion: string, +) => { + // If the library version is the default value, then we don't want to + // prefix it with a "v" to indicate that it is a version number. + let prefix = "v"; + if (libraryVersion === "__lib_version__") { + prefix = ""; + } + const formattedVersion = `${prefix}${libraryVersion}`; + + if (typeof globalThis !== "undefined") { + globalThis.__perseus_debug__ = globalThis.__perseus_debug__ ?? {}; + + const existingVersionEntry = globalThis.__perseus_debug__[libraryName]; + if (existingVersionEntry) { + // If we already have an entry and it doesn't match the registered + // version, we morph the entry into an array and log a warning. + if (existingVersionEntry !== formattedVersion) { + // Existing entry might be an array already (oops, at least 2 + // versions of the library already loaded!). + const allVersions = Array.isArray(existingVersionEntry) + ? existingVersionEntry + : [existingVersionEntry]; + allVersions.push(formattedVersion); + + globalThis.__perseus_debug__[libraryName] = allVersions; + + // eslint-disable-next-line no-console + console.warn( + `Multiple versions of ${libraryName} loaded on this page: ${allVersions + .sort() + .join(", ")}`, + ); + } + } else { + globalThis.__perseus_debug__[libraryName] = formattedVersion; + } + } else { + // eslint-disable-next-line no-console + console.warn(`globalThis not found found (${formattedVersion})`); + } +}; diff --git a/packages/perseus-core/src/version.ts b/packages/perseus-core/src/version.ts new file mode 100644 index 0000000000..127cfabbe9 --- /dev/null +++ b/packages/perseus-core/src/version.ts @@ -0,0 +1,10 @@ +// This file is processed by a Rollup plugin (replace) to inject the production +// version number during the release build. +// In dev, you'll never see the version number. + +import {addLibraryVersionToPerseusDebug} from "@khanacademy/perseus-core"; + +const libName = "@khanacademy/perseus-core"; +export const libVersion = "__lib_version__"; + +addLibraryVersionToPerseusDebug(libName, libVersion); diff --git a/packages/perseus-editor/CHANGELOG.md b/packages/perseus-editor/CHANGELOG.md index 16f5a2d380..b355496b61 100644 --- a/packages/perseus-editor/CHANGELOG.md +++ b/packages/perseus-editor/CHANGELOG.md @@ -1,5 +1,23 @@ # @khanacademy/perseus-editor +## 2.7.6 + +### Patch Changes + +- Updated dependencies [0761377a] +- Updated dependencies [14138bb0] +- Updated dependencies [14138bb0] +- Updated dependencies [332d5d6d] + - @khanacademy/perseus@11.6.0 + +## 2.7.5 + +### Patch Changes + +- Updated dependencies [7e2ae0ef] +- Updated dependencies [1dc460c7] + - @khanacademy/perseus@11.5.1 + ## 2.7.4 ### Patch Changes diff --git a/packages/perseus-editor/package.json b/packages/perseus-editor/package.json index 57fc2248ae..9739673772 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.7.4", + "version": "2.7.6", "publishConfig": { "access": "public" }, @@ -24,7 +24,8 @@ "dependencies": { "@khanacademy/kas": "^0.3.1", "@khanacademy/kmath": "^0.1.2", - "@khanacademy/perseus": "^11.5.0" + "@khanacademy/perseus": "^11.6.0", + "@khanacademy/perseus-core": "1.1.0" }, "devDependencies": { "@khanacademy/wonder-blocks-button": "^4.1.3", diff --git a/packages/perseus-editor/src/index.ts b/packages/perseus-editor/src/index.ts index 2a78c3e267..a1aae85f05 100644 --- a/packages/perseus-editor/src/index.ts +++ b/packages/perseus-editor/src/index.ts @@ -1,3 +1,5 @@ +export {libVersion} from "./version"; + export {default as ArticleEditor} from "./article-editor"; export {default as DeviceFramer} from "./components/device-framer"; export {default as ViewportResizer} from "./components/viewport-resizer"; diff --git a/packages/perseus-editor/src/version.ts b/packages/perseus-editor/src/version.ts new file mode 100644 index 0000000000..b095fe3b53 --- /dev/null +++ b/packages/perseus-editor/src/version.ts @@ -0,0 +1,10 @@ +// This file is processed by a Rollup plugin (replace) to inject the production +// version number during the release build. +// In dev, you'll never see the version number. + +import {addLibraryVersionToPerseusDebug} from "@khanacademy/perseus-core"; + +const libName = "@khanacademy/perseus-editor"; +export const libVersion = "__lib_version__"; + +addLibraryVersionToPerseusDebug(libName, libVersion); diff --git a/packages/perseus-editor/tsconfig-build.json b/packages/perseus-editor/tsconfig-build.json index 1c77e5b3b0..f9d2f0b6ad 100644 --- a/packages/perseus-editor/tsconfig-build.json +++ b/packages/perseus-editor/tsconfig-build.json @@ -17,5 +17,6 @@ {"path": "../kas/tsconfig-build.json"}, {"path": "../kmath/tsconfig-build.json"}, {"path": "../perseus/tsconfig-build.json"}, + {"path": "../perseus-core/tsconfig-build.json"}, ] } diff --git a/packages/perseus-error/package.json b/packages/perseus-error/package.json index c6966becc4..94d68521d4 100644 --- a/packages/perseus-error/package.json +++ b/packages/perseus-error/package.json @@ -21,7 +21,9 @@ "scripts": { "test": "bash -c 'yarn --silent --cwd \"../..\" test ${@:0} $($([[ ${@: -1} = -* ]] || [[ ${@: -1} = bash ]]) && echo $PWD)'" }, - "dependencies": {}, + "dependencies": { + "@khanacademy/perseus-core": "1.1.0" + }, "devDependencies": { "@khanacademy/wonder-stuff-core": "^1.5.1" }, diff --git a/packages/perseus-error/src/index.ts b/packages/perseus-error/src/index.ts index a2f0878ef3..79dab8543c 100644 --- a/packages/perseus-error/src/index.ts +++ b/packages/perseus-error/src/index.ts @@ -1,5 +1,7 @@ import type {Metadata} from "@khanacademy/wonder-stuff-core"; +export {libVersion} from "./version"; + /** * @typedef {Object} Errors utility for referencing the Perseus error taxonomy. */ diff --git a/packages/perseus-error/src/version.ts b/packages/perseus-error/src/version.ts new file mode 100644 index 0000000000..37d7275d24 --- /dev/null +++ b/packages/perseus-error/src/version.ts @@ -0,0 +1,10 @@ +// This file is processed by a Rollup plugin (replace) to inject the production +// version number during the release build. +// In dev, you'll never see the version number. + +import {addLibraryVersionToPerseusDebug} from "@khanacademy/perseus-core"; + +const libName = "@khanacademy/perseus-error"; +export const libVersion = "__lib_version__"; + +addLibraryVersionToPerseusDebug(libName, libVersion); diff --git a/packages/perseus-error/tsconfig-build.json b/packages/perseus-error/tsconfig-build.json index 5e7f4bce86..08e1bfcdf9 100644 --- a/packages/perseus-error/tsconfig-build.json +++ b/packages/perseus-error/tsconfig-build.json @@ -4,5 +4,7 @@ "outDir": "./dist", "rootDir": "src", }, - "references": [] + "references": [ + {"path": "../perseus-core/tsconfig-build.json"}, + ] } diff --git a/packages/perseus-linter/package.json b/packages/perseus-linter/package.json index 02d1265942..29c046185f 100644 --- a/packages/perseus-linter/package.json +++ b/packages/perseus-linter/package.json @@ -22,6 +22,7 @@ "test": "bash -c 'yarn --silent --cwd \"../..\" test ${@:0} $($([[ ${@: -1} = -* ]] || [[ ${@: -1} = bash ]]) && echo $PWD)'" }, "dependencies": { + "@khanacademy/perseus-core": "1.1.0", "@khanacademy/perseus-error": "^0.2.3" }, "devDependencies": { diff --git a/packages/perseus-linter/src/index.ts b/packages/perseus-linter/src/index.ts index 3e6bc00fa2..db5b2757a9 100644 --- a/packages/perseus-linter/src/index.ts +++ b/packages/perseus-linter/src/index.ts @@ -2,6 +2,8 @@ import Rule from "./rule"; import AllRules from "./rules/all-rules"; import TreeTransformer from "./tree-transformer"; +export {libVersion} from "./version"; + export {linterContextProps, linterContextDefault} from "./proptypes"; export type {LinterContextProps} from "./types"; diff --git a/packages/perseus-linter/src/version.ts b/packages/perseus-linter/src/version.ts new file mode 100644 index 0000000000..65c87dc640 --- /dev/null +++ b/packages/perseus-linter/src/version.ts @@ -0,0 +1,10 @@ +// This file is processed by a Rollup plugin (replace) to inject the production +// version number during the release build. +// In dev, you'll never see the version number. + +import {addLibraryVersionToPerseusDebug} from "@khanacademy/perseus-core"; + +const libName = "@khanacademy/perseus-linter"; +export const libVersion = "__lib_version__"; + +addLibraryVersionToPerseusDebug(libName, libVersion); diff --git a/packages/perseus-linter/tsconfig-build.json b/packages/perseus-linter/tsconfig-build.json index 67aba1da7e..e6a8a7f342 100644 --- a/packages/perseus-linter/tsconfig-build.json +++ b/packages/perseus-linter/tsconfig-build.json @@ -5,6 +5,7 @@ "rootDir": "src", }, "references": [ + {"path": "../perseus-core/tsconfig-build.json"}, {"path": "../perseus-error/tsconfig-build.json"}, {"path": "../pure-markdown/tsconfig-build.json"}, ] diff --git a/packages/perseus/CHANGELOG.md b/packages/perseus/CHANGELOG.md index 727bbf4738..8932e0c2e6 100644 --- a/packages/perseus/CHANGELOG.md +++ b/packages/perseus/CHANGELOG.md @@ -1,5 +1,31 @@ # @khanacademy/perseus +## 11.6.0 + +### Minor Changes + +- 0761377a: Ensured we're still validating against pi when strict is set to false and pi is in the answerforms. + +### Patch Changes + +- 14138bb0: Move StatefulKeypadContextProvider into math-input +- 14138bb0: Hoist keypad active state into keypad context +- 332d5d6d: Fix list item rendering in exercises and HintRenderer (LC-1022 & LC-1225) +- Updated dependencies [14138bb0] +- Updated dependencies [14138bb0] + - @khanacademy/math-input@14.0.0 + +## 11.5.1 + +### Patch Changes + +- 7e2ae0ef: Bugfix for fraction button in v2 fraction keypad +- 1dc460c7: Add tests for mobile MathInput +- Updated dependencies [7e2ae0ef] +- Updated dependencies [1dc460c7] +- Updated dependencies [3b19a1bf] + - @khanacademy/math-input@13.1.0 + ## 11.5.0 ### Minor Changes diff --git a/packages/perseus/package.json b/packages/perseus/package.json index a593fd9fd7..e6e3feedcb 100644 --- a/packages/perseus/package.json +++ b/packages/perseus/package.json @@ -3,7 +3,7 @@ "description": "Core Perseus API (includes renderers and widgets)", "author": "Khan Academy", "license": "MIT", - "version": "11.5.0", + "version": "11.6.0", "publishConfig": { "access": "public" }, @@ -24,7 +24,7 @@ "dependencies": { "@khanacademy/kas": "^0.3.1", "@khanacademy/kmath": "^0.1.2", - "@khanacademy/math-input": "^13.0.0", + "@khanacademy/math-input": "^14.0.0", "@khanacademy/perseus-core": "1.1.0", "@khanacademy/perseus-linter": "^0.3.4", "@khanacademy/pure-markdown": "^0.2.6", diff --git a/packages/perseus/src/__tests__/__snapshots__/server-item-renderer.test.tsx.snap b/packages/perseus/src/__tests__/__snapshots__/server-item-renderer.test.tsx.snap index 12d8c237a9..32394690a6 100644 --- a/packages/perseus/src/__tests__/__snapshots__/server-item-renderer.test.tsx.snap +++ b/packages/perseus/src/__tests__/__snapshots__/server-item-renderer.test.tsx.snap @@ -189,7 +189,7 @@ exports[`server item renderer should snapshot: initial render 1`] = ` class="" >
- (renderer = node)} - apiOptions={apiOptions} - item={question} - problemNum={0} - reviewMode={false} - savedState="" - controlPeripherals={false} - dependencies={testDependenciesV2} - {...optionalProps} - /> - {/* The ItemRenderer _requires_ two divs: a work area and hints + + (renderer = node)} + apiOptions={apiOptions} + item={question} + problemNum={0} + reviewMode={false} + savedState="" + controlPeripherals={false} + dependencies={testDependenciesV2} + {...optionalProps} + /> + {/* The ItemRenderer _requires_ two divs: a work area and hints area. Without both of these, it fails to render anything! */} -
-
+
+
- + + , ); if (!renderer) { @@ -309,18 +312,21 @@ describe("item renderer", () => { ); }); - it("should activate the keypad when widget with input is focused", () => { + it("should activate the keypad when widget with input is focused", async () => { // Arrange const {renderer} = renderQuestion(itemWithInput, { isMobile: true, customKeypad: true, + useV2Keypad: true, }); // Act renderer.focus(); // Assert - expect(screen.getByLabelText("7")).toBeVisible(); + await waitFor(() => { + expect(screen.getByLabelText("7")).toBeVisible(); + }); }); it("should provide current and previous focus paths on focus change to, and away from, a single widget", () => { diff --git a/packages/perseus/src/hint-renderer.tsx b/packages/perseus/src/hint-renderer.tsx index 439e4d2e1f..24d8af3bdf 100644 --- a/packages/perseus/src/hint-renderer.tsx +++ b/packages/perseus/src/hint-renderer.tsx @@ -62,6 +62,7 @@ class HintRenderer extends React.Component { const {isMobile} = apiOptions; const classNames = classnames( + "hint", !isMobile && "perseus-hint-renderer", isMobile && css(styles.newHint), isMobile && lastRendered && css(styles.lastRenderedNewHint), diff --git a/packages/perseus/src/index.ts b/packages/perseus/src/index.ts index 40b0159669..8c22f44fe5 100644 --- a/packages/perseus/src/index.ts +++ b/packages/perseus/src/index.ts @@ -1,16 +1,13 @@ -/* eslint-disable @khanacademy/ts-no-error-suppressions */ /** * Main entry point */ -import version from "./version"; - export {default as init} from "./init"; export {ApiOptions, ClassNames} from "./perseus-api"; -export const apiVersion = version.apiVersion; -export const itemDataVersion = version.itemDataVersion; +export {libVersion} from "./version"; +export {apiVersion, itemDataVersion} from "./perseus-version"; export {default as itemVersion} from "./item-version"; /** diff --git a/packages/perseus/src/item-version.ts b/packages/perseus/src/item-version.ts index 551ff4fd50..bfadb94c12 100644 --- a/packages/perseus/src/item-version.ts +++ b/packages/perseus/src/item-version.ts @@ -1,10 +1,10 @@ import allWidgets from "./all-widgets"; -import version from "./version"; +import {itemDataVersion} from "./perseus-version"; import * as Widgets from "./widgets"; Widgets.registerWidgets(allWidgets); const ItemVersion = Widgets.getVersionVector(); -ItemVersion["::renderer::"] = version.itemDataVersion; +ItemVersion["::renderer::"] = itemDataVersion; export default ItemVersion; diff --git a/packages/perseus/src/multi-items/__tests__/__snapshots__/multi-renderer.test.tsx.snap b/packages/perseus/src/multi-items/__tests__/__snapshots__/multi-renderer.test.tsx.snap index bf6a6554f5..fbe93d0efe 100644 --- a/packages/perseus/src/multi-items/__tests__/__snapshots__/multi-renderer.test.tsx.snap +++ b/packages/perseus/src/multi-items/__tests__/__snapshots__/multi-renderer.test.tsx.snap @@ -1128,7 +1128,7 @@ exports[`multi-item renderer should snapshot: initial render 1`] = ` class="" >
; - // Whether the answerForms should be strictly matched + // Whether we should check the answer strictly against the the configured answerForms (strict = true) + // or include the set of default answerForms (strict = false). strict: boolean; // A range of error +/- the value // NOTE: perseus_data.go says this is non-nullable even though we handle null values. diff --git a/packages/perseus/src/perseus-version.ts b/packages/perseus/src/perseus-version.ts new file mode 100644 index 0000000000..f7dca0ba0f --- /dev/null +++ b/packages/perseus/src/perseus-version.ts @@ -0,0 +1,9 @@ +export const apiVersion = { + major: 12, + minor: 0, +}; + +export const itemDataVersion = { + major: 0, + minor: 1, +}; diff --git a/packages/perseus/src/styles/khan-exercise.css b/packages/perseus/src/styles/khan-exercise.css index 9b9033f588..3f9d1b6654 100644 --- a/packages/perseus/src/styles/khan-exercise.css +++ b/packages/perseus/src/styles/khan-exercise.css @@ -10,6 +10,12 @@ var { font-style: normal; } +.hint.last-hint { + .paragraph { + font-weight: bold; + } +} + .hint_blue { color: #6495ed; } @@ -32,10 +38,6 @@ var { color: #9d38bd; } -.last-hint { - font-weight: bold; -} - div.subhint { border: 1px solid #aaaaaa; background: #f9f9f9; diff --git a/packages/perseus/src/styles/perseus-renderer.less b/packages/perseus/src/styles/perseus-renderer.less index f7b99ce7fa..53c41bf0b0 100644 --- a/packages/perseus/src/styles/perseus-renderer.less +++ b/packages/perseus/src/styles/perseus-renderer.less @@ -144,7 +144,6 @@ } .paragraph ul:not(.perseus-widget-radio) { - .legacy-typography; padding-left: 35px; list-style-type: disc; } diff --git a/packages/perseus/src/version.ts b/packages/perseus/src/version.ts index 7592346d3a..098e55bfe4 100644 --- a/packages/perseus/src/version.ts +++ b/packages/perseus/src/version.ts @@ -1,11 +1,10 @@ -export default { - apiVersion: { - major: 12, - minor: 0, - }, +// This file is processed by a Rollup plugin (replace) to inject the production +// version number during the release build. +// In dev, you'll never see the version number. - itemDataVersion: { - major: 0, - minor: 1, - }, -}; +import {addLibraryVersionToPerseusDebug} from "@khanacademy/perseus-core"; + +const libName = "@khanacademy/perseus"; +export const libVersion = "__lib_version__"; + +addLibraryVersionToPerseusDebug(libName, libVersion); diff --git a/packages/perseus/src/widgets/__stories__/test-keypad-context-wrapper.tsx b/packages/perseus/src/widgets/__stories__/test-keypad-context-wrapper.tsx index e244af6a05..004e5f49fe 100644 --- a/packages/perseus/src/widgets/__stories__/test-keypad-context-wrapper.tsx +++ b/packages/perseus/src/widgets/__stories__/test-keypad-context-wrapper.tsx @@ -1,4 +1,8 @@ -import {KeypadContext, MobileKeypad} from "@khanacademy/math-input"; +import { + KeypadContext, + StatefulKeypadContextProvider, + MobileKeypad, +} from "@khanacademy/math-input"; import {View} from "@khanacademy/wonder-blocks-core"; import {action} from "@storybook/addon-actions"; import {StyleSheet} from "aphrodite"; @@ -29,29 +33,11 @@ type Props = { }; const TestKeypadContextWrapper = (props: Props): React.ReactElement => { - const [keypadElement, setKeypadElement] = React.useState(null); - const [renderer, setRenderer] = React.useState(null); - const [scrollableElement, setScrollableElement] = React.useState( - document.body, - ); - return ( - >' is not assignable to type '(scrollableElement?: HTMLElement | null | undefined) => void'. - setKeypadElement, - keypadElement, - // @ts-expect-error - TS2322 - Type 'Dispatch>' is not assignable to type '(scrollableElement?: HTMLElement | null | undefined) => void'. - setRenderer, - renderer, - // @ts-expect-error - TS2322 - Type 'Dispatch>' is not assignable to type '(scrollableElement?: HTMLElement | null | undefined) => void'. - setScrollableElement, - scrollableElement, - }} - > + {props.children}