diff --git a/.changeset/curly-jobs-sit.md b/.changeset/curly-jobs-sit.md deleted file mode 100644 index cc59fba023..0000000000 --- a/.changeset/curly-jobs-sit.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@khanacademy/perseus-editor": patch ---- - -Add tests for RadioEditor diff --git a/.changeset/fair-kids-prove.md b/.changeset/fair-kids-prove.md deleted file mode 100644 index 2a62fbbdb1..0000000000 --- a/.changeset/fair-kids-prove.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@khanacademy/perseus-editor": patch ---- - -Add tests for DropdownEditor diff --git a/.changeset/perfect-eels-divide.md b/.changeset/perfect-eels-divide.md new file mode 100644 index 0000000000..5506c856e5 --- /dev/null +++ b/.changeset/perfect-eels-divide.md @@ -0,0 +1,5 @@ +--- +"@khanacademy/perseus": minor +--- + +Update Perseus' Storybook to use MathJax instead of KaTeX diff --git a/.changeset/red-boats-learn.md b/.changeset/red-boats-learn.md deleted file mode 100644 index a845151cc8..0000000000 --- a/.changeset/red-boats-learn.md +++ /dev/null @@ -1,2 +0,0 @@ ---- ---- diff --git a/.changeset/tender-tomatoes-flow.md b/.changeset/tender-tomatoes-flow.md deleted file mode 100644 index 28b746082a..0000000000 --- a/.changeset/tender-tomatoes-flow.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@khanacademy/perseus-editor": patch ---- - -Add tests for ExplanationEditor diff --git a/.changeset/tricky-suns-sneeze.md b/.changeset/tricky-suns-sneeze.md deleted file mode 100644 index 06796e1394..0000000000 --- a/.changeset/tricky-suns-sneeze.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@khanacademy/perseus-editor": patch ---- - -Add tests for NumericInputEditor diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index c7d4d15921..8111fa6527 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,3 +1,22 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/typescript-node { - "containerEnv": { "NODE_OPTIONS": "--openssl-legacy-provider" } + "name": "Node.js & TypeScript", + // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile + "image": "mcr.microsoft.com/devcontainers/typescript-node:16", + + // Features to add to the dev container. More info: https://containers.dev/features. + // "features": {}, + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + "forwardPorts": [6006], + + // Use 'postCreateCommand' to run commands after the container is created. + "postCreateCommand": "yarn install" + + // Configure tool-specific properties. + // "customizations": {}, + + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "root" } diff --git a/.github/actions/shared-node-cache/action.yml b/.github/actions/shared-node-cache/action.yml new file mode 100644 index 0000000000..2ada46366c --- /dev/null +++ b/.github/actions/shared-node-cache/action.yml @@ -0,0 +1,27 @@ +name: "Shared Node Cache" +description: "Install & cache our npm dependencies" + +# The inputs this action expects. +inputs: + node-version: + description: "Node version to use" + required: true + ssh-private-key: + description: "A private SSH key so that we can obtain private dependencies like our event schema package" + required: true + +# The steps this action runs. +# The order of the two steps below needs to be maintained. +# The ssh-agent step needs to be before the Node.js/Install step because our package.json uses +# git+ssh. As this requires a valid SSH key, the ssh-agent action must be before the yarn install. +runs: + using: "composite" + steps: + - uses: webfactory/ssh-agent@v0.7.0 + with: + ssh-private-key: ${{ inputs.ssh-private-key }} + + - name: Use Node.js ${{ inputs.node-version }} & Install & cache node_modules + uses: Khan/actions@shared-node-cache-v0 + with: + node-version: ${{ inputs.node-version }} diff --git a/.github/workflows/node-ci-main.yml b/.github/workflows/node-ci-main.yml index ec878e025f..7c3e531fd4 100644 --- a/.github/workflows/node-ci-main.yml +++ b/.github/workflows/node-ci-main.yml @@ -25,11 +25,12 @@ jobs: os: [ubuntu-latest] node-version: [20.x] steps: - - uses: actions/checkout@v3 - - name: Use Node.js ${{ matrix.node-version }} & Install & cache node_modules - uses: Khan/actions@shared-node-cache-v0 + - uses: actions/checkout@v4 + - name: Install & cache node_modules + uses: ./.github/actions/shared-node-cache with: node-version: ${{ matrix.node-version }} + ssh-private-key: ${{ secrets.KHAN_ACTIONS_BOT_SSH_PRIVATE_KEY }} coverage: needs: [prime_cache_primary] @@ -40,12 +41,13 @@ jobs: os: [ubuntu-latest] node-version: [20.x] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - - name: Use Node.js ${{ matrix.node-version }} & Install & cache node_modules - uses: Khan/actions@shared-node-cache-v0 + - name: Install & cache node_modules + uses: ./.github/actions/shared-node-cache with: node-version: ${{ matrix.node-version }} + ssh-private-key: ${{ secrets.KHAN_ACTIONS_BOT_SSH_PRIVATE_KEY }} - name: Jest with coverage run: yarn coverage @@ -66,12 +68,13 @@ jobs: node-version: [20.x] steps: - name: Checking out latest commit - uses: actions/checkout@v3 + uses: actions/checkout@v4 - - name: Use Node.js ${{ matrix.node-version }} & Install & cache node_modules - uses: Khan/actions@shared-node-cache-v0 + - name: Install & cache node_modules + uses: ./.github/actions/shared-node-cache with: node-version: ${{ matrix.node-version }} + ssh-private-key: ${{ secrets.KHAN_ACTIONS_BOT_SSH_PRIVATE_KEY }} - name: Run test with coverage run: yarn cypress:ci diff --git a/.github/workflows/node-ci.yml b/.github/workflows/node-ci.yml index 97e985e43a..172e389cfc 100644 --- a/.github/workflows/node-ci.yml +++ b/.github/workflows/node-ci.yml @@ -1,6 +1,7 @@ name: Node CI on: + workflow_dispatch: pull_request: # edited is needed because that's the trigger when the base branch is # changed on a PR @@ -18,7 +19,7 @@ jobs: node-version: [20.x] steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 @@ -36,6 +37,10 @@ jobs: 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 + - uses: webfactory/ssh-agent@v0.7.0 + with: + ssh-private-key: ${{ secrets.KHAN_ACTIONS_BOT_SSH_PRIVATE_KEY }} + - name: Verify changeset entries uses: Khan/changeset-per-package@v1.0.1 with: @@ -50,12 +55,13 @@ jobs: node-version: [20.x] steps: - name: Checking out latest commit - uses: actions/checkout@v3 + uses: actions/checkout@v4 - - name: Use Node.js ${{ matrix.node-version }} & Install & cache node_modules - uses: Khan/actions@shared-node-cache-v0 + - name: Install & cache node_modules + uses: ./.github/actions/shared-node-cache with: node-version: ${{ matrix.node-version }} + ssh-private-key: ${{ secrets.KHAN_ACTIONS_BOT_SSH_PRIVATE_KEY }} - name: Get All Changed Files uses: Khan/actions@get-changed-files-v1 @@ -128,26 +134,27 @@ jobs: node-version: [20.x] steps: - name: Checking out latest commit - uses: actions/checkout@v3 + uses: actions/checkout@v4 - - name: Use Node.js ${{ matrix.node-version }} & Install & cache node_modules - uses: Khan/actions@shared-node-cache-v0 + - name: Install & cache node_modules + uses: ./.github/actions/shared-node-cache with: node-version: ${{ matrix.node-version }} + ssh-private-key: ${{ secrets.KHAN_ACTIONS_BOT_SSH_PRIVATE_KEY }} - 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 + - name: Upload Cypress Coverage + uses: actions/upload-artifact@v4 with: name: cypress-coverage path: ./.nyc_output/out.json - name: Upload Cypress Screenshots - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 if: failure() with: name: cypress-screenshots @@ -161,20 +168,21 @@ jobs: os: [ubuntu-latest] node-version: [20.x] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - - name: Use Node.js ${{ matrix.node-version }} & Install & cache node_modules - uses: Khan/actions@shared-node-cache-v0 + - name: Install & cache node_modules + uses: ./.github/actions/shared-node-cache with: node-version: ${{ matrix.node-version }} + ssh-private-key: ${{ secrets.KHAN_ACTIONS_BOT_SSH_PRIVATE_KEY }} - 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 + - name: Upload Jest Coverage + uses: actions/upload-artifact@v4 with: name: jest-coverage path: ./coverage/coverage-final.json @@ -184,15 +192,16 @@ jobs: runs-on: ubuntu-latest needs: [cypress, coverage] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - - name: Use Node.js ${{ matrix.node-version }} & Install & cache node_modules - uses: Khan/actions@shared-node-cache-v0 + - name: Install & cache node_modules + uses: ./.github/actions/shared-node-cache with: node-version: ${{ matrix.node-version }} + ssh-private-key: ${{ secrets.KHAN_ACTIONS_BOT_SSH_PRIVATE_KEY }} - name: Download Jest Coverage - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v4 with: name: jest-coverage # path to decompress the artifact into, decompressed file @@ -200,7 +209,7 @@ jobs: path: ./ - name: Download Cypress Coverage - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v4 with: name: cypress-coverage # path to decompress the artifact into, decompressed file @@ -225,15 +234,16 @@ jobs: node-version: [20.x] steps: - name: Checking out latest commit - uses: actions/checkout@v3 + uses: actions/checkout@v4 - - name: Use Node.js ${{ matrix.node-version }} & Install & cache node_modules - uses: Khan/actions@shared-node-cache-v0 + - name: Install & cache node_modules + uses: ./.github/actions/shared-node-cache with: node-version: ${{ matrix.node-version }} + ssh-private-key: ${{ secrets.KHAN_ACTIONS_BOT_SSH_PRIVATE_KEY }} # Make sure our packages aren't growing unexpectedly # This must come last as it builds the old code last and so leaves the - # wrong code in place for the next job. + # wrong code in place for the next job; in other words, it leaves the repo on a base branch. - name: Check Builds uses: preactjs/compressed-size-action@v2 with: @@ -246,6 +256,10 @@ jobs: # Build production build-script: "build:prodsizecheck" + # + # Do not place any steps after "Check Builds" + # + extract_strings: name: Extract i18n strings runs-on: ${{ matrix.os }} @@ -254,12 +268,13 @@ jobs: os: [ubuntu-latest] node-version: [20.x] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - - name: Use Node.js ${{ matrix.node-version }} & Install & cache node_modules - uses: Khan/actions@shared-node-cache-v0 + - name: Install & cache node_modules + uses: ./.github/actions/shared-node-cache with: node-version: ${{ matrix.node-version }} + ssh-private-key: ${{ secrets.KHAN_ACTIONS_BOT_SSH_PRIVATE_KEY }} - name: Extract strings run: yarn extract-strings @@ -279,19 +294,20 @@ jobs: steps: # We need to checkout all history, so that the changeseat tool can diff it - name: Checkout current commit - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: "0" - - name: Ensure main branch is avaialble + - name: Ensure main branch is available run: | REF=$(git rev-parse HEAD) git checkout main git checkout $REF - - name: Use Node.js ${{ matrix.node-version }} & Install & cache node_modules - uses: Khan/actions@shared-node-cache-v0 + - name: Install & cache node_modules + uses: ./.github/actions/shared-node-cache with: node-version: ${{ matrix.node-version }} + ssh-private-key: ${{ secrets.KHAN_ACTIONS_BOT_SSH_PRIVATE_KEY }} - name: Publish Snapshot Release to npm id: publish-snapshot @@ -329,7 +345,7 @@ jobs: body: | # npm Snapshot: Published - 🎉 Good news!! We've packaged up the latest commit from this PR (${{ + Good news!! We've packaged up the latest commit from this PR (${{ steps.short-sha.outputs.short_sha }}) and published it to npm. You can install it using the tag `${{ steps.publish-snapshot.outputs.npm_snapshot_tag }}`. @@ -350,7 +366,7 @@ jobs: body: | # npm Snapshot: **NOT** Published - 🤕 Oh noes!! We couldn't find any changesets in this PR (${{ + Oh noes!! We couldn't find any changesets in this PR (${{ steps.short-sha.outputs.short_sha }}). As a result, we did not publish an npm snapshot for you. @@ -365,14 +381,15 @@ jobs: # chromaui/@action doesn't work with shallow checkouts which is the # default for actions/checkout so we need to force it to checkout # more stuff. - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 - - name: Use Node.js ${{ matrix.node-version }} & Install & cache node_modules - uses: Khan/actions@shared-node-cache-v0 + - name: Install & cache node_modules + uses: ./.github/actions/shared-node-cache with: node-version: ${{ matrix.node-version }} + ssh-private-key: ${{ secrets.KHAN_ACTIONS_BOT_SSH_PRIVATE_KEY }} - name: Publish to Chromatic uses: chromaui/action@v1 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 533ad44fc1..9f0880bc19 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -37,21 +37,22 @@ jobs: os: [ubuntu-latest] node-version: [20.x] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 - - name: Use Node.js ${{ matrix.node-version }} & Install & cache node_modules - uses: Khan/actions@shared-node-cache-v0 + - name: Install & cache node_modules + uses: ./.github/actions/shared-node-cache with: node-version: ${{ matrix.node-version }} + ssh-private-key: ${{ secrets.KHAN_ACTIONS_BOT_SSH_PRIVATE_KEY }} - name: Build Storybook # Generate a static version of storybook inside "storybook-static/" run: yarn build-storybook - name: Deploy to GitHub pages - uses: JamesIves/github-pages-deploy-action@v4.4.2 + uses: JamesIves/github-pages-deploy-action@v4.5.0 with: # The branch the action should deploy to. branch: gh-pages @@ -94,7 +95,7 @@ jobs: SLACK_MSG_AUTHOR: ${{ github.event.pull_request.user.login }} SLACK_USERNAME: GithubGoose SLACK_ICON_EMOJI: ":goose:" - SLACK_MESSAGE: "A new version of ${{ github.event.repository.name }} was published! 🎉 \nRelease notes → https://github.com/${{ github.repository }}/releases/" + SLACK_MESSAGE: "A new version of ${{ github.event.repository.name }} was published! \nRelease notes → https://github.com/${{ github.repository }}/releases/" SLACK_TITLE: "New Perseus release!" SLACK_FOOTER: Perseus Slack Notification MSG_MINIMAL: true diff --git a/.gitignore b/.gitignore index a089e9fa0d..ea8edd869c 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ storybook-static .nyc_output *.tsbuildinfo /cypress/ +.idea diff --git a/package.json b/package.json index ddca045714..e2f00bb385 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "@jest/globals": "^29.5.0", "@khanacademy/eslint-config": "^3.0.1", "@khanacademy/eslint-plugin": "^2.1.1", - "@khanacademy/wonder-blocks-button": "6.2.4", + "@khanacademy/wonder-blocks-button": "6.2.5", "@khanacademy/wonder-stuff-i18n": "^2.1.2", "@popperjs/core": "^2.10.1", "@rollup/plugin-alias": "^3.1.9", diff --git a/packages/math-input/CHANGELOG.md b/packages/math-input/CHANGELOG.md index 3de147b2dc..cff2ed1fb3 100644 --- a/packages/math-input/CHANGELOG.md +++ b/packages/math-input/CHANGELOG.md @@ -1,5 +1,18 @@ # @khanacademy/math-input +## 16.5.0 + +### Minor Changes + +- [#921](https://github.com/Khan/perseus/pull/921) [`81b9a562`](https://github.com/Khan/perseus/commit/81b9a562d0fb8ff2cd82e708781432bff8437116) Thanks [@benchristel](https://github.com/benchristel)! - Make the Expression widget treat `sen` as equivalent to `sin`. The spelling + `sen` is used in Portuguese. + +## 16.4.1 + +### Patch Changes + +- [#881](https://github.com/Khan/perseus/pull/881) [`f02eb991`](https://github.com/Khan/perseus/commit/f02eb991cec37dcff02056c0d6b54fc6dfd96948) Thanks [@nedredmond](https://github.com/nedredmond)! - Swap out Label Image custom dropdown for WonderBlocks + ## 16.4.0 ### Minor Changes diff --git a/packages/math-input/package.json b/packages/math-input/package.json index fd0add8b0a..631551831b 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": "16.4.0", + "version": "16.5.0", "publishConfig": { "access": "public" }, @@ -25,11 +25,11 @@ "performance-now": "^0.2.0" }, "devDependencies": { - "@khanacademy/wonder-blocks-clickable": "^4.0.11", + "@khanacademy/wonder-blocks-clickable": "^4.0.12", "@khanacademy/wonder-blocks-color": "^3.0.0", - "@khanacademy/wonder-blocks-core": "^6.3.0", + "@khanacademy/wonder-blocks-core": "^6.3.1", "@khanacademy/wonder-blocks-i18n": "^2.0.2", - "@khanacademy/wonder-blocks-popover": "^3.0.19", + "@khanacademy/wonder-blocks-popover": "^3.0.22", "@khanacademy/wonder-blocks-timing": "^4.0.2", "@khanacademy/wonder-stuff-core": "^1.5.1", "@phosphor-icons/core": "^2.0.2", @@ -45,11 +45,11 @@ "react-transition-group": "^4.4.1" }, "peerDependencies": { - "@khanacademy/wonder-blocks-clickable": "^4.0.11", + "@khanacademy/wonder-blocks-clickable": "^4.0.12", "@khanacademy/wonder-blocks-color": "^3.0.0", - "@khanacademy/wonder-blocks-core": "^6.3.0", + "@khanacademy/wonder-blocks-core": "^6.3.1", "@khanacademy/wonder-blocks-i18n": "^2.0.2", - "@khanacademy/wonder-blocks-popover": "^3.0.19", + "@khanacademy/wonder-blocks-popover": "^3.0.22", "@khanacademy/wonder-blocks-timing": "^4.0.2", "@khanacademy/wonder-stuff-core": "^1.5.1", "@phosphor-icons/core": "^2.0.2", diff --git a/packages/math-input/src/components/input/mathquill-instance.ts b/packages/math-input/src/components/input/mathquill-instance.ts index eda6d64f24..cedc29a3fa 100644 --- a/packages/math-input/src/components/input/mathquill-instance.ts +++ b/packages/math-input/src/components/input/mathquill-instance.ts @@ -14,6 +14,46 @@ function createBaseConfig(): MathFieldConfig { // appropriate symbol. This does not include ln, log, or any of the // trig functions; those are always interpreted as commands. autoCommands: "pi theta phi sqrt nthroot", + // Most of these autoOperatorNames are simply the MathQuill defaults. + // We have to list them all in order to add the `sen` operator (see + // comment below). + autoOperatorNames: [ + "arccos", + "arcsin", + "arctan", + "arg", + "cos", + "cosh", + "cot", + "coth", + "csc", + "deg", + "det", + "dim", + "exp", + "gcd", + "hom", + "inf", + "ker", + "lg", + "lim", + "liminf", + "limsup", + "ln", + "log", + "max", + "min", + "Pr", + "projlim", + "sec", + // sen is used instead of sin in e.g. Portuguese + "sen", + "sin", + "sinh", + "sup", + "tan", + "tanh", + ].join(" "), // Pop the cursor out of super/subscripts on arithmetic operators // or (in)equalities. diff --git a/packages/perseus-editor/CHANGELOG.md b/packages/perseus-editor/CHANGELOG.md index d2a2810c43..3a1270d0c2 100644 --- a/packages/perseus-editor/CHANGELOG.md +++ b/packages/perseus-editor/CHANGELOG.md @@ -1,5 +1,89 @@ # @khanacademy/perseus-editor +## 2.16.2 + +### Patch Changes + +- Updated dependencies [[`81b9a562`](https://github.com/Khan/perseus/commit/81b9a562d0fb8ff2cd82e708781432bff8437116)]: + - @khanacademy/math-input@16.5.0 + - @khanacademy/perseus@17.7.0 + +## 2.16.1 + +### Patch Changes + +- Updated dependencies [[`21222f55`](https://github.com/Khan/perseus/commit/21222f55b1efd46acbc0fe1dcc8aa0399b8555ee)]: + - @khanacademy/perseus@17.6.2 + +## 2.16.0 + +### Minor Changes + +- [#897](https://github.com/Khan/perseus/pull/897) [`e8020f58`](https://github.com/Khan/perseus/commit/e8020f58bb538372f77785a14a31dc11be2bc441) Thanks [@jeremywiebe](https://github.com/jeremywiebe)! - Convert many string refs to React refs + +### Patch Changes + +- [#881](https://github.com/Khan/perseus/pull/881) [`f02eb991`](https://github.com/Khan/perseus/commit/f02eb991cec37dcff02056c0d6b54fc6dfd96948) Thanks [@nedredmond](https://github.com/nedredmond)! - Swap out Label Image custom dropdown for WonderBlocks + +* [#870](https://github.com/Khan/perseus/pull/870) [`9354fb55`](https://github.com/Khan/perseus/commit/9354fb55357f2441a2ca6198c52cca33edeba3c0) Thanks [@nixterrimus](https://github.com/nixterrimus)! - Replace transformer widget with a deprecated-standin widget + +* Updated dependencies [[`f02eb991`](https://github.com/Khan/perseus/commit/f02eb991cec37dcff02056c0d6b54fc6dfd96948), [`9354fb55`](https://github.com/Khan/perseus/commit/9354fb55357f2441a2ca6198c52cca33edeba3c0)]: + - @khanacademy/math-input@16.4.1 + - @khanacademy/perseus@17.6.1 + +## 2.15.7 + +### Patch Changes + +- Updated dependencies [[`83884550`](https://github.com/Khan/perseus/commit/83884550df8b394e9afa6e95947c987614e2d242)]: + - @khanacademy/perseus@17.6.0 + +## 2.15.6 + +### Patch Changes + +- [#905](https://github.com/Khan/perseus/pull/905) [`b18ddb28`](https://github.com/Khan/perseus/commit/b18ddb28d9b1f77b1263b3cf24b55a862998fb78) Thanks [@handeyeco](https://github.com/handeyeco)! - Convert interaction-editor components from PropTypes to TS + +* [#911](https://github.com/Khan/perseus/pull/911) [`e9a8808d`](https://github.com/Khan/perseus/commit/e9a8808de00132db4cb992c1428d3ac3628e389c) Thanks [@handeyeco](https://github.com/handeyeco)! - Restructure interaction-editor subcomponents + +## 2.15.5 + +### Patch Changes + +- [#904](https://github.com/Khan/perseus/pull/904) [`ca241171`](https://github.com/Khan/perseus/commit/ca241171e5fa893fc114241ac1ebc0260c9d57c5) Thanks [@handeyeco](https://github.com/handeyeco)! - Restructure interaction-editor subcomponents + +- Updated dependencies [[`29563723`](https://github.com/Khan/perseus/commit/29563723cf229a9169d0c78a0174a8dbc8029861), [`6c841f55`](https://github.com/Khan/perseus/commit/6c841f55027c87bfc8339816dac582f175a84193)]: + - @khanacademy/perseus@17.5.0 + +## 2.15.4 + +### Patch Changes + +- [#892](https://github.com/Khan/perseus/pull/892) [`22a8f42c`](https://github.com/Khan/perseus/commit/22a8f42c9e31cc74f1ab2f5a375ce5166353153f) Thanks [@handeyeco](https://github.com/handeyeco)! - add tests for NumberLineEditor + +* [#887](https://github.com/Khan/perseus/pull/887) [`d09fdb98`](https://github.com/Khan/perseus/commit/d09fdb98963f23dbeb2009518513db671e0f09bb) Thanks [@jeremywiebe](https://github.com/jeremywiebe)! - Add Storybook story for EditorPage component + +- [#882](https://github.com/Khan/perseus/pull/882) [`6f1ddaa3`](https://github.com/Khan/perseus/commit/6f1ddaa3eae41d87aaf3514ab0f4cd7875b3125b) Thanks [@handeyeco](https://github.com/handeyeco)! - Add tests for RadioEditor + +* [#893](https://github.com/Khan/perseus/pull/893) [`cbd51e81`](https://github.com/Khan/perseus/commit/cbd51e810cc6f6f406fa92b5faceb4fb3655bbb4) Thanks [@handeyeco](https://github.com/handeyeco)! - add tests for InputNumber + +- [#884](https://github.com/Khan/perseus/pull/884) [`c2172fb9`](https://github.com/Khan/perseus/commit/c2172fb90f247b3a914eaf7eb01b7b15ceb1f0c0) Thanks [@handeyeco](https://github.com/handeyeco)! - Add tests for DropdownEditor + +* [#891](https://github.com/Khan/perseus/pull/891) [`4fe720db`](https://github.com/Khan/perseus/commit/4fe720dbb388b71606bbf98c0606523b3eb3e395) Thanks [@handeyeco](https://github.com/handeyeco)! - add tests for DefinitionEditor + +- [#890](https://github.com/Khan/perseus/pull/890) [`6607ed0d`](https://github.com/Khan/perseus/commit/6607ed0d81adedc84a201ddb4b55f32a78e92dc0) Thanks [@handeyeco](https://github.com/handeyeco)! - add tests for CategorizerEditor + +* [#888](https://github.com/Khan/perseus/pull/888) [`68d8a766`](https://github.com/Khan/perseus/commit/68d8a766bfa769b8ab57c60e79be7080c6b32593) Thanks [@handeyeco](https://github.com/handeyeco)! - add tests for MatcherEditor + +- [#886](https://github.com/Khan/perseus/pull/886) [`eeac31b7`](https://github.com/Khan/perseus/commit/eeac31b7be6ef2526f0f7ae20cc6bfa237581798) Thanks [@handeyeco](https://github.com/handeyeco)! - Add tests for SorterEditor + +* [#885](https://github.com/Khan/perseus/pull/885) [`8e0eb5bc`](https://github.com/Khan/perseus/commit/8e0eb5bc7059c99054f9b13af9780c1103ebf5ee) Thanks [@handeyeco](https://github.com/handeyeco)! - Add tests for ExplanationEditor + +- [#883](https://github.com/Khan/perseus/pull/883) [`0b90e681`](https://github.com/Khan/perseus/commit/0b90e681b7c81f76bca419c879c0985e3fa1226f) Thanks [@handeyeco](https://github.com/handeyeco)! - Add tests for NumericInputEditor + +- Updated dependencies [[`c9db8185`](https://github.com/Khan/perseus/commit/c9db818510e2e0fd142c23298890dcad89a7549a), [`d09fdb98`](https://github.com/Khan/perseus/commit/d09fdb98963f23dbeb2009518513db671e0f09bb)]: + - @khanacademy/perseus@17.4.0 + ## 2.15.3 ### Patch Changes diff --git a/packages/perseus-editor/package.json b/packages/perseus-editor/package.json index 07f798ee80..6abe76d0d2 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.15.3", + "version": "2.16.2", "publishConfig": { "access": "public" }, @@ -24,18 +24,19 @@ "dependencies": { "@khanacademy/kas": "^0.3.7", "@khanacademy/kmath": "^0.1.8", - "@khanacademy/perseus": "^17.3.3", + "@khanacademy/math-input": "^16.5.0", + "@khanacademy/perseus": "^17.7.0", "@khanacademy/perseus-core": "1.4.1" }, "devDependencies": { - "@khanacademy/wonder-blocks-button": "^6.2.4", - "@khanacademy/wonder-blocks-clickable": "^4.0.11", + "@khanacademy/wonder-blocks-button": "^6.2.5", + "@khanacademy/wonder-blocks-clickable": "^4.0.12", "@khanacademy/wonder-blocks-color": "^3.0.0", - "@khanacademy/wonder-blocks-core": "^6.3.0", + "@khanacademy/wonder-blocks-core": "^6.3.1", "@khanacademy/wonder-blocks-i18n": "^2.0.2", - "@khanacademy/wonder-blocks-icon": "4.0.0", + "@khanacademy/wonder-blocks-icon": "4.0.1", "@khanacademy/wonder-blocks-spacing": "^4.0.1", - "@khanacademy/wonder-blocks-typography": "^2.1.9", + "@khanacademy/wonder-blocks-typography": "^2.1.10", "@khanacademy/wonder-stuff-core": "^1.5.1", "@phosphor-icons/core": "^2.0.2", "aphrodite": "^1.2.5", @@ -50,14 +51,14 @@ "underscore": "^1.4.4" }, "peerDependencies": { - "@khanacademy/wonder-blocks-button": "^6.2.4", - "@khanacademy/wonder-blocks-clickable": "^4.0.11", + "@khanacademy/wonder-blocks-button": "^6.2.5", + "@khanacademy/wonder-blocks-clickable": "^4.0.12", "@khanacademy/wonder-blocks-color": "^3.0.0", - "@khanacademy/wonder-blocks-core": "^6.3.0", + "@khanacademy/wonder-blocks-core": "^6.3.1", "@khanacademy/wonder-blocks-i18n": "^2.0.2", - "@khanacademy/wonder-blocks-icon": "4.0.0", + "@khanacademy/wonder-blocks-icon": "4.0.1", "@khanacademy/wonder-blocks-spacing": "^4.0.1", - "@khanacademy/wonder-blocks-typography": "^2.1.9", + "@khanacademy/wonder-blocks-typography": "^2.1.10", "@khanacademy/wonder-stuff-core": "^1.5.1", "@phosphor-icons/core": "^2.0.2", "aphrodite": "^1.2.5", diff --git a/packages/perseus-editor/src/__stories__/editor-page.stories.tsx b/packages/perseus-editor/src/__stories__/editor-page.stories.tsx new file mode 100644 index 0000000000..208bf80fda --- /dev/null +++ b/packages/perseus-editor/src/__stories__/editor-page.stories.tsx @@ -0,0 +1,65 @@ +import {action} from "@storybook/addon-actions"; +import * as React from "react"; + +import {EditorPage} from ".."; +import {registerAllWidgetsAndEditorsForTesting} from "../util/register-all-widgets-and-editors-for-testing"; + +import type { + DeviceType, + Hint, + PerseusAnswerArea, + PerseusRenderer, +} from "@khanacademy/perseus"; + +registerAllWidgetsAndEditorsForTesting(); // SIDE_EFFECTY!!!! :cry: + +export default { + title: "Perseus/Editor/EditorPage", +}; + +const onChangeAction = action("onChange"); + +export const Demo = (): React.ReactElement => { + const [previewDevice, setPreviewDevice] = + React.useState("phone"); + const [jsonMode, setJsonMode] = React.useState(false); + const [answerArea, setAnswerArea] = React.useState< + PerseusAnswerArea | undefined | null + >(); + const [question, setQuestion] = React.useState< + PerseusRenderer | undefined + >(); + const [hints, setHints] = React.useState | undefined>(); + + return ( + setPreviewDevice(newDevice)} + developerMode={true} + jsonMode={jsonMode} + answerArea={answerArea} + question={question} + hints={hints} + frameSource="about:blank" + previewURL="about:blank" + itemId="1" + onChange={(props) => { + onChangeAction(props); + + if ("jsonMode" in props) { + setJsonMode(props.jsonMode); + } + if ("answerArea" in props) { + setAnswerArea(props.answerArea); + } + if ("question" in props) { + setQuestion(props.question); + } + if ("hints" in props) { + setHints(props.hints); + } + }} + /> + ); +}; diff --git a/packages/perseus-editor/src/__stories__/editor.stories.tsx b/packages/perseus-editor/src/__stories__/editor.stories.tsx index 5a89e04160..b691ea84aa 100644 --- a/packages/perseus-editor/src/__stories__/editor.stories.tsx +++ b/packages/perseus-editor/src/__stories__/editor.stories.tsx @@ -4,19 +4,17 @@ import {View} from "@khanacademy/wonder-blocks-core"; import {action} from "@storybook/addon-actions"; import * as React from "react"; +import {Editor} from ".."; import SideBySide from "../../../../testing/side-by-side"; import {question1} from "../__testdata__/input-number.testdata"; -import Editor from "../editor"; import {registerAllWidgetsAndEditorsForTesting} from "../util/register-all-widgets-and-editors-for-testing"; import type {PerseusRenderer} from "@khanacademy/perseus"; -import "../styles/perseus-editor.less"; - registerAllWidgetsAndEditorsForTesting(); // SIDE_EFFECTY!!!! :cry: export default { - title: "Perseus/Editor", + title: "Perseus/Editor/Editor", }; export const Demo = (): React.ReactElement => { diff --git a/packages/perseus-editor/src/components/__tests__/blur-input.test.tsx b/packages/perseus-editor/src/components/__tests__/blur-input.test.tsx new file mode 100644 index 0000000000..1c7fe676ea --- /dev/null +++ b/packages/perseus-editor/src/components/__tests__/blur-input.test.tsx @@ -0,0 +1,67 @@ +import {render, screen} from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import * as React from "react"; + +import BlurInput from "../blur-input"; + +import "@testing-library/jest-dom"; // Imports custom matchers + +describe("BlurInput", () => { + it("should render", () => { + render( {}} />); + }); + + it("should call onChange prop on blur", () => { + // Arrange + const onChange = jest.fn(); + render(); + + // Act + userEvent.type(screen.getByRole("textbox"), "Hello Khan Academy!"); + userEvent.tab(); + + // Assert + expect(onChange).toHaveBeenCalledTimes(1); + }); + + it("should call onChange prop when typing into input", () => { + // Arrange + const onChange = jest.fn(); + render(); + + // Act + userEvent.type(screen.getByRole("textbox"), "Hello Khan Academy!"); + // NO TAB + + // Assert + expect(onChange).not.toHaveBeenCalledTimes(1); + }); + + it("should call onChange prop when pasting into input", () => { + // Arrange + const onChange = jest.fn(); + render(); + + // Act + userEvent.paste(screen.getByRole("textbox"), "Hello Khan Academy!"); + // NO TAB + + // Assert + expect(onChange).not.toHaveBeenCalledTimes(1); + }); + + it("should focus input through focus function", () => { + const ref = React.createRef(); + + // Arrange + render( + {}} />, + ); + + // Act + ref.current?.focus(); + + // Assert + expect(screen.getByRole("textbox")).toHaveFocus(); + }); +}); diff --git a/packages/perseus-editor/src/components/blur-input.tsx b/packages/perseus-editor/src/components/blur-input.tsx index 1412d44c06..e7a71a98c4 100644 --- a/packages/perseus-editor/src/components/blur-input.tsx +++ b/packages/perseus-editor/src/components/blur-input.tsx @@ -27,6 +27,8 @@ type State = { */ // eslint-disable-next-line react/no-unsafe class BlurInput extends React.Component { + input = React.createRef(); + constructor(props: Props) { super(props); this.state = {value: this.props.value}; @@ -45,9 +47,14 @@ class BlurInput extends React.Component { this.props.onChange(e.target.value); }; + focus() { + this.input.current?.focus(); + } + render(): React.ReactNode { return ( ; - // Only used in the perseus demos. Consider removing. + // "Power user" mode. Shows the raw JSON of the question. developerMode: boolean; // Source HTML for the iframe to render frameSource: string; @@ -53,15 +54,8 @@ type Props = { previewURL: string; }; -type PerseusJson = { - question: any; - answerArea: any; - hints: ReadonlyArray; - itemDataVersion?: Version; -}; - type State = { - json: PerseusJson; + json: PerseusItem; gradeMessage: string; wasAnswered: boolean; highlightLint: boolean; @@ -69,10 +63,11 @@ type State = { class EditorPage extends React.Component { _isMounted: boolean; - // @ts-expect-error - TS2564 - Property 'rendererMountNode' has no initializer and is not definitely assigned in the constructor. - rendererMountNode: HTMLDivElement; renderer: any; + itemEditor = React.createRef(); + hintsEditor = React.createRef(); + static defaultProps: { developerMode: boolean; jsonMode: boolean; @@ -108,7 +103,6 @@ class EditorPage extends React.Component { // this.isMounted() but is still considered an anti-pattern. this._isMounted = true; - this.rendererMountNode = document.createElement("div"); this.updateRenderer(); } @@ -160,9 +154,7 @@ class EditorPage extends React.Component { isMobile: touch, }; - // eslint-disable-next-line react/no-string-refs - // @ts-expect-error - TS2339 - Property 'triggerPreviewUpdate' does not exist on type 'ReactInstance'. - this.refs.itemEditor.triggerPreviewUpdate({ + this.itemEditor.current?.triggerPreviewUpdate({ type: "question", data: _({ item: this.serialize(), @@ -176,9 +168,7 @@ class EditorPage extends React.Component { paths: this.props.contentPaths || [], }, reviewMode: true, - // eslint-disable-next-line react/no-string-refs - // @ts-expect-error - TS2339 - Property 'getSaveWarnings' does not exist on type 'ReactInstance'. - legacyPerseusLint: this.refs.itemEditor.getSaveWarnings(), + legacyPerseusLint: this.itemEditor.current?.getSaveWarnings(), }).extend( _(this.props).pick( "workAreaSelector", @@ -198,25 +188,17 @@ class EditorPage extends React.Component { } getSaveWarnings(): any { - // eslint-disable-next-line react/no-string-refs - // @ts-expect-error - TS2339 - Property 'getSaveWarnings' does not exist on type 'ReactInstance'. - const issues1 = this.refs.itemEditor.getSaveWarnings(); - // eslint-disable-next-line react/no-string-refs - // @ts-expect-error - TS2339 - Property 'getSaveWarnings' does not exist on type 'ReactInstance'. - const issues2 = this.refs.hintsEditor.getSaveWarnings(); + const issues1 = this.itemEditor.current?.getSaveWarnings(); + const issues2 = this.hintsEditor.current?.getSaveWarnings(); return issues1.concat(issues2); } - serialize(options?: {keepDeletedWidgets?: boolean}): any | PerseusJson { + serialize(options?: {keepDeletedWidgets?: boolean}): any | PerseusItem { if (this.props.jsonMode) { return this.state.json; } - // eslint-disable-next-line react/no-string-refs - // @ts-expect-error - TS2339 - Property 'serialize' does not exist on type 'ReactInstance'. - return _.extend(this.refs.itemEditor.serialize(options), { - // eslint-disable-next-line react/no-string-refs - // @ts-expect-error - TS2339 - Property 'serialize' does not exist on type 'ReactInstance'. - hints: this.refs.hintsEditor.serialize(options), + return _.extend(this.itemEditor.current?.serialize(options), { + hints: this.hintsEditor.current?.serialize(options), }); } @@ -226,7 +208,7 @@ class EditorPage extends React.Component { this.props.onChange(newProps, cb, silent); }; - changeJSON: (newJson: PerseusJson) => void = (newJson: PerseusJson) => { + changeJSON: (newJson: PerseusItem) => void = (newJson: PerseusItem) => { this.setState({ json: newJson, }); @@ -308,8 +290,7 @@ class EditorPage extends React.Component { {(!this.props.developerMode || !this.props.jsonMode) && ( { {(!this.props.developerMode || !this.props.jsonMode) && ( = []; + const result: Array = []; // eslint-disable-next-line no-constant-condition while (true) { const match = regex.exec(str); @@ -106,7 +104,7 @@ const allMatches = function (regex: RegExp, str: string) { * markdown. */ const imageUrlsFromContent = function (content: string) { - return _.map(allMatches(IMAGE_REGEX, content), (capture) => capture[1]); + return allMatches(IMAGE_REGEX, content).map((capture) => capture[1]); }; type Props = Readonly<{ @@ -157,6 +155,9 @@ class Editor extends React.Component { deferredChange: any | null | undefined; widgetIds: any | null | undefined; + underlay = React.createRef(); + textarea = React.createRef(); + static defaultProps: DefaultProps = { content: "", placeholder: "", @@ -183,11 +184,14 @@ class Editor extends React.Component { // this.props.onChange during that, since it calls our parent's // setState this._sizeImages(this.props); - // eslint-disable-next-line react/no-string-refs - // @ts-expect-error - TS2769 - No overload matches this call. - $(ReactDOM.findDOMNode(this.refs.textarea)) + // NOTE(jeremy): We use the non-null assertion here (!) because refs + // are guaranteed to be up-to-date before componentDidMount or + // componentDidUpdate fires. + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + $(this.textarea.current!) // @ts-expect-error - TS2339 - Property 'on' does not exist on type 'JQueryStatic'. .on("copy cut", this._maybeCopyWidgets) + // @ts-expect-error - TS2769 - No overload matches this call. .on("paste", this._maybePasteWidgets); } @@ -200,9 +204,7 @@ class Editor extends React.Component { } componentDidUpdate(prevProps: Props) { - // TODO(alpert): Maybe fix React so this isn't necessary - // eslint-disable-next-line react/no-string-refs - const textarea = ReactDOM.findDOMNode(this.refs.textarea); + const textarea = this.textarea.current; // Slightly unorthodox method to ensure that programmatic text changes // are in the browser's undo stack. @@ -211,20 +213,11 @@ class Editor extends React.Component { // will return false. However at least in Firefox setting `value` on a // textbox clears the undo stack so we don't get unexpected undo // behavior. - if (this.lastUserValue !== null && textarea) { - /** - * TODO(somewhatabstract, JIRA-XXXX): - * textarea should be refined with an instanceof check to - * HTMLTextAreaElement so that these props are available. - */ - // @ts-expect-error - TS2339 - Property 'focus' does not exist on type 'Element | Text'. + if (this.lastUserValue != null && textarea) { textarea.focus(); - // @ts-expect-error - TS2339 - Property 'value' does not exist on type 'Element | Text'. textarea.value = this.lastUserValue; - // @ts-expect-error - TS2339 - Property 'selectionStart' does not exist on type 'Element | Text'. textarea.selectionStart = 0; - // @ts-expect-error - TS2339 - Property 'select' does not exist on type 'Element | Text'. - textarea.select(0, prevProps.content.length); + textarea.setSelectionRange(0, prevProps.content.length); if ( document.execCommand( "insertText", @@ -234,7 +227,6 @@ class Editor extends React.Component { ) { // This command is not implemented. Fall back to setting `value` // directly. - // @ts-expect-error - TS2339 - Property 'value' does not exist on type 'Element | Text'. textarea.value = this.props.content; } this.lastUserValue = null; @@ -298,11 +290,9 @@ class Editor extends React.Component { return; } - // eslint-disable-next-line react/no-string-refs - const textarea = this.refs.textarea; + const textarea = this.textarea.current; const re = new RegExp(widgetRegExp.replace("{id}", id), "gm"); - // @ts-expect-error - TS2339 - Property 'value' does not exist on type 'ReactInstance'. - this.props.onChange({content: textarea.value.replace(re, "")}); + this.props.onChange({content: textarea?.value.replace(re, "")}); }; /** @@ -455,11 +445,12 @@ class Editor extends React.Component { // Tab-completion of widgets. For example, to insert an image: // type `[[im`, then tab. if (e.key === "Tab") { - // eslint-disable-next-line react/no-string-refs - const textarea = ReactDOM.findDOMNode(this.refs.textarea); + // We're in an event handler attached to the textarea, so the ref + // can't be empty/undefined! (which is why its safe to use the + // non-null-assertion here. aka the `!` suffix) + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const textarea = this.textarea.current!; - // findDOMNode can also return Text, but we know it's an element. - // @ts-expect-error - TS2345 - Argument of type 'Element | Text | null' is not assignable to parameter of type 'HTMLTextAreaElement'. const word = Util.textarea.getWordBeforeCursor(textarea); const matches = word.string.toLowerCase().match(shortcutRegexp); @@ -650,9 +641,6 @@ class Editor extends React.Component { cursorRange: ReadonlyArray, widgetType: string, ) => { - // eslint-disable-next-line react/no-string-refs - const textarea = ReactDOM.findDOMNode(this.refs.textarea); - // Note: we have to use _.map here instead of Array::map // because the results of a .match might be null if no // widgets were found. @@ -715,11 +703,13 @@ class Editor extends React.Component { content: newContent, widgets: newWidgets, }, - function () { + () => { + if (!this.textarea.current) { + return; + } + Util.textarea.moveCursor( - // findDOMNode can return Text but we know this is Element - // @ts-expect-error - TS2345 - Argument of type 'Element | Text | null' is not assignable to parameter of type 'HTMLTextAreaElement'. - textarea, + this.textarea.current, // We want to put the cursor after the widget // and after any added newlines newContent.length - postlude.length, @@ -729,21 +719,21 @@ class Editor extends React.Component { }; _addWidget: (widgetType: string) => void = (widgetType: string) => { - // eslint-disable-next-line react/no-string-refs - const textarea = this.refs.textarea; + const textarea = this.textarea.current; + if (!textarea) { + return; + } this._addWidgetToContent( this.props.content, - // @ts-expect-error - TS2339 - Property 'selectionStart' does not exist on type 'ReactInstance'. | TS2339 - Property 'selectionEnd' does not exist on type 'ReactInstance'. [textarea.selectionStart, textarea.selectionEnd], widgetType, ); - // @ts-expect-error - TS2339 - Property 'focus' does not exist on type 'ReactInstance'. textarea.focus(); }; - addTemplate: (e: React.SyntheticEvent) => void = ( - e: React.SyntheticEvent, + addTemplate: (e: React.SyntheticEvent) => void = ( + e: React.SyntheticEvent, ) => { const templateType = e.currentTarget.value; if (templateType === "") { @@ -825,32 +815,17 @@ class Editor extends React.Component { }; focus: () => void = () => { - // eslint-disable-next-line react/no-string-refs - const textarea = ReactDOM.findDOMNode(this.refs.textarea); + const textarea = this.textarea.current; if (textarea) { - /** - * TODO(somewhatabstract, JIRA-XXXX): - * textarea should be refined with an instanceof check to - * HTMLTextAreaElement so that these props are available. - */ - // @ts-expect-error - TS2339 - Property 'focus' does not exist on type 'Element | Text'. textarea.focus(); } }; focusAndMoveToEnd: () => void = () => { this.focus(); - // eslint-disable-next-line react/no-string-refs - const textarea = ReactDOM.findDOMNode(this.refs.textarea); + const textarea = this.textarea.current; if (textarea) { - /** - * TODO(somewhatabstract, JIRA-XXXX): - * textarea should be refined with an instanceof check to - * HTMLTextAreaElement so that these props are available. - */ - // @ts-expect-error - TS2339 - Property 'selectionStart' does not exist on type 'Element | Text'. | TS2339 - Property 'value' does not exist on type 'Element | Text'. textarea.selectionStart = textarea.value.length; - // @ts-expect-error - TS2339 - Property 'selectionEnd' does not exist on type 'Element | Text'. | TS2339 - Property 'value' does not exist on type 'Element | Text'. textarea.selectionEnd = textarea.value.length; } }; @@ -1010,7 +985,6 @@ class Editor extends React.Component { const insertTemplateString = "Insert template\u2026"; templatesDropDown = ( - // @ts-expect-error - TS2322 - Type '(e: SyntheticEvent) => void' is not assignable to type 'ChangeEventHandler'.