From 40d2ebb75fdadfb361330236e0fb9e54a32d0fc2 Mon Sep 17 00:00:00 2001 From: Jeremy Wiebe Date: Thu, 21 Nov 2024 15:18:11 -0800 Subject: [PATCH 01/42] Numeric-Input: Extract validation from scorer (#1882) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary: This PR extracts validation from the `numeric-input`'s scorer function. In reality, it's an empty function, but it follows our conventions for having the scorer call a validator first. I've created the standard tests to ensure that scorer calls the validator. Issue: LEMS-2607 ## Test plan: `yarn test` `yarn typecheck` Author: jeremywiebe Reviewers: handeyeco, jeremywiebe, Myranae Required Reviewers: Approved By: handeyeco Checks: ✅ Publish npm snapshot (ubuntu-latest, 20.x), ✅ Check builds for changes in size (ubuntu-latest, 20.x), ✅ Lint, Typecheck, Format, and Test (ubuntu-latest, 20.x), ✅ Check for .changeset entries for all changed files (ubuntu-latest, 20.x), ✅ Cypress (ubuntu-latest, 20.x), ✅ gerald, ✅ Publish npm snapshot (ubuntu-latest, 20.x), ✅ Lint, Typecheck, Format, and Test (ubuntu-latest, 20.x), ✅ Cypress (ubuntu-latest, 20.x), ✅ Check for .changeset entries for all changed files (ubuntu-latest, 20.x), ✅ Check builds for changes in size (ubuntu-latest, 20.x), ✅ Publish Storybook to Chromatic (ubuntu-latest, 20.x), ✅ gerald Pull Request URL: https://github.com/Khan/perseus/pull/1882 --- .changeset/honest-avocados-accept.md | 5 +++++ .../numeric-input/score-numeric-input.test.ts | 20 +++++++++---------- .../numeric-input/score-numeric-input.ts | 2 +- 3 files changed, 16 insertions(+), 11 deletions(-) create mode 100644 .changeset/honest-avocados-accept.md diff --git a/.changeset/honest-avocados-accept.md b/.changeset/honest-avocados-accept.md new file mode 100644 index 0000000000..4d7d3129cf --- /dev/null +++ b/.changeset/honest-avocados-accept.md @@ -0,0 +1,5 @@ +--- +"@khanacademy/perseus": minor +--- + +Introduces a validation function for the numeric-input widget (extracted from numeric-input scoring function). diff --git a/packages/perseus/src/widgets/numeric-input/score-numeric-input.test.ts b/packages/perseus/src/widgets/numeric-input/score-numeric-input.test.ts index c6ee90ec2c..de2a226656 100644 --- a/packages/perseus/src/widgets/numeric-input/score-numeric-input.test.ts +++ b/packages/perseus/src/widgets/numeric-input/score-numeric-input.test.ts @@ -28,11 +28,11 @@ describe("static function validate", () => { coefficient: true, }; - const useInput = { + const userInput = { currentValue: "1", } as const; - const score = scoreNumericInput(useInput, rubric, mockStrings); + const score = scoreNumericInput(userInput, rubric, mockStrings); expect(score).toHaveBeenAnsweredCorrectly(); }); @@ -52,11 +52,11 @@ describe("static function validate", () => { coefficient: true, }; - const useInput = { + const userInput = { currentValue: "sadasdfas", } as const; - const score = scoreNumericInput(useInput, rubric, mockStrings); + const score = scoreNumericInput(userInput, rubric, mockStrings); expect(score).toHaveInvalidInput( "We could not understand your answer. Please check your answer for extra text or symbols.", @@ -143,11 +143,11 @@ describe("static function validate", () => { coefficient: true, }; - const useInput = { + const userInput = { currentValue: "1.0", } as const; - const score = scoreNumericInput(useInput, rubric, mockStrings); + const score = scoreNumericInput(userInput, rubric, mockStrings); expect(score).toHaveBeenAnsweredCorrectly(); }); @@ -167,11 +167,11 @@ describe("static function validate", () => { coefficient: true, }; - const useInput = { + const userInput = { currentValue: "1.3", } as const; - const score = scoreNumericInput(useInput, rubric, mockStrings); + const score = scoreNumericInput(userInput, rubric, mockStrings); expect(score).toHaveBeenAnsweredIncorrectly(); }); @@ -191,11 +191,11 @@ describe("static function validate", () => { coefficient: true, }; - const useInput = { + const userInput = { currentValue: "1.12", } as const; - const score = scoreNumericInput(useInput, rubric, mockStrings); + const score = scoreNumericInput(userInput, rubric, mockStrings); expect(score).toHaveBeenAnsweredCorrectly(); }); diff --git a/packages/perseus/src/widgets/numeric-input/score-numeric-input.ts b/packages/perseus/src/widgets/numeric-input/score-numeric-input.ts index 46a98eaf12..d4637185ec 100644 --- a/packages/perseus/src/widgets/numeric-input/score-numeric-input.ts +++ b/packages/perseus/src/widgets/numeric-input/score-numeric-input.ts @@ -61,7 +61,7 @@ export function maybeParsePercentInput( return value / 100; } - // Otherwise, we return input valuåe (number) stripped of the "%". + // Otherwise, we return input value (number) stripped of the "%". return value; } From f43edd42ccfacd1500d2f73ccb0d3f8dce777173 Mon Sep 17 00:00:00 2001 From: Tamara <60857422+Myranae@users.noreply.github.com> Date: Tue, 26 Nov 2024 14:27:44 -0600 Subject: [PATCH 02/42] Orderer: Extract validation out of scoring (#1869) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary: To complete server-side scoring, we are separating out validation logic from scoring logic. This separates that logic and updates associated tests. Issue: LEMS-2603 ## Test plan: - Confirm all checks pass - Confirm widget still works as expected Author: Myranae Reviewers: jeremywiebe, handeyeco, Myranae Required Reviewers: Approved By: jeremywiebe, handeyeco Checks: ✅ Publish npm snapshot (ubuntu-latest, 20.x), ✅ Lint, Typecheck, Format, and Test (ubuntu-latest, 20.x), ✅ Cypress (ubuntu-latest, 20.x), ✅ Check for .changeset entries for all changed files (ubuntu-latest, 20.x), ✅ Check builds for changes in size (ubuntu-latest, 20.x), ✅ gerald, ✅ Publish npm snapshot (ubuntu-latest, 20.x), ✅ Lint, Typecheck, Format, and Test (ubuntu-latest, 20.x), ✅ Cypress (ubuntu-latest, 20.x), ✅ Check for .changeset entries for all changed files (ubuntu-latest, 20.x), ✅ Check builds for changes in size (ubuntu-latest, 20.x), ✅ gerald, ✅ Publish npm snapshot (ubuntu-latest, 20.x), ✅ Lint, Typecheck, Format, and Test (ubuntu-latest, 20.x), ✅ Cypress (ubuntu-latest, 20.x), ✅ Check for .changeset entries for all changed files (ubuntu-latest, 20.x), ✅ Check builds for changes in size (ubuntu-latest, 20.x), ✅ Publish Storybook to Chromatic (ubuntu-latest, 20.x), ✅ gerald Pull Request URL: https://github.com/Khan/perseus/pull/1869 --- .changeset/sharp-radios-burn.md | 5 +++ .../src/widgets/orderer/score-orderer.test.ts | 43 ++++++++++++++++--- .../src/widgets/orderer/score-orderer.ts | 12 +++--- .../widgets/orderer/validate-orderer.test.ts | 31 +++++++++++++ .../src/widgets/orderer/validate-orderer.ts | 23 ++++++++++ 5 files changed, 102 insertions(+), 12 deletions(-) create mode 100644 .changeset/sharp-radios-burn.md create mode 100644 packages/perseus/src/widgets/orderer/validate-orderer.test.ts create mode 100644 packages/perseus/src/widgets/orderer/validate-orderer.ts diff --git a/.changeset/sharp-radios-burn.md b/.changeset/sharp-radios-burn.md new file mode 100644 index 0000000000..8517ff1850 --- /dev/null +++ b/.changeset/sharp-radios-burn.md @@ -0,0 +1,5 @@ +--- +"@khanacademy/perseus": minor +--- + +Split out validation function for the `orderer` widget. This can be used to check if the user has ordered at least one option, confirming the question is ready to be scored. diff --git a/packages/perseus/src/widgets/orderer/score-orderer.test.ts b/packages/perseus/src/widgets/orderer/score-orderer.test.ts index dd4478a188..a5595f5249 100644 --- a/packages/perseus/src/widgets/orderer/score-orderer.test.ts +++ b/packages/perseus/src/widgets/orderer/score-orderer.test.ts @@ -1,12 +1,13 @@ import {question1} from "./orderer.testdata"; import {scoreOrderer} from "./score-orderer"; +import * as OrdererValidator from "./validate-orderer"; import type { PerseusOrdererRubric, PerseusOrdererUserInput, } from "../../validation.types"; -describe("ordererValiator", () => { +describe("scoreOrderer", () => { it("is correct when the userInput is in the same order and is the same length as the rubric's correctOption content items", () => { // Arrange const rubric: PerseusOrdererRubric = @@ -21,7 +22,7 @@ describe("ordererValiator", () => { // Act const result = scoreOrderer(userInput, rubric); - // assert + // Assert expect(result).toHaveBeenAnsweredCorrectly(); }); @@ -37,7 +38,7 @@ describe("ordererValiator", () => { // Act const result = scoreOrderer(userInput, rubric); - // assert + // Assert expect(result).toHaveBeenAnsweredIncorrectly(); }); @@ -53,12 +54,41 @@ describe("ordererValiator", () => { // Act const result = scoreOrderer(userInput, rubric); - // assert + // Assert expect(result).toHaveBeenAnsweredIncorrectly(); }); - it("is invalid when the when the user has not started ordering the options and current is empty", () => { + it("should be correctly answerable if validation passes", () => { // Arrange + const mockValidator = jest + .spyOn(OrdererValidator, "default") + .mockReturnValue(null); + + const rubric: PerseusOrdererRubric = + question1.widgets["orderer 1"].options; + + const userInput: PerseusOrdererUserInput = { + current: question1.widgets["orderer 1"].options.correctOptions.map( + (option) => option.content, + ), + }; + // Act + const result = scoreOrderer(userInput, rubric); + + // Assert + expect(mockValidator).toHaveBeenCalledWith(userInput); + expect(result).toHaveBeenAnsweredCorrectly(); + }); + + it("should return an invalid response if validation fails", () => { + // Arrange + const mockValidator = jest + .spyOn(OrdererValidator, "default") + .mockReturnValue({ + type: "invalid", + message: null, + }); + const rubric: PerseusOrdererRubric = question1.widgets["orderer 1"].options; @@ -69,7 +99,8 @@ describe("ordererValiator", () => { // Act const result = scoreOrderer(userInput, rubric); - // assert + // Assert + expect(mockValidator).toHaveBeenCalledWith(userInput); expect(result).toHaveInvalidInput(); }); }); diff --git a/packages/perseus/src/widgets/orderer/score-orderer.ts b/packages/perseus/src/widgets/orderer/score-orderer.ts index a887bb5381..a1d5336ccf 100644 --- a/packages/perseus/src/widgets/orderer/score-orderer.ts +++ b/packages/perseus/src/widgets/orderer/score-orderer.ts @@ -1,5 +1,7 @@ import _ from "underscore"; +import validateOrderer from "./validate-orderer"; + import type {PerseusScore} from "../../types"; import type { PerseusOrdererRubric, @@ -10,16 +12,14 @@ export function scoreOrderer( userInput: PerseusOrdererUserInput, rubric: PerseusOrdererRubric, ): PerseusScore { - if (userInput.current.length === 0) { - return { - type: "invalid", - message: null, - }; + const validateError = validateOrderer(userInput); + if (validateError) { + return validateError; } const correct = _.isEqual( userInput.current, - _.pluck(rubric.correctOptions, "content"), + rubric.correctOptions.map((option) => option.content), ); return { diff --git a/packages/perseus/src/widgets/orderer/validate-orderer.test.ts b/packages/perseus/src/widgets/orderer/validate-orderer.test.ts new file mode 100644 index 0000000000..7f48781dda --- /dev/null +++ b/packages/perseus/src/widgets/orderer/validate-orderer.test.ts @@ -0,0 +1,31 @@ +import validateOrderer from "./validate-orderer"; + +import type {PerseusOrdererUserInput} from "../../validation.types"; + +describe("validateOrderer", () => { + it("is invalid when the user has not started ordering the options and current is empty", () => { + // Arrange + const userInput: PerseusOrdererUserInput = { + current: [], + }; + + // Act + const result = validateOrderer(userInput); + + // Assert + expect(result).toHaveInvalidInput(); + }); + + it("is null when the user has started ordering the options and current has at least one option", () => { + // Arrange + const userInput: PerseusOrdererUserInput = { + current: ["$10.9$"], + }; + + // Act + const result = validateOrderer(userInput); + + // Assert + expect(result).toBeNull(); + }); +}); diff --git a/packages/perseus/src/widgets/orderer/validate-orderer.ts b/packages/perseus/src/widgets/orderer/validate-orderer.ts new file mode 100644 index 0000000000..1d28599271 --- /dev/null +++ b/packages/perseus/src/widgets/orderer/validate-orderer.ts @@ -0,0 +1,23 @@ +import type {PerseusScore} from "../../types"; +import type {PerseusOrdererUserInput} from "../../validation.types"; + +/** + * Checks user input from the orderer widget to see if the user has started + * ordering the options, making the widget scorable. + * @param userInput + * @see `scoreOrderer` for more details. + */ +function validateOrderer( + userInput: PerseusOrdererUserInput, +): Extract | null { + if (userInput.current.length === 0) { + return { + type: "invalid", + message: null, + }; + } + + return null; +} + +export default validateOrderer; From 0bd4270ade576bec1ac0c86b251f972a2c354056 Mon Sep 17 00:00:00 2001 From: Tamara <60857422+Myranae@users.noreply.github.com> Date: Tue, 26 Nov 2024 14:31:00 -0600 Subject: [PATCH 03/42] Sorter: Extract validation out of scoring (#1876) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary: To complete server-side scoring, we are separating out validation logic from scoring logic. This separates that logic for the sorter widget and updates associated tests. Issue: LEMS-2605 ## Test plan: - Confirm all checks pass - Confirm widget still works as expected via Storybook Author: Myranae Reviewers: jeremywiebe, handeyeco, Myranae Required Reviewers: Approved By: jeremywiebe, handeyeco Checks: ✅ Lint, Typecheck, Format, and Test (ubuntu-latest, 20.x), ❌ Publish npm snapshot (ubuntu-latest, 20.x), ✅ Cypress (ubuntu-latest, 20.x), ✅ Check for .changeset entries for all changed files (ubuntu-latest, 20.x), ✅ Check builds for changes in size (ubuntu-latest, 20.x), ✅ gerald, ✅ Publish npm snapshot (ubuntu-latest, 20.x), ✅ Lint, Typecheck, Format, and Test (ubuntu-latest, 20.x), ✅ Check for .changeset entries for all changed files (ubuntu-latest, 20.x), ✅ Cypress (ubuntu-latest, 20.x), ✅ Publish Storybook to Chromatic (ubuntu-latest, 20.x), ✅ Check builds for changes in size (ubuntu-latest, 20.x), ✅ gerald Pull Request URL: https://github.com/Khan/perseus/pull/1876 --- .changeset/sweet-trainers-drop.md | 5 ++ .../src/widgets/sorter/score-sorter.test.ts | 47 ++++++++++++++++++- .../src/widgets/sorter/score-sorter.ts | 16 ++----- .../widgets/sorter/validate-sorter.test.ts | 33 +++++++++++++ .../src/widgets/sorter/validate-sorter.ts | 29 ++++++++++++ 5 files changed, 118 insertions(+), 12 deletions(-) create mode 100644 .changeset/sweet-trainers-drop.md create mode 100644 packages/perseus/src/widgets/sorter/validate-sorter.test.ts create mode 100644 packages/perseus/src/widgets/sorter/validate-sorter.ts diff --git a/.changeset/sweet-trainers-drop.md b/.changeset/sweet-trainers-drop.md new file mode 100644 index 0000000000..9291f281b2 --- /dev/null +++ b/.changeset/sweet-trainers-drop.md @@ -0,0 +1,5 @@ +--- +"@khanacademy/perseus": minor +--- + +Split out validation function for the `sorter` widget. This can be used to check if the user has made any changes to the sorting order, confirming whether or not the question can be scored. diff --git a/packages/perseus/src/widgets/sorter/score-sorter.test.ts b/packages/perseus/src/widgets/sorter/score-sorter.test.ts index 6504fb3a8b..8420df08e0 100644 --- a/packages/perseus/src/widgets/sorter/score-sorter.test.ts +++ b/packages/perseus/src/widgets/sorter/score-sorter.test.ts @@ -1,4 +1,5 @@ import scoreSorter from "./score-sorter"; +import * as SorterValidator from "./validate-sorter"; import type { PerseusSorterRubric, @@ -7,6 +8,7 @@ import type { describe("scoreSorter", () => { it("is correct when the user input values are in the order defined in the rubric", () => { + // Arrange const userInput: PerseusSorterUserInput = { options: ["$0.005$ kilograms", "$15$ grams", "$55$ grams"], changed: true, @@ -14,11 +16,16 @@ describe("scoreSorter", () => { const rubric: PerseusSorterRubric = { correct: ["$0.005$ kilograms", "$15$ grams", "$55$ grams"], }; + + // Act const result = scoreSorter(userInput, rubric); + + // Assert expect(result).toHaveBeenAnsweredCorrectly(); }); it("is incorrect when the user input values are not in the order defined in the rubric", () => { + // Arrange const userInput: PerseusSorterUserInput = { options: ["$15$ grams", "$55$ grams", "$0.005$ kilograms"], changed: true, @@ -26,11 +33,21 @@ describe("scoreSorter", () => { const rubric: PerseusSorterRubric = { correct: ["$0.005$ kilograms", "$15$ grams", "$55$ grams"], }; + + // Act const result = scoreSorter(userInput, rubric); + + // Assert expect(result).toHaveBeenAnsweredIncorrectly(); }); - it("is invalid when the user has not made any changes", () => { + it("should abort if validator returns invalid", () => { + // Arrange + // Mock validator saying input is invalid + const mockValidate = jest + .spyOn(SorterValidator, "default") + .mockReturnValue({type: "invalid", message: null}); + const userInput: PerseusSorterUserInput = { options: ["$15$ grams", "$55$ grams", "$0.005$ kilograms"], changed: false, @@ -38,7 +55,35 @@ describe("scoreSorter", () => { const rubric: PerseusSorterRubric = { correct: ["$0.005$ kilograms", "$15$ grams", "$55$ grams"], }; + + // Act const result = scoreSorter(userInput, rubric); + + // Assert + expect(mockValidate).toHaveBeenCalledWith(userInput); expect(result).toHaveInvalidInput(); }); + + it("should score if validator passes", () => { + // Arrange + // Mock validator saying "all good" + const mockValidate = jest + .spyOn(SorterValidator, "default") + .mockReturnValue(null); + + const userInput: PerseusSorterUserInput = { + options: ["$0.005$ kilograms", "$15$ grams", "$55$ grams"], + changed: true, + }; + const rubric: PerseusSorterRubric = { + correct: ["$0.005$ kilograms", "$15$ grams", "$55$ grams"], + }; + + // Act + const result = scoreSorter(userInput, rubric); + + // Assert + expect(mockValidate).toHaveBeenCalledWith(userInput); + expect(result).toHaveBeenAnsweredCorrectly(); + }); }); diff --git a/packages/perseus/src/widgets/sorter/score-sorter.ts b/packages/perseus/src/widgets/sorter/score-sorter.ts index 738a9e8481..f86579d3e9 100644 --- a/packages/perseus/src/widgets/sorter/score-sorter.ts +++ b/packages/perseus/src/widgets/sorter/score-sorter.ts @@ -1,5 +1,7 @@ import Util from "../../util"; +import validateSorter from "./validate-sorter"; + import type {PerseusScore} from "../../types"; import type { PerseusSorterRubric, @@ -10,17 +12,9 @@ function scoreSorter( userInput: PerseusSorterUserInput, rubric: PerseusSorterRubric, ): PerseusScore { - // If the sorter widget hasn't been changed yet, we treat it as "empty" which - // prevents the "Check" button from becoming active. We want the user - // to make a change before trying to move forward. This makes an - // assumption that the initial order isn't the correct order! However, - // this should be rare if it happens, and interacting with the list - // will enable the button, so they won't be locked out of progressing. - if (!userInput.changed) { - return { - type: "invalid", - message: null, - }; + const validationError = validateSorter(userInput); + if (validationError) { + return validationError; } const correct = Util.deepEq(userInput.options, rubric.correct); diff --git a/packages/perseus/src/widgets/sorter/validate-sorter.test.ts b/packages/perseus/src/widgets/sorter/validate-sorter.test.ts new file mode 100644 index 0000000000..c79beca6c0 --- /dev/null +++ b/packages/perseus/src/widgets/sorter/validate-sorter.test.ts @@ -0,0 +1,33 @@ +import validateSorter from "./validate-sorter"; + +import type {PerseusSorterUserInput} from "../../validation.types"; + +describe("validateSorter", () => { + it("is invalid when the user has not made any changes", () => { + // Arrange + const userInput: PerseusSorterUserInput = { + options: ["$15$ grams", "$55$ grams", "$0.005$ kilograms"], + changed: false, + }; + + // Act + const result = validateSorter(userInput); + + // Assert + expect(result).toHaveInvalidInput(); + }); + + it("returns null when the user has made any changes", () => { + // Arrange + const userInput: PerseusSorterUserInput = { + options: ["$55$ grams", "$0.005$ kilograms", "$15$ grams"], + changed: true, + }; + + // Act + const result = validateSorter(userInput); + + // Assert + expect(result).toBeNull(); + }); +}); diff --git a/packages/perseus/src/widgets/sorter/validate-sorter.ts b/packages/perseus/src/widgets/sorter/validate-sorter.ts new file mode 100644 index 0000000000..8e76066ad4 --- /dev/null +++ b/packages/perseus/src/widgets/sorter/validate-sorter.ts @@ -0,0 +1,29 @@ +import type {PerseusScore} from "../../types"; +import type {PerseusSorterUserInput} from "../../validation.types"; + +/** + * Checks user input for the sorter widget to ensure that the user has made + * changes before attempting to score the widget. + * @param userInput + * @see 'scoreSorter' in 'packages/perseus/src/widgets/sorter/score-sorter.ts' + * for more details on how the sorter widget is scored. + */ +function validateSorter( + userInput: PerseusSorterUserInput, +): Extract | null { + // If the sorter widget hasn't been changed yet, we treat it as "empty" which + // prevents the "Check" button from becoming active. We want the user + // to make a change before trying to move forward. This makes an + // assumption that the initial order isn't the correct order! However, + // this should be rare if it happens, and interacting with the list + // will enable the button, so they won't be locked out of progressing. + if (!userInput.changed) { + return { + type: "invalid", + message: null, + }; + } + return null; +} + +export default validateSorter; From 3a9b5921bff7ae038f59ecb6817babd2b21df0bb Mon Sep 17 00:00:00 2001 From: Tamara <60857422+Myranae@users.noreply.github.com> Date: Tue, 26 Nov 2024 14:44:38 -0600 Subject: [PATCH 04/42] Dropdown: Extract validation out of scoring (#1898) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary: To complete server-side scoring, we are separating out validation logic from scoring logic. This PR separates that logic and updates associated tests. Issue: LEMS-2597 ## Test plan: - Confirm all checks pass - Confirm widget still works as expected Author: Myranae Reviewers: Myranae, jeremywiebe, handeyeco Required Reviewers: Approved By: jeremywiebe Checks: ✅ Check builds for changes in size (ubuntu-latest, 20.x), ✅ Publish npm snapshot (ubuntu-latest, 20.x), ✅ Cypress (ubuntu-latest, 20.x), ✅ Check for .changeset entries for all changed files (ubuntu-latest, 20.x), ✅ Publish Storybook to Chromatic (ubuntu-latest, 20.x), ✅ Lint, Typecheck, Format, and Test (ubuntu-latest, 20.x), ✅ gerald Pull Request URL: https://github.com/Khan/perseus/pull/1898 --- .changeset/famous-horses-grab.md | 5 +++ .../widgets/dropdown/score-dropdown.test.ts | 24 +++----------- .../src/widgets/dropdown/score-dropdown.ts | 13 ++++---- .../dropdown/validate-dropdown.test.ts | 31 +++++++++++++++++++ .../src/widgets/dropdown/validate-dropdown.ts | 20 ++++++++++++ 5 files changed, 66 insertions(+), 27 deletions(-) create mode 100644 .changeset/famous-horses-grab.md create mode 100644 packages/perseus/src/widgets/dropdown/validate-dropdown.test.ts create mode 100644 packages/perseus/src/widgets/dropdown/validate-dropdown.ts diff --git a/.changeset/famous-horses-grab.md b/.changeset/famous-horses-grab.md new file mode 100644 index 0000000000..827d88abf1 --- /dev/null +++ b/.changeset/famous-horses-grab.md @@ -0,0 +1,5 @@ +--- +"@khanacademy/perseus": minor +--- + +Introduces a validation function for the dropdown widget (extracted from dropdown scoring function). diff --git a/packages/perseus/src/widgets/dropdown/score-dropdown.test.ts b/packages/perseus/src/widgets/dropdown/score-dropdown.test.ts index 68030226b6..d583aba2fa 100644 --- a/packages/perseus/src/widgets/dropdown/score-dropdown.test.ts +++ b/packages/perseus/src/widgets/dropdown/score-dropdown.test.ts @@ -7,22 +7,6 @@ import type { } from "../../validation.types"; describe("scoreDropdown", () => { - it("returns invalid for user input of 0", () => { - // Arrange - const userInput: PerseusDropdownUserInput = { - value: 0, - }; - const rubric: PerseusDropdownRubric = { - choices: question1.widgets["dropdown 1"].options.choices, - }; - - // Act - const result = scoreDropdown(userInput, rubric); - - // Assert - expect(result).toHaveInvalidInput(); - }); - it("returns 0 points for incorrect answer", () => { // Arrange const userInput: PerseusDropdownUserInput = { @@ -33,10 +17,10 @@ describe("scoreDropdown", () => { }; // Act - const result = scoreDropdown(userInput, rubric); + const score = scoreDropdown(userInput, rubric); // Assert - expect(result).toHaveBeenAnsweredIncorrectly(); + expect(score).toHaveBeenAnsweredIncorrectly(); }); it("returns 1 point for correct answer", () => { @@ -49,9 +33,9 @@ describe("scoreDropdown", () => { }; // Act - const result = scoreDropdown(userInput, rubric); + const score = scoreDropdown(userInput, rubric); // Assert - expect(result).toHaveBeenAnsweredCorrectly(); + expect(score).toHaveBeenAnsweredCorrectly(); }); }); diff --git a/packages/perseus/src/widgets/dropdown/score-dropdown.ts b/packages/perseus/src/widgets/dropdown/score-dropdown.ts index 129a7857f0..76ec2f96b1 100644 --- a/packages/perseus/src/widgets/dropdown/score-dropdown.ts +++ b/packages/perseus/src/widgets/dropdown/score-dropdown.ts @@ -1,3 +1,5 @@ +import validateDropdown from "./validate-dropdown"; + import type {PerseusScore} from "../../types"; import type { PerseusDropdownRubric, @@ -8,14 +10,11 @@ function scoreDropdown( userInput: PerseusDropdownUserInput, rubric: PerseusDropdownRubric, ): PerseusScore { - const selected = userInput.value; - if (selected === 0) { - return { - type: "invalid", - message: null, - }; + const validationError = validateDropdown(userInput); + if (validationError) { + return validationError; } - const correct = rubric.choices[selected - 1].correct; + const correct = rubric.choices[userInput.value - 1].correct; return { type: "points", earned: correct ? 1 : 0, diff --git a/packages/perseus/src/widgets/dropdown/validate-dropdown.test.ts b/packages/perseus/src/widgets/dropdown/validate-dropdown.test.ts new file mode 100644 index 0000000000..5a457ef7f4 --- /dev/null +++ b/packages/perseus/src/widgets/dropdown/validate-dropdown.test.ts @@ -0,0 +1,31 @@ +import validateDropdown from "./validate-dropdown"; + +import type {PerseusDropdownUserInput} from "../../validation.types"; + +describe("validateDropdown", () => { + it("returns invalid for invalid input (user input of 0)", () => { + // Arrange + const userInput: PerseusDropdownUserInput = { + value: 0, + }; + + // Act + const validationError = validateDropdown(userInput); + + // Assert + expect(validationError).toHaveInvalidInput(); + }); + + it("returns null for a valid answer (user input that is not 0)", () => { + // Arrange + const userInput: PerseusDropdownUserInput = { + value: 2, + }; + + // Act + const validationError = validateDropdown(userInput); + + // Assert + expect(validationError).toBeNull(); + }); +}); diff --git a/packages/perseus/src/widgets/dropdown/validate-dropdown.ts b/packages/perseus/src/widgets/dropdown/validate-dropdown.ts new file mode 100644 index 0000000000..e1b843151c --- /dev/null +++ b/packages/perseus/src/widgets/dropdown/validate-dropdown.ts @@ -0,0 +1,20 @@ +import type {PerseusScore} from "../../types"; +import type {PerseusDropdownUserInput} from "../../validation.types"; + +/** + * Checks if the user has selected an item from the dropdown before scoring. + * This is shown with a userInput value / index other than 0. + */ +function validateDropdown( + userInput: PerseusDropdownUserInput, +): Extract | null { + if (userInput.value === 0) { + return { + type: "invalid", + message: null, + }; + } + return null; +} + +export default validateDropdown; From 2437ce61bae1aef2db28e89956aa73463ada16cc Mon Sep 17 00:00:00 2001 From: Tamara <60857422+Myranae@users.noreply.github.com> Date: Tue, 26 Nov 2024 14:45:36 -0600 Subject: [PATCH 05/42] Plotter: Extract validation out of scoring (#1899) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary: To complete server-side scoring, we are separating out validation logic from scoring logic. This PR separates that logic and associated tests for the plotter widget. Issue: LEMS-2604 ## Test plan: - Confirm checks pass - Confirm widget still works as expected Author: Myranae Reviewers: jeremywiebe, Myranae, handeyeco Required Reviewers: Approved By: jeremywiebe Checks: ✅ Publish npm snapshot (ubuntu-latest, 20.x), ✅ Check builds for changes in size (ubuntu-latest, 20.x), ✅ Lint, Typecheck, Format, and Test (ubuntu-latest, 20.x), ✅ Cypress (ubuntu-latest, 20.x), ✅ Check for .changeset entries for all changed files (ubuntu-latest, 20.x), ✅ gerald, ✅ Publish npm snapshot (ubuntu-latest, 20.x), ✅ Check builds for changes in size (ubuntu-latest, 20.x), ✅ Lint, Typecheck, Format, and Test (ubuntu-latest, 20.x), ✅ Cypress (ubuntu-latest, 20.x), ✅ Publish Storybook to Chromatic (ubuntu-latest, 20.x), ✅ Check for .changeset entries for all changed files (ubuntu-latest, 20.x), ✅ gerald Pull Request URL: https://github.com/Khan/perseus/pull/1899 --- .changeset/nice-fans-swim.md | 5 ++ packages/perseus/src/validation.types.ts | 13 +++- .../perseus/src/widgets/plotter/plotter.tsx | 4 +- .../src/widgets/plotter/score-plotter.test.ts | 63 +++++-------------- .../src/widgets/plotter/score-plotter.ts | 16 ++--- .../widgets/plotter/validate-plotter.test.ts | 38 +++++++++++ .../src/widgets/plotter/validate-plotter.ts | 30 +++++++++ 7 files changed, 107 insertions(+), 62 deletions(-) create mode 100644 .changeset/nice-fans-swim.md create mode 100644 packages/perseus/src/widgets/plotter/validate-plotter.test.ts create mode 100644 packages/perseus/src/widgets/plotter/validate-plotter.ts diff --git a/.changeset/nice-fans-swim.md b/.changeset/nice-fans-swim.md new file mode 100644 index 0000000000..04c7fe08ce --- /dev/null +++ b/.changeset/nice-fans-swim.md @@ -0,0 +1,5 @@ +--- +"@khanacademy/perseus": minor +--- + +Introduces a validation function for the plotter widget (extracted from the scoring function). diff --git a/packages/perseus/src/validation.types.ts b/packages/perseus/src/validation.types.ts index 82f5b58062..01cf14af45 100644 --- a/packages/perseus/src/validation.types.ts +++ b/packages/perseus/src/validation.types.ts @@ -41,7 +41,6 @@ import type { PerseusNumberLineWidgetOptions, PerseusNumericInputAnswer, PerseusOrdererWidgetOptions, - PerseusPlotterWidgetOptions, PerseusRadioChoice, PerseusGraphCorrectType, } from "./perseus-types"; @@ -188,7 +187,15 @@ export type PerseusOrdererUserInput = { current: ReadonlyArray; }; -export type PerseusPlotterRubric = PerseusPlotterWidgetOptions; +export type PerseusPlotterScoringData = { + // The Y values that represent the correct answer expected + correct: ReadonlyArray; +} & PerseusPlotterValidationData; + +export type PerseusPlotterValidationData = { + // The Y values the graph should start with + starting: ReadonlyArray; +}; export type PerseusPlotterUserInput = ReadonlyArray; @@ -236,7 +243,7 @@ export type Rubric = | PerseusNumberLineRubric | PerseusNumericInputRubric | PerseusOrdererRubric - | PerseusPlotterRubric + | PerseusPlotterScoringData | PerseusRadioRubric | PerseusSorterRubric | PerseusTableRubric; diff --git a/packages/perseus/src/widgets/plotter/plotter.tsx b/packages/perseus/src/widgets/plotter/plotter.tsx index a0b1178d6c..8e985698ac 100644 --- a/packages/perseus/src/widgets/plotter/plotter.tsx +++ b/packages/perseus/src/widgets/plotter/plotter.tsx @@ -18,14 +18,14 @@ import scorePlotter from "./score-plotter"; import type {PerseusPlotterWidgetOptions} from "../../perseus-types"; import type {Widget, WidgetExports, WidgetProps} from "../../types"; import type { - PerseusPlotterRubric, + PerseusPlotterScoringData, PerseusPlotterUserInput, } from "../../validation.types"; import type {UnsupportedWidgetPromptJSON} from "../../widget-ai-utils/unsupported-widget"; type RenderProps = PerseusPlotterWidgetOptions; -type Props = WidgetProps & { +type Props = WidgetProps & { labelInterval: NonNullable; picSize: NonNullable; }; diff --git a/packages/perseus/src/widgets/plotter/score-plotter.test.ts b/packages/perseus/src/widgets/plotter/score-plotter.test.ts index 381caf1847..96dc8595fc 100644 --- a/packages/perseus/src/widgets/plotter/score-plotter.test.ts +++ b/packages/perseus/src/widgets/plotter/score-plotter.test.ts @@ -1,75 +1,40 @@ import scorePlotter from "./score-plotter"; import type { - PerseusPlotterRubric, + PerseusPlotterScoringData, PerseusPlotterUserInput, } from "../../validation.types"; -const baseRubric: PerseusPlotterRubric = { - categories: [ - "$1^{\\text{st}} \\text{}$", - "$2^{\\text{nd}} \\text{}$", - "$3^{\\text{rd}} \\text{}$", - "$4^{\\text{th}} \\text{}$", - "$5^{\\text{th}} \\text{}$", - ], - picBoxHeight: 300, - picSize: 300, - picUrl: "", - plotDimensions: [0, 0], - correct: [15, 25, 5, 10, 10], - labelInterval: 1, - labels: ["School grade", "Number of absent students"], - maxY: 30, - scaleY: 5, - snapsPerLine: 1, - starting: [0, 0, 0, 0, 0], - type: "bar", -}; - -function generateRubric( - extend?: Partial, -): PerseusPlotterRubric { - return {...baseRubric, ...extend}; -} - describe("scorePlotter", () => { - it("is invalid if the start and end are the same", () => { - // Arrange - const rubric = generateRubric(); - - const userInput: PerseusPlotterUserInput = rubric.starting; - - // Act - const result = scorePlotter(userInput, rubric); - - // Assert - expect(result).toHaveInvalidInput(); - }); - it("can be answered correctly", () => { // Arrange - const rubric = generateRubric(); + const scoringData: PerseusPlotterScoringData = { + correct: [15, 25, 5, 10, 10], + starting: [0, 0, 0, 0, 0], + }; - const userInput: PerseusPlotterUserInput = rubric.correct; + const userInput: PerseusPlotterUserInput = scoringData.correct; // Act - const result = scorePlotter(userInput, rubric); + const score = scorePlotter(userInput, scoringData); // Assert - expect(result).toHaveBeenAnsweredCorrectly(); + expect(score).toHaveBeenAnsweredCorrectly(); }); it("can be answered incorrectly", () => { // Arrange - const rubric = generateRubric(); + const scoringData: PerseusPlotterScoringData = { + correct: [15, 25, 5, 10, 10], + starting: [0, 0, 0, 0, 0], + }; const userInput: PerseusPlotterUserInput = [8, 6, 7, 5, 3, 0, 9]; // Act - const result = scorePlotter(userInput, rubric); + const score = scorePlotter(userInput, scoringData); // Assert - expect(result).toHaveBeenAnsweredIncorrectly(); + expect(score).toHaveBeenAnsweredIncorrectly(); }); }); diff --git a/packages/perseus/src/widgets/plotter/score-plotter.ts b/packages/perseus/src/widgets/plotter/score-plotter.ts index 0af3432de6..41674387e9 100644 --- a/packages/perseus/src/widgets/plotter/score-plotter.ts +++ b/packages/perseus/src/widgets/plotter/score-plotter.ts @@ -1,8 +1,10 @@ import Util from "../../util"; +import validatePlotter from "./validate-plotter"; + import type {PerseusScore} from "../../types"; import type { - PerseusPlotterRubric, + PerseusPlotterScoringData, PerseusPlotterUserInput, } from "../../validation.types"; @@ -10,17 +12,15 @@ const {deepEq} = Util; function scorePlotter( userInput: PerseusPlotterUserInput, - rubric: PerseusPlotterRubric, + scoringData: PerseusPlotterScoringData, ): PerseusScore { - if (deepEq(userInput, rubric.starting)) { - return { - type: "invalid", - message: null, - }; + const validationError = validatePlotter(userInput, scoringData); + if (validationError) { + return validationError; } return { type: "points", - earned: deepEq(userInput, rubric.correct) ? 1 : 0, + earned: deepEq(userInput, scoringData.correct) ? 1 : 0, total: 1, message: null, }; diff --git a/packages/perseus/src/widgets/plotter/validate-plotter.test.ts b/packages/perseus/src/widgets/plotter/validate-plotter.test.ts new file mode 100644 index 0000000000..a4c298f351 --- /dev/null +++ b/packages/perseus/src/widgets/plotter/validate-plotter.test.ts @@ -0,0 +1,38 @@ +import validatePlotter from "./validate-plotter"; + +import type { + PerseusPlotterUserInput, + PerseusPlotterValidationData, +} from "../../validation.types"; + +describe("validatePlotter", () => { + it("is invalid if the start and end are the same", () => { + // Arrange + const validationData: PerseusPlotterValidationData = { + starting: [0, 0, 0, 0, 0], + }; + + const userInput: PerseusPlotterUserInput = validationData.starting; + + // Act + const validationError = validatePlotter(userInput, validationData); + + // Assert + expect(validationError).toHaveInvalidInput(); + }); + + it("returns null if the start and end are not the same and the user has modified the graph", () => { + // Arrange + const validationData: PerseusPlotterValidationData = { + starting: [0, 0, 0, 0, 0], + }; + + const userInput: PerseusPlotterUserInput = [0, 1, 2, 3, 4]; + + // Act + const validationError = validatePlotter(userInput, validationData); + + // Assert + expect(validationError).toBeNull(); + }); +}); diff --git a/packages/perseus/src/widgets/plotter/validate-plotter.ts b/packages/perseus/src/widgets/plotter/validate-plotter.ts new file mode 100644 index 0000000000..0904ba8f8e --- /dev/null +++ b/packages/perseus/src/widgets/plotter/validate-plotter.ts @@ -0,0 +1,30 @@ +import Util from "../../util"; + +import type {PerseusScore} from "../../types"; +import type { + PerseusPlotterUserInput, + PerseusPlotterValidationData, +} from "../../validation.types"; + +const {deepEq} = Util; + +/** + * Checks user input to confirm it is not the same as the starting values for the graph. + * This means the user has modified the graph, and the question can be scored. + * + * @see 'scorePlotter' for more details on scoring. + */ +function validatePlotter( + userInput: PerseusPlotterUserInput, + validationData: PerseusPlotterValidationData, +): Extract | null { + if (deepEq(userInput, validationData.starting)) { + return { + type: "invalid", + message: null, + }; + } + return null; +} + +export default validatePlotter; From 0cec7628c4a061f14b126fd1e3dab6df45fc0178 Mon Sep 17 00:00:00 2001 From: Tamara <60857422+Myranae@users.noreply.github.com> Date: Tue, 26 Nov 2024 14:47:05 -0600 Subject: [PATCH 06/42] Radio: Extract validation out of scoring (#1902) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary: To complete server-side scoring, we are separating out validation logic from scoring logic. This PR separates validation logic that does not depend on answer information and also separates associated tests for the radio widget. Issue: LEMS-2594 ## Test plan: - Confirm checks pass - Confirm widget still works as expected - Confirm the validation logic remaining in the scoring function cannot be removed from scoring Author: Myranae Reviewers: Myranae, jeremywiebe, handeyeco Required Reviewers: Approved By: jeremywiebe Checks: ✅ Publish npm snapshot (ubuntu-latest, 20.x), ✅ Cypress (ubuntu-latest, 20.x), ✅ Lint, Typecheck, Format, and Test (ubuntu-latest, 20.x), ✅ Check builds for changes in size (ubuntu-latest, 20.x), ✅ Check for .changeset entries for all changed files (ubuntu-latest, 20.x), ✅ gerald, ✅ gerald, ✅ Publish npm snapshot (ubuntu-latest, 20.x), ✅ Lint, Typecheck, Format, and Test (ubuntu-latest, 20.x), ✅ Cypress (ubuntu-latest, 20.x), ✅ Check builds for changes in size (ubuntu-latest, 20.x), ✅ Publish Storybook to Chromatic (ubuntu-latest, 20.x), ✅ Check for .changeset entries for all changed files (ubuntu-latest, 20.x), ✅ gerald Pull Request URL: https://github.com/Khan/perseus/pull/1902 --- .changeset/spotty-moles-reply.md | 5 ++ .../src/widgets/radio/score-radio.test.ts | 51 ++++++------------- .../perseus/src/widgets/radio/score-radio.ts | 14 ++--- .../src/widgets/radio/validate-radio.test.ts | 25 +++++++++ .../src/widgets/radio/validate-radio.ts | 29 +++++++++++ 5 files changed, 82 insertions(+), 42 deletions(-) create mode 100644 .changeset/spotty-moles-reply.md create mode 100644 packages/perseus/src/widgets/radio/validate-radio.test.ts create mode 100644 packages/perseus/src/widgets/radio/validate-radio.ts diff --git a/.changeset/spotty-moles-reply.md b/.changeset/spotty-moles-reply.md new file mode 100644 index 0000000000..8ab9761a4d --- /dev/null +++ b/.changeset/spotty-moles-reply.md @@ -0,0 +1,5 @@ +--- +"@khanacademy/perseus": minor +--- + +Introduces a validation function for the radio widget (extracted from the scoring function), though not all validation logic can be extracted. diff --git a/packages/perseus/src/widgets/radio/score-radio.test.ts b/packages/perseus/src/widgets/radio/score-radio.test.ts index 4a13196603..0ad4c50b1c 100644 --- a/packages/perseus/src/widgets/radio/score-radio.test.ts +++ b/packages/perseus/src/widgets/radio/score-radio.test.ts @@ -8,25 +8,6 @@ import type { } from "../../validation.types"; describe("scoreRadio", () => { - it("is invalid when no options are selected", () => { - const userInput: PerseusRadioUserInput = { - choicesSelected: [false, false, false, false], - }; - - const rubric: PerseusRadioRubric = { - choices: [ - {content: "Choice 1"}, - {content: "Choice 2"}, - {content: "Choice 3"}, - {content: "Choice 4"}, - ], - }; - - const result = scoreRadio(userInput, rubric, mockStrings); - - expect(result).toHaveInvalidInput(); - }); - it("is invalid when number selected does not match number correct", () => { const userInput: PerseusRadioUserInput = { choicesSelected: [true, false, false, false], @@ -41,9 +22,9 @@ describe("scoreRadio", () => { ], }; - const result = scoreRadio(userInput, rubric, mockStrings); + const score = scoreRadio(userInput, rubric, mockStrings); - expect(result).toHaveInvalidInput(); + expect(score).toHaveInvalidInput(); }); it("is invalid when none of the above and an answer are both selected", () => { @@ -65,9 +46,9 @@ describe("scoreRadio", () => { ], }; - const result = scoreRadio(userInput, rubric, mockStrings); + const score = scoreRadio(userInput, rubric, mockStrings); - expect(result).toHaveInvalidInput(); + expect(score).toHaveInvalidInput(); }); it("can handle single correct answer", () => { @@ -84,9 +65,9 @@ describe("scoreRadio", () => { ], }; - const result = scoreRadio(userInput, rubric, mockStrings); + const score = scoreRadio(userInput, rubric, mockStrings); - expect(result).toHaveBeenAnsweredCorrectly(); + expect(score).toHaveBeenAnsweredCorrectly(); }); it("can handle single incorrect answer", () => { @@ -103,9 +84,9 @@ describe("scoreRadio", () => { ], }; - const result = scoreRadio(userInput, rubric, mockStrings); + const score = scoreRadio(userInput, rubric, mockStrings); - expect(result).toHaveBeenAnsweredIncorrectly(); + expect(score).toHaveBeenAnsweredIncorrectly(); }); it("can handle multiple correct answer", () => { @@ -122,9 +103,9 @@ describe("scoreRadio", () => { ], }; - const result = scoreRadio(userInput, rubric, mockStrings); + const score = scoreRadio(userInput, rubric, mockStrings); - expect(result).toHaveBeenAnsweredCorrectly(); + expect(score).toHaveBeenAnsweredCorrectly(); }); it("can handle multiple incorrect answer", () => { @@ -141,9 +122,9 @@ describe("scoreRadio", () => { ], }; - const result = scoreRadio(userInput, rubric, mockStrings); + const score = scoreRadio(userInput, rubric, mockStrings); - expect(result).toHaveBeenAnsweredIncorrectly(); + expect(score).toHaveBeenAnsweredIncorrectly(); }); it("can handle none of the above correct answer", () => { @@ -161,9 +142,9 @@ describe("scoreRadio", () => { ], }; - const result = scoreRadio(userInput, rubric, mockStrings); + const score = scoreRadio(userInput, rubric, mockStrings); - expect(result).toHaveBeenAnsweredCorrectly(); + expect(score).toHaveBeenAnsweredCorrectly(); }); it("can handle none of the above incorrect answer", () => { @@ -181,8 +162,8 @@ describe("scoreRadio", () => { ], }; - const result = scoreRadio(userInput, rubric, mockStrings); + const score = scoreRadio(userInput, rubric, mockStrings); - expect(result).toHaveBeenAnsweredIncorrectly(); + expect(score).toHaveBeenAnsweredIncorrectly(); }); }); diff --git a/packages/perseus/src/widgets/radio/score-radio.ts b/packages/perseus/src/widgets/radio/score-radio.ts index 4b907e7965..51586d3c41 100644 --- a/packages/perseus/src/widgets/radio/score-radio.ts +++ b/packages/perseus/src/widgets/radio/score-radio.ts @@ -1,3 +1,5 @@ +import validateRadio from "./validate-radio"; + import type {PerseusStrings} from "../../strings"; import type {PerseusScore} from "../../types"; import type { @@ -10,17 +12,15 @@ function scoreRadio( rubric: PerseusRadioRubric, strings: PerseusStrings, ): PerseusScore { + const validationError = validateRadio(userInput); + if (validationError) { + return validationError; + } + const numSelected = userInput.choicesSelected.reduce((sum, selected) => { return sum + (selected ? 1 : 0); }, 0); - if (numSelected === 0) { - return { - type: "invalid", - message: null, - }; - } - const numCorrect: number = rubric.choices.reduce((sum, currentChoice) => { return currentChoice.correct ? sum + 1 : sum; }, 0); diff --git a/packages/perseus/src/widgets/radio/validate-radio.test.ts b/packages/perseus/src/widgets/radio/validate-radio.test.ts new file mode 100644 index 0000000000..e1b6dd41c2 --- /dev/null +++ b/packages/perseus/src/widgets/radio/validate-radio.test.ts @@ -0,0 +1,25 @@ +import validateRadio from "./validate-radio"; + +import type {PerseusRadioUserInput} from "../../validation.types"; + +describe("validateRadio", () => { + it("is invalid when no options are selected", () => { + const userInput: PerseusRadioUserInput = { + choicesSelected: [false, false, false, false], + }; + + const validationError = validateRadio(userInput); + + expect(validationError).toHaveInvalidInput(); + }); + + it("returns null when validation passes", () => { + const userInput: PerseusRadioUserInput = { + choicesSelected: [true, false, false, false], + }; + + const validationError = validateRadio(userInput); + + expect(validationError).toBeNull(); + }); +}); diff --git a/packages/perseus/src/widgets/radio/validate-radio.ts b/packages/perseus/src/widgets/radio/validate-radio.ts new file mode 100644 index 0000000000..6ca069b852 --- /dev/null +++ b/packages/perseus/src/widgets/radio/validate-radio.ts @@ -0,0 +1,29 @@ +import type {PerseusScore} from "../../types"; +import type {PerseusRadioUserInput} from "../../validation.types"; + +/** + * Checks if the user has selected at least one option. Additional validation + * is done in scoreRadio to check if the number of selected options is correct + * and if the user has selected both a correct option and the "none of the above" + * option. + * @param userInput + * @see `scoreRadio` for the additional validation logic and the scoring logic. + */ +function validateRadio( + userInput: PerseusRadioUserInput, +): Extract | null { + const numSelected = userInput.choicesSelected.reduce((sum, selected) => { + return sum + (selected ? 1 : 0); + }, 0); + + if (numSelected === 0) { + return { + type: "invalid", + message: null, + }; + } + + return null; +} + +export default validateRadio; From 451de899fd3d40bf415cb2318048e90fb1e6670f Mon Sep 17 00:00:00 2001 From: Tamara <60857422+Myranae@users.noreply.github.com> Date: Tue, 26 Nov 2024 14:48:06 -0600 Subject: [PATCH 07/42] Categorizer: Extract validation out of scoring (#1862) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary: In order to complete scoring server-side, we need to separate out the validation logic from the scoring logic. This work separates those two functions and updates associated tests. Issue: LEMS-2596 ## Test plan: - Confirm all checks pass - Confirm the widget still behaves as it should. Author: Myranae Reviewers: Myranae, handeyeco, jeremywiebe Required Reviewers: Approved By: handeyeco, jeremywiebe Checks: ✅ Check builds for changes in size (ubuntu-latest, 20.x), ✅ Publish npm snapshot (ubuntu-latest, 20.x), ✅ Lint, Typecheck, Format, and Test (ubuntu-latest, 20.x), ✅ Check for .changeset entries for all changed files (ubuntu-latest, 20.x), ✅ Cypress (ubuntu-latest, 20.x), ✅ gerald, ✅ Publish npm snapshot (ubuntu-latest, 20.x), ✅ Lint, Typecheck, Format, and Test (ubuntu-latest, 20.x), ✅ Check builds for changes in size (ubuntu-latest, 20.x), ✅ Cypress (ubuntu-latest, 20.x), ✅ Check for .changeset entries for all changed files (ubuntu-latest, 20.x), ✅ Publish Storybook to Chromatic (ubuntu-latest, 20.x), ✅ gerald Pull Request URL: https://github.com/Khan/perseus/pull/1862 --- .changeset/green-ghosts-burn.md | 5 +++ packages/perseus/src/validation.types.ts | 14 ++++-- .../src/widgets/categorizer/categorizer.tsx | 4 +- .../categorizer/score-categorizer.test.ts | 27 +++--------- .../widgets/categorizer/score-categorizer.ts | 27 ++++++------ .../categorizer/validate-categorizer.test.ts | 43 +++++++++++++++++++ .../categorizer/validate-categorizer.ts | 34 +++++++++++++++ 7 files changed, 115 insertions(+), 39 deletions(-) create mode 100644 .changeset/green-ghosts-burn.md create mode 100644 packages/perseus/src/widgets/categorizer/validate-categorizer.test.ts create mode 100644 packages/perseus/src/widgets/categorizer/validate-categorizer.ts diff --git a/.changeset/green-ghosts-burn.md b/.changeset/green-ghosts-burn.md new file mode 100644 index 0000000000..58af29901b --- /dev/null +++ b/.changeset/green-ghosts-burn.md @@ -0,0 +1,5 @@ +--- +"@khanacademy/perseus": minor +--- + +Split out validation function for the `categorizer` widget. This can be used to check if the user selected an answer for every row, confirming the question is ready to be scored. diff --git a/packages/perseus/src/validation.types.ts b/packages/perseus/src/validation.types.ts index 01cf14af45..3f92fe36a8 100644 --- a/packages/perseus/src/validation.types.ts +++ b/packages/perseus/src/validation.types.ts @@ -49,15 +49,21 @@ import type {Relationship} from "./widgets/number-line/number-line"; export type UserInputStatus = "correct" | "incorrect" | "incomplete"; -export type PerseusCategorizerRubric = { +export type PerseusCategorizerScoringData = { // The correct answers where index relates to the items and value relates // to the category. e.g. [0, 1, 0, 1, 2] values: ReadonlyArray; -}; +} & PerseusCategorizerValidationData; export type PerseusCategorizerUserInput = { - values: PerseusCategorizerRubric["values"]; + values: PerseusCategorizerScoringData["values"]; +}; + +export type PerseusCategorizerValidationData = { + // Translatable text; a list of items to categorize. e.g. ["banana", "yellow", "apple", "purple", "shirt"] + items: ReadonlyArray; }; + // TODO(LEMS-2440): Can possibly be removed during 2440? // This is not used for grading at all. The only place it is used is to define // Props type in cs-program.tsx, but RenderProps already contains WidgetOptions @@ -226,7 +232,7 @@ export type PerseusTableRubric = { export type PerseusTableUserInput = ReadonlyArray>; export type Rubric = - | PerseusCategorizerRubric + | PerseusCategorizerScoringData | PerseusCSProgramRubric | PerseusDropdownRubric | PerseusExpressionRubric diff --git a/packages/perseus/src/widgets/categorizer/categorizer.tsx b/packages/perseus/src/widgets/categorizer/categorizer.tsx index bf7e228dce..27069da230 100644 --- a/packages/perseus/src/widgets/categorizer/categorizer.tsx +++ b/packages/perseus/src/widgets/categorizer/categorizer.tsx @@ -21,12 +21,12 @@ import scoreCategorizer from "./score-categorizer"; import type {PerseusCategorizerWidgetOptions} from "../../perseus-types"; import type {Widget, WidgetExports, WidgetProps} from "../../types"; import type { - PerseusCategorizerRubric, + PerseusCategorizerScoringData, PerseusCategorizerUserInput, } from "../../validation.types"; import type {CategorizerPromptJSON} from "../../widget-ai-utils/categorizer/categorizer-ai-utils"; -type Props = WidgetProps & { +type Props = WidgetProps & { values: ReadonlyArray; }; diff --git a/packages/perseus/src/widgets/categorizer/score-categorizer.test.ts b/packages/perseus/src/widgets/categorizer/score-categorizer.test.ts index 4de4d58c61..7a8c0192f9 100644 --- a/packages/perseus/src/widgets/categorizer/score-categorizer.test.ts +++ b/packages/perseus/src/widgets/categorizer/score-categorizer.test.ts @@ -2,47 +2,34 @@ import {mockStrings} from "../../strings"; import scoreCategorizer from "./score-categorizer"; -import type {PerseusCategorizerRubric} from "../../validation.types"; +import type {PerseusCategorizerScoringData} from "../../validation.types"; describe("scoreCategorizer", () => { it("gives points when the answer is correct", () => { - const rubric: PerseusCategorizerRubric = { + const scoringData: PerseusCategorizerScoringData = { values: [1, 3], + items: ["apples", "oranges"], }; const userInput = { values: [1, 3], } as const; - const score = scoreCategorizer(userInput, rubric, mockStrings); + const score = scoreCategorizer(userInput, scoringData, mockStrings); expect(score).toHaveBeenAnsweredCorrectly(); }); it("does not give points when incorrectly answered", () => { - const rubric: PerseusCategorizerRubric = { + const scoringData: PerseusCategorizerScoringData = { values: [1, 3], + items: ["apples", "oranges"], }; const userInput = { values: [2, 3], } as const; - const score = scoreCategorizer(userInput, rubric, mockStrings); + const score = scoreCategorizer(userInput, scoringData, mockStrings); expect(score).toHaveBeenAnsweredIncorrectly(); }); - - it("tells the learner its not complete if not selected", () => { - const rubric: PerseusCategorizerRubric = { - values: [1, 3], - }; - - const userInput = { - values: [2], - } as const; - const score = scoreCategorizer(userInput, rubric, mockStrings); - - expect(score).toHaveInvalidInput( - "Make sure you select something for every row.", - ); - }); }); diff --git a/packages/perseus/src/widgets/categorizer/score-categorizer.ts b/packages/perseus/src/widgets/categorizer/score-categorizer.ts index e0fa4b77f3..b44d0f1465 100644 --- a/packages/perseus/src/widgets/categorizer/score-categorizer.ts +++ b/packages/perseus/src/widgets/categorizer/score-categorizer.ts @@ -1,31 +1,32 @@ +import validateCategorizer from "./validate-categorizer"; + import type {PerseusStrings} from "../../strings"; import type {PerseusScore} from "../../types"; import type { - PerseusCategorizerRubric, + PerseusCategorizerScoringData, PerseusCategorizerUserInput, } from "../../validation.types"; function scoreCategorizer( userInput: PerseusCategorizerUserInput, - rubric: PerseusCategorizerRubric, + scoringData: PerseusCategorizerScoringData, strings: PerseusStrings, ): PerseusScore { - let completed = true; + const validationError = validateCategorizer( + userInput, + scoringData, + strings, + ); + if (validationError) { + return validationError; + } + let allCorrect = true; - rubric.values.forEach((value, i) => { - if (userInput.values[i] == null) { - completed = false; - } + scoringData.values.forEach((value, i) => { if (userInput.values[i] !== value) { allCorrect = false; } }); - if (!completed) { - return { - type: "invalid", - message: strings.invalidSelection, - }; - } return { type: "points", earned: allCorrect ? 1 : 0, diff --git a/packages/perseus/src/widgets/categorizer/validate-categorizer.test.ts b/packages/perseus/src/widgets/categorizer/validate-categorizer.test.ts new file mode 100644 index 0000000000..fc8ce058ba --- /dev/null +++ b/packages/perseus/src/widgets/categorizer/validate-categorizer.test.ts @@ -0,0 +1,43 @@ +import {mockStrings} from "../../strings"; + +import validateCategorizer from "./validate-categorizer"; + +import type {PerseusCategorizerValidationData} from "../../validation.types"; + +describe("validateCategorizer", () => { + it("tells the learner its not complete if not selected", () => { + const validationData: PerseusCategorizerValidationData = { + items: ["apples", "oranges"], + }; + + const userInput = { + values: [2], + } as const; + const score = validateCategorizer( + userInput, + validationData, + mockStrings, + ); + + expect(score).toHaveInvalidInput( + "Make sure you select something for every row.", + ); + }); + + it("returns null if the userInput is valid", () => { + const validationData: PerseusCategorizerValidationData = { + items: ["apples", "oranges"], + }; + + const userInput = { + values: [2, 4], + } as const; + const score = validateCategorizer( + userInput, + validationData, + mockStrings, + ); + + expect(score).toBeNull(); + }); +}); diff --git a/packages/perseus/src/widgets/categorizer/validate-categorizer.ts b/packages/perseus/src/widgets/categorizer/validate-categorizer.ts new file mode 100644 index 0000000000..f2e1b4d876 --- /dev/null +++ b/packages/perseus/src/widgets/categorizer/validate-categorizer.ts @@ -0,0 +1,34 @@ +import type {PerseusStrings} from "../../strings"; +import type {PerseusScore} from "../../types"; +import type { + PerseusCategorizerUserInput, + PerseusCategorizerValidationData, +} from "../../validation.types"; + +/** + * Checks userInput from the categorizer widget to see if the user has selected + * a category for each item. + * @param userInput - The user's input corresponding to an array of indices that + * represent the selected category for each row/item. + * @param validationData - An array of strings corresponding to each row/item + * @param strings - Used to provide a validation message + */ +function validateCategorizer( + userInput: PerseusCategorizerUserInput, + validationData: PerseusCategorizerValidationData, + strings: PerseusStrings, +): Extract | null { + const incomplete = validationData.items.some( + (_, i) => userInput.values[i] == null, + ); + + if (incomplete) { + return { + type: "invalid", + message: strings.invalidSelection, + }; + } + return null; +} + +export default validateCategorizer; From 88ba71bef0cdd75fa0c8b467dcea2cccc637d034 Mon Sep 17 00:00:00 2001 From: Matthew Date: Wed, 27 Nov 2024 08:46:57 -0600 Subject: [PATCH 08/42] remove some error suppressions (#1920) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary: idk, just following some threads by un-excepting errors. Author: handeyeco Reviewers: handeyeco, jeremywiebe Required Reviewers: Approved By: jeremywiebe Checks: ✅ Publish npm snapshot (ubuntu-latest, 20.x), ✅ Check for .changeset entries for all changed files (ubuntu-latest, 20.x), ✅ Cypress (ubuntu-latest, 20.x), ✅ Lint, Typecheck, Format, and Test (ubuntu-latest, 20.x), ✅ Check builds for changes in size (ubuntu-latest, 20.x), ✅ Publish Storybook to Chromatic (ubuntu-latest, 20.x), ✅ gerald Pull Request URL: https://github.com/Khan/perseus/pull/1920 --- .changeset/few-jokes-travel.md | 8 ++ config/test/test-setup.ts | 1 - packages/kas/src/nodes.ts | 3 +- .../src/__stories__/item-editor.stories.tsx | 3 +- .../src/multirenderer-editor.tsx | 3 +- .../locked-function-settings.test.tsx | 3 - .../src/widgets/orderer-editor.tsx | 10 +-- packages/perseus/src/hints-renderer.tsx | 1 - .../src/interactive2/wrapped-drawing.ts | 1 - packages/perseus/src/perseus-markdown.tsx | 3 +- packages/perseus/src/perseus-types.ts | 4 +- packages/perseus/src/util.ts | 5 +- packages/perseus/src/util/graphie.ts | 1 - .../perseus-parsers/orderer-widget.ts | 6 +- .../perseus-parsers/widgets-map.test.ts | 4 +- packages/perseus/src/widgets/grapher/util.tsx | 89 +++++++++---------- .../perseus/src/widgets/orderer/orderer.tsx | 60 +++++-------- packages/simple-markdown/src/index.ts | 7 +- 18 files changed, 96 insertions(+), 116 deletions(-) create mode 100644 .changeset/few-jokes-travel.md diff --git a/.changeset/few-jokes-travel.md b/.changeset/few-jokes-travel.md new file mode 100644 index 0000000000..b4a57f6823 --- /dev/null +++ b/.changeset/few-jokes-travel.md @@ -0,0 +1,8 @@ +--- +"@khanacademy/kas": patch +"@khanacademy/perseus": patch +"@khanacademy/perseus-editor": patch +"@khanacademy/simple-markdown": patch +--- + +Fix some file-wide error suppressions diff --git a/config/test/test-setup.ts b/config/test/test-setup.ts index f907ec797a..cd4bf6f5cf 100644 --- a/config/test/test-setup.ts +++ b/config/test/test-setup.ts @@ -35,7 +35,6 @@ if (typeof window !== "undefined") { // Override the window.location implementation to mock out assign() // We need to access window.location.assign to verify that we're // redirecting to the right place. - /* eslint-disable no-restricted-syntax */ const oldLocation = window.location; // @ts-expect-error - TS2790 - The operand of a 'delete' operator must be optional. delete window.location; diff --git a/packages/kas/src/nodes.ts b/packages/kas/src/nodes.ts index 6aca9a49e2..9d33c1fe05 100644 --- a/packages/kas/src/nodes.ts +++ b/packages/kas/src/nodes.ts @@ -1,7 +1,7 @@ /* eslint-disable prettier/prettier */ /* eslint-disable import/order */ /* TODO(charlie): fix these lint errors (http://eslint.org/docs/rules): */ -/* eslint-disable indent, no-undef, no-var, one-var, no-dupe-keys, no-new-func, no-redeclare, @typescript-eslint/no-unused-vars, comma-dangle, max-len, prefer-spread, space-infix-ops, space-unary-ops */ +/* eslint-disable indent, no-undef, no-var, no-dupe-keys, no-new-func, no-redeclare, comma-dangle, max-len, prefer-spread, space-infix-ops, space-unary-ops */ import _ from "underscore"; import {unitParser} from "./__genfiles__/unitparser"; @@ -1414,7 +1414,6 @@ export class Mul extends Seq { rational = rational.addHint("fraction"); } - var result; if (num.n < 0 && right.n < 0) { rational.d = -rational.d; return left.replace(num, [NumNeg, rational]); diff --git a/packages/perseus-editor/src/__stories__/item-editor.stories.tsx b/packages/perseus-editor/src/__stories__/item-editor.stories.tsx index 26862e4c5d..cef53e11bf 100644 --- a/packages/perseus-editor/src/__stories__/item-editor.stories.tsx +++ b/packages/perseus-editor/src/__stories__/item-editor.stories.tsx @@ -10,7 +10,6 @@ import "../styles/perseus-editor.less"; type Props = React.ComponentProps; const Wrapper = (props: Props) => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars const {onChange, ...rest} = props; const [extras, setExtras] = React.useState>(rest); @@ -19,7 +18,7 @@ const Wrapper = (props: Props) => { { - props.onChange?.(e); // to register action in storybook + onChange?.(e); // to register action in storybook setExtras((prevExtras) => ({...prevExtras, ...e})); }} /> diff --git a/packages/perseus-editor/src/multirenderer-editor.tsx b/packages/perseus-editor/src/multirenderer-editor.tsx index 9a9264b1f8..2d0445c7fd 100644 --- a/packages/perseus-editor/src/multirenderer-editor.tsx +++ b/packages/perseus-editor/src/multirenderer-editor.tsx @@ -31,8 +31,7 @@ import type { // Multi-item item types Item, - // ItemTree is used below, the linter is confused. - ItemTree, // eslint-disable-line @typescript-eslint/no-unused-vars + ItemTree, ItemObjectNode, ItemArrayNode, ContentNode, diff --git a/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-function-settings.test.tsx b/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-function-settings.test.tsx index 5caaf10b6e..3d9df676d1 100644 --- a/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-function-settings.test.tsx +++ b/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-function-settings.test.tsx @@ -5,9 +5,6 @@ import * as React from "react"; import {flags} from "../../../__stories__/flags-for-api-options"; -// Disabling the following linting error because the import is needed for mocking purposes. -// eslint-disable-next-line @typescript-eslint/no-unused-vars -import examples from "./locked-function-examples"; import LockedFunctionSettings from "./locked-function-settings"; import { getDefaultFigureForType, diff --git a/packages/perseus-editor/src/widgets/orderer-editor.tsx b/packages/perseus-editor/src/widgets/orderer-editor.tsx index c4a9b20b97..d841ce0799 100644 --- a/packages/perseus-editor/src/widgets/orderer-editor.tsx +++ b/packages/perseus-editor/src/widgets/orderer-editor.tsx @@ -1,5 +1,5 @@ /* eslint-disable @khanacademy/ts-no-error-suppressions */ -/* eslint-disable one-var, react/forbid-prop-types */ +/* eslint-disable react/forbid-prop-types */ import {components} from "@khanacademy/perseus"; import PropTypes from "prop-types"; import * as React from "react"; @@ -7,10 +7,10 @@ import _ from "underscore"; const {InfoTip, TextListEditor} = components; -const NORMAL = "normal", - AUTO = "auto", - HORIZONTAL = "horizontal", - VERTICAL = "vertical"; +const NORMAL = "normal"; +const AUTO = "auto"; +const HORIZONTAL = "horizontal"; +const VERTICAL = "vertical"; type Props = any; diff --git a/packages/perseus/src/hints-renderer.tsx b/packages/perseus/src/hints-renderer.tsx index 8cf5145039..ee57f01b99 100644 --- a/packages/perseus/src/hints-renderer.tsx +++ b/packages/perseus/src/hints-renderer.tsx @@ -1,4 +1,3 @@ -/* eslint-disable @khanacademy/ts-no-error-suppressions */ import * as PerseusLinter from "@khanacademy/perseus-linter"; import {StyleSheet, css} from "aphrodite"; import classnames from "classnames"; diff --git a/packages/perseus/src/interactive2/wrapped-drawing.ts b/packages/perseus/src/interactive2/wrapped-drawing.ts index a6e4599a5e..6b9c47b136 100644 --- a/packages/perseus/src/interactive2/wrapped-drawing.ts +++ b/packages/perseus/src/interactive2/wrapped-drawing.ts @@ -1,4 +1,3 @@ -/* eslint-disable @babel/no-invalid-this */ /** * Default methods for a wrapped movable. */ diff --git a/packages/perseus/src/perseus-markdown.tsx b/packages/perseus/src/perseus-markdown.tsx index 47be9731b1..472cce4403 100644 --- a/packages/perseus/src/perseus-markdown.tsx +++ b/packages/perseus/src/perseus-markdown.tsx @@ -1,4 +1,3 @@ -/* eslint-disable no-useless-escape, no-prototype-builtins */ import {pureMarkdownRules} from "@khanacademy/pure-markdown"; import SimpleMarkdown from "@khanacademy/simple-markdown"; import * as React from "react"; @@ -201,7 +200,7 @@ const rules = { // link was not put together properly, let's make sure it's there so we // don't break the entire page. const isKAUrl = href - ? href.match(/https?:\/\/[^\/]*khanacademy.org|^\//) + ? href.match(/https?:\/\/[^/]*khanacademy.org|^\//) : false; if (!isKAUrl) { // Prevents "reverse tabnabbing" phishing attacks diff --git a/packages/perseus/src/perseus-types.ts b/packages/perseus/src/perseus-types.ts index 95dd9993d5..f0d251c768 100644 --- a/packages/perseus/src/perseus-types.ts +++ b/packages/perseus/src/perseus-types.ts @@ -1171,9 +1171,9 @@ export type PerseusOrdererWidgetOptions = { // Cards that are not part of the answer otherOptions: ReadonlyArray; // "normal" for text options. "auto" for image options. - height: string; + height: "normal" | "auto"; // Use the "horizontal" layout for short text and small images. The "vertical" layout is best for longer text (e.g. proofs). - layout: string; + layout: "horizontal" | "vertical"; }; export type PerseusPassageWidgetOptions = { diff --git a/packages/perseus/src/util.ts b/packages/perseus/src/util.ts index 0efb5a3da8..d50781e975 100644 --- a/packages/perseus/src/util.ts +++ b/packages/perseus/src/util.ts @@ -1,4 +1,3 @@ -/* eslint-disable @babel/no-invalid-this, getter-return, one-var */ import {Errors, PerseusError} from "@khanacademy/perseus-core"; import _ from "underscore"; @@ -150,8 +149,8 @@ function shuffle( do { // Fischer-Yates shuffle for (let top = shuffled.length; top > 0; top--) { - const newEnd = Math.floor(random() * top), - temp = shuffled[newEnd]; + const newEnd = Math.floor(random() * top); + const temp = shuffled[newEnd]; // @ts-expect-error - TS2542 - Index signature in type 'readonly T[]' only permits reading. shuffled[newEnd] = shuffled[top - 1]; diff --git a/packages/perseus/src/util/graphie.ts b/packages/perseus/src/util/graphie.ts index 308efaf7f3..7909feeb95 100644 --- a/packages/perseus/src/util/graphie.ts +++ b/packages/perseus/src/util/graphie.ts @@ -1,4 +1,3 @@ -/* eslint-disable @babel/no-invalid-this */ import { point as kpoint, vector as kvector, diff --git a/packages/perseus/src/util/parse-perseus-json/perseus-parsers/orderer-widget.ts b/packages/perseus/src/util/parse-perseus-json/perseus-parsers/orderer-widget.ts index 4c8f72e52c..b0c72ab602 100644 --- a/packages/perseus/src/util/parse-perseus-json/perseus-parsers/orderer-widget.ts +++ b/packages/perseus/src/util/parse-perseus-json/perseus-parsers/orderer-widget.ts @@ -1,4 +1,4 @@ -import {array, constant, object, string} from "../general-purpose-parsers"; +import {array, constant, enumeration, object} from "../general-purpose-parsers"; import {parsePerseusRenderer} from "./perseus-renderer"; import {parseWidget} from "./widget"; @@ -19,7 +19,7 @@ export const parseOrdererWidget: Parser = parseWidget( options: array(parseRenderer), correctOptions: array(parseRenderer), otherOptions: array(parseRenderer), - height: string, - layout: string, + height: enumeration("normal", "auto"), + layout: enumeration("horizontal", "vertical"), }), ); diff --git a/packages/perseus/src/util/parse-perseus-json/perseus-parsers/widgets-map.test.ts b/packages/perseus/src/util/parse-perseus-json/perseus-parsers/widgets-map.test.ts index 9d6a672364..3b21b880e4 100644 --- a/packages/perseus/src/util/parse-perseus-json/perseus-parsers/widgets-map.test.ts +++ b/packages/perseus/src/util/parse-perseus-json/perseus-parsers/widgets-map.test.ts @@ -512,8 +512,8 @@ describe("parseWidgetsMap", () => { options: [], correctOptions: [], otherOptions: [], - height: "", - layout: "", + height: "normal", + layout: "horizontal", }, }, }; diff --git a/packages/perseus/src/widgets/grapher/util.tsx b/packages/perseus/src/widgets/grapher/util.tsx index fa3a684e58..ed7c6af887 100644 --- a/packages/perseus/src/widgets/grapher/util.tsx +++ b/packages/perseus/src/widgets/grapher/util.tsx @@ -1,4 +1,3 @@ -/* eslint-disable one-var */ import {point as kpoint} from "@khanacademy/kmath"; import * as React from "react"; import _ from "underscore"; @@ -141,15 +140,15 @@ const Linear: LinearType = _.extend({}, PlotDefaults, { }, getFunctionForCoeffs: function (coeffs: ReadonlyArray, x: number) { - const m = coeffs[0], - b = coeffs[1]; + const m = coeffs[0]; + const b = coeffs[1]; return m * x + b; }, getEquationString: function (coords: Coords) { const coeffs: ReadonlyArray = this.getCoefficients(coords); - const m: number = coeffs[0], - b: number = coeffs[1]; + const m: number = coeffs[0]; + const b: number = coeffs[1]; return "y = " + m.toFixed(3) + "x + " + b.toFixed(3); }, }); @@ -184,9 +183,9 @@ const Quadratic: QuadraticType = _.extend({}, PlotDefaults, { coeffs: ReadonlyArray, x: number, ): number { - const a = coeffs[0], - b = coeffs[1], - c = coeffs[2]; + const a = coeffs[0]; + const b = coeffs[1]; + const c = coeffs[2]; return (a * x + b) * x + c; }, @@ -204,9 +203,9 @@ const Quadratic: QuadraticType = _.extend({}, PlotDefaults, { getEquationString: function (coords: Coords) { const coeffs = this.getCoefficients(coords); - const a = coeffs[0], - b = coeffs[1], - c = coeffs[2]; + const a = coeffs[0]; + const b = coeffs[1]; + const c = coeffs[2]; return ( "y = " + a.toFixed(3) + @@ -241,10 +240,10 @@ const Sinusoid: SinusoidType = _.extend({}, PlotDefaults, { }, getFunctionForCoeffs: function (coeffs: ReadonlyArray, x: number) { - const a = coeffs[0], - b = coeffs[1], - c = coeffs[2], - d = coeffs[3]; + const a = coeffs[0]; + const b = coeffs[1]; + const c = coeffs[2]; + const d = coeffs[3]; return a * Math.sin(b * x - c) + d; }, @@ -259,10 +258,10 @@ const Sinusoid: SinusoidType = _.extend({}, PlotDefaults, { getEquationString: function (coords: Coords) { const coeffs = this.getCoefficients(coords); - const a = coeffs[0], - b = coeffs[1], - c = coeffs[2], - d = coeffs[3]; + const a = coeffs[0]; + const b = coeffs[1]; + const c = coeffs[2]; + const d = coeffs[3]; return ( "y = " + a.toFixed(3) + @@ -307,19 +306,19 @@ const Tangent: TangentType = _.extend({}, PlotDefaults, { }, getFunctionForCoeffs: function (coeffs: ReadonlyArray, x: number) { - const a = coeffs[0], - b = coeffs[1], - c = coeffs[2], - d = coeffs[3]; + const a = coeffs[0]; + const b = coeffs[1]; + const c = coeffs[2]; + const d = coeffs[3]; return a * Math.tan(b * x - c) + d; }, getEquationString: function (coords: Coords) { const coeffs = this.getCoefficients(coords); - const a = coeffs[0], - b = coeffs[1], - c = coeffs[2], - d = coeffs[3]; + const a = coeffs[0]; + const b = coeffs[1]; + const c = coeffs[2]; + const d = coeffs[3]; return ( "y = " + a.toFixed(3) + @@ -430,9 +429,9 @@ const Exponential: ExponentialType = _.extend({}, PlotDefaults, { coeffs: ReadonlyArray, x: number, ): number { - const a = coeffs[0], - b = coeffs[1], - c = coeffs[2]; + const a = coeffs[0]; + const b = coeffs[1]; + const c = coeffs[2]; return a * Math.exp(b * x) + c; }, @@ -441,9 +440,9 @@ const Exponential: ExponentialType = _.extend({}, PlotDefaults, { return null; } const coeffs = this.getCoefficients(coords, asymptote); - const a = coeffs[0], - b = coeffs[1], - c = coeffs[2]; + const a = coeffs[0]; + const b = coeffs[1]; + const c = coeffs[2]; return ( "y = " + a.toFixed(3) + @@ -537,9 +536,9 @@ const Logarithm: LogarithmType = _.extend({}, PlotDefaults, { x: number, asymptote: Coords, ) { - const a = coeffs[0], - b = coeffs[1], - c = coeffs[2]; + const a = coeffs[0]; + const b = coeffs[1]; + const c = coeffs[2]; return a * Math.log(b * x + c); }, @@ -551,9 +550,9 @@ const Logarithm: LogarithmType = _.extend({}, PlotDefaults, { coords, asymptote, ); - const a = coeffs[0], - b = coeffs[1], - c = coeffs[2]; + const a = coeffs[0]; + const b = coeffs[1]; + const c = coeffs[2]; return ( "y = ln(" + a.toFixed(3) + @@ -597,17 +596,17 @@ const AbsoluteValue: AbsoluteValueType = _.extend({}, PlotDefaults, { }, getFunctionForCoeffs: function (coeffs: ReadonlyArray, x: number) { - const m = coeffs[0], - horizontalOffset = coeffs[1], - verticalOffset = coeffs[2]; + const m = coeffs[0]; + const horizontalOffset = coeffs[1]; + const verticalOffset = coeffs[2]; return m * Math.abs(x - horizontalOffset) + verticalOffset; }, getEquationString: function (coords: Coords) { const coeffs: ReadonlyArray = this.getCoefficients(coords); - const m = coeffs[0], - horizontalOffset = coeffs[1], - verticalOffset = coeffs[2]; + const m = coeffs[0]; + const horizontalOffset = coeffs[1]; + const verticalOffset = coeffs[2]; return ( "y = " + m.toFixed(3) + diff --git a/packages/perseus/src/widgets/orderer/orderer.tsx b/packages/perseus/src/widgets/orderer/orderer.tsx index 7324daa325..972f50fd02 100644 --- a/packages/perseus/src/widgets/orderer/orderer.tsx +++ b/packages/perseus/src/widgets/orderer/orderer.tsx @@ -1,5 +1,5 @@ /* eslint-disable @khanacademy/ts-no-error-suppressions */ -/* eslint-disable @babel/no-invalid-this, @typescript-eslint/no-unused-vars, one-var, react/no-unsafe */ +/* eslint-disable @babel/no-invalid-this, react/no-unsafe */ import {Errors} from "@khanacademy/perseus-core"; import {linterContextDefault} from "@khanacademy/perseus-linter"; import $ from "jquery"; @@ -17,12 +17,7 @@ import {getPromptJSON as _getPromptJSON} from "../../widget-ai-utils/orderer/ord import {scoreOrderer} from "./score-orderer"; import type {PerseusOrdererWidgetOptions} from "../../perseus-types"; -import type { - PerseusScore, - WidgetExports, - WidgetProps, - Widget, -} from "../../types"; +import type {WidgetExports, WidgetProps, Widget} from "../../types"; import type { PerseusOrdererRubric, PerseusOrdererUserInput, @@ -300,11 +295,6 @@ class Card extends React.Component { } } -const NORMAL = "normal"; -const AUTO = "auto"; -const HORIZONTAL = "horizontal"; -const VERTICAL = "vertical"; - type RenderProps = PerseusOrdererWidgetOptions & { current: any; }; @@ -344,8 +334,8 @@ class Orderer current: [], options: [], correctOptions: [], - height: NORMAL, - layout: HORIZONTAL, + height: "normal", + layout: "horizontal", linterContext: linterContextDefault, }; @@ -521,7 +511,7 @@ class Orderer findCorrectIndex: (arg1: any, arg2: any) => any = (draggable, list) => { // Find the correct index for a card given the current cards. - const isHorizontal = this.props.layout === HORIZONTAL; + const isHorizontal = this.props.layout === "horizontal"; // eslint-disable-next-line react/no-string-refs // @ts-expect-error - TS2769 - No overload matches this call. const $dragList = $(ReactDOM.findDOMNode(this.refs.dragList)); @@ -585,28 +575,24 @@ class Orderer return false; } - const isHorizontal = this.props.layout === HORIZONTAL, - // @ts-expect-error - TS2769 - No overload matches this call. - $draggable = $(ReactDOM.findDOMNode(draggable)), - // eslint-disable-next-line react/no-string-refs - // @ts-expect-error - TS2769 - No overload matches this call. - $bank = $(ReactDOM.findDOMNode(this.refs.bank)), - // @ts-expect-error - TS2339 - Property 'offset' does not exist on type 'JQueryStatic'. - draggableOffset = $draggable.offset(), - // @ts-expect-error - TS2339 - Property 'offset' does not exist on type 'JQueryStatic'. - bankOffset = $bank.offset(), - // @ts-expect-error - TS2339 - Property 'outerHeight' does not exist on type 'JQueryStatic'. - draggableHeight = $draggable.outerHeight(true), - // @ts-expect-error - TS2339 - Property 'outerHeight' does not exist on type 'JQueryStatic'. - bankHeight = $bank.outerHeight(true), - // @ts-expect-error - TS2339 - Property 'outerWidth' does not exist on type 'JQueryStatic'. - bankWidth = $bank.outerWidth(true), - // eslint-disable-next-line react/no-string-refs - dragList = ReactDOM.findDOMNode(this.refs.dragList), - // @ts-expect-error - TS2769 - No overload matches this call. | TS2339 - Property 'width' does not exist on type 'JQueryStatic'. - dragListWidth = $(dragList).width(), - // @ts-expect-error - TS2339 - Property 'outerWidth' does not exist on type 'JQueryStatic'. - draggableWidth = $draggable.outerWidth(true); + const isHorizontal = this.props.layout === "horizontal"; + // @ts-expect-error - TS2769 - No overload matches this call. + const $draggable = $(ReactDOM.findDOMNode(draggable)); + // eslint-disable-next-line react/no-string-refs + // @ts-expect-error - TS2769 - No overload matches this call. + const $bank = $(ReactDOM.findDOMNode(this.refs.bank)); + // @ts-expect-error - TS2339 - Property 'offset' does not exist on type 'JQueryStatic'. + const draggableOffset = $draggable.offset(); + // @ts-expect-error - TS2339 - Property 'offset' does not exist on type 'JQueryStatic'. + const bankOffset = $bank.offset(); + // @ts-expect-error - TS2339 - Property 'outerHeight' does not exist on type 'JQueryStatic'. + const draggableHeight = $draggable.outerHeight(true); + // @ts-expect-error - TS2339 - Property 'outerHeight' does not exist on type 'JQueryStatic'. + const bankHeight = $bank.outerHeight(true); + // @ts-expect-error - TS2339 - Property 'outerWidth' does not exist on type 'JQueryStatic'. + const bankWidth = $bank.outerWidth(true); + // @ts-expect-error - TS2339 - Property 'outerWidth' does not exist on type 'JQueryStatic'. + const draggableWidth = $draggable.outerWidth(true); if (isHorizontal) { return ( diff --git a/packages/simple-markdown/src/index.ts b/packages/simple-markdown/src/index.ts index 18feefe347..e4ad7187b2 100644 --- a/packages/simple-markdown/src/index.ts +++ b/packages/simple-markdown/src/index.ts @@ -1,4 +1,4 @@ -/* eslint-disable prefer-spread, no-regex-spaces, @typescript-eslint/no-unused-vars, guard-for-in, no-console, no-var */ +/* eslint-disable prefer-spread, no-regex-spaces, guard-for-in, no-console, no-var */ /** * Simple-Markdown * =============== @@ -710,7 +710,6 @@ var TABLES = (function () { // predefine regexes so we don't have to create them inside functions // sure, regex literals should be fast, even inside functions, but they // aren't in all browsers. - var TABLE_BLOCK_TRIM = /\n+/g; var TABLE_ROW_SEPARATOR_TRIM = /^ *\| *| *\| *$/g; var TABLE_CELL_END_TRIM = / *$/; var TABLE_RIGHT_ALIGN = /^ *-+: *$/; @@ -931,7 +930,7 @@ var defaultRules: DefaultRules = { // map output over the ast, except group any text // nodes together into a single string output. - for (var i = 0, key = 0; i < arr.length; i++) { + for (var i = 0; i < arr.length; i++) { var node = arr[i]; if (node.type === "text") { node = {type: "text", content: node.content}; @@ -1901,7 +1900,7 @@ var markdownToHtml = function (source: string, state?: State | null): string { // TODO: This needs definition type Props = any; -var ReactMarkdown = function (props): React.ReactElement { +var ReactMarkdown = function (props: Props): React.ReactElement { var divProps: Record = {}; for (var prop in props) { From 64ea2ee86264a20f1d0e34968831945fea8ed36b Mon Sep 17 00:00:00 2001 From: Matthew Date: Wed, 27 Nov 2024 08:47:44 -0600 Subject: [PATCH 09/42] remove findDOMNode from text-input (#1919) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary: `findDOMNode` is deprecated and React whines about it constantly when running tests, so I'm trying to chip away at removing uses of it. Author: handeyeco Reviewers: handeyeco, jeremywiebe Required Reviewers: Approved By: jeremywiebe Checks: ✅ Publish npm snapshot (ubuntu-latest, 20.x), ✅ Lint, Typecheck, Format, and Test (ubuntu-latest, 20.x), ✅ Check for .changeset entries for all changed files (ubuntu-latest, 20.x), ✅ Cypress (ubuntu-latest, 20.x), ✅ Check builds for changes in size (ubuntu-latest, 20.x), ✅ Publish Storybook to Chromatic (ubuntu-latest, 20.x), ✅ gerald Pull Request URL: https://github.com/Khan/perseus/pull/1919 --- .changeset/giant-tables-impress.md | 5 +++ .../perseus/src/components/text-input.tsx | 43 ++++++++++--------- 2 files changed, 28 insertions(+), 20 deletions(-) create mode 100644 .changeset/giant-tables-impress.md diff --git a/.changeset/giant-tables-impress.md b/.changeset/giant-tables-impress.md new file mode 100644 index 0000000000..32f5de4204 --- /dev/null +++ b/.changeset/giant-tables-impress.md @@ -0,0 +1,5 @@ +--- +"@khanacademy/perseus": patch +--- + +Remove usage of findDOMNode in text-input component diff --git a/packages/perseus/src/components/text-input.tsx b/packages/perseus/src/components/text-input.tsx index abcb259260..83ad468395 100644 --- a/packages/perseus/src/components/text-input.tsx +++ b/packages/perseus/src/components/text-input.tsx @@ -1,7 +1,7 @@ /* eslint-disable @khanacademy/ts-no-error-suppressions */ +import {Errors, PerseusError} from "@khanacademy/perseus-core"; import {TextField} from "@khanacademy/wonder-blocks-form"; import * as React from "react"; -import ReactDOM from "react-dom"; import type {StyleType} from "@khanacademy/wonder-blocks-core"; @@ -31,6 +31,7 @@ function uniqueIdForInput(prefix = "input-") { } class TextInput extends React.Component { + inputRef = React.createRef(); static defaultProps: DefaultProps = { value: "", disabled: false, @@ -47,45 +48,46 @@ class TextInput extends React.Component { } } + _getInput: () => HTMLInputElement = () => { + if (!this.inputRef.current) { + throw new PerseusError( + "Input ref accessed before set", + Errors.Internal, + ); + } + + return this.inputRef.current; + }; + focus: () => void = () => { - // @ts-expect-error - TS2531 - Object is possibly 'null'. | TS2339 - Property 'focus' does not exist on type 'Element | Text'. - ReactDOM.findDOMNode(this).focus(); + this._getInput().focus(); }; blur: () => void = () => { - // @ts-expect-error - TS2531 - Object is possibly 'null'. | TS2339 - Property 'blur' does not exist on type 'Element | Text'. - ReactDOM.findDOMNode(this).blur(); + this._getInput().blur(); }; getValue: () => string | null | undefined = () => { - // @ts-expect-error - TS2339 - Property 'value' does not exist on type 'Element | Text'. - return ReactDOM.findDOMNode(this)?.value; + return this.inputRef.current?.value; }; getStringValue: () => string | null | undefined = () => { - // @ts-expect-error - TS2339 - Property 'value' does not exist on type 'Element | Text'. - return ReactDOM.findDOMNode(this)?.value.toString(); + return this.inputRef.current?.value.toString(); }; setSelectionRange: (arg1: number, arg2: number) => void = ( selectionStart, selectionEnd, ) => { - // @ts-expect-error - TS2531 - Object is possibly 'null'. | TS2339 - Property 'setSelectionRange' does not exist on type 'Element | Text'. - ReactDOM.findDOMNode(this).setSelectionRange( - selectionStart, - selectionEnd, - ); + this._getInput().setSelectionRange(selectionStart, selectionEnd); }; - getSelectionStart: () => number = () => { - // @ts-expect-error - TS2531 - Object is possibly 'null'. | TS2339 - Property 'selectionStart' does not exist on type 'Element | Text'. - return ReactDOM.findDOMNode(this).selectionStart; + getSelectionStart: () => number | null = () => { + return this._getInput().selectionStart; }; - getSelectionEnd: () => number = () => { - // @ts-expect-error - TS2531 - Object is possibly 'null'. | TS2339 - Property 'selectionEnd' does not exist on type 'Element | Text'. - return ReactDOM.findDOMNode(this).selectionEnd; + getSelectionEnd: () => number | null = () => { + return this._getInput().selectionEnd; }; render(): React.ReactNode { @@ -104,6 +106,7 @@ class TextInput extends React.Component { return ( Date: Wed, 27 Nov 2024 08:48:35 -0600 Subject: [PATCH 10/42] remove use of findDOMNode in number-input (#1915) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary: When running tests, I was tired of seeing: > Warning: findDOMNode is deprecated and will be removed in the next major release. Instead, add a ref directly to the element you want to reference. Learn more about using refs safely here: https://reactjs.org/link/strict-mode-find-node So I thought I'd start chipping away at them. ## Test plan: Nothing should change, just an implementation detail. Author: handeyeco Reviewers: jeremywiebe, handeyeco, nishasy Required Reviewers: Approved By: jeremywiebe Checks: ✅ Publish npm snapshot (ubuntu-latest, 20.x), ✅ Lint, Typecheck, Format, and Test (ubuntu-latest, 20.x), ✅ Cypress (ubuntu-latest, 20.x), ✅ Check builds for changes in size (ubuntu-latest, 20.x), ✅ Publish Storybook to Chromatic (ubuntu-latest, 20.x), ✅ Check for .changeset entries for all changed files (ubuntu-latest, 20.x), ✅ gerald Pull Request URL: https://github.com/Khan/perseus/pull/1915 --- .changeset/chilly-carrots-wink.md | 5 ++ .../perseus/src/components/number-input.tsx | 57 ++++++++----------- 2 files changed, 30 insertions(+), 32 deletions(-) create mode 100644 .changeset/chilly-carrots-wink.md diff --git a/.changeset/chilly-carrots-wink.md b/.changeset/chilly-carrots-wink.md new file mode 100644 index 0000000000..eb31515686 --- /dev/null +++ b/.changeset/chilly-carrots-wink.md @@ -0,0 +1,5 @@ +--- +"@khanacademy/perseus": patch +--- + +Remove use of findDOMNode in number-input component diff --git a/packages/perseus/src/components/number-input.tsx b/packages/perseus/src/components/number-input.tsx index 2d7f20efd8..d375a882c4 100644 --- a/packages/perseus/src/components/number-input.tsx +++ b/packages/perseus/src/components/number-input.tsx @@ -1,10 +1,8 @@ -/* eslint-disable @khanacademy/ts-no-error-suppressions */ import {number as knumber} from "@khanacademy/kmath"; +import {Errors, PerseusError} from "@khanacademy/perseus-core"; import classNames from "classnames"; -import $ from "jquery"; import PropTypes from "prop-types"; import * as React from "react"; -import ReactDOM from "react-dom"; import _ from "underscore"; import Util from "../util"; @@ -40,6 +38,7 @@ const getNumericFormat = KhanMath.getNumericFormat; class NumberInput extends React.Component { static contextType = PerseusI18nContext; declare context: React.ContextType; + inputRef = React.createRef(); static propTypes = { value: PropTypes.number, @@ -71,20 +70,27 @@ class NumberInput extends React.Component { } } + _getInput: () => HTMLInputElement = () => { + if (!this.inputRef.current) { + throw new PerseusError( + "Input ref accessed before set", + Errors.Internal, + ); + } + + return this.inputRef.current; + }; + /* Return the current "value" of this input * If empty, it returns the placeholder (if it is a number) or null */ getValue: () => any = () => { - return this.parseInputValue( - // @ts-expect-error - TS2531 - Object is possibly 'null'. | TS2339 - Property 'value' does not exist on type 'Element | Text'. - ReactDOM.findDOMNode(this.refs.input).value, // eslint-disable-line react/no-string-refs - ); + return this.parseInputValue(this._getInput().value); }; /* Return the current string value of this input */ getStringValue: () => string = () => { - // @ts-expect-error - TS2531 - Object is possibly 'null'. | TS2339 - Property 'value' does not exist on type 'Element | Text'. - return ReactDOM.findDOMNode(this.refs.input).value.toString(); // eslint-disable-line react/no-string-refs + return this._getInput().toString(); }; parseInputValue: (arg1: any) => any = (value) => { @@ -98,36 +104,28 @@ class NumberInput extends React.Component { /* Set text input focus to this input */ focus: () => void = () => { - // @ts-expect-error - TS2531 - Object is possibly 'null'. | TS2339 - Property 'focus' does not exist on type 'Element | Text'. - ReactDOM.findDOMNode(this.refs.input).focus(); // eslint-disable-line react/no-string-refs + this._getInput().focus(); this._handleFocus(); }; blur: () => void = () => { - // @ts-expect-error - TS2531 - Object is possibly 'null'. | TS2339 - Property 'blur' does not exist on type 'Element | Text'. - ReactDOM.findDOMNode(this.refs.input).blur(); // eslint-disable-line react/no-string-refs + this._getInput().blur(); this._handleBlur(); }; - setSelectionRange: (arg1: number, arg2: number) => any = ( + setSelectionRange: (arg1: number, arg2: number) => void = ( selectionStart, selectionEnd, ) => { - // @ts-expect-error - TS2531 - Object is possibly 'null'. | TS2339 - Property 'setSelectionRange' does not exist on type 'Element | Text'. - ReactDOM.findDOMNode(this).setSelectionRange( - selectionStart, - selectionEnd, - ); + this._getInput().setSelectionRange(selectionStart, selectionEnd); }; - getSelectionStart: () => number = () => { - // @ts-expect-error - TS2531 - Object is possibly 'null'. | TS2339 - Property 'selectionStart' does not exist on type 'Element | Text'. - return ReactDOM.findDOMNode(this).selectionStart; + getSelectionStart: () => number | null = () => { + return this._getInput().selectionStart; }; - getSelectionEnd: () => number = () => { - // @ts-expect-error - TS2531 - Object is possibly 'null'. | TS2339 - Property 'selectionEnd' does not exist on type 'Element | Text'. - return ReactDOM.findDOMNode(this).selectionEnd; + getSelectionEnd: () => number | null = () => { + return this._getInput().selectionEnd; }; _checkValidity: (arg1: any) => boolean = (value) => { @@ -203,11 +201,7 @@ class NumberInput extends React.Component { }; _setValue: (arg1: number, arg2: MathFormat) => void = (val, format) => { - // eslint-disable-next-line react/no-string-refs - // @ts-expect-error - TS2769 - No overload matches this call. | TS2339 - Property 'val' does not exist on type 'JQueryStatic'. - $(ReactDOM.findDOMNode(this.refs.input)).val( - toNumericString(val, format), - ); + this._getInput().value = toNumericString(val, format); }; render(): React.ReactNode { @@ -237,8 +231,7 @@ class NumberInput extends React.Component { {...restProps} className={classes} type="text" - // eslint-disable-next-line react/no-string-refs - ref="input" + ref={this.inputRef} onChange={this._handleChange} onFocus={this._handleFocus} onBlur={this._handleBlur} From 3e98b7cd300052eeacbe9fcdbd312091c678107b Mon Sep 17 00:00:00 2001 From: Matthew Date: Wed, 27 Nov 2024 08:49:30 -0600 Subject: [PATCH 11/42] Add tests for propUpgrades functions (#1914) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary: Only 3 widgets seem to use the `propUpgrades` mechanism: - Radio - Expression - Measurer None of them seemed to have tests (at least not direct tests), so this adds them. I also took out underscore after I wrote the tests. ## Test plan: This is the test plan Author: handeyeco Reviewers: jeremywiebe, handeyeco Required Reviewers: Approved By: jeremywiebe Checks: ✅ Publish npm snapshot (ubuntu-latest, 20.x), ✅ Lint, Typecheck, Format, and Test (ubuntu-latest, 20.x), ✅ Check for .changeset entries for all changed files (ubuntu-latest, 20.x), ✅ Check builds for changes in size (ubuntu-latest, 20.x), ✅ Cypress (ubuntu-latest, 20.x), ✅ Publish Storybook to Chromatic (ubuntu-latest, 20.x), ✅ gerald Pull Request URL: https://github.com/Khan/perseus/pull/1914 --- .changeset/two-feet-care.md | 5 +++ .../widgets/expression/expression.test.tsx | 37 +++++++++++++++- .../src/widgets/measurer/measurer.test.tsx | 44 +++++++++++++++++++ .../perseus/src/widgets/measurer/measurer.tsx | 24 +++++----- .../src/widgets/radio/__tests__/radio.test.ts | 35 ++++++++++++++- packages/perseus/src/widgets/radio/radio.ts | 20 ++++----- 6 files changed, 138 insertions(+), 27 deletions(-) create mode 100644 .changeset/two-feet-care.md create mode 100644 packages/perseus/src/widgets/measurer/measurer.test.tsx diff --git a/.changeset/two-feet-care.md b/.changeset/two-feet-care.md new file mode 100644 index 0000000000..3a774776ad --- /dev/null +++ b/.changeset/two-feet-care.md @@ -0,0 +1,5 @@ +--- +"@khanacademy/perseus": patch +--- + +Add tests for propUpgrades functions (and remove underscore usage) diff --git a/packages/perseus/src/widgets/expression/expression.test.tsx b/packages/perseus/src/widgets/expression/expression.test.tsx index be8d76b63c..2f5ee2a239 100644 --- a/packages/perseus/src/widgets/expression/expression.test.tsx +++ b/packages/perseus/src/widgets/expression/expression.test.tsx @@ -17,7 +17,10 @@ import { expressionItemWithLabels, } from "./expression.testdata"; -import type {PerseusItem} from "../../perseus-types"; +import type { + PerseusExpressionWidgetOptions, + PerseusItem, +} from "../../perseus-types"; import type {UserEvent} from "@testing-library/user-event"; const renderAndAnswer = async ( @@ -523,4 +526,36 @@ describe("Expression Widget", function () { ).toBeNull(); }); }); + + describe("propUpgrades", () => { + it("can upgrade from v0 to v1", () => { + const v0props = { + times: false, + buttonSets: ["basic"], + functions: [], + form: false, + simplify: false, + value: "42", + }; + + const expected: PerseusExpressionWidgetOptions = { + times: false, + buttonSets: ["basic"], + functions: [], + answerForms: [ + { + considered: "correct", + form: false, + simplify: false, + value: "42", + }, + ], + }; + + const result: PerseusExpressionWidgetOptions = + ExpressionWidgetExport.propUpgrades["1"](v0props); + + expect(result).toEqual(expected); + }); + }); }); diff --git a/packages/perseus/src/widgets/measurer/measurer.test.tsx b/packages/perseus/src/widgets/measurer/measurer.test.tsx new file mode 100644 index 0000000000..6a35c0ca4c --- /dev/null +++ b/packages/perseus/src/widgets/measurer/measurer.test.tsx @@ -0,0 +1,44 @@ +import MeasurerWidgetExport from "./measurer"; + +import type {PerseusMeasurerWidgetOptions} from "../../perseus-types"; + +describe("measurer", () => { + describe("propUpgrades", () => { + it("can upgrade from v0 to v1", () => { + const v0props = { + imageUrl: "url", + imageTop: 42, + imageLeft: 42, + showProtractor: false, + showRuler: false, + rulerLabel: "test", + rulerTicks: 4, + rulerPixels: 4, + rulerLength: 4, + box: [4, 4], + static: false, + }; + + const expected: PerseusMeasurerWidgetOptions = { + image: { + url: "url", + top: 42, + left: 42, + }, + showProtractor: false, + showRuler: false, + rulerLabel: "test", + rulerTicks: 4, + rulerPixels: 4, + rulerLength: 4, + box: [4, 4], + static: false, + }; + + const result: PerseusMeasurerWidgetOptions = + MeasurerWidgetExport.propUpgrades["1"](v0props); + + expect(result).toEqual(expected); + }); + }); +}); diff --git a/packages/perseus/src/widgets/measurer/measurer.tsx b/packages/perseus/src/widgets/measurer/measurer.tsx index 4347ec677e..db4b00a07b 100644 --- a/packages/perseus/src/widgets/measurer/measurer.tsx +++ b/packages/perseus/src/widgets/measurer/measurer.tsx @@ -182,19 +182,17 @@ class Measurer extends React.Component implements Widget { } const propUpgrades = { - "1": (v0props: any): any => { - const v1props = _(v0props) - .chain() - .omit("imageUrl", "imageTop", "imageLeft") - .extend({ - image: { - url: v0props.imageUrl, - top: v0props.imageTop, - left: v0props.imageLeft, - }, - }) - .value(); - return v1props; + "1": (v0props: any): PerseusMeasurerWidgetOptions => { + const {imageUrl, imageTop, imageLeft, ...rest} = v0props; + + return { + ...rest, + image: { + url: imageUrl, + top: imageTop, + left: imageLeft, + }, + }; }, } as const; diff --git a/packages/perseus/src/widgets/radio/__tests__/radio.test.ts b/packages/perseus/src/widgets/radio/__tests__/radio.test.ts index 4eeb1de264..593f5a94c4 100644 --- a/packages/perseus/src/widgets/radio/__tests__/radio.test.ts +++ b/packages/perseus/src/widgets/radio/__tests__/radio.test.ts @@ -8,6 +8,7 @@ import * as Dependencies from "../../../dependencies"; import {mockStrings} from "../../../strings"; import {renderQuestion} from "../../__testutils__/renderQuestion"; import PassageWidget from "../../passage"; +import RadioWidgetExport from "../radio"; import scoreRadio from "../score-radio"; import { @@ -17,7 +18,10 @@ import { shuffledNoneQuestion, } from "./radio.testdata"; -import type {PerseusRenderer} from "../../../perseus-types"; +import type { + PerseusRadioWidgetOptions, + PerseusRenderer, +} from "../../../perseus-types"; import type {APIOptions} from "../../../types"; import type {PerseusRadioUserInput} from "../../../validation.types"; import type {UserEvent} from "@testing-library/user-event"; @@ -984,3 +988,32 @@ describe("scoring", () => { expect(renderer).toHaveBeenAnsweredIncorrectly(); }); }); + +describe("propsUpgrade", () => { + it("can upgrade from v0 to v1", () => { + const v0props = { + choices: [{content: "Choice 1"}, {content: "Choice 2"}], + }; + + const expected: PerseusRadioWidgetOptions = { + choices: [{content: "Choice 1"}, {content: "Choice 2"}], + hasNoneOfTheAbove: false, + }; + + const result: PerseusRadioWidgetOptions = + RadioWidgetExport.propUpgrades["1"](v0props); + + expect(result).toEqual(expected); + }); + + it("throws from noneOfTheAbove", () => { + const v0props = { + choices: [{content: "Choice 1"}, {content: "Choice 2"}], + noneOfTheAbove: true, + }; + + expect(() => RadioWidgetExport.propUpgrades["1"](v0props)).toThrow( + "radio widget v0 no longer supports auto noneOfTheAbove", + ); + }); +}); diff --git a/packages/perseus/src/widgets/radio/radio.ts b/packages/perseus/src/widgets/radio/radio.ts index 815320c5e5..528514e7c9 100644 --- a/packages/perseus/src/widgets/radio/radio.ts +++ b/packages/perseus/src/widgets/radio/radio.ts @@ -126,23 +126,19 @@ const transform = ( }; const propUpgrades = { - "1": (v0props: any): any => { - let choices; - let hasNoneOfTheAbove; - - if (!v0props.noneOfTheAbove) { - choices = v0props.choices; - hasNoneOfTheAbove = false; - } else { + "1": (v0props: any): PerseusRadioWidgetOptions => { + const {noneOfTheAbove, ...rest} = v0props; + + if (noneOfTheAbove) { throw new Error( "radio widget v0 no longer supports auto noneOfTheAbove", ); } - return _.extend(_.omit(v0props, "noneOfTheAbove"), { - choices: choices, - hasNoneOfTheAbove: hasNoneOfTheAbove, - }); + return { + ...rest, + hasNoneOfTheAbove: false, + }; }, } as const; From 8ec06f444d8f4559eda5c3dbf189e5183b1c5b42 Mon Sep 17 00:00:00 2001 From: Ben Christel Date: Wed, 27 Nov 2024 10:57:37 -0700 Subject: [PATCH 12/42] Inline version in expression widget parser (#1921) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Expression widget imports the KAS parser, which contains generated code with some syntax that `@swc-node/register` (which we use to run TypeScript from the command line) can't parse. The parse error was preventing the exhaustive tests for the parsers (packages/perseus/src/util/parse-perseus-json/exhaustive-test-tool/index.ts) from running. Since we can't change the generated code, the solution is to avoid importing the version number from the Expression widget, and hardcode it instead. I think this is okay, because the migration function that used the version number is called `migrateV0ToV1`, so we actually want to hardcode version 1 rather than taking the current version from the widget. Issue: none ## Test plan: Run the exhaustive test tool according to the instructions in the file. Author: benchristel Reviewers: jeremywiebe Required Reviewers: Approved By: jeremywiebe Checks: ✅ Publish npm snapshot (ubuntu-latest, 20.x), ✅ Cypress (ubuntu-latest, 20.x), ✅ Check builds for changes in size (ubuntu-latest, 20.x), ✅ Lint, Typecheck, Format, and Test (ubuntu-latest, 20.x), ✅ Check for .changeset entries for all changed files (ubuntu-latest, 20.x), ✅ Publish Storybook to Chromatic (ubuntu-latest, 20.x), ✅ gerald Pull Request URL: https://github.com/Khan/perseus/pull/1921 --- .changeset/seven-owls-explain.md | 5 +++++ .../parse-perseus-json/perseus-parsers/expression-widget.ts | 3 +-- 2 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 .changeset/seven-owls-explain.md diff --git a/.changeset/seven-owls-explain.md b/.changeset/seven-owls-explain.md new file mode 100644 index 0000000000..6e1d852646 --- /dev/null +++ b/.changeset/seven-owls-explain.md @@ -0,0 +1,5 @@ +--- +"@khanacademy/perseus": patch +--- + +Internal: Inline widget version into Expression widget parser. diff --git a/packages/perseus/src/util/parse-perseus-json/perseus-parsers/expression-widget.ts b/packages/perseus/src/util/parse-perseus-json/perseus-parsers/expression-widget.ts index 3d181092fe..9bff1a6a46 100644 --- a/packages/perseus/src/util/parse-perseus-json/perseus-parsers/expression-widget.ts +++ b/packages/perseus/src/util/parse-perseus-json/perseus-parsers/expression-widget.ts @@ -1,4 +1,3 @@ -import ExpressionWidgetModule from "../../../widgets/expression/expression"; import { array, boolean, @@ -93,7 +92,7 @@ function migrateV0ToV1( const {options} = widget; return ctx.success({ ...widget, - version: ExpressionWidgetModule.version, + version: {major: 1, minor: 0}, options: { times: options.times, buttonSets: options.buttonSets, From 584153588be04c6deb7b5d76ed2b258d0f75a3e1 Mon Sep 17 00:00:00 2001 From: Jeremy Wiebe Date: Wed, 27 Nov 2024 10:42:06 -0800 Subject: [PATCH 13/42] Enable dependabot updates for WB/WS (and group them) (#1863) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary: Juan and I were discussing keeping Perseus dependnecies up to date with webapp (especially WB and WS). This is a trial run to see if Dependabot will help us. It should open up a new PR any time WB or WS packages are released. And all WB or WS packages that received an update should be updated together in the same Perseus PR. Issue: "none" ## Test plan: I suspect I'll need to land this and monitor it to test. Github action infra is notoriously difficult to test locally. Author: jeremywiebe Reviewers: jandrade, jeremywiebe, #perseus Required Reviewers: Approved By: jandrade Checks: ✅ Publish npm snapshot (ubuntu-latest, 20.x), ✅ Cypress (ubuntu-latest, 20.x), ✅ Check builds for changes in size (ubuntu-latest, 20.x), ✅ Lint, Typecheck, Format, and Test (ubuntu-latest, 20.x), ✅ Check for .changeset entries for all changed files (ubuntu-latest, 20.x), ✅ Publish Storybook to Chromatic (ubuntu-latest, 20.x), ✅ gerald Pull Request URL: https://github.com/Khan/perseus/pull/1863 --- .changeset/mighty-rules-talk.md | 5 +++++ .eslintrc.js | 5 +++++ .github/dependabot.yml | 12 ++++++++++++ .../components/__stories__/device-framer.stories.tsx | 5 ++--- .../perseus-editor/src/components/widget-editor.tsx | 4 ++-- 5 files changed, 26 insertions(+), 5 deletions(-) create mode 100644 .changeset/mighty-rules-talk.md diff --git a/.changeset/mighty-rules-talk.md b/.changeset/mighty-rules-talk.md new file mode 100644 index 0000000000..7946251c38 --- /dev/null +++ b/.changeset/mighty-rules-talk.md @@ -0,0 +1,5 @@ +--- +"@khanacademy/perseus-editor": patch +--- + +Switch two corner usages of deprecated @khanacademy/wonder-blocks-spacing to @khanacademy/wonder-blocks-tokens diff --git a/.eslintrc.js b/.eslintrc.js index aee5d9fe7d..c7cadf436c 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -172,6 +172,11 @@ module.exports = { "no-invalid-this": "off", "@typescript-eslint/no-this-alias": "off", "no-unused-expressions": "off", + "no-restricted-imports": [ + "error", + "@khanacademy/wonder-blocks-color", + "@khanacademy/wonder-blocks-spacing", + ], "object-curly-spacing": "off", semi: "off", diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 2be174af9d..126d6a8650 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -10,3 +10,15 @@ updates: allow: - dependency-name: "@khanacademy/eslint-config" - dependency-name: "@khanacademy/eslint-plugin" + assignees: + - "@Khan/perseus" + + # Grouped updates for Wonder Blocks and Wonder Stuff releases. + # This helps us to stay in sync with the latest releases of these packages. + groups: + wonder-stuff: + patterns: + - "@khanacademy/wonder-stuff-*" + wonder-blocks: + patterns: + - "@khanacademy/wonder-blocks-*" diff --git a/packages/perseus-editor/src/components/__stories__/device-framer.stories.tsx b/packages/perseus-editor/src/components/__stories__/device-framer.stories.tsx index 79ae5e8f18..a6f9f06cfc 100644 --- a/packages/perseus-editor/src/components/__stories__/device-framer.stories.tsx +++ b/packages/perseus-editor/src/components/__stories__/device-framer.stories.tsx @@ -1,5 +1,4 @@ -import Spacing from "@khanacademy/wonder-blocks-spacing"; -import {color} from "@khanacademy/wonder-blocks-tokens"; +import {color, spacing} from "@khanacademy/wonder-blocks-tokens"; import DeviceFramer from "../device-framer"; @@ -21,7 +20,7 @@ const SampleContent = () => { color: color.offWhite, width: "90%", height: "300px", - padding: Spacing.medium_16, + padding: spacing.medium_16, }} > The DeviceFramer controls the size of the content inside the frame. diff --git a/packages/perseus-editor/src/components/widget-editor.tsx b/packages/perseus-editor/src/components/widget-editor.tsx index 5b21deadd2..7c406660a0 100644 --- a/packages/perseus-editor/src/components/widget-editor.tsx +++ b/packages/perseus-editor/src/components/widget-editor.tsx @@ -8,8 +8,8 @@ import { } from "@khanacademy/perseus"; import {useUniqueIdWithMock} from "@khanacademy/wonder-blocks-core"; import {Strut} from "@khanacademy/wonder-blocks-layout"; -import Spacing from "@khanacademy/wonder-blocks-spacing"; import Switch from "@khanacademy/wonder-blocks-switch"; +import {spacing} from "@khanacademy/wonder-blocks-tokens"; import * as React from "react"; import _ from "underscore"; @@ -241,7 +241,7 @@ function LabeledSwitch(props: { return ( <> - + ); From 84d8a9f26ffb9df185f3ecd62cc210e9c523e51b Mon Sep 17 00:00:00 2001 From: Khan Actions Bot <56267880+khan-actions-bot@users.noreply.github.com> Date: Wed, 27 Nov 2024 16:20:23 -0500 Subject: [PATCH 14/42] Update browserslist (#1909) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Summary Updates the `browserslist` and `caniuse-lite` npm packages ## Reviewing notes: There should only be changes to the `yarn.lock` file in this PR. Check that there is only 1 `caniuse-lite` package reference in the `yarn.lock` file (the intent of this update is to ensure that `caniuse-lite` is on the latest version and that there aren't multiple, conflicting versions that different tools might see). If everything looks fine, please approve this PR and then land it (either with the Big Green Merge Button ™️ or by using `git land ` in a terminal). Author: khan-actions-bot Reviewers: jeremywiebe, #frontend-infra-web Required Reviewers: Approved By: jeremywiebe Checks: ✅ Publish npm snapshot (ubuntu-latest, 20.x), ✅ Cypress (ubuntu-latest, 20.x), ✅ Check builds for changes in size (ubuntu-latest, 20.x), ✅ Check for .changeset entries for all changed files (ubuntu-latest, 20.x), ✅ Publish Storybook to Chromatic (ubuntu-latest, 20.x), ✅ Lint, Typecheck, Format, and Test (ubuntu-latest, 20.x), ✅ gerald Pull Request URL: https://github.com/Khan/perseus/pull/1909 --- yarn.lock | 37 +++++++++++++++++++++++++++++++------ 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/yarn.lock b/yarn.lock index 24856b4d99..01e32c45b6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6162,9 +6162,9 @@ caniuse-api@^3.0.0: lodash.uniq "^4.5.0" caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001332, caniuse-lite@^1.0.30001541, caniuse-lite@^1.0.30001587: - version "1.0.30001680" - resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001680.tgz" - integrity sha512-rPQy70G6AGUMnbwS1z6Xg+RkHYPAi18ihs47GH0jcxIG7wArmPgY3XbS2sRdBbxJljp3thdT8BIqv9ccCypiPA== + version "1.0.30001684" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001684.tgz" + integrity sha512-G1LRwLIQjBQoyq0ZJGqGIJUXzJ8irpbjHLpVRXDvBEScFJ9b17sgK6vlx0GAJFE21okD7zXl08rRRUfq6HdoEQ== caseless@~0.12.0: version "0.12.0" @@ -15170,7 +15170,7 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -15188,6 +15188,15 @@ string-width@^1.0.1: is-fullwidth-code-point "^1.0.0" strip-ansi "^3.0.0" +"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + string-width@^5.0.1, string-width@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" @@ -15272,7 +15281,7 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -15286,6 +15295,13 @@ strip-ansi@^3.0.0, strip-ansi@^3.0.1: dependencies: ansi-regex "^2.0.0" +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + strip-ansi@^7.0.1: version "7.1.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" @@ -16519,7 +16535,7 @@ wordwrap@^1.0.0: resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -16537,6 +16553,15 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" From 060fbe18f6b57fe11b10159ff8b82863493563a1 Mon Sep 17 00:00:00 2001 From: Khan Actions Bot <56267880+khan-actions-bot@users.noreply.github.com> Date: Wed, 27 Nov 2024 17:25:37 -0500 Subject: [PATCH 15/42] Version Packages (#1918) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR was opened by the [Changesets release](https://github.com/changesets/action) GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated. # Releases ## @khanacademy/perseus@43.1.0 ### Minor Changes - [#1898](https://github.com/Khan/perseus/pull/1898) [`3a9b5921b`](https://github.com/Khan/perseus/commit/3a9b5921bff7ae038f59ecb6817babd2b21df0bb) Thanks [@Myranae](https://github.com/Myranae)! - Introduces a validation function for the dropdown widget (extracted from dropdown scoring function). - [#1862](https://github.com/Khan/perseus/pull/1862) [`451de899f`](https://github.com/Khan/perseus/commit/451de899fd3d40bf415cb2318048e90fb1e6670f) Thanks [@Myranae](https://github.com/Myranae)! - Split out validation function for the `categorizer` widget. This can be used to check if the user selected an answer for every row, confirming the question is ready to be scored. - [#1882](https://github.com/Khan/perseus/pull/1882) [`40d2ebb75`](https://github.com/Khan/perseus/commit/40d2ebb75fdadfb361330236e0fb9e54a32d0fc2) Thanks [@jeremywiebe](https://github.com/jeremywiebe)! - Introduces a validation function for the numeric-input widget (extracted from numeric-input scoring function). - [#1899](https://github.com/Khan/perseus/pull/1899) [`2437ce61b`](https://github.com/Khan/perseus/commit/2437ce61bae1aef2db28e89956aa73463ada16cc) Thanks [@Myranae](https://github.com/Myranae)! - Introduces a validation function for the plotter widget (extracted from the scoring function). - [#1869](https://github.com/Khan/perseus/pull/1869) [`f43edd42c`](https://github.com/Khan/perseus/commit/f43edd42ccfacd1500d2f73ccb0d3f8dce777173) Thanks [@Myranae](https://github.com/Myranae)! - Split out validation function for the `orderer` widget. This can be used to check if the user has ordered at least one option, confirming the question is ready to be scored. - [#1902](https://github.com/Khan/perseus/pull/1902) [`0cec7628c`](https://github.com/Khan/perseus/commit/0cec7628c4a061f14b126fd1e3dab6df45fc0178) Thanks [@Myranae](https://github.com/Myranae)! - Introduces a validation function for the radio widget (extracted from the scoring function), though not all validation logic can be extracted. - [#1876](https://github.com/Khan/perseus/pull/1876) [`0bd4270ad`](https://github.com/Khan/perseus/commit/0bd4270ade576bec1ac0c86b251f972a2c354056) Thanks [@Myranae](https://github.com/Myranae)! - Split out validation function for the `sorter` widget. This can be used to check if the user has made any changes to the sorting order, confirming whether or not the question can be scored. ### Patch Changes - [#1907](https://github.com/Khan/perseus/pull/1907) [`3dbca965a`](https://github.com/Khan/perseus/commit/3dbca965a2bbaa2d980c1cc4c439469157e0bd33) Thanks [@benchristel](https://github.com/benchristel)! - Internal: add and pass regression tests for `PerseusItem` parser. - [#1915](https://github.com/Khan/perseus/pull/1915) [`ee09e9fc0`](https://github.com/Khan/perseus/commit/ee09e9fc0ad5eb65340d0f1cbe240741ebfcd3c3) Thanks [@handeyeco](https://github.com/handeyeco)! - Remove use of findDOMNode in number-input component - [#1920](https://github.com/Khan/perseus/pull/1920) [`88ba71bef`](https://github.com/Khan/perseus/commit/88ba71bef0cdd75fa0c8b467dcea2cccc637d034) Thanks [@handeyeco](https://github.com/handeyeco)! - Fix some file-wide error suppressions - [#1919](https://github.com/Khan/perseus/pull/1919) [`64ea2ee86`](https://github.com/Khan/perseus/commit/64ea2ee86264a20f1d0e34968831945fea8ed36b) Thanks [@handeyeco](https://github.com/handeyeco)! - Remove usage of findDOMNode in text-input component - [#1906](https://github.com/Khan/perseus/pull/1906) [`799ffe4a5`](https://github.com/Khan/perseus/commit/799ffe4a50e3e3bc435d0ef96388c1e8fae2167d) Thanks [@anakaren-rojas](https://github.com/anakaren-rojas)! - update moveable point component and use control point method to have optional params - [#1895](https://github.com/Khan/perseus/pull/1895) [`841551a65`](https://github.com/Khan/perseus/commit/841551a65732a276266690ddaaa51a3810398d03) Thanks [@benchristel](https://github.com/benchristel)! - Internal: remove unused fields from `answerArea` when parsing `PerseusItem`s. - [#1921](https://github.com/Khan/perseus/pull/1921) [`8ec06f444`](https://github.com/Khan/perseus/commit/8ec06f444d8f4559eda5c3dbf189e5183b1c5b42) Thanks [@benchristel](https://github.com/benchristel)! - Internal: Inline widget version into Expression widget parser. - [#1914](https://github.com/Khan/perseus/pull/1914) [`3e98b7cd3`](https://github.com/Khan/perseus/commit/3e98b7cd300052eeacbe9fcdbd312091c678107b) Thanks [@handeyeco](https://github.com/handeyeco)! - Add tests for propUpgrades functions (and remove underscore usage) - [#1908](https://github.com/Khan/perseus/pull/1908) [`7f2866cf4`](https://github.com/Khan/perseus/commit/7f2866cf401aa4fc7a3b2b15d8cdc247a953e9f8) Thanks [@benchristel](https://github.com/benchristel)! - Internal: Migrate expression widget options to the latest version in parseAndTypecheckPerseusItem (not yet used in production). - Updated dependencies \[[`88ba71bef`](https://github.com/Khan/perseus/commit/88ba71bef0cdd75fa0c8b467dcea2cccc637d034)]: - @khanacademy/kas@0.4.2 - @khanacademy/simple-markdown@0.13.6 - @khanacademy/pure-markdown@0.3.13 ## @khanacademy/kas@0.4.2 ### Patch Changes - [#1920](https://github.com/Khan/perseus/pull/1920) [`88ba71bef`](https://github.com/Khan/perseus/commit/88ba71bef0cdd75fa0c8b467dcea2cccc637d034) Thanks [@handeyeco](https://github.com/handeyeco)! - Fix some file-wide error suppressions ## @khanacademy/perseus-editor@15.0.2 ### Patch Changes - [#1920](https://github.com/Khan/perseus/pull/1920) [`88ba71bef`](https://github.com/Khan/perseus/commit/88ba71bef0cdd75fa0c8b467dcea2cccc637d034) Thanks [@handeyeco](https://github.com/handeyeco)! - Fix some file-wide error suppressions - [#1863](https://github.com/Khan/perseus/pull/1863) [`584153588`](https://github.com/Khan/perseus/commit/584153588be04c6deb7b5d76ed2b258d0f75a3e1) Thanks [@jeremywiebe](https://github.com/jeremywiebe)! - Switch two corner usages of deprecated @khanacademy/wonder-blocks-spacing to @khanacademy/wonder-blocks-tokens - Updated dependencies \[[`3dbca965a`](https://github.com/Khan/perseus/commit/3dbca965a2bbaa2d980c1cc4c439469157e0bd33), [`ee09e9fc0`](https://github.com/Khan/perseus/commit/ee09e9fc0ad5eb65340d0f1cbe240741ebfcd3c3), [`3a9b5921b`](https://github.com/Khan/perseus/commit/3a9b5921bff7ae038f59ecb6817babd2b21df0bb), [`88ba71bef`](https://github.com/Khan/perseus/commit/88ba71bef0cdd75fa0c8b467dcea2cccc637d034), [`64ea2ee86`](https://github.com/Khan/perseus/commit/64ea2ee86264a20f1d0e34968831945fea8ed36b), [`451de899f`](https://github.com/Khan/perseus/commit/451de899fd3d40bf415cb2318048e90fb1e6670f), [`40d2ebb75`](https://github.com/Khan/perseus/commit/40d2ebb75fdadfb361330236e0fb9e54a32d0fc2), [`799ffe4a5`](https://github.com/Khan/perseus/commit/799ffe4a50e3e3bc435d0ef96388c1e8fae2167d), [`2437ce61b`](https://github.com/Khan/perseus/commit/2437ce61bae1aef2db28e89956aa73463ada16cc), [`841551a65`](https://github.com/Khan/perseus/commit/841551a65732a276266690ddaaa51a3810398d03), [`8ec06f444`](https://github.com/Khan/perseus/commit/8ec06f444d8f4559eda5c3dbf189e5183b1c5b42), [`f43edd42c`](https://github.com/Khan/perseus/commit/f43edd42ccfacd1500d2f73ccb0d3f8dce777173), [`0cec7628c`](https://github.com/Khan/perseus/commit/0cec7628c4a061f14b126fd1e3dab6df45fc0178), [`0bd4270ad`](https://github.com/Khan/perseus/commit/0bd4270ade576bec1ac0c86b251f972a2c354056), [`3e98b7cd3`](https://github.com/Khan/perseus/commit/3e98b7cd300052eeacbe9fcdbd312091c678107b), [`7f2866cf4`](https://github.com/Khan/perseus/commit/7f2866cf401aa4fc7a3b2b15d8cdc247a953e9f8)]: - @khanacademy/perseus@43.1.0 - @khanacademy/kas@0.4.2 - @khanacademy/pure-markdown@0.3.13 ## @khanacademy/pure-markdown@0.3.13 ### Patch Changes - Updated dependencies \[[`88ba71bef`](https://github.com/Khan/perseus/commit/88ba71bef0cdd75fa0c8b467dcea2cccc637d034)]: - @khanacademy/simple-markdown@0.13.6 ## @khanacademy/simple-markdown@0.13.6 ### Patch Changes - [#1920](https://github.com/Khan/perseus/pull/1920) [`88ba71bef`](https://github.com/Khan/perseus/commit/88ba71bef0cdd75fa0c8b467dcea2cccc637d034) Thanks [@handeyeco](https://github.com/handeyeco)! - Fix some file-wide error suppressions ## @khanacademy/perseus-dev-ui@4.0.1 ### Patch Changes - Updated dependencies \[[`88ba71bef`](https://github.com/Khan/perseus/commit/88ba71bef0cdd75fa0c8b467dcea2cccc637d034)]: - @khanacademy/kas@0.4.2 - @khanacademy/simple-markdown@0.13.6 - @khanacademy/pure-markdown@0.3.13 Author: khan-actions-bot Reviewers: jeremywiebe Required Reviewers: Approved By: jeremywiebe Checks: ✅ Cypress (ubuntu-latest, 20.x), ⏭️ Publish npm snapshot, ✅ Check builds for changes in size (ubuntu-latest, 20.x), ✅ Lint, Typecheck, Format, and Test (ubuntu-latest, 20.x), ✅ Check for .changeset entries for all changed files (ubuntu-latest, 20.x), ✅ gerald Pull Request URL: https://github.com/Khan/perseus/pull/1918 --- .changeset/chilled-turtles-drive.md | 5 ---- .changeset/chilly-carrots-wink.md | 5 ---- .changeset/famous-horses-grab.md | 5 ---- .changeset/few-jokes-travel.md | 8 ----- .changeset/giant-tables-impress.md | 5 ---- .changeset/green-ghosts-burn.md | 5 ---- .changeset/honest-avocados-accept.md | 5 ---- .changeset/lazy-geckos-suffer.md | 5 ---- .changeset/mighty-rules-talk.md | 5 ---- .changeset/nice-fans-swim.md | 5 ---- .changeset/rotten-peaches-move.md | 5 ---- .changeset/seven-owls-explain.md | 5 ---- .changeset/sharp-radios-burn.md | 5 ---- .changeset/spotty-moles-reply.md | 5 ---- .changeset/sweet-trainers-drop.md | 5 ---- .changeset/two-feet-care.md | 5 ---- .changeset/yellow-ducks-march.md | 5 ---- dev/CHANGELOG.md | 9 ++++++ dev/package.json | 8 ++--- packages/kas/CHANGELOG.md | 6 ++++ packages/kas/package.json | 2 +- packages/perseus-editor/CHANGELOG.md | 13 ++++++++ packages/perseus-editor/package.json | 8 ++--- packages/perseus-linter/package.json | 2 +- packages/perseus/CHANGELOG.md | 43 +++++++++++++++++++++++++++ packages/perseus/package.json | 8 ++--- packages/pure-markdown/CHANGELOG.md | 7 +++++ packages/pure-markdown/package.json | 4 +-- packages/simple-markdown/CHANGELOG.md | 6 ++++ packages/simple-markdown/package.json | 2 +- 30 files changed, 101 insertions(+), 105 deletions(-) delete mode 100644 .changeset/chilled-turtles-drive.md delete mode 100644 .changeset/chilly-carrots-wink.md delete mode 100644 .changeset/famous-horses-grab.md delete mode 100644 .changeset/few-jokes-travel.md delete mode 100644 .changeset/giant-tables-impress.md delete mode 100644 .changeset/green-ghosts-burn.md delete mode 100644 .changeset/honest-avocados-accept.md delete mode 100644 .changeset/lazy-geckos-suffer.md delete mode 100644 .changeset/mighty-rules-talk.md delete mode 100644 .changeset/nice-fans-swim.md delete mode 100644 .changeset/rotten-peaches-move.md delete mode 100644 .changeset/seven-owls-explain.md delete mode 100644 .changeset/sharp-radios-burn.md delete mode 100644 .changeset/spotty-moles-reply.md delete mode 100644 .changeset/sweet-trainers-drop.md delete mode 100644 .changeset/two-feet-care.md delete mode 100644 .changeset/yellow-ducks-march.md diff --git a/.changeset/chilled-turtles-drive.md b/.changeset/chilled-turtles-drive.md deleted file mode 100644 index 5eb3519556..0000000000 --- a/.changeset/chilled-turtles-drive.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@khanacademy/perseus": patch ---- - -Internal: add and pass regression tests for `PerseusItem` parser. diff --git a/.changeset/chilly-carrots-wink.md b/.changeset/chilly-carrots-wink.md deleted file mode 100644 index eb31515686..0000000000 --- a/.changeset/chilly-carrots-wink.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@khanacademy/perseus": patch ---- - -Remove use of findDOMNode in number-input component diff --git a/.changeset/famous-horses-grab.md b/.changeset/famous-horses-grab.md deleted file mode 100644 index 827d88abf1..0000000000 --- a/.changeset/famous-horses-grab.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@khanacademy/perseus": minor ---- - -Introduces a validation function for the dropdown widget (extracted from dropdown scoring function). diff --git a/.changeset/few-jokes-travel.md b/.changeset/few-jokes-travel.md deleted file mode 100644 index b4a57f6823..0000000000 --- a/.changeset/few-jokes-travel.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -"@khanacademy/kas": patch -"@khanacademy/perseus": patch -"@khanacademy/perseus-editor": patch -"@khanacademy/simple-markdown": patch ---- - -Fix some file-wide error suppressions diff --git a/.changeset/giant-tables-impress.md b/.changeset/giant-tables-impress.md deleted file mode 100644 index 32f5de4204..0000000000 --- a/.changeset/giant-tables-impress.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@khanacademy/perseus": patch ---- - -Remove usage of findDOMNode in text-input component diff --git a/.changeset/green-ghosts-burn.md b/.changeset/green-ghosts-burn.md deleted file mode 100644 index 58af29901b..0000000000 --- a/.changeset/green-ghosts-burn.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@khanacademy/perseus": minor ---- - -Split out validation function for the `categorizer` widget. This can be used to check if the user selected an answer for every row, confirming the question is ready to be scored. diff --git a/.changeset/honest-avocados-accept.md b/.changeset/honest-avocados-accept.md deleted file mode 100644 index 4d7d3129cf..0000000000 --- a/.changeset/honest-avocados-accept.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@khanacademy/perseus": minor ---- - -Introduces a validation function for the numeric-input widget (extracted from numeric-input scoring function). diff --git a/.changeset/lazy-geckos-suffer.md b/.changeset/lazy-geckos-suffer.md deleted file mode 100644 index 9b5853b83f..0000000000 --- a/.changeset/lazy-geckos-suffer.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@khanacademy/perseus": patch ---- - -update moveable point component and use control point method to have optional params diff --git a/.changeset/mighty-rules-talk.md b/.changeset/mighty-rules-talk.md deleted file mode 100644 index 7946251c38..0000000000 --- a/.changeset/mighty-rules-talk.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@khanacademy/perseus-editor": patch ---- - -Switch two corner usages of deprecated @khanacademy/wonder-blocks-spacing to @khanacademy/wonder-blocks-tokens diff --git a/.changeset/nice-fans-swim.md b/.changeset/nice-fans-swim.md deleted file mode 100644 index 04c7fe08ce..0000000000 --- a/.changeset/nice-fans-swim.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@khanacademy/perseus": minor ---- - -Introduces a validation function for the plotter widget (extracted from the scoring function). diff --git a/.changeset/rotten-peaches-move.md b/.changeset/rotten-peaches-move.md deleted file mode 100644 index 695cab47de..0000000000 --- a/.changeset/rotten-peaches-move.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@khanacademy/perseus": patch ---- - -Internal: remove unused fields from `answerArea` when parsing `PerseusItem`s. diff --git a/.changeset/seven-owls-explain.md b/.changeset/seven-owls-explain.md deleted file mode 100644 index 6e1d852646..0000000000 --- a/.changeset/seven-owls-explain.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@khanacademy/perseus": patch ---- - -Internal: Inline widget version into Expression widget parser. diff --git a/.changeset/sharp-radios-burn.md b/.changeset/sharp-radios-burn.md deleted file mode 100644 index 8517ff1850..0000000000 --- a/.changeset/sharp-radios-burn.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@khanacademy/perseus": minor ---- - -Split out validation function for the `orderer` widget. This can be used to check if the user has ordered at least one option, confirming the question is ready to be scored. diff --git a/.changeset/spotty-moles-reply.md b/.changeset/spotty-moles-reply.md deleted file mode 100644 index 8ab9761a4d..0000000000 --- a/.changeset/spotty-moles-reply.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@khanacademy/perseus": minor ---- - -Introduces a validation function for the radio widget (extracted from the scoring function), though not all validation logic can be extracted. diff --git a/.changeset/sweet-trainers-drop.md b/.changeset/sweet-trainers-drop.md deleted file mode 100644 index 9291f281b2..0000000000 --- a/.changeset/sweet-trainers-drop.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@khanacademy/perseus": minor ---- - -Split out validation function for the `sorter` widget. This can be used to check if the user has made any changes to the sorting order, confirming whether or not the question can be scored. diff --git a/.changeset/two-feet-care.md b/.changeset/two-feet-care.md deleted file mode 100644 index 3a774776ad..0000000000 --- a/.changeset/two-feet-care.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@khanacademy/perseus": patch ---- - -Add tests for propUpgrades functions (and remove underscore usage) diff --git a/.changeset/yellow-ducks-march.md b/.changeset/yellow-ducks-march.md deleted file mode 100644 index 6346118468..0000000000 --- a/.changeset/yellow-ducks-march.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@khanacademy/perseus": patch ---- - -Internal: Migrate expression widget options to the latest version in parseAndTypecheckPerseusItem (not yet used in production). diff --git a/dev/CHANGELOG.md b/dev/CHANGELOG.md index b5b34f4701..81e0a3d67c 100644 --- a/dev/CHANGELOG.md +++ b/dev/CHANGELOG.md @@ -1,5 +1,14 @@ # @khanacademy/perseus-dev-ui +## 4.0.1 + +### Patch Changes + +- Updated dependencies [[`88ba71bef`](https://github.com/Khan/perseus/commit/88ba71bef0cdd75fa0c8b467dcea2cccc637d034)]: + - @khanacademy/kas@0.4.2 + - @khanacademy/simple-markdown@0.13.6 + - @khanacademy/pure-markdown@0.3.13 + ## 4.0.0 ### Major Changes diff --git a/dev/package.json b/dev/package.json index a7417e90f0..05c73b0c24 100644 --- a/dev/package.json +++ b/dev/package.json @@ -3,7 +3,7 @@ "description": "Perseus dev UI", "author": "Khan Academy", "license": "MIT", - "version": "4.0.0", + "version": "4.0.1", "private": true, "repository": { "type": "git", @@ -14,13 +14,13 @@ "dev": "vite" }, "dependencies": { - "@khanacademy/kas": "^0.4.1", + "@khanacademy/kas": "^0.4.2", "@khanacademy/kmath": "^0.1.16", "@khanacademy/math-input": "^21.1.5", "@khanacademy/perseus-core": "1.5.3", "@khanacademy/perseus-linter": "^1.2.4", - "@khanacademy/pure-markdown": "^0.3.12", - "@khanacademy/simple-markdown": "^0.13.5", + "@khanacademy/pure-markdown": "^0.3.13", + "@khanacademy/simple-markdown": "^0.13.6", "@khanacademy/wonder-blocks-banner": "3.1.7", "@khanacademy/wonder-blocks-icon": "4.1.3", "@khanacademy/wonder-blocks-icon-button": "5.3.4", diff --git a/packages/kas/CHANGELOG.md b/packages/kas/CHANGELOG.md index 6760f07994..2510c98ba5 100644 --- a/packages/kas/CHANGELOG.md +++ b/packages/kas/CHANGELOG.md @@ -1,5 +1,11 @@ # @khanacademy/kas +## 0.4.2 + +### Patch Changes + +- [#1920](https://github.com/Khan/perseus/pull/1920) [`88ba71bef`](https://github.com/Khan/perseus/commit/88ba71bef0cdd75fa0c8b467dcea2cccc637d034) Thanks [@handeyeco](https://github.com/handeyeco)! - Fix some file-wide error suppressions + ## 0.4.1 ### Patch Changes diff --git a/packages/kas/package.json b/packages/kas/package.json index 0dc1d24065..238809566c 100644 --- a/packages/kas/package.json +++ b/packages/kas/package.json @@ -3,7 +3,7 @@ "description": "A lightweight JavaScript CAS for comparing expressions and equations.", "author": "Khan Academy", "license": "MIT", - "version": "0.4.1", + "version": "0.4.2", "publishConfig": { "access": "public" }, diff --git a/packages/perseus-editor/CHANGELOG.md b/packages/perseus-editor/CHANGELOG.md index 3628400ded..a1670ad017 100644 --- a/packages/perseus-editor/CHANGELOG.md +++ b/packages/perseus-editor/CHANGELOG.md @@ -1,5 +1,18 @@ # @khanacademy/perseus-editor +## 15.0.2 + +### Patch Changes + +- [#1920](https://github.com/Khan/perseus/pull/1920) [`88ba71bef`](https://github.com/Khan/perseus/commit/88ba71bef0cdd75fa0c8b467dcea2cccc637d034) Thanks [@handeyeco](https://github.com/handeyeco)! - Fix some file-wide error suppressions + +* [#1863](https://github.com/Khan/perseus/pull/1863) [`584153588`](https://github.com/Khan/perseus/commit/584153588be04c6deb7b5d76ed2b258d0f75a3e1) Thanks [@jeremywiebe](https://github.com/jeremywiebe)! - Switch two corner usages of deprecated @khanacademy/wonder-blocks-spacing to @khanacademy/wonder-blocks-tokens + +* Updated dependencies [[`3dbca965a`](https://github.com/Khan/perseus/commit/3dbca965a2bbaa2d980c1cc4c439469157e0bd33), [`ee09e9fc0`](https://github.com/Khan/perseus/commit/ee09e9fc0ad5eb65340d0f1cbe240741ebfcd3c3), [`3a9b5921b`](https://github.com/Khan/perseus/commit/3a9b5921bff7ae038f59ecb6817babd2b21df0bb), [`88ba71bef`](https://github.com/Khan/perseus/commit/88ba71bef0cdd75fa0c8b467dcea2cccc637d034), [`64ea2ee86`](https://github.com/Khan/perseus/commit/64ea2ee86264a20f1d0e34968831945fea8ed36b), [`451de899f`](https://github.com/Khan/perseus/commit/451de899fd3d40bf415cb2318048e90fb1e6670f), [`40d2ebb75`](https://github.com/Khan/perseus/commit/40d2ebb75fdadfb361330236e0fb9e54a32d0fc2), [`799ffe4a5`](https://github.com/Khan/perseus/commit/799ffe4a50e3e3bc435d0ef96388c1e8fae2167d), [`2437ce61b`](https://github.com/Khan/perseus/commit/2437ce61bae1aef2db28e89956aa73463ada16cc), [`841551a65`](https://github.com/Khan/perseus/commit/841551a65732a276266690ddaaa51a3810398d03), [`8ec06f444`](https://github.com/Khan/perseus/commit/8ec06f444d8f4559eda5c3dbf189e5183b1c5b42), [`f43edd42c`](https://github.com/Khan/perseus/commit/f43edd42ccfacd1500d2f73ccb0d3f8dce777173), [`0cec7628c`](https://github.com/Khan/perseus/commit/0cec7628c4a061f14b126fd1e3dab6df45fc0178), [`0bd4270ad`](https://github.com/Khan/perseus/commit/0bd4270ade576bec1ac0c86b251f972a2c354056), [`3e98b7cd3`](https://github.com/Khan/perseus/commit/3e98b7cd300052eeacbe9fcdbd312091c678107b), [`7f2866cf4`](https://github.com/Khan/perseus/commit/7f2866cf401aa4fc7a3b2b15d8cdc247a953e9f8)]: + - @khanacademy/perseus@43.1.0 + - @khanacademy/kas@0.4.2 + - @khanacademy/pure-markdown@0.3.13 + ## 15.0.1 ### Patch Changes diff --git a/packages/perseus-editor/package.json b/packages/perseus-editor/package.json index 5a8f23f580..462c4c5304 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": "15.0.1", + "version": "15.0.2", "publishConfig": { "access": "public" }, @@ -34,13 +34,13 @@ "test": "bash -c 'yarn --silent --cwd \"../..\" test ${@:0} $($([[ ${@: -1} = -* ]] || [[ ${@: -1} = bash ]]) && echo $PWD)'" }, "dependencies": { - "@khanacademy/kas": "^0.4.1", + "@khanacademy/kas": "^0.4.2", "@khanacademy/keypad-context": "^1.0.4", "@khanacademy/kmath": "^0.1.16", "@khanacademy/math-input": "^21.1.5", - "@khanacademy/perseus": "^43.0.1", + "@khanacademy/perseus": "^43.1.0", "@khanacademy/perseus-core": "1.5.3", - "@khanacademy/pure-markdown": "^0.3.12", + "@khanacademy/pure-markdown": "^0.3.13", "mafs": "^0.19.0" }, "devDependencies": { diff --git a/packages/perseus-linter/package.json b/packages/perseus-linter/package.json index 19125e667f..0db263a939 100644 --- a/packages/perseus-linter/package.json +++ b/packages/perseus-linter/package.json @@ -28,7 +28,7 @@ "@khanacademy/perseus-core": "1.5.3" }, "devDependencies": { - "@khanacademy/pure-markdown": "^0.3.12", + "@khanacademy/pure-markdown": "^0.3.13", "prop-types": "15.6.1" }, "peerDependencies": { diff --git a/packages/perseus/CHANGELOG.md b/packages/perseus/CHANGELOG.md index 217c033daf..f415910501 100644 --- a/packages/perseus/CHANGELOG.md +++ b/packages/perseus/CHANGELOG.md @@ -1,5 +1,48 @@ # @khanacademy/perseus +## 43.1.0 + +### Minor Changes + +- [#1898](https://github.com/Khan/perseus/pull/1898) [`3a9b5921b`](https://github.com/Khan/perseus/commit/3a9b5921bff7ae038f59ecb6817babd2b21df0bb) Thanks [@Myranae](https://github.com/Myranae)! - Introduces a validation function for the dropdown widget (extracted from dropdown scoring function). + +* [#1862](https://github.com/Khan/perseus/pull/1862) [`451de899f`](https://github.com/Khan/perseus/commit/451de899fd3d40bf415cb2318048e90fb1e6670f) Thanks [@Myranae](https://github.com/Myranae)! - Split out validation function for the `categorizer` widget. This can be used to check if the user selected an answer for every row, confirming the question is ready to be scored. + +- [#1882](https://github.com/Khan/perseus/pull/1882) [`40d2ebb75`](https://github.com/Khan/perseus/commit/40d2ebb75fdadfb361330236e0fb9e54a32d0fc2) Thanks [@jeremywiebe](https://github.com/jeremywiebe)! - Introduces a validation function for the numeric-input widget (extracted from numeric-input scoring function). + +* [#1899](https://github.com/Khan/perseus/pull/1899) [`2437ce61b`](https://github.com/Khan/perseus/commit/2437ce61bae1aef2db28e89956aa73463ada16cc) Thanks [@Myranae](https://github.com/Myranae)! - Introduces a validation function for the plotter widget (extracted from the scoring function). + +- [#1869](https://github.com/Khan/perseus/pull/1869) [`f43edd42c`](https://github.com/Khan/perseus/commit/f43edd42ccfacd1500d2f73ccb0d3f8dce777173) Thanks [@Myranae](https://github.com/Myranae)! - Split out validation function for the `orderer` widget. This can be used to check if the user has ordered at least one option, confirming the question is ready to be scored. + +* [#1902](https://github.com/Khan/perseus/pull/1902) [`0cec7628c`](https://github.com/Khan/perseus/commit/0cec7628c4a061f14b126fd1e3dab6df45fc0178) Thanks [@Myranae](https://github.com/Myranae)! - Introduces a validation function for the radio widget (extracted from the scoring function), though not all validation logic can be extracted. + +- [#1876](https://github.com/Khan/perseus/pull/1876) [`0bd4270ad`](https://github.com/Khan/perseus/commit/0bd4270ade576bec1ac0c86b251f972a2c354056) Thanks [@Myranae](https://github.com/Myranae)! - Split out validation function for the `sorter` widget. This can be used to check if the user has made any changes to the sorting order, confirming whether or not the question can be scored. + +### Patch Changes + +- [#1907](https://github.com/Khan/perseus/pull/1907) [`3dbca965a`](https://github.com/Khan/perseus/commit/3dbca965a2bbaa2d980c1cc4c439469157e0bd33) Thanks [@benchristel](https://github.com/benchristel)! - Internal: add and pass regression tests for `PerseusItem` parser. + +* [#1915](https://github.com/Khan/perseus/pull/1915) [`ee09e9fc0`](https://github.com/Khan/perseus/commit/ee09e9fc0ad5eb65340d0f1cbe240741ebfcd3c3) Thanks [@handeyeco](https://github.com/handeyeco)! - Remove use of findDOMNode in number-input component + +- [#1920](https://github.com/Khan/perseus/pull/1920) [`88ba71bef`](https://github.com/Khan/perseus/commit/88ba71bef0cdd75fa0c8b467dcea2cccc637d034) Thanks [@handeyeco](https://github.com/handeyeco)! - Fix some file-wide error suppressions + +* [#1919](https://github.com/Khan/perseus/pull/1919) [`64ea2ee86`](https://github.com/Khan/perseus/commit/64ea2ee86264a20f1d0e34968831945fea8ed36b) Thanks [@handeyeco](https://github.com/handeyeco)! - Remove usage of findDOMNode in text-input component + +- [#1906](https://github.com/Khan/perseus/pull/1906) [`799ffe4a5`](https://github.com/Khan/perseus/commit/799ffe4a50e3e3bc435d0ef96388c1e8fae2167d) Thanks [@anakaren-rojas](https://github.com/anakaren-rojas)! - update moveable point component and use control point method to have optional params + +* [#1895](https://github.com/Khan/perseus/pull/1895) [`841551a65`](https://github.com/Khan/perseus/commit/841551a65732a276266690ddaaa51a3810398d03) Thanks [@benchristel](https://github.com/benchristel)! - Internal: remove unused fields from `answerArea` when parsing `PerseusItem`s. + +- [#1921](https://github.com/Khan/perseus/pull/1921) [`8ec06f444`](https://github.com/Khan/perseus/commit/8ec06f444d8f4559eda5c3dbf189e5183b1c5b42) Thanks [@benchristel](https://github.com/benchristel)! - Internal: Inline widget version into Expression widget parser. + +* [#1914](https://github.com/Khan/perseus/pull/1914) [`3e98b7cd3`](https://github.com/Khan/perseus/commit/3e98b7cd300052eeacbe9fcdbd312091c678107b) Thanks [@handeyeco](https://github.com/handeyeco)! - Add tests for propUpgrades functions (and remove underscore usage) + +- [#1908](https://github.com/Khan/perseus/pull/1908) [`7f2866cf4`](https://github.com/Khan/perseus/commit/7f2866cf401aa4fc7a3b2b15d8cdc247a953e9f8) Thanks [@benchristel](https://github.com/benchristel)! - Internal: Migrate expression widget options to the latest version in parseAndTypecheckPerseusItem (not yet used in production). + +- Updated dependencies [[`88ba71bef`](https://github.com/Khan/perseus/commit/88ba71bef0cdd75fa0c8b467dcea2cccc637d034)]: + - @khanacademy/kas@0.4.2 + - @khanacademy/simple-markdown@0.13.6 + - @khanacademy/pure-markdown@0.3.13 + ## 43.0.1 ### Patch Changes diff --git a/packages/perseus/package.json b/packages/perseus/package.json index f27c775958..d3f46647b0 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": "43.0.1", + "version": "43.1.0", "publishConfig": { "access": "public" }, @@ -40,14 +40,14 @@ "test": "bash -c 'yarn --silent --cwd \"../..\" test ${@:0} $($([[ ${@: -1} = -* ]] || [[ ${@: -1} = bash ]]) && echo $PWD)'" }, "dependencies": { - "@khanacademy/kas": "^0.4.1", + "@khanacademy/kas": "^0.4.2", "@khanacademy/keypad-context": "^1.0.4", "@khanacademy/kmath": "^0.1.16", "@khanacademy/math-input": "^21.1.5", "@khanacademy/perseus-core": "1.5.3", "@khanacademy/perseus-linter": "^1.2.4", - "@khanacademy/pure-markdown": "^0.3.12", - "@khanacademy/simple-markdown": "^0.13.5", + "@khanacademy/pure-markdown": "^0.3.13", + "@khanacademy/simple-markdown": "^0.13.6", "@types/classnames": "2.2.0", "@use-gesture/react": "^10.2.27", "mafs": "0.19.0", diff --git a/packages/pure-markdown/CHANGELOG.md b/packages/pure-markdown/CHANGELOG.md index 944960fb3e..7983af68d3 100644 --- a/packages/pure-markdown/CHANGELOG.md +++ b/packages/pure-markdown/CHANGELOG.md @@ -1,5 +1,12 @@ # @khanacademy/pure-markdown +## 0.3.13 + +### Patch Changes + +- Updated dependencies [[`88ba71bef`](https://github.com/Khan/perseus/commit/88ba71bef0cdd75fa0c8b467dcea2cccc637d034)]: + - @khanacademy/simple-markdown@0.13.6 + ## 0.3.12 ### Patch Changes diff --git a/packages/pure-markdown/package.json b/packages/pure-markdown/package.json index 05ff0d30db..c33fa41a6c 100644 --- a/packages/pure-markdown/package.json +++ b/packages/pure-markdown/package.json @@ -3,7 +3,7 @@ "description": "SimpleMarkdown instance with non-react Perseus rules", "author": "Khan Academy", "license": "MIT", - "version": "0.3.12", + "version": "0.3.13", "publishConfig": { "access": "public" }, @@ -26,7 +26,7 @@ }, "dependencies": { "@khanacademy/perseus-core": "1.5.3", - "@khanacademy/simple-markdown": "^0.13.5" + "@khanacademy/simple-markdown": "^0.13.6" }, "devDependencies": {}, "peerDependencies": {}, diff --git a/packages/simple-markdown/CHANGELOG.md b/packages/simple-markdown/CHANGELOG.md index 7ed1ee74f4..b5a2e8106d 100644 --- a/packages/simple-markdown/CHANGELOG.md +++ b/packages/simple-markdown/CHANGELOG.md @@ -1,5 +1,11 @@ # @khanacademy/simple-markdown +## 0.13.6 + +### Patch Changes + +- [#1920](https://github.com/Khan/perseus/pull/1920) [`88ba71bef`](https://github.com/Khan/perseus/commit/88ba71bef0cdd75fa0c8b467dcea2cccc637d034) Thanks [@handeyeco](https://github.com/handeyeco)! - Fix some file-wide error suppressions + ## 0.13.5 ### Patch Changes diff --git a/packages/simple-markdown/package.json b/packages/simple-markdown/package.json index 586b43555c..7bd62e6c71 100644 --- a/packages/simple-markdown/package.json +++ b/packages/simple-markdown/package.json @@ -3,7 +3,7 @@ "description": "Javascript markdown parsing, made simple", "author": "Khan Academy", "license": "MIT", - "version": "0.13.5", + "version": "0.13.6", "publishConfig": { "access": "public" }, From 3d777d1c13eefedee250d8a494e08b513ddec937 Mon Sep 17 00:00:00 2001 From: Cat Johnson <123020281+catandthemachines@users.noreply.github.com> Date: Wed, 27 Nov 2024 15:21:09 -0800 Subject: [PATCH 16/42] Removing createReactClass usage in 3 files. (#1893) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary: Removing createReactClass usage in the following three files: - drag-target.tsx - sortable.tsx - stateful-editor-page.tsx Issue: LEMS-365 ## Test plan: Run "yarn test" and confirm no errors or regressions are introduced. Run "yarn start" and look and view if there are any issues with any Perseus components. Author: catandthemachines Reviewers: catandthemachines, jeremywiebe, benchristel, nishasy, handeyeco, anakaren-rojas Required Reviewers: Approved By: benchristel, jeremywiebe Checks: ✅ Publish npm snapshot (ubuntu-latest, 20.x), ✅ Check builds for changes in size (ubuntu-latest, 20.x), ✅ Cypress (ubuntu-latest, 20.x), ✅ Lint, Typecheck, Format, and Test (ubuntu-latest, 20.x), ✅ Publish Storybook to Chromatic (ubuntu-latest, 20.x), ✅ Check for .changeset entries for all changed files (ubuntu-latest, 20.x), ✅ gerald Pull Request URL: https://github.com/Khan/perseus/pull/1893 --- .changeset/khaki-keys-approve.md | 5 + .../src/components/drag-target.tsx | 96 ++++++----- .../src/components/sortable.tsx | 156 +++++++++++------- packages/perseus-editor/src/index.ts | 1 - .../src/stateful-editor-page.tsx | 83 ---------- 5 files changed, 157 insertions(+), 184 deletions(-) create mode 100644 .changeset/khaki-keys-approve.md delete mode 100644 packages/perseus-editor/src/stateful-editor-page.tsx diff --git a/.changeset/khaki-keys-approve.md b/.changeset/khaki-keys-approve.md new file mode 100644 index 0000000000..57858e31c7 --- /dev/null +++ b/.changeset/khaki-keys-approve.md @@ -0,0 +1,5 @@ +--- +"@khanacademy/perseus-editor": patch +--- + +Removing usage of createReactClass from several component files. diff --git a/packages/perseus-editor/src/components/drag-target.tsx b/packages/perseus-editor/src/components/drag-target.tsx index 4e78e7378f..6319ccbcb4 100644 --- a/packages/perseus-editor/src/components/drag-target.tsx +++ b/packages/perseus-editor/src/components/drag-target.tsx @@ -22,53 +22,75 @@ // * custom styles for global drag and dragOver // * only respond to certain types of drags (only images for instance)! -import createReactClass from "create-react-class"; -import PropTypes from "prop-types"; import * as React from "react"; -const DragTarget = createReactClass({ - propTypes: { - // All props not listed here are forwarded to the root element without - // modification. - onDrop: PropTypes.func.isRequired, - component: PropTypes.any, // component type - shouldDragHighlight: PropTypes.func, - style: PropTypes.any, - }, - getDefaultProps: function () { - return { - component: "div", - shouldDragHighlight: () => true, +type Props = { + onDrop: (e: DragEvent) => void; + component?: any; + shouldDragHighlight: (any) => boolean; + style?: any; + children?: any; + className?: string; +}; + +type DefaultProps = { + component: Props["component"]; + shouldDragHighlight: Props["shouldDragHighlight"]; +}; + +type State = { + dragHover: boolean; +}; +class DragTarget extends React.Component { + static defaultProps: DefaultProps = { + component: "div", + shouldDragHighlight: () => true, + }; + + constructor(props) { + super(props); + this.state = { + dragHover: false, }; - }, - getInitialState: function () { - return {dragHover: false}; - }, - handleDrop: function (e) { + + this.handleDrop = this.handleDrop.bind(this); + this.handleDragEnd = this.handleDragEnd.bind(this); + this.handleDragOver = this.handleDragOver.bind(this); + this.handleDragLeave = this.handleDragLeave.bind(this); + this.handleDragEnter = this.handleDragEnter.bind(this); + } + + handleDrop(e: DragEvent) { e.stopPropagation(); e.preventDefault(); this.setState({dragHover: false}); this.props.onDrop(e); - }, - handleDragEnd: function () { + } + + handleDragEnd() { this.setState({dragHover: false}); - }, - handleDragOver: function (e) { + } + + handleDragOver(e) { e.preventDefault(); - }, - handleDragLeave: function () { + } + + handleDragLeave() { this.setState({dragHover: false}); - }, - handleDragEnter: function (e) { + } + + handleDragEnter(e) { this.setState({dragHover: this.props.shouldDragHighlight(e)}); - }, - render: function () { - const opacity = this.state.dragHover ? {opacity: 0.3} : {}; - const Component = this.props.component; + } - const forwardProps = Object.assign({}, this.props); - delete forwardProps.component; - delete forwardProps.shouldDragHighlight; + render() { + const opacity = this.state.dragHover ? {opacity: 0.3} : {}; + const { + component: Component, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + shouldDragHighlight, + ...forwardProps + } = this.props; return ( ); - }, -}); + } +} export default DragTarget; diff --git a/packages/perseus-editor/src/components/sortable.tsx b/packages/perseus-editor/src/components/sortable.tsx index 6ecdcc07d8..a1228350be 100644 --- a/packages/perseus-editor/src/components/sortable.tsx +++ b/packages/perseus-editor/src/components/sortable.tsx @@ -1,59 +1,84 @@ import {css, StyleSheet} from "aphrodite"; -import createReactClass from "create-react-class"; -import PropTypes from "prop-types"; import * as React from "react"; import ReactDOM from "react-dom"; -const PT = PropTypes; +type Props = { + className?: string; + components: React.ReactElement[]; + onReorder: (i: any[]) => void; + style?: any; + verify: (i: any) => boolean; +}; + +type DefaultProps = { + verify: Props["verify"]; +}; + +type State = { + dragging: number; + components: React.ReactElement[]; +}; /** + * TODO(LEMS-2667): 11/26/24, at the time of writing this comment + * it has been identified that this file has been broken long before + * the refactoring of createReactClass. Future implementation need + * to determine how to fix this functionality or deprecate it. + * * * Takes an array of components to sort. * As of 08/05/24, there are two sortable components * (one in perseus and one in perseus-editor). * As far as I can tell, this one is only used in ExpressionEditor. */ // eslint-disable-next-line react/no-unsafe -const SortableArea = createReactClass({ - propTypes: { - className: PT.string, - components: PT.arrayOf(PT.node).isRequired, - onReorder: PT.func.isRequired, - style: PT.any, - verify: PT.func, - }, - getDefaultProps: function () { - return {verify: () => true}; - }, - getInitialState: function () { - return { +class SortableArea extends React.Component { + _dragItems: any; + + static defaultProps: DefaultProps = { + verify: () => true, + }; + + constructor(props) { + super(props); + this.state = { // index of the component being dragged - dragging: null, + dragging: -1, components: this.props.components, }; - }, + + this.onDrop = this.onDrop.bind(this); + this.onDragStart = this.onDragStart.bind(this); + this.onDragEnter = this.onDragEnter.bind(this); + } + // Firefox refuses to drag an element unless you set data on it. Hackily // add data each time an item is dragged. - componentDidMount: function () { + componentDidMount() { this._setDragEvents(); - }, + } + // eslint-disable-next-line react/no-unsafe - UNSAFE_componentWillReceiveProps: function (nextProps) { + UNSAFE_componentWillReceiveProps(nextProps) { this.setState({components: nextProps.components}); - }, - componentDidUpdate: function () { + } + + componentDidUpdate() { this._setDragEvents(); - }, + } + // Alternatively send each handler to each component individually, // partially applied - onDragStart: function (startIndex) { + onDragStart(startIndex) { this.setState({dragging: startIndex}); - }, - onDrop: function () { + } + + onDrop() { // tell the parent component - this.setState({dragging: null}); + this.setState({dragging: -1}); this.props.onReorder(this.state.components); - }, - onDragEnter: function (enterIndex) { + } + + onDragEnter(enterIndex) { // When a label is first dragged it triggers a dragEnter with itself, // which we don't care about. if (this.state.dragging === enterIndex) { @@ -75,15 +100,18 @@ const SortableArea = createReactClass({ }); } return verified; - }, - _listenEvent: function (e) { + } + + _listenEvent(e) { e.dataTransfer.setData("hackhackhack", "because browsers!"); - }, - _cancelEvent: function (e) { + } + + _cancelEvent(e) { // prevent the browser from redirecting to 'because browsers!' e.preventDefault(); - }, - _setDragEvents: function () { + } + + _setDragEvents() { this._dragItems = this._dragItems || []; const items = // @ts-expect-error - TS2531 - Object is possibly 'null' @@ -117,8 +145,9 @@ const SortableArea = createReactClass({ dragItem.removeEventListener("dragstart", this._listenEvent); dragItem.removeEventListener("drop", this._cancelEvent); } - }, - render: function () { + } + + render() { const sortables = this.state.components.map((component, index) => ( ); - }, -}); + } +} + +type ItemProps = { + area: any; + component: React.ReactNode; + dragging: boolean; + draggable: boolean; + index: number; +}; // An individual sortable item -const SortableItem = createReactClass({ - propTypes: { - area: PT.shape({ - onDragEnter: PT.func.isRequired, - onDragStart: PT.func.isRequired, - onDrop: PT.func.isRequired, - }), - component: PT.node.isRequired, - dragging: PT.bool.isRequired, - draggable: PT.bool.isRequired, - index: PT.number.isRequired, - }, - handleDragStart: function (e) { +class SortableItem extends React.Component { + handleDragStart(e) { e.nativeEvent.dataTransfer.effectAllowed = "move"; this.props.area.onDragStart(this.props.index); - }, - handleDrop: function () { + } + + handleDrop() { this.props.area.onDrop(this.props.index); - }, - handleDragEnter: function (e) { + } + + handleDragEnter(e) { const verified = this.props.area.onDragEnter(this.props.index); // Ideally this would change the cursor based on whether this is a // valid place to drop. e.nativeEvent.dataTransfer.effectAllowed = verified ? "move" : "none"; - }, - handleDragOver: function (e) { + } + + handleDragOver(e) { // allow a drop by preventing default handling e.preventDefault(); - }, - render: function () { + } + + render() { // I think these might be getting styles from Webapp let dragState = "sortable-disabled"; if (this.props.dragging) { @@ -188,8 +218,8 @@ const SortableItem = createReactClass({ {this.props.component} ); - }, -}); + } +} const styles = StyleSheet.create({ sortableListItem: { diff --git a/packages/perseus-editor/src/index.ts b/packages/perseus-editor/src/index.ts index 14438825fe..73e1b5921f 100644 --- a/packages/perseus-editor/src/index.ts +++ b/packages/perseus-editor/src/index.ts @@ -11,7 +11,6 @@ export {default as Editor} from "./editor"; export {default as i18n} from "./i18n"; export {default as IframeContentRenderer} from "./iframe-content-renderer"; export {default as MultiRendererEditor} from "./multirenderer-editor"; -export {default as StatefulEditorPage} from "./stateful-editor-page"; import "./styles/perseus-editor.less"; diff --git a/packages/perseus-editor/src/stateful-editor-page.tsx b/packages/perseus-editor/src/stateful-editor-page.tsx deleted file mode 100644 index 852e285561..0000000000 --- a/packages/perseus-editor/src/stateful-editor-page.tsx +++ /dev/null @@ -1,83 +0,0 @@ -/* eslint-disable react/no-unsafe */ -import createReactClass from "create-react-class"; -import PropTypes from "prop-types"; -import * as React from "react"; -import _ from "underscore"; - -import EditorPage from "./editor-page"; - -/* Renders an EditorPage (or an ArticleEditor) as a non-controlled component. - * - * Normally the parent of EditorPage must pass it an onChange callback and then - * respond to any changes by modifying the EditorPage props to reflect those - * changes. With StatefulEditorPage changes are stored in state so you can - * query them with serialize. - */ -const StatefulEditorPage = createReactClass({ - displayName: "StatefulEditorPage", - - propTypes: { - componentClass: PropTypes.func, - }, - - getDefaultProps: function () { - return { - componentClass: EditorPage, - }; - }, - - getInitialState: function () { - return _({}).extend(_.omit(this.props, "componentClass"), { - onChange: this.handleChange, - ref: "editor", - }); - }, - - componentDidMount: function () { - this._isMounted = true; - }, - - // getInitialState isn't called if the react component is re-rendered - // in-place on the dom, in which case this is called instead, so we - // need to update the state here. - // (This component is currently re-rendered by the "Add image" button.) - UNSAFE_componentWillReceiveProps: function (nextProps) { - this.setState( - _(nextProps).pick( - "apiOptions", - "imageUploader", - "developerMode", - "problemNum", - "previewDevice", - "frameSource", - ), - ); - }, - - componentWillUnmount: function () { - this._isMounted = false; - }, - - getSaveWarnings: function () { - // eslint-disable-next-line react/no-string-refs - return this.refs.editor.getSaveWarnings(); - }, - - serialize: function () { - // eslint-disable-next-line react/no-string-refs - return this.refs.editor.serialize(); - }, - - handleChange: function (newState, cb) { - if (this._isMounted) { - this.setState(newState, cb); - } - }, - - render: function () { - const Component = this.props.componentClass; - return ; - }, -}); - -export default StatefulEditorPage; From 834bd8bfbc063d0a1935ae9a697e5505c5ee606d Mon Sep 17 00:00:00 2001 From: Jeremy Wiebe Date: Thu, 28 Nov 2024 18:01:14 -0800 Subject: [PATCH 17/42] Remove unused types for widgets that no longer exist (#1930) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary: I was working in the `perseus-types.ts` file a bit and noticed a few types that defined options for widgets that have been deleted. This PR cleans up the types. Issue: "none" ## Test plan: `yarn typecheck` is fine Author: jeremywiebe Reviewers: jeremywiebe, SonicScrewdriver, handeyeco Required Reviewers: Approved By: SonicScrewdriver Checks: ✅ gerald, ✅ Publish npm snapshot (ubuntu-latest, 20.x), ✅ Cypress (ubuntu-latest, 20.x), ✅ Lint, Typecheck, Format, and Test (ubuntu-latest, 20.x), ✅ Check for .changeset entries for all changed files (ubuntu-latest, 20.x), ✅ Check builds for changes in size (ubuntu-latest, 20.x), ✅ Publish Storybook to Chromatic (ubuntu-latest, 20.x), ✅ gerald Pull Request URL: https://github.com/Khan/perseus/pull/1930 --- .changeset/strong-flies-thank.md | 5 +++++ packages/perseus/src/index.ts | 2 -- packages/perseus/src/perseus-types.ts | 15 --------------- 3 files changed, 5 insertions(+), 17 deletions(-) create mode 100644 .changeset/strong-flies-thank.md diff --git a/.changeset/strong-flies-thank.md b/.changeset/strong-flies-thank.md new file mode 100644 index 0000000000..4f3cb7f40d --- /dev/null +++ b/.changeset/strong-flies-thank.md @@ -0,0 +1,5 @@ +--- +"@khanacademy/perseus": major +--- + +Remove PerseusExampleWidgetOptions, PerseusSimpleMarkdownTesterWidgetOptions, and PerseusExampleWidgetOptions types - widgets no longer exist diff --git a/packages/perseus/src/index.ts b/packages/perseus/src/index.ts index 57207e73ea..ecf8aab1b7 100644 --- a/packages/perseus/src/index.ts +++ b/packages/perseus/src/index.ts @@ -219,8 +219,6 @@ export type { PerseusPlotterWidgetOptions, PerseusPythonProgramWidgetOptions, PerseusRadioWidgetOptions, - PerseusExampleWidgetOptions, - PerseusSimpleMarkdownTesterWidgetOptions, PerseusRenderer, PerseusWidget, PerseusWidgetsMap, diff --git a/packages/perseus/src/perseus-types.ts b/packages/perseus/src/perseus-types.ts index f0d251c768..f31451e81b 100644 --- a/packages/perseus/src/perseus-types.ts +++ b/packages/perseus/src/perseus-types.ts @@ -385,10 +385,6 @@ export type PerseusDropdownChoice = { correct: boolean; }; -export type PerseusExampleWidgetOptions = { - value: string; -}; - export type PerseusExplanationWidgetOptions = { // Translatable Text; The clickable text to expand an explanation. e.g. "What is an apple?" showPrompt: string; @@ -1630,20 +1626,11 @@ export type PerseusPassageRefTargetWidgetOptions = { content: string; }; -export type PerseusSimpleMarkdownTesterWidgetOptions = { - value: string; -}; - -type PerseusUnitInputWidgetOptions = { - value: string; -}; - export type PerseusWidgetOptions = | PerseusCategorizerWidgetOptions | PerseusCSProgramWidgetOptions | PerseusDefinitionWidgetOptions | PerseusDropdownWidgetOptions - | PerseusExampleWidgetOptions | PerseusExplanationWidgetOptions | PerseusExpressionWidgetOptions | PerseusGradedGroupSetWidgetOptions @@ -1667,8 +1654,6 @@ export type PerseusWidgetOptions = | PerseusPhetSimulationWidgetOptions | PerseusPlotterWidgetOptions | PerseusRadioWidgetOptions - | PerseusSimpleMarkdownTesterWidgetOptions | PerseusSorterWidgetOptions | PerseusTableWidgetOptions - | PerseusUnitInputWidgetOptions | PerseusVideoWidgetOptions; From e79553435df4d4f83a8e0219080fe773a6980c82 Mon Sep 17 00:00:00 2001 From: Jeremy Wiebe Date: Fri, 29 Nov 2024 09:48:34 -0800 Subject: [PATCH 18/42] Allow WB/WS packages otherwise groups don't match anything (#1931) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary: I set up Dependabot updates for WonderBlocks and WonderStuff packages last week and set them so they'd be updated in groups. However, I missed adding them to the `allow` list, which prevented any updates to them. This PR adds them to the `allow` list so that Dependabot actually works. Issue: "none" ## Test plan: Land and watch tonights Dependabot run. Author: jeremywiebe Reviewers: jandrade, #perseus Required Reviewers: Approved By: jandrade Checks: ✅ gerald, ✅ Publish npm snapshot (ubuntu-latest, 20.x), ✅ Check builds for changes in size (ubuntu-latest, 20.x), ✅ Lint, Typecheck, Format, and Test (ubuntu-latest, 20.x), ✅ Cypress (ubuntu-latest, 20.x), ✅ Check for .changeset entries for all changed files (ubuntu-latest, 20.x), ✅ Publish Storybook to Chromatic (ubuntu-latest, 20.x), ✅ gerald Pull Request URL: https://github.com/Khan/perseus/pull/1931 --- .github/dependabot.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 126d6a8650..c6c103ba5e 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -10,6 +10,8 @@ updates: allow: - dependency-name: "@khanacademy/eslint-config" - dependency-name: "@khanacademy/eslint-plugin" + - dependency-name: "@khanacademy/wonder-blocks-*" + - dependency-name: "@khanacademy/wonder-stuff-*" assignees: - "@Khan/perseus" From 89244ccc0d7384d7f76678204cd49dd7e8301185 Mon Sep 17 00:00:00 2001 From: Matthew Date: Mon, 2 Dec 2024 09:43:49 -0600 Subject: [PATCH 19/42] Refactor more findDOMNodes (#1925) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary: More removal of findDOMNode which is deprecated Author: handeyeco Reviewers: jeremywiebe Required Reviewers: Approved By: jeremywiebe Checks: ✅ Publish npm snapshot (ubuntu-latest, 20.x), ✅ Cypress (ubuntu-latest, 20.x), ✅ Check builds for changes in size (ubuntu-latest, 20.x), ✅ Lint, Typecheck, Format, and Test (ubuntu-latest, 20.x), ✅ Check for .changeset entries for all changed files (ubuntu-latest, 20.x), ✅ Publish Storybook to Chromatic (ubuntu-latest, 20.x), ✅ gerald, ✅ .github/dependabot.yml Pull Request URL: https://github.com/Khan/perseus/pull/1925 --- .changeset/strong-rocks-beg.md | 5 +++++ packages/perseus/src/components/graph.tsx | 9 ++++----- packages/perseus/src/components/graphie.tsx | 3 +-- .../perseus/src/components/multi-button-group.tsx | 12 ++++++++---- packages/perseus/src/components/tooltip.tsx | 14 ++++++-------- 5 files changed, 24 insertions(+), 19 deletions(-) create mode 100644 .changeset/strong-rocks-beg.md diff --git a/.changeset/strong-rocks-beg.md b/.changeset/strong-rocks-beg.md new file mode 100644 index 0000000000..54cc69d06b --- /dev/null +++ b/.changeset/strong-rocks-beg.md @@ -0,0 +1,5 @@ +--- +"@khanacademy/perseus": patch +--- + +Remove some uses of findDOMNode diff --git a/packages/perseus/src/components/graph.tsx b/packages/perseus/src/components/graph.tsx index e8e8af9210..ca760d64c6 100644 --- a/packages/perseus/src/components/graph.tsx +++ b/packages/perseus/src/components/graph.tsx @@ -3,7 +3,6 @@ import {vector as kvector} from "@khanacademy/kmath"; import $ from "jquery"; import * as React from "react"; -import ReactDOM from "react-dom"; import _ from "underscore"; import AssetContext from "../asset-context"; @@ -83,6 +82,8 @@ type DefaultProps = { }; class Graph extends React.Component { + graphieDivRef = React.createRef(); + protractor: any; ruler: any; _graphie: any; @@ -187,8 +188,7 @@ class Graph extends React.Component { return; } - // eslint-disable-next-line react/no-string-refs - const graphieDiv = ReactDOM.findDOMNode(this.refs.graphieDiv); + const graphieDiv = this.graphieDivRef.current; // @ts-expect-error - TS2769 - No overload matches this call. | TS2339 - Property 'empty' does not exist on type 'JQueryStatic'. $(graphieDiv).empty(); @@ -431,8 +431,7 @@ class Graph extends React.Component { onClick={this.onClick} > {image} - {/* eslint-disable-next-line react/no-string-refs */} -
+
); } diff --git a/packages/perseus/src/components/graphie.tsx b/packages/perseus/src/components/graphie.tsx index ff77591665..3b1182b4a8 100644 --- a/packages/perseus/src/components/graphie.tsx +++ b/packages/perseus/src/components/graphie.tsx @@ -1,7 +1,6 @@ import {Errors} from "@khanacademy/perseus-core"; import $ from "jquery"; import * as React from "react"; -import ReactDOM from "react-dom"; import _ from "underscore"; import InteractiveUtil from "../interactive2/interactive-util"; @@ -176,7 +175,7 @@ class Graphie extends React.Component { _setupGraphie: () => void = () => { this._removeMovables(); - const graphieDiv = ReactDOM.findDOMNode(this.graphieDivRef.current); + const graphieDiv = this.graphieDivRef.current; if (graphieDiv == null || graphieDiv instanceof Text) { throw new Error("No graphie container div found"); } diff --git a/packages/perseus/src/components/multi-button-group.tsx b/packages/perseus/src/components/multi-button-group.tsx index 533334addf..d492ad702a 100644 --- a/packages/perseus/src/components/multi-button-group.tsx +++ b/packages/perseus/src/components/multi-button-group.tsx @@ -1,6 +1,5 @@ import {css, StyleSheet} from "aphrodite"; import * as React from "react"; -import * as ReactDOM from "react-dom"; type Props = { /** @@ -52,14 +51,15 @@ type DefaultProps = { * this component allows multiple selection! */ class MultiButtonGroup extends React.Component { + buttonContainerRef = React.createRef(); + static defaultProps: DefaultProps = { values: [], allowEmpty: true, }; focus(): boolean { - // @ts-expect-error - TS2339 - Property 'focus' does not exist on type 'Element | Text'. - ReactDOM.findDOMNode(this)?.focus(); + this.buttonContainerRef.current?.focus(); return true; } @@ -108,7 +108,11 @@ class MultiButtonGroup extends React.Component { const outerStyle = { display: "inline-block", } as const; - return
{buttons}
; + return ( +
+ {buttons} +
+ ); } } diff --git a/packages/perseus/src/components/tooltip.tsx b/packages/perseus/src/components/tooltip.tsx index d5a13e13ea..c795f76491 100644 --- a/packages/perseus/src/components/tooltip.tsx +++ b/packages/perseus/src/components/tooltip.tsx @@ -4,7 +4,6 @@ // z-index: 3 on perseus-formats-tooltip seemed to work import * as React from "react"; -import ReactDOM from "react-dom"; import type {Property} from "csstype"; @@ -237,6 +236,8 @@ type State = { * ``` */ class Tooltip extends React.Component { + tooltipContainerRef = React.createRef(); + static defaultProps: DefaultProps = { className: "", arrowSize: 10, @@ -337,8 +338,7 @@ class Tooltip extends React.Component { }} >
{ } _updateHeight() { - const tooltipContainer = ReactDOM.findDOMNode( - // eslint-disable-next-line react/no-string-refs - this.refs.tooltipContainer, - ) as HTMLDivElement; - const height = tooltipContainer.offsetHeight; + const tooltipContainer = this.tooltipContainerRef.current; + + const height = tooltipContainer?.offsetHeight || 0; if (height !== this.state.height) { this.setState({height}); } From 066daab0ea8463e8b2a5381e90ed8392ea20a5bf Mon Sep 17 00:00:00 2001 From: daniellewhyte <30729058+daniellewhyte@users.noreply.github.com> Date: Mon, 2 Dec 2024 12:46:33 -0600 Subject: [PATCH 20/42] Add visible label and ARIA label to Dropdown widget (#1845) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary: This commit adds two new fields to the Dropdown widget: `ariaLabel` and `visibleLabel`. Issue: LIT-1424 ## Test plan: Screen reader walkthrough with `visibleLabel`: https://github.com/user-attachments/assets/47f4ca21-09a6-4b82-b92a-81841c315f89 Screen reader walkthrough with `ariaLabel` only: (This will probably be most instances as the dropdown widget is mostly used inline, and the visible label makes things too cramped.) https://github.com/user-attachments/assets/ad1dfd76-5bc0-48e5-ab15-f57f2f92d0c3 Updates to widget editor: Screenshot 2024-11-27 at 3 37 07 PM You can also see examples in the course editor on this ZND: https://prod-znd-241122-danielle-dw2.khanacademy.org/devadmin/content/articles/dropdown-with-labels/x56e2ea23d30e405c Author: daniellewhyte Reviewers: mark-fitzgerald Required Reviewers: Approved By: mark-fitzgerald Checks: ✅ Publish npm snapshot (ubuntu-latest, 20.x), ✅ Cypress (ubuntu-latest, 20.x), ✅ Check for .changeset entries for all changed files (ubuntu-latest, 20.x), ✅ Lint, Typecheck, Format, and Test (ubuntu-latest, 20.x), ✅ Check builds for changes in size (ubuntu-latest, 20.x), ✅ gerald, ✅ Publish npm snapshot (ubuntu-latest, 20.x), ✅ Check builds for changes in size (ubuntu-latest, 20.x), ✅ Cypress (ubuntu-latest, 20.x), ✅ Lint, Typecheck, Format, and Test (ubuntu-latest, 20.x), ✅ Publish Storybook to Chromatic (ubuntu-latest, 20.x), ✅ Check for .changeset entries for all changed files (ubuntu-latest, 20.x), ✅ gerald Pull Request URL: https://github.com/Khan/perseus/pull/1845 --- .changeset/hot-chairs-sell.md | 6 + .../src/styles/perseus-editor.less | 11 +- .../__stories__/dropdown-editor.stories.tsx | 6 +- .../src/widgets/dropdown-editor.tsx | 74 +++++++++-- .../src/__testdata__/renderer.testdata.ts | 2 + .../__snapshots__/renderer.test.tsx.snap | 51 ++++++-- .../__tests__/extract-perseus-data.test.ts | 1 + .../perseus/src/__tests__/renderer.test.tsx | 21 ++-- packages/perseus/src/perseus-types.ts | 4 + packages/perseus/src/strings.ts | 3 + .../perseus-parsers/dropdown-widget.ts | 3 + .../parse-perseus-json-snapshot.test.ts.snap | 2 + .../dropdown/dropdown-ai-utils.test.ts | 4 +- .../__snapshots__/dropdown.test.ts.snap | 22 +++- .../src/widgets/dropdown/dropdown.stories.tsx | 18 ++- .../src/widgets/dropdown/dropdown.test.ts | 16 ++- .../src/widgets/dropdown/dropdown.testdata.ts | 119 ++++++++++++++++++ .../perseus/src/widgets/dropdown/dropdown.tsx | 75 ++++++++--- 18 files changed, 370 insertions(+), 68 deletions(-) create mode 100644 .changeset/hot-chairs-sell.md diff --git a/.changeset/hot-chairs-sell.md b/.changeset/hot-chairs-sell.md new file mode 100644 index 0000000000..901ba07b9b --- /dev/null +++ b/.changeset/hot-chairs-sell.md @@ -0,0 +1,6 @@ +--- +"@khanacademy/perseus": minor +"@khanacademy/perseus-editor": minor +--- + +Add labels to Dropdown widget diff --git a/packages/perseus-editor/src/styles/perseus-editor.less b/packages/perseus-editor/src/styles/perseus-editor.less index 102b64120e..a7f12136be 100644 --- a/packages/perseus-editor/src/styles/perseus-editor.less +++ b/packages/perseus-editor/src/styles/perseus-editor.less @@ -517,11 +517,16 @@ } .dropdown-info { - float: left; + display: inline-flex; + margin-bottom: 16px; } - .dropdown-placeholder { - float: right; + .dropdown-field { + display: flex; + flex-direction: row; + align-items: center; + min-width: 0; + margin-bottom: 16px; } .remove-choice { diff --git a/packages/perseus-editor/src/widgets/__stories__/dropdown-editor.stories.tsx b/packages/perseus-editor/src/widgets/__stories__/dropdown-editor.stories.tsx index ceebb3b0cb..8fdaff9f6e 100644 --- a/packages/perseus-editor/src/widgets/__stories__/dropdown-editor.stories.tsx +++ b/packages/perseus-editor/src/widgets/__stories__/dropdown-editor.stories.tsx @@ -14,5 +14,9 @@ export default { } as Story; export const Default = (args: StoryArgs): React.ReactElement => { - return ; + return ( +
+ +
+ ); }; diff --git a/packages/perseus-editor/src/widgets/dropdown-editor.tsx b/packages/perseus-editor/src/widgets/dropdown-editor.tsx index ac47c87c1f..6ad21df9cf 100644 --- a/packages/perseus-editor/src/widgets/dropdown-editor.tsx +++ b/packages/perseus-editor/src/widgets/dropdown-editor.tsx @@ -1,5 +1,7 @@ /* eslint-disable @khanacademy/ts-no-error-suppressions */ import {components, EditorJsonify, iconTrash} from "@khanacademy/perseus"; +import {TextField} from "@khanacademy/wonder-blocks-form"; +import {LabelLarge, LabelMedium} from "@khanacademy/wonder-blocks-typography"; import PropTypes from "prop-types"; import * as React from "react"; import ReactDOM from "react-dom"; @@ -34,11 +36,16 @@ class DropdownEditor extends React.Component { ], }; - onPlaceholderChange: (arg1: React.ChangeEvent) => void = ( - e, - ) => { - const placeholder = e.target.value; - this.props.onChange({placeholder: placeholder}); + onVisibleLabelChange: (arg1: string) => void = (visibleLabel) => { + this.props.onChange({visibleLabel}); + }; + + onAriaLabelChange: (arg1: string) => void = (ariaLabel) => { + this.props.onChange({ariaLabel}); + }; + + onPlaceholderChange: (arg1: string) => void = (placeholder) => { + this.props.onChange({placeholder}); }; onCorrectChange: ( @@ -104,7 +111,7 @@ class DropdownEditor extends React.Component { return (
- Dropdown + Dropdown

The drop down is useful for making inequalities in a @@ -115,13 +122,53 @@ class DropdownEditor extends React.Component {

-
- +
+ + Visible label + + + +

Optional visible label

+
+
+
+ + Aria label + + + +

+ Label text that's read by screen readers. Highly + recommend adding a label here to ensure your + exercise is accessible. For more information on + writing accessible labels, please see{" "} + + this article. + {" "} + If left blank, the value will default to "Select an + answer". +

+
+
+
+ + Placeholder + +

This value will appear as the drop down default. It @@ -132,6 +179,7 @@ class DropdownEditor extends React.Component {

+ Choices
    {this.props.choices.map(function (choice, i) { const checkedClass = choice.correct diff --git a/packages/perseus/src/__testdata__/renderer.testdata.ts b/packages/perseus/src/__testdata__/renderer.testdata.ts index 66dde7d706..9160ef2a84 100644 --- a/packages/perseus/src/__testdata__/renderer.testdata.ts +++ b/packages/perseus/src/__testdata__/renderer.testdata.ts @@ -13,6 +13,8 @@ export const dropdownWidget: DropdownWidget = { graded: true, options: { static: false, + ariaLabel: "Test ARIA label", + visibleLabel: "Test visible label", placeholder: "greater/less than or equal to", choices: [ { diff --git a/packages/perseus/src/__tests__/__snapshots__/renderer.test.tsx.snap b/packages/perseus/src/__tests__/__snapshots__/renderer.test.tsx.snap index 444ee98190..393a4e9009 100644 --- a/packages/perseus/src/__tests__/__snapshots__/renderer.test.tsx.snap +++ b/packages/perseus/src/__tests__/__snapshots__/renderer.test.tsx.snap @@ -345,7 +345,15 @@ exports[`renderer snapshots correct answer: correct answer 1`] = `
    -
    +
    +
    @@ -357,12 +365,15 @@ exports[`renderer snapshots correct answer: correct answer 1`] = ` data-testid="dropdown-live-region" />
- - + + @@ -288,175 +286,173 @@ exports[`graded-group-set should render all graded groups 1`] = `
-
- -
- -
+ +
+ +
+
@@ -517,175 +513,173 @@ exports[`graded-group-set should render all graded groups 1`] = `
-
- -
- -
+ +
+ +
+
diff --git a/packages/perseus/src/widgets/graded-group-set/__snapshots__/graded-group-set.test.ts.snap b/packages/perseus/src/widgets/graded-group-set/__snapshots__/graded-group-set.test.ts.snap index ce69ebd6c0..edff67474d 100644 --- a/packages/perseus/src/widgets/graded-group-set/__snapshots__/graded-group-set.test.ts.snap +++ b/packages/perseus/src/widgets/graded-group-set/__snapshots__/graded-group-set.test.ts.snap @@ -125,175 +125,173 @@ exports[`graded group widget should snapshot 1`] = `
-
- -
- -
+ +
+ +
+
diff --git a/packages/perseus/src/widgets/group/__snapshots__/group.test.tsx.snap b/packages/perseus/src/widgets/group/__snapshots__/group.test.tsx.snap index 828a6ab9b6..899ca9bb11 100644 --- a/packages/perseus/src/widgets/group/__snapshots__/group.test.tsx.snap +++ b/packages/perseus/src/widgets/group/__snapshots__/group.test.tsx.snap @@ -752,175 +752,173 @@ exports[`group widget should snapshot: initial render 1`] = `
-
- -
- -
+ +
+ +
+
@@ -959,175 +957,173 @@ exports[`group widget should snapshot: initial render 1`] = `
-
- -
- -
+ +
+ +
+
diff --git a/packages/perseus/src/widgets/numeric-input/__snapshots__/numeric-input.test.ts.snap b/packages/perseus/src/widgets/numeric-input/__snapshots__/numeric-input.test.ts.snap index 53e9f2f3e5..a6cd5c0053 100644 --- a/packages/perseus/src/widgets/numeric-input/__snapshots__/numeric-input.test.ts.snap +++ b/packages/perseus/src/widgets/numeric-input/__snapshots__/numeric-input.test.ts.snap @@ -90,175 +90,173 @@ exports[`numeric-input widget Should render predictably: after interaction 1`] =
-
- -
- -
+ +
+ +
+
@@ -294,175 +292,173 @@ exports[`numeric-input widget Should render predictably: first render 1`] = `
-
- -
- -
+ +
+ +
+
diff --git a/packages/perseus/src/widgets/numeric-input/numeric-input.tsx b/packages/perseus/src/widgets/numeric-input/numeric-input.tsx index 5a115b6692..0e57bad2f8 100644 --- a/packages/perseus/src/widgets/numeric-input/numeric-input.tsx +++ b/packages/perseus/src/widgets/numeric-input/numeric-input.tsx @@ -75,6 +75,7 @@ type State = { // keeps track of the other set of values when switching // between 0 and finite solutions previousValues: ReadonlyArray; + isFocused: boolean; }; export class NumericInput @@ -107,6 +108,7 @@ export class NumericInput // keeps track of the other set of values when switching // between 0 and finite solutions previousValues: [""], + isFocused: false, }; // TODO(Nicole, Jeremy): This is maybe never used and should be removed @@ -197,10 +199,16 @@ export class NumericInput _handleFocus: () => void = () => { this.props.onFocus([]); + this.setState((currentState) => { + return {...currentState, isFocused: true}; + }); }; _handleBlur: () => void = () => { this.props.onBlur([]); + this.setState((currentState) => { + return {...currentState, isFocused: false}; + }); }; render(): React.ReactNode { @@ -241,29 +249,33 @@ export class NumericInput // component. const styles = StyleSheet.create({ input: { + borderRadius: "3px", + borderWidth: this.state.isFocused ? "2px" : "1px", + display: "inline-block", + fontFamily: `Symbola, "Times New Roman", serif`, + fontSize: "18px", + height: "32px", + lineHeight: "18px", + padding: this.state.isFocused ? "4px" : "4px 5px", // account for added focus border thickness textAlign: this.props.rightAlign ? "right" : "left", width: this.props.size === "small" ? 40 : 80, - padding: 0, - height: "auto", }, }); return ( -
- (this.inputRef = ref)} - value={this.props.currentValue} - onChange={this.handleChange} - labelText={labelText} - examples={this.examples()} - shouldShowExamples={this.shouldShowExamples()} - onFocus={this._handleFocus} - onBlur={this._handleBlur} - id={this.props.widgetId} - disabled={this.props.apiOptions.readOnly} - style={styles.input} - /> -
+ (this.inputRef = ref)} + value={this.props.currentValue} + onChange={this.handleChange} + labelText={labelText} + examples={this.examples()} + shouldShowExamples={this.shouldShowExamples()} + onFocus={this._handleFocus} + onBlur={this._handleBlur} + id={this.props.widgetId} + disabled={this.props.apiOptions.readOnly} + style={styles.input} + /> ); } } From 129adebefe25e25c1b2a9e37f707cd13c673b64f Mon Sep 17 00:00:00 2001 From: Jeremy Wiebe Date: Tue, 3 Dec 2024 12:16:28 -0800 Subject: [PATCH 27/42] Improve some comments (#1934) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary: As it says, just some comments that I wanted to improve as I was working on SSS. Issue: "none" ## Test plan: Read the comments. Author: jeremywiebe Reviewers: SonicScrewdriver, jeremywiebe, nishasy, benchristel, handeyeco, catandthemachines, anakaren-rojas, mark-fitzgerald Required Reviewers: Approved By: SonicScrewdriver Checks: ✅ Publish npm snapshot (ubuntu-latest, 20.x), ✅ Cypress (ubuntu-latest, 20.x), ✅ Check builds for changes in size (ubuntu-latest, 20.x), ✅ Lint, Typecheck, Format, and Test (ubuntu-latest, 20.x), ✅ Check for .changeset entries for all changed files (ubuntu-latest, 20.x), ✅ Publish Storybook to Chromatic (ubuntu-latest, 20.x), ✅ gerald Pull Request URL: https://github.com/Khan/perseus/pull/1934 --- .changeset/tall-pianos-tap.md | 5 ++ packages/perseus/src/perseus-types.ts | 37 ++++++++--- packages/perseus/src/renderer.tsx | 6 ++ packages/perseus/src/types.ts | 92 +++++++++++++++++---------- packages/tsconfig-shared.json | 2 +- 5 files changed, 99 insertions(+), 43 deletions(-) create mode 100644 .changeset/tall-pianos-tap.md diff --git a/.changeset/tall-pianos-tap.md b/.changeset/tall-pianos-tap.md new file mode 100644 index 0000000000..f83cd1ff79 --- /dev/null +++ b/.changeset/tall-pianos-tap.md @@ -0,0 +1,5 @@ +--- +"@khanacademy/perseus": patch +--- + +Improve comments on some Perseus types diff --git a/packages/perseus/src/perseus-types.ts b/packages/perseus/src/perseus-types.ts index ff17d0c7b1..37ad9949a4 100644 --- a/packages/perseus/src/perseus-types.ts +++ b/packages/perseus/src/perseus-types.ts @@ -114,6 +114,9 @@ export type PerseusArticle = PerseusRenderer | ReadonlyArray; * A "MultiItem" is an advanced Perseus item. It is rendered by the * `MultiRenderer` and you can control the layout of individual parts of the * item. + * + * @deprecated MultiItem support is slated for removal in a future Perseus + * release. */ export type MultiItem = { // Multi-item should only show up in Test Prep content and it is a variant of a PerseusItem @@ -128,20 +131,28 @@ export type Version = { }; export type PerseusRenderer = { - // Translatable Markdown content to be rendered. May include references to - // widgets (as [[☃ widgetName]]) or images (as ![image text](imageUrl)). - // For each image found in this content, there can be an entry in the - // `images` dict (below) with the key being the image's url which defines - // additional attributes for the image. + /** + * Translatable Markdown content to be rendered. May include references to + * widgets (as [[☃ widgetName]]) or images (as ![image text](imageUrl)). + * For each image found in this content, there can be an entry in the + * `images` dict (below) with the key being the image's url which defines + * additional attributes for the image. + */ content: string; - // A dictionary of {[widgetName]: Widget} to be referenced from the content field + /** + * A dictionary of {[widgetName]: Widget} to be referenced from the content + * field. + */ widgets: PerseusWidgetsMap; - // Used in the PerseusGradedGroup widget. A list of "tags" that are keys that represent other content in the system. Not rendered to the user. + // Used in the PerseusGradedGroup widget. A list of "tags" that are keys + // that represent other content in the system. Not rendered to the user. // NOTE: perseus_data.go says this is required even though it isn't necessary. metadata?: ReadonlyArray; - // A dictionary of {[imageUrl]: PerseusImageDetail}. + /** + * A dictionary of {[imageUrl]: PerseusImageDetail}. + */ images: { - [key: string]: PerseusImageDetail; + [imageUrl: string]: PerseusImageDetail; }; }; @@ -183,6 +194,10 @@ export const ItemExtras = [ ] as const; export type PerseusAnswerArea = Record<(typeof ItemExtras)[number], boolean>; +/** + * The type representing the common structure of all widget's options. The + * `Options` generic type represents the widget-specific option data. + */ export type WidgetOptions = { // The "type" of widget which will define what the Options field looks like type: Type; @@ -310,7 +325,9 @@ export type PerseusWidget = | VideoWidget | AutoCorrectWidget; -// A background image applied to various widgets. +/** + * A background image applied to various widgets. + */ export type PerseusImageBackground = { // The URL of the image url: string | null | undefined; diff --git a/packages/perseus/src/renderer.tsx b/packages/perseus/src/renderer.tsx index e2f291c589..ff930e7711 100644 --- a/packages/perseus/src/renderer.tsx +++ b/packages/perseus/src/renderer.tsx @@ -1573,6 +1573,12 @@ class Renderer return state; }; + /** + * Returns an array of widget ids that are empty (meaning widgets where the + * learner has not interacted with the widget yet or has not filled in all + * fields). For example, the `interactive-graph` widget is considered + * empty if the graph is in the starting state. + */ emptyWidgets(): ReadonlyArray { return emptyWidgetsFunctional( this.state.widgetInfo, diff --git a/packages/perseus/src/types.ts b/packages/perseus/src/types.ts index 6cc7f2babc..eb7f3dd0a6 100644 --- a/packages/perseus/src/types.ts +++ b/packages/perseus/src/types.ts @@ -65,13 +65,15 @@ export interface Widget { getDOMNodeForPath?: (path: FocusPath) => Element | Text | null; deselectIncorrectSelectedChoices?: () => void; + /** + * Returns widget state that can be passed back to `restoreSerializedState` + * to put the widget back into exactly the same state. If the widget does + * not implement this function, the renderer simply returns all of the + * widget's props. + */ // TODO(jeremy): I think this return value is wrong. The widget // getSerializedState should just return _its_ serialized state, not a // key/value list of all widget states (i think!) - // Returns widget state that can be passed back to `restoreSerializedState` - // to put the widget back into exactly the same state. If the widget does - // not implement this function, the renderer simply returns all of the - // widget's props. getSerializedState?: () => SerializedState; // SUSPECT, restoreSerializedState?: (props: any, callback: () => void) => any; @@ -459,15 +461,17 @@ type InitialRequestUrlInterface = { export type VideoKind = "YOUTUBE_ID" | "READABLE_ID"; -// An object for dependency injection, to allow different clients -// to provide different methods for logging, translation, network -// requests, etc. -// -// NOTE: You should avoid adding new dependencies here as this type was added -// as a quick fix to get around the fact that some of the dependencies Perseus -// needs are used in places where neither `APIOptions` nor a React Context -// could be used. Aim to shrink the footprint of PerseusDependencies and try to -// use alternative methods where possible. +/** + * An object for dependency injection, to allow different clients + * to provide different methods for logging, translation, network + * requests, etc. + * + * NOTE: You should avoid adding new dependencies here as this type was added + * as a quick fix to get around the fact that some of the dependencies Perseus + * needs are used in places where neither `APIOptions` nor a React Context + * could be used. Aim to shrink the footprint of PerseusDependencies and try to + * use alternative methods where possible. + */ export type PerseusDependencies = { // JIPT JIPT: JIPT; @@ -563,9 +567,11 @@ export type Alignment = type WidgetOptions = any; -// A transform that maps the WidgetOptions (sometimes referred to as -// EditorProps) to the props used to render the widget. Often this is an -// identity transform. +/** + * A transform that maps the WidgetOptions (sometimes referred to as + * EditorProps) to the props used to render the widget. Often this is an + * identity transform. + */ // TODO(jeremy): Make this generic so that the WidgetOptions and output type // become strongly typed. export type WidgetTransform = ( @@ -602,11 +608,12 @@ export type WidgetExports< /** Supresses widget from showing up in the dropdown in the content editor */ hidden?: boolean; /** - The widget version. Any time the _major_ version changes, the widget - should provide a new entry in the propUpgrades map to migrate from the - older version to the current (new) version. Minor version changes must - be backwards compatible with previous minor versions widget options. - This key defaults to `{major: 0, minor: 0}` if not provided. + * The widget version. Any time the _major_ version changes, the widget + * should provide a new entry in the propUpgrades map to migrate from the + * older version to the current (new) version. Minor version changes must + * be backwards compatible with previous minor versions widget options. + * + * This key defaults to `{major: 0, minor: 0}` if not provided. */ version?: Version; supportedAlignments?: ReadonlyArray; @@ -617,27 +624,40 @@ export type WidgetExports< traverseChildWidgets?: any; // (Props, traverseRenderer) => NewProps, - /** transforms the widget options to the props used to render the widget */ + /** + * Transforms the widget options to the props used to render the widget. + */ transform?: WidgetTransform; - /** transforms the widget options to the props used to render the widget for - static renders */ + /** + * Transforms the widget options to the props used to render the widget for + * static renders. + */ staticTransform?: WidgetTransform; // this is a function of some sort, + /** + * A function that scores user input (the guess) for the widget. + */ scorer?: WidgetScorerFunction; + getOneCorrectAnswerFromRubric?: ( rubric: Rubric, ) => string | null | undefined; /** - A map of major version numbers (as a string, eg "1") to a function that - migrates from the _previous_ major version. - Example: - propUpgrades: {'1': (options) => ({...options})} - would migrate from major version 0 to 1. - */ + * A map of major version numbers (as a string, eg "1") to a function that + * migrates from the _previous_ major version. + * + * Example: + * ``` + * propUpgrades: {'1': (options) => ({...options})} + * ``` + * + * This configuration would migrate options from major version 0 to 1. + */ propUpgrades?: { - [key: string]: (arg1: any) => any; - }; // OldProps => NewProps, + // OldProps => NewProps, + [targetMajorVersion: string]: (arg1: any) => any; + }; }>; export type FilterCriterion = @@ -648,6 +668,14 @@ export type FilterCriterion = widget?: Widget | null | undefined, ) => boolean); +/** + * The full set of props provided to all widgets when they are rendered. The + * `RenderProps` generic argument are the widget-specific props that originate + * from the stored PerseusItem. Note that they may not match the serialized + * widget options exactly as they are the result of running the options through + * any `propUpgrades` the widget defines as well as its `transform` or + * `staticTransform` functions (depending on the options `static` flag). + */ // NOTE: Rubric should always be the corresponding widget options type for the component. // TODO: in fact, is it really the rubric? WidgetOptions is what we use to configure the widget // (which is what this seems to be for) diff --git a/packages/tsconfig-shared.json b/packages/tsconfig-shared.json index 7b37b07c9e..2454c5c1b7 100644 --- a/packages/tsconfig-shared.json +++ b/packages/tsconfig-shared.json @@ -1,4 +1,4 @@ -// This file is used by the tsconfig.json files in each package. +// This file is used by the tsconfig-build.json files in each package. /* Visit https://aka.ms/tsconfig to read more about this file */ { "exclude": [ From 763a4ba380ac95f38e24e966107054dcd2dddd4c Mon Sep 17 00:00:00 2001 From: Mark Fitzgerald <13896410+mark-fitzgerald@users.noreply.github.com> Date: Tue, 3 Dec 2024 13:03:53 -0800 Subject: [PATCH 28/42] [Numeric Input] - Show "Format Options" Tooltip as List (#1861) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary: Upon receiving focus, the Numeric Input field will show a tooltip indicating the format of the answer that it is expecting (assuming the input was configured with suggested formats). When multiple formats are indicated, they should be displayed as a list. Currently, the formats are indeed HTML list elements, but they don't display as list items (all formatting has been removed). This change fixes the visual formatting to display the items as a list, but only if there are two or more items. In the case of just one format example, the HTML list element is removed, and the content is displayed as regular text. Also, the first line in the tooltip is more of an introductory text than a format option, and therefore should not be included in the HTML list markup. Issue: LEMS-2457 ## Test plan: **NOTE:** Only the HTML modifications can be testing locally (aka Storybook). The visual modifications must be verified in a ZND (or locally in Webapp containerized) because the styling that interferes with the list styling does not exist in Storybook. ### Storybook: 1. Launch Storybook 1. Navigate to Perseus Editor => Editor => [Demo](http://localhost:6006/?path=/story/perseuseditor-editorpage--demo) 1. Add a Numeric Input widget 1. Configure the widget to have just one format option ![Storybook - 1 Format Option](https://github.com/user-attachments/assets/b75a0b98-9132-425f-a272-6606c30ef80a) 1. Move focus to the input field in the preview area * The tooltip should **NOT** be a list ![Storybook - 1 Format Tooltip](https://github.com/user-attachments/assets/f7e52579-0bce-45fd-af35-bf6c22352bb2) 1. Configure the widget to have more than one format option ![Storybook - 3 Format Option](https://github.com/user-attachments/assets/8df2c22a-7a3d-44a7-9cd9-723cd1542bfa) 1. Move focus to the input field in the preview area * The tooltip should be a list, with the first line of text **NOT** in the list ![Storybook - 3 Format Tooltip](https://github.com/user-attachments/assets/17a34627-7ba6-4367-b45e-c858cd63c33e) ### Webapp (locally or in a [ZND](https://prod-znd-241118-markfitz-m06.khanacademy.org/)): 1. Log into the app with a valid account for use in editing exercises 1. Navigate to a Numeric Input example * Test Everything > Unit 2 > Lesson 2: Numeric input ([ZND](https://prod-znd-241118-markfitz-m06.khanacademy.org/internal-courses/test-everything/test-everything-2-without-mastery/te-numeric-input/e/numeric-input-exercise)) 1. Edit the exercise 1. Configure the widget to have just one format option ![Storybook - 1 Format Option](https://github.com/user-attachments/assets/b75a0b98-9132-425f-a272-6606c30ef80a) 1. Move focus to the input field in the preview area (make sure the "Desktop" preview is selected) * The tooltip should **NOT** be a list ![Webapp - 1 Format Tooltip](https://github.com/user-attachments/assets/403091c1-e2ba-45aa-b5a7-c19aa76a6009) 1. Configure the widget to have more than one format option ![Storybook - 3 Format Option](https://github.com/user-attachments/assets/8df2c22a-7a3d-44a7-9cd9-723cd1542bfa) 1. Move focus to the input field in the preview area * The tooltip should be a list, with the first line of text **NOT** in the list ![Webapp - 3 Format Tooltip](https://github.com/user-attachments/assets/426f73bb-992a-44e8-8a8b-ed5b846fc2d0) Author: mark-fitzgerald Reviewers: mark-fitzgerald, jeremywiebe, SonicScrewdriver, catandthemachines Required Reviewers: Approved By: jeremywiebe, SonicScrewdriver, catandthemachines Checks: ✅ Publish npm snapshot (ubuntu-latest, 20.x), ✅ Lint, Typecheck, Format, and Test (ubuntu-latest, 20.x), ✅ Cypress (ubuntu-latest, 20.x), ✅ Check builds for changes in size (ubuntu-latest, 20.x), ✅ Check for .changeset entries for all changed files (ubuntu-latest, 20.x), ✅ Publish Storybook to Chromatic (ubuntu-latest, 20.x), ✅ gerald Pull Request URL: https://github.com/Khan/perseus/pull/1861 --- .changeset/many-keys-smash.md | 5 + .../server-item-renderer.test.tsx.snap | 18 +- .../src/components/input-with-examples.tsx | 15 +- .../multi-renderer.test.tsx.snap | 18 +- .../perseus/src/styles/perseus-renderer.less | 22 +- .../graded-group-set-jipt.test.ts.snap | 54 +++- .../graded-group-set.test.ts.snap | 18 +- .../group/__snapshots__/group.test.tsx.snap | 36 ++- .../__snapshots__/numeric-input.test.ts.snap | 298 +++++++++++++++++- .../numeric-input/numeric-input.test.ts | 42 ++- 10 files changed, 453 insertions(+), 73 deletions(-) create mode 100644 .changeset/many-keys-smash.md diff --git a/.changeset/many-keys-smash.md b/.changeset/many-keys-smash.md new file mode 100644 index 0000000000..3435bb94bd --- /dev/null +++ b/.changeset/many-keys-smash.md @@ -0,0 +1,5 @@ +--- +"@khanacademy/perseus": patch +--- + +[Numeric Input] - Show format options as a list 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 08232c08ee..8e79ffdd65 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 @@ -81,13 +81,21 @@ exports[`server item renderer should snapshot: initial render 1`] = `
+
+ + Your answer should be + + +
+
+
    -
  • - - Your answer should be - -
  • an integer, like { render(): React.ReactNode { const input = this._renderInput(); - const examplesContent = _.map(this.props.examples, (example) => { - return "- " + example; - }).join("\n"); + const examplesContent = + this.props.examples.length <= 2 + ? this.props.examples.join(" ") // A single item (with or without leading text) is not a "list" + : this.props.examples // 2 or more items should display as a list + .map((example, index) => { + // If the first example is bold, then it is most likely a heading/leading text. + // So, it shouldn't be part of the list. + return index === 0 && example.startsWith("**") + ? `${example}\n` + : `- ${example}`; + }) + .join("\n"); const showExamples = this.props.shouldShowExamples && this.state.showExamples; 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 078e779ff6..d2cda3f5fa 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 @@ -1015,13 +1015,21 @@ exports[`multi-item renderer should snapshot: initial render 1`] = `
    +
    + + Your answer should be + + +
    +
    +
      -
    • - - Your answer should be - -
    • an integer, like ul { - padding: 0; - margin: -20px 0 -16px 0; - > li { - list-style-type: none; - } +/* + Extra specificity needed to override other styles that are too broad. + Once we get a better framework in place (like CSS Modules), we can fix this selector. +*/ +.framework-perseus .perseus-formats-tooltip .paragraph, +.framework-perseus .tooltipContainer .perseus-formats-tooltip .paragraph ul { + margin: 0; } .box-shadow(@shadow: 0 1px 3px rgba(0,0,0,0.25)) { diff --git a/packages/perseus/src/widgets/graded-group-set/__snapshots__/graded-group-set-jipt.test.ts.snap b/packages/perseus/src/widgets/graded-group-set/__snapshots__/graded-group-set-jipt.test.ts.snap index 3926e342d6..0b1f5c6cb2 100644 --- a/packages/perseus/src/widgets/graded-group-set/__snapshots__/graded-group-set-jipt.test.ts.snap +++ b/packages/perseus/src/widgets/graded-group-set/__snapshots__/graded-group-set-jipt.test.ts.snap @@ -103,13 +103,21 @@ exports[`graded-group-set should render all graded groups 1`] = `
      +
      + + Your answer should be + + +
      +
      +
        -
      • - - Your answer should be - -
      • an integer, like +
        + + Your answer should be + + +
        +
      +
        -
      • - - Your answer should be - -
      • an integer, like +
        + + Your answer should be + + +
        +
      +
        -
      • - - Your answer should be - -
      • an integer, like +
        + + Your answer should be + + +
        +
      +
        -
      • - - Your answer should be - -
      • an integer, like +
        + + Your answer should be + + +
        +
      +
        -
      • - - Your answer should be - -
      • an integer, like +
        + + Your answer should be + + +
        +
      +
        -
      • - - Your answer should be - -
      • an integer, like +
        + + Your answer should be + + +
        +
      +
        -
      • - - Your answer should be - -
      • an integer, like +
        + + Your answer should be + + +
        +
      +
        -
      • - - Your answer should be - -
      • an integer, like
      `; + +exports[`numeric-input widget Should render tooltip as list when multiple format options are given: render with format list tooltip 1`] = ` +
      +
      +
      +
      + + + + 5008 \\div 4 = + + + + +
      + +
      + +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      + + Your answer should be + + +
      +
      +
      +
        +
      • + a + + simplified proper + + fraction, like + + + + 3/5 + + + +
      • +
      • + a + + simplified improper + + fraction, like + + + + 7/4 + + + +
      • +
      • + a mixed number, like + + + + 1\\ 3/4 + + + +
      • +
      +
      +
      +
      +
      +
      +
      + +
      + +
      +
      +
      +
      +`; + +exports[`numeric-input widget Should render tooltip when format option is given: render with format tooltip 1`] = ` +
      +
      +
      +
      + + + + 5008 \\div 4 = + + + + +
      + +
      + +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      + + Your answer should be + + a + + simplified proper + + fraction, like + + + + 3/5 + + + +
      +
      +
      +
      +
      +
      +
      + +
      + +
      +
      +
      +
      +`; diff --git a/packages/perseus/src/widgets/numeric-input/numeric-input.test.ts b/packages/perseus/src/widgets/numeric-input/numeric-input.test.ts index 2740d841d3..7a36eaa481 100644 --- a/packages/perseus/src/widgets/numeric-input/numeric-input.test.ts +++ b/packages/perseus/src/widgets/numeric-input/numeric-input.test.ts @@ -47,6 +47,20 @@ describe("numeric-input widget", () => { expect(renderer).toHaveBeenAnsweredCorrectly(); }); + it("should reject an incorrect answer", async () => { + // Arrange + const {renderer} = renderQuestion(question); + + // Act + await userEvent.type( + screen.getByRole("textbox", {hidden: true}), + incorrect, + ); + + // Assert + expect(renderer).toHaveBeenAnsweredIncorrectly(); + }); + it("Should render predictably", async () => { // Arrange const {container} = renderQuestion(question); @@ -62,18 +76,32 @@ describe("numeric-input widget", () => { expect(container).toMatchSnapshot("after interaction"); }); - it("should reject an incorrect answer", async () => { + it("Should render tooltip when format option is given", async () => { // Arrange - const {renderer} = renderQuestion(question); + const questionWithFormatOptions = JSON.parse(JSON.stringify(question1)); + questionWithFormatOptions.widgets[ + "numeric-input 1" + ].options.answers[0].answerForms = ["proper"]; // Act - await userEvent.type( - screen.getByRole("textbox", {hidden: true}), - incorrect, - ); + const {container} = renderQuestion(questionWithFormatOptions); // Assert - expect(renderer).toHaveBeenAnsweredIncorrectly(); + expect(container).toMatchSnapshot("render with format tooltip"); + }); + + it("Should render tooltip as list when multiple format options are given", async () => { + // Arrange + const questionWithFormatOptions = JSON.parse(JSON.stringify(question1)); + questionWithFormatOptions.widgets[ + "numeric-input 1" + ].options.answers[0].answerForms = ["proper", "improper", "mixed"]; + + // Act + const {container} = renderQuestion(questionWithFormatOptions); + + // Assert + expect(container).toMatchSnapshot("render with format list tooltip"); }); }); From 5e8d8468bb307ab8168be8e15d30af2be61ffd4a Mon Sep 17 00:00:00 2001 From: Jeremy Wiebe Date: Tue, 3 Dec 2024 13:10:58 -0800 Subject: [PATCH 29/42] Fix types so that we can remove a bunch of TS suppressions (#1938) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary: I was running unit tests and came across some TS suppressions that were fairly easy to resolve. Issue: "none" ## Test plan: `yarn lint` `yarn typecheck` `yarn test` Author: jeremywiebe Reviewers: catandthemachines, jeremywiebe Required Reviewers: Approved By: catandthemachines Checks: ✅ gerald, ✅ Publish npm snapshot (ubuntu-latest, 20.x), ✅ Cypress (ubuntu-latest, 20.x), ✅ Check builds for changes in size (ubuntu-latest, 20.x), ✅ Lint, Typecheck, Format, and Test (ubuntu-latest, 20.x), ✅ Check for .changeset entries for all changed files (ubuntu-latest, 20.x), ✅ Publish Storybook to Chromatic (ubuntu-latest, 20.x), ✅ gerald Pull Request URL: https://github.com/Khan/perseus/pull/1938 --- .changeset/early-phones-remain.md | 6 +++++ .../perseus-editor/src/article-editor.tsx | 24 ++++++------------- .../__stories__/color-select.stories.tsx | 7 +----- .../locked-figures/color-select.tsx | 5 ++-- .../locked-figures/line-stroke-select.tsx | 4 ++-- .../locked-ellipse-settings.tsx | 20 +++++----------- .../locked-figures/locked-figure-select.tsx | 10 ++++---- .../locked-figures/locked-figures-section.tsx | 3 +-- .../locked-function-settings.tsx | 14 ++++------- .../locked-figures/locked-label-settings.tsx | 13 ++++------ .../locked-figures/locked-line-settings.tsx | 15 ++++-------- .../locked-figures/locked-point-settings.tsx | 11 ++++----- .../locked-polygon-settings.tsx | 20 +++++----------- .../locked-figures/locked-vector-settings.tsx | 14 ++++------- packages/perseus/src/renderer.tsx | 6 ++--- packages/perseus/src/types.ts | 2 +- .../widgets/__testutils__/renderQuestion.tsx | 14 +++++------ .../radio/__stories__/radio.stories.tsx | 4 +--- 18 files changed, 71 insertions(+), 121 deletions(-) create mode 100644 .changeset/early-phones-remain.md diff --git a/.changeset/early-phones-remain.md b/.changeset/early-phones-remain.md new file mode 100644 index 0000000000..66ef76e75b --- /dev/null +++ b/.changeset/early-phones-remain.md @@ -0,0 +1,6 @@ +--- +"@khanacademy/perseus": patch +"@khanacademy/perseus-editor": patch +--- + +Type fixes diff --git a/packages/perseus-editor/src/article-editor.tsx b/packages/perseus-editor/src/article-editor.tsx index 6dad830adc..3fecafed7b 100644 --- a/packages/perseus-editor/src/article-editor.tsx +++ b/packages/perseus-editor/src/article-editor.tsx @@ -208,12 +208,8 @@ export default class ArticleEditor extends React.Component { {...section} apiOptions={apiOptions} imageUploader={imageUploader} - // TODO(LEMS-2656): remove TS suppression - onChange={ - _.partial( - this._handleEditorChange, - i, - ) as any + onChange={(newProps) => + this._handleEditorChange(i, newProps) } placeholder="Type your section text here..." ref={"editor" + i} @@ -304,9 +300,8 @@ export default class ArticleEditor extends React.Component { i, newProps, ) => { - const sections = _.clone(this._sections()); - // @ts-expect-error - TS2542 - Index signature in type 'readonly RendererProps[]' only permits reading. - sections[i] = _.extend({}, sections[i], newProps); + const sections = [...this._sections()]; + sections[i] = {...sections[i], ...newProps}; this.props.onChange({json: sections}); }; @@ -314,11 +309,9 @@ export default class ArticleEditor extends React.Component { if (i === 0) { return; } - const sections = _.clone(this._sections()); + const sections = [...this._sections()]; const section = sections[i]; - // @ts-expect-error - TS2551 - Property 'splice' does not exist on type 'readonly RendererProps[]'. Did you mean 'slice'? sections.splice(i, 1); - // @ts-expect-error - TS2551 - Property 'splice' does not exist on type 'readonly RendererProps[]'. Did you mean 'slice'? sections.splice(i - 1, 0, section); this.props.onChange({ json: sections, @@ -326,14 +319,12 @@ export default class ArticleEditor extends React.Component { } _handleMoveSectionLater(i: number) { - const sections = _.clone(this._sections()); + const sections = [...this._sections()]; if (i + 1 === sections.length) { return; } const section = sections[i]; - // @ts-expect-error - TS2551 - Property 'splice' does not exist on type 'readonly RendererProps[]'. Did you mean 'slice'? sections.splice(i, 1); - // @ts-expect-error - TS2551 - Property 'splice' does not exist on type 'readonly RendererProps[]'. Did you mean 'slice'? sections.splice(i + 1, 0, section); this.props.onChange({ json: sections, @@ -364,8 +355,7 @@ export default class ArticleEditor extends React.Component { } _handleRemoveSection(i: number) { - const sections = _.clone(this._sections()); - // @ts-expect-error - TS2551 - Property 'splice' does not exist on type 'readonly RendererProps[]'. Did you mean 'slice'? + const sections = [...this._sections()]; sections.splice(i, 1); this.props.onChange({ json: sections, diff --git a/packages/perseus-editor/src/components/__stories__/color-select.stories.tsx b/packages/perseus-editor/src/components/__stories__/color-select.stories.tsx index 24c2cebbeb..5a0bf94c06 100644 --- a/packages/perseus-editor/src/components/__stories__/color-select.stories.tsx +++ b/packages/perseus-editor/src/components/__stories__/color-select.stories.tsx @@ -29,15 +29,10 @@ export const Controlled = { const [selectedValue, setSelectedValue] = React.useState(defaultColor); - const handleColorChange = (color: LockedFigureColor) => { - setSelectedValue(color); - }; - return ( ); }, diff --git a/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/color-select.tsx b/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/color-select.tsx index df8cafec96..2485b79286 100644 --- a/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/color-select.tsx +++ b/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/color-select.tsx @@ -17,7 +17,7 @@ const possibleColors = Object.keys(lockedFigureColors) as LockedFigureColor[]; type Props = { selectedValue: LockedFigureColor; style?: StyleType; - onChange: (newValue: string) => void; + onChange: (newColor: LockedFigureColor) => void; }; const ColorSelect = (props: Props) => { @@ -30,7 +30,8 @@ const ColorSelect = (props: Props) => { diff --git a/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/line-stroke-select.tsx b/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/line-stroke-select.tsx index f0051f63ab..b5c93d01a3 100644 --- a/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/line-stroke-select.tsx +++ b/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/line-stroke-select.tsx @@ -8,7 +8,7 @@ import * as React from "react"; type StyleOptions = "solid" | "dashed"; type Props = { selectedValue: StyleOptions; - onChange: (newValue: string) => void; + onChange: (newValue: StyleOptions) => void; }; const LineStrokeSelect = (props: Props) => { @@ -20,7 +20,7 @@ const LineStrokeSelect = (props: Props) => { diff --git a/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-ellipse-settings.tsx b/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-ellipse-settings.tsx index 77b6e410ad..f9bb17c479 100644 --- a/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-ellipse-settings.tsx +++ b/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-ellipse-settings.tsx @@ -126,7 +126,7 @@ const LockedEllipseSettings = (props: Props) => { } function handleLabelChange( - updatedLabel: LockedLabelType, + updatedLabel: Partial, labelIndex: number, ) { if (!labels) { @@ -206,8 +206,7 @@ const LockedEllipseSettings = (props: Props) => { {/* Color */} @@ -242,11 +241,7 @@ const LockedEllipseSettings = (props: Props) => { {/* Stroke style */} - onChangeProps({strokeStyle: value})) as any - } + onChange={(value) => onChangeProps({strokeStyle: value})} /> {/* Aria label */} @@ -278,12 +273,9 @@ const LockedEllipseSettings = (props: Props) => { { - handleLabelChange(newLabel, labelIndex); - }) as any - } + onChangeProps={(newLabel) => { + handleLabelChange(newLabel, labelIndex); + }} onRemove={() => { handleLabelRemove(labelIndex); }} diff --git a/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-figure-select.tsx b/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-figure-select.tsx index 71debfccf9..bbf6deed4a 100644 --- a/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-figure-select.tsx +++ b/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-figure-select.tsx @@ -11,29 +11,29 @@ import {spacing, color} from "@khanacademy/wonder-blocks-tokens"; import {StyleSheet} from "aphrodite"; import * as React from "react"; +import type {LockedFigureType} from "@khanacademy/perseus"; + type Props = { // Whether to show the locked labels in the locked figure settings. // TODO(LEMS-2274): Remove this prop once the label flag is // sfully rolled out. showLabelsFlag?: boolean; id: string; - onChange: (value: string) => void; + onChange: (value: LockedFigureType) => void; }; const LockedFigureSelect = (props: Props) => { const {id, onChange} = props; - const figureTypes = [ + const figureTypes: ReadonlyArray = [ "point", "line", "vector", "ellipse", "polygon", "function", + ...(props.showLabelsFlag ? ["label" as const] : []), ]; - if (props.showLabelsFlag) { - figureTypes.push("label"); - } return ( diff --git a/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-figures-section.tsx b/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-figures-section.tsx index 18d6468b06..6cbba741c2 100644 --- a/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-figures-section.tsx +++ b/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-figures-section.tsx @@ -191,8 +191,7 @@ const LockedFiguresSection = (props: Props) => { {showExpandButton && ( diff --git a/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-function-settings.tsx b/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-function-settings.tsx index a303ec87e2..7c62cf23d0 100644 --- a/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-function-settings.tsx +++ b/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-function-settings.tsx @@ -161,7 +161,7 @@ const LockedFunctionSettings = (props: Props) => { } function handleLabelChange( - updatedLabel: LockedLabelType, + updatedLabel: Partial, labelIndex: number, ) { if (!labels) { @@ -205,8 +205,7 @@ const LockedFunctionSettings = (props: Props) => { {/* Line color settings */} @@ -356,12 +355,9 @@ const LockedFunctionSettings = (props: Props) => { key={labelIndex} {...label} expanded={true} - // TODO(LEMS-2656): remove TS suppression - onChangeProps={ - ((newLabel: LockedLabelType) => { - handleLabelChange(newLabel, labelIndex); - }) as any - } + onChangeProps={(newLabel) => { + handleLabelChange(newLabel, labelIndex); + }} onRemove={() => { handleLabelRemove(labelIndex); }} diff --git a/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-label-settings.tsx b/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-label-settings.tsx index 346b6af5ad..60308011e1 100644 --- a/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-label-settings.tsx +++ b/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-label-settings.tsx @@ -7,8 +7,6 @@ */ import { lockedFigureColors, - type LockedFigure, - type LockedFigureColor, type LockedLabelType, components, } from "@khanacademy/perseus"; @@ -36,7 +34,7 @@ export type Props = LockedLabelType & { /** * Called when the props (coord, color, etc.) are updated. */ - onChangeProps: (newProps: Partial) => void; + onChangeProps: (newProps: Partial) => void; // Movement props. Used for standalone label actions. // Not used within other locked figure settings. @@ -150,12 +148,9 @@ export default function LockedLabelSettings(props: Props) { { - onChangeProps({color: newColor}); - }) as any - } + onChange={(newColor) => { + onChangeProps({color: newColor}); + }} style={styles.spaceUnder} /> diff --git a/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-line-settings.tsx b/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-line-settings.tsx index 4aaece85cf..454974644e 100644 --- a/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-line-settings.tsx +++ b/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-line-settings.tsx @@ -162,7 +162,7 @@ const LockedLineSettings = (props: Props) => { } function handleLabelChange( - updatedLabel: LockedLabelType, + updatedLabel: Partial, labelIndex: number, ) { if (!labels) { @@ -224,15 +224,13 @@ const LockedLineSettings = (props: Props) => { {/* Line color settings */} {/* Line style settings */} onChangeProps({lineStyle: value})) as any @@ -300,12 +298,9 @@ const LockedLineSettings = (props: Props) => { { - handleLabelChange(newLabel, labelIndex); - }) as any - } + onChangeProps={(newLabel) => { + handleLabelChange(newLabel, labelIndex); + }} onRemove={() => { handleLabelRemove(labelIndex); }} diff --git a/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-point-settings.tsx b/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-point-settings.tsx index b82dc3f938..8a25c756a3 100644 --- a/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-point-settings.tsx +++ b/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-point-settings.tsx @@ -164,7 +164,7 @@ const LockedPointSettings = (props: Props) => { } function handleLabelChange( - updatedLabel: LockedLabelType, + updatedLabel: Partial, labelIndex: number, ) { if (!labels) { @@ -275,12 +275,9 @@ const LockedPointSettings = (props: Props) => { styles.lockedPointLabelContainer } expanded={true} - // TODO(LEMS-2656): remove TS suppression - onChangeProps={ - ((newLabel: LockedLabelType) => { - handleLabelChange(newLabel, labelIndex); - }) as any - } + onChangeProps={(newLabel) => { + handleLabelChange(newLabel, labelIndex); + }} onRemove={() => { handleLabelRemove(labelIndex); }} diff --git a/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-polygon-settings.tsx b/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-polygon-settings.tsx index e56c9075f5..fbd3eb5789 100644 --- a/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-polygon-settings.tsx +++ b/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-polygon-settings.tsx @@ -142,7 +142,7 @@ const LockedPolygonSettings = (props: Props) => { } function handleLabelChange( - updatedLabel: LockedLabelType, + updatedLabel: Partial, labelIndex: number, ) { if (!labels) { @@ -189,8 +189,7 @@ const LockedPolygonSettings = (props: Props) => { {/* Color */} @@ -225,11 +224,7 @@ const LockedPolygonSettings = (props: Props) => { {/* Stroke style */} - onChangeProps({strokeStyle: value})) as any - } + onChange={(value) => onChangeProps({strokeStyle: value})} /> {/* Show vertices switch */} @@ -371,12 +366,9 @@ const LockedPolygonSettings = (props: Props) => { { - handleLabelChange(newLabel, labelIndex); - }) as any - } + onChangeProps={(newLabel) => { + handleLabelChange(newLabel, labelIndex); + }} onRemove={() => { handleLabelRemove(labelIndex); }} diff --git a/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-vector-settings.tsx b/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-vector-settings.tsx index 098dd27c9c..9cba61984b 100644 --- a/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-vector-settings.tsx +++ b/packages/perseus-editor/src/widgets/interactive-graph-editor/locked-figures/locked-vector-settings.tsx @@ -117,7 +117,7 @@ const LockedVectorSettings = (props: Props) => { } function handleLabelChange( - updatedLabel: LockedLabelType, + updatedLabel: Partial, labelIndex: number, ) { if (!labels) { @@ -159,8 +159,7 @@ const LockedVectorSettings = (props: Props) => { {/* Line color settings */} @@ -237,12 +236,9 @@ const LockedVectorSettings = (props: Props) => { { - handleLabelChange(newLabel, labelIndex); - }) as any - } + onChangeProps={(newLabel) => { + handleLabelChange(newLabel, labelIndex); + }} onRemove={() => { handleLabelRemove(labelIndex); }} diff --git a/packages/perseus/src/renderer.tsx b/packages/perseus/src/renderer.tsx index ff930e7711..fd89f39834 100644 --- a/packages/perseus/src/renderer.tsx +++ b/packages/perseus/src/renderer.tsx @@ -1652,8 +1652,8 @@ class Renderer setInputValue: ( path: FocusPath, newValue: string, - focus?: () => unknown, - ) => void = (path, newValue, focus) => { + cb?: () => void, + ) => void = (path, newValue, cb) => { // @ts-expect-error - TS2345 - Argument of type 'FocusPath' is not assignable to parameter of type 'List'. const widgetId = _.first(path); // @ts-expect-error - TS2345 - Argument of type 'FocusPath' is not assignable to parameter of type 'List'. @@ -1661,7 +1661,7 @@ class Renderer const widget = this.getWidgetInstance(widgetId); // Widget handles parsing of the interWidgetPath. - widget?.setInputValue?.(interWidgetPath, newValue, focus); + widget?.setInputValue?.(interWidgetPath, newValue, cb); }; /** diff --git a/packages/perseus/src/types.ts b/packages/perseus/src/types.ts index eb7f3dd0a6..41e4514cdd 100644 --- a/packages/perseus/src/types.ts +++ b/packages/perseus/src/types.ts @@ -156,7 +156,7 @@ export type ChangeHandler = ( // Interactive Graph callback (see legacy: interactive-graph.tsx) graph?: PerseusGraphType; }, - callback?: () => unknown | null | undefined, + callback?: () => void, silent?: boolean, ) => unknown; diff --git a/packages/perseus/src/widgets/__testutils__/renderQuestion.tsx b/packages/perseus/src/widgets/__testutils__/renderQuestion.tsx index baaa469c4c..40a62a67e0 100644 --- a/packages/perseus/src/widgets/__testutils__/renderQuestion.tsx +++ b/packages/perseus/src/widgets/__testutils__/renderQuestion.tsx @@ -22,17 +22,16 @@ import type {PropsFor} from "@khanacademy/wonder-blocks-core"; type RenderResult = ReturnType; +type ExtraProps = Omit, "strings">; + export const renderQuestion = ( question: PerseusRenderer, apiOptions: APIOptions = Object.freeze({}), - extraProps?: Omit, "strings">, + extraProps?: ExtraProps, ): { container: HTMLElement; renderer: Perseus.Renderer; - rerender: ( - question: PerseusRenderer, - extraProps?: Omit, "strings">, - ) => void; + rerender: (question: PerseusRenderer, extraProps?: ExtraProps) => void; unmount: RenderResult["unmount"]; } => { setDependencies(testDependencies); @@ -59,7 +58,7 @@ export const renderQuestion = ( } const renderAgain = ( question: PerseusRenderer, - extraProps: undefined | React.ComponentProps, + extraProps?: ExtraProps, ) => { rerender( @@ -81,8 +80,7 @@ export const renderQuestion = ( } }; - // TODO(LEMS-2656): remove TS suppression - return {container, renderer, rerender: renderAgain as any, unmount}; + return {container, renderer, rerender: renderAgain, unmount}; }; const Renderer = React.forwardRef< diff --git a/packages/perseus/src/widgets/radio/__stories__/radio.stories.tsx b/packages/perseus/src/widgets/radio/__stories__/radio.stories.tsx index ead3e91f55..bc1a4a36b3 100644 --- a/packages/perseus/src/widgets/radio/__stories__/radio.stories.tsx +++ b/packages/perseus/src/widgets/radio/__stories__/radio.stories.tsx @@ -41,8 +41,6 @@ export default { }, }, }, - // TODO(LEMS-2656): remove TS suppression - // @ts-expect-error: Type 'Args' is not assignable to type 'StoryArgs'. render: (args: StoryArgs) => ( ), -} satisfies Meta; +} satisfies Meta; const applyStoryArgs = (args: StoryArgs): PerseusRenderer => { const q = { From 1d2b4e7bf13ec299f92da43024889702230014fa Mon Sep 17 00:00:00 2001 From: Sarah Third Date: Tue, 3 Dec 2024 13:21:40 -0800 Subject: [PATCH 30/42] Adding alt tag to image widget on zoom (#1942) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary: This is a simple PR to ensure that we're retaining any pre-existing image alt tags when the user zooms in on an image. While I created a storybook view for testing this work, I have removed it before opening up the PR for review as I am unsure about potential image copyright issues. Issue: A11Y-41 ## Test plan: - manual test with storybook - manual test in webapp with PR Author: SonicScrewdriver Reviewers: mark-fitzgerald, catandthemachines Required Reviewers: Approved By: mark-fitzgerald, catandthemachines Checks: ✅ gerald, ✅ Cypress (ubuntu-latest, 20.x), ✅ Publish npm snapshot (ubuntu-latest, 20.x), ✅ Check builds for changes in size (ubuntu-latest, 20.x), ✅ Check for .changeset entries for all changed files (ubuntu-latest, 20.x), ✅ Lint, Typecheck, Format, and Test (ubuntu-latest, 20.x), ✅ gerald, ✅ Publish npm snapshot (ubuntu-latest, 20.x), ✅ Lint, Typecheck, Format, and Test (ubuntu-latest, 20.x), ✅ Cypress (ubuntu-latest, 20.x), ✅ Check for .changeset entries for all changed files (ubuntu-latest, 20.x), ✅ Check builds for changes in size (ubuntu-latest, 20.x), ✅ Publish Storybook to Chromatic (ubuntu-latest, 20.x), ✅ gerald Pull Request URL: https://github.com/Khan/perseus/pull/1942 --- .changeset/hip-pandas-argue.md | 5 +++++ packages/perseus/src/zoom.ts | 1 + 2 files changed, 6 insertions(+) create mode 100644 .changeset/hip-pandas-argue.md diff --git a/.changeset/hip-pandas-argue.md b/.changeset/hip-pandas-argue.md new file mode 100644 index 0000000000..b139d775f5 --- /dev/null +++ b/.changeset/hip-pandas-argue.md @@ -0,0 +1,5 @@ +--- +"@khanacademy/perseus": patch +--- + +Ensure that zoomed-in images retain alt text diff --git a/packages/perseus/src/zoom.ts b/packages/perseus/src/zoom.ts index a260d1048e..d20aae254a 100644 --- a/packages/perseus/src/zoom.ts +++ b/packages/perseus/src/zoom.ts @@ -364,6 +364,7 @@ Zoom.prototype.zoomImage = function () { }.bind(this); img.src = this._targetImage.src; + img.alt = this._targetImage.alt; this.$zoomedImage = $zoomedImage; }; From be8c06c755f2fe5726e03866d1a15541e4258ad4 Mon Sep 17 00:00:00 2001 From: Ben Christel Date: Tue, 3 Dec 2024 14:22:00 -0700 Subject: [PATCH 31/42] Convert backgroundImage dimensions to numbers during parsing (#1923) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Issue: LEMS-2582 ## Test plan: `yarn test` Author: benchristel Reviewers: jeremywiebe, benchristel Required Reviewers: Approved By: jeremywiebe Checks: ✅ Publish npm snapshot (ubuntu-latest, 20.x), ✅ Check builds for changes in size (ubuntu-latest, 20.x), ✅ Cypress (ubuntu-latest, 20.x), ✅ Lint, Typecheck, Format, and Test (ubuntu-latest, 20.x), ✅ Check for .changeset entries for all changed files (ubuntu-latest, 20.x), ✅ Publish Storybook to Chromatic (ubuntu-latest, 20.x), ✅ gerald Pull Request URL: https://github.com/Khan/perseus/pull/1923 --- .changeset/smooth-taxis-mate.md | 5 + .../string-to-number.test.ts | 40 +++++ .../string-to-number.ts | 17 ++ .../perseus-image-background.ts | 19 ++- .../parse-perseus-json-snapshot.test.ts.snap | 160 ++++++++++++++++++ ...raph-with-string-backgroundImage-left.json | 125 ++++++++++++++ 6 files changed, 359 insertions(+), 7 deletions(-) create mode 100644 .changeset/smooth-taxis-mate.md create mode 100644 packages/perseus/src/util/parse-perseus-json/general-purpose-parsers/string-to-number.test.ts create mode 100644 packages/perseus/src/util/parse-perseus-json/general-purpose-parsers/string-to-number.ts create mode 100644 packages/perseus/src/util/parse-perseus-json/regression-tests/data/interactive-graph-with-string-backgroundImage-left.json diff --git a/.changeset/smooth-taxis-mate.md b/.changeset/smooth-taxis-mate.md new file mode 100644 index 0000000000..10221029c8 --- /dev/null +++ b/.changeset/smooth-taxis-mate.md @@ -0,0 +1,5 @@ +--- +"@khanacademy/perseus": patch +--- + +Internal: convert backgroundImage dimensions to numbers during parsing. diff --git a/packages/perseus/src/util/parse-perseus-json/general-purpose-parsers/string-to-number.test.ts b/packages/perseus/src/util/parse-perseus-json/general-purpose-parsers/string-to-number.test.ts new file mode 100644 index 0000000000..bb75d46206 --- /dev/null +++ b/packages/perseus/src/util/parse-perseus-json/general-purpose-parsers/string-to-number.test.ts @@ -0,0 +1,40 @@ +import {success} from "../result"; + +import {stringToNumber} from "./string-to-number"; +import {ctx, parseFailureWith} from "./test-helpers"; + +describe("stringToNumber", () => { + it("accepts a number", () => { + expect(stringToNumber(42, ctx())).toEqual(success(42)); + }); + + it("accepts a numeric string", () => { + expect(stringToNumber("7", ctx())).toEqual(success(7)); + }); + + it("parses a decimal", () => { + expect(stringToNumber("5.5", ctx())).toEqual(success(5.5)); + }); + + it("parses a negative number", () => { + expect(stringToNumber("-2", ctx())).toEqual(success(-2)); + }); + + it("rejects the empty string", () => { + expect(stringToNumber("", ctx())).toEqual( + parseFailureWith({ + badValue: "", + expected: ["a number or numeric string"], + }), + ); + }); + + it("rejects a non-numeric string", () => { + expect(stringToNumber("3a", ctx())).toEqual( + parseFailureWith({ + badValue: "3a", + expected: ["a number or numeric string"], + }), + ); + }); +}); diff --git a/packages/perseus/src/util/parse-perseus-json/general-purpose-parsers/string-to-number.ts b/packages/perseus/src/util/parse-perseus-json/general-purpose-parsers/string-to-number.ts new file mode 100644 index 0000000000..0569035c4f --- /dev/null +++ b/packages/perseus/src/util/parse-perseus-json/general-purpose-parsers/string-to-number.ts @@ -0,0 +1,17 @@ +import type {PartialParser} from "../parser-types"; + +export const stringToNumber: PartialParser = ( + rawValue, + ctx, +) => { + if (typeof rawValue === "number") { + return ctx.success(rawValue); + } + + const parsedNumber = +rawValue; + if (rawValue === "" || isNaN(parsedNumber)) { + return ctx.failure("a number or numeric string", rawValue); + } + + return ctx.success(parsedNumber); +}; diff --git a/packages/perseus/src/util/parse-perseus-json/perseus-parsers/perseus-image-background.ts b/packages/perseus/src/util/parse-perseus-json/perseus-parsers/perseus-image-background.ts index ae8c21b164..248644ce53 100644 --- a/packages/perseus/src/util/parse-perseus-json/perseus-parsers/perseus-image-background.ts +++ b/packages/perseus/src/util/parse-perseus-json/perseus-parsers/perseus-image-background.ts @@ -3,21 +3,26 @@ import { number, object, optional, + pipeParsers, string, union, } from "../general-purpose-parsers"; +import {stringToNumber} from "../general-purpose-parsers/string-to-number"; import type {Parser} from "../parser-types"; import type {PerseusImageBackground} from "@khanacademy/perseus"; +const numericToNumber = pipeParsers(union(number).or(string).parser).then( + stringToNumber, +).parser; + export const parsePerseusImageBackground: Parser = object({ url: optional(nullable(string)), - width: optional(number), - height: optional(number), - top: optional(number), - left: optional(number), - bottom: optional(number), - // TODO(benchristel): convert scale to a number - scale: optional(union(number).or(string).parser), + width: optional(numericToNumber), + height: optional(numericToNumber), + top: optional(numericToNumber), + left: optional(numericToNumber), + bottom: optional(numericToNumber), + scale: optional(numericToNumber), }); diff --git a/packages/perseus/src/util/parse-perseus-json/regression-tests/__snapshots__/parse-perseus-json-snapshot.test.ts.snap b/packages/perseus/src/util/parse-perseus-json/regression-tests/__snapshots__/parse-perseus-json-snapshot.test.ts.snap index 1aa0ec1b21..a5b884de44 100644 --- a/packages/perseus/src/util/parse-perseus-json/regression-tests/__snapshots__/parse-perseus-json-snapshot.test.ts.snap +++ b/packages/perseus/src/util/parse-perseus-json/regression-tests/__snapshots__/parse-perseus-json-snapshot.test.ts.snap @@ -473,6 +473,166 @@ exports[`parseAndTypecheckPerseusItem correctly parses data/iframe-missing-stati } `; +exports[`parseAndTypecheckPerseusItem correctly parses data/interactive-graph-with-string-backgroundImage-left.json 1`] = ` +{ + "answer": undefined, + "answerArea": { + "calculator": false, + "chi2Table": false, + "periodicTable": false, + "tTable": false, + "zTable": false, + }, + "hints": [], + "itemDataVersion": { + "major": 0, + "minor": 1, + }, + "question": { + "content": "Ally is excited to compete in a $6$-mile race. The race organizers plotted the course on a coordinate map. The starting point is at $(4,3)$, and the ending point is at $(4,9)$. Ally's family decides to stand at $(4,6)$ on the map. + +**Plot the starting point, ending point, and place where Ally's family stands on the map.** + +[[☃ interactive-graph 1]] + +**How far along will Ally be in the race when she reaches her family?** +[[☃ radio 1]]", + "images": {}, + "metadata": undefined, + "widgets": { + "interactive-graph 1": { + "alignment": "default", + "graded": true, + "key": undefined, + "options": { + "backgroundImage": { + "bottom": 4, + "height": 0, + "left": 0, + "scale": 1, + "top": undefined, + "url": "", + "width": 0, + }, + "correct": { + "coord": undefined, + "coords": [ + [ + 4, + 3, + ], + [ + 4, + 6, + ], + [ + 4, + 9, + ], + ], + "numPoints": 3, + "startCoords": undefined, + "type": "point", + }, + "fullGraphAriaDescription": undefined, + "fullGraphLabel": undefined, + "graph": { + "coord": undefined, + "coords": undefined, + "numPoints": 3, + "startCoords": undefined, + "type": "point", + }, + "gridStep": [ + 1, + 1, + ], + "labels": [ + "x", + "y", + ], + "lockedFigures": undefined, + "markings": "graph", + "range": [ + [ + -1, + 10, + ], + [ + -1, + 10, + ], + ], + "rulerLabel": "", + "rulerTicks": 10, + "showProtractor": false, + "showRuler": false, + "showTooltips": false, + "snapStep": [ + 0.5, + 0.5, + ], + "step": [ + 1, + 1, + ], + }, + "static": false, + "type": "interactive-graph", + "version": { + "major": 0, + "minor": 0, + }, + }, + "radio 1": { + "alignment": "default", + "graded": true, + "key": undefined, + "options": { + "choices": [ + { + "clue": undefined, + "content": "Less than halfway through the race", + "correct": false, + "isNoneOfTheAbove": undefined, + "widgets": undefined, + }, + { + "clue": undefined, + "content": "Halfway through the race", + "correct": true, + "isNoneOfTheAbove": undefined, + "widgets": undefined, + }, + { + "clue": undefined, + "content": "More than halfway through the race", + "correct": undefined, + "isNoneOfTheAbove": undefined, + "widgets": undefined, + }, + ], + "countChoices": false, + "deselectEnabled": false, + "displayCount": null, + "hasNoneOfTheAbove": false, + "multipleSelect": false, + "noneOfTheAbove": undefined, + "onePerLine": undefined, + "randomize": false, + }, + "static": false, + "type": "radio", + "version": { + "major": 1, + "minor": 0, + }, + }, + }, + }, +} +`; + exports[`parseAndTypecheckPerseusItem correctly parses data/item-missing-answerArea.json 1`] = ` { "_multi": { diff --git a/packages/perseus/src/util/parse-perseus-json/regression-tests/data/interactive-graph-with-string-backgroundImage-left.json b/packages/perseus/src/util/parse-perseus-json/regression-tests/data/interactive-graph-with-string-backgroundImage-left.json new file mode 100644 index 0000000000..5e348cd967 --- /dev/null +++ b/packages/perseus/src/util/parse-perseus-json/regression-tests/data/interactive-graph-with-string-backgroundImage-left.json @@ -0,0 +1,125 @@ +{ + "question": { + "content": "Ally is excited to compete in a $6$-mile race. The race organizers plotted the course on a coordinate map. The starting point is at $(4,3)$, and the ending point is at $(4,9)$. Ally's family decides to stand at $(4,6)$ on the map.\n\n**Plot the starting point, ending point, and place where Ally's family stands on the map.**\n\n[[☃ interactive-graph 1]]\n\n**How far along will Ally be in the race when she reaches her family?** \n[[☃ radio 1]]", + "images": {}, + "widgets": { + "interactive-graph 1": { + "type": "interactive-graph", + "alignment": "default", + "static": false, + "graded": true, + "options": { + "step": [ + 1, + 1 + ], + "backgroundImage": { + "url": "", + "scale": "1", + "bottom": "4", + "left": "0", + "width": 0, + "height": 0 + }, + "markings": "graph", + "labels": [ + "x", + "y" + ], + "showProtractor": false, + "showRuler": false, + "showTooltips": false, + "rulerLabel": "", + "rulerTicks": 10, + "range": [ + [ + -1, + 10 + ], + [ + -1, + 10 + ] + ], + "gridStep": [ + 1, + 1 + ], + "snapStep": [ + 0.5, + 0.5 + ], + "graph": { + "type": "point", + "numPoints": 3 + }, + "correct": { + "type": "point", + "coords": [ + [ + 4, + 3 + ], + [ + 4, + 6 + ], + [ + 4, + 9 + ] + ], + "numPoints": 3 + } + }, + "version": { + "major": 0, + "minor": 0 + } + }, + "radio 1": { + "type": "radio", + "alignment": "default", + "static": false, + "graded": true, + "options": { + "choices": [ + { + "correct": false, + "content": "Less than halfway through the race" + }, + { + "correct": true, + "content": "Halfway through the race" + }, + { + "content": "More than halfway through the race" + } + ], + "randomize": false, + "multipleSelect": false, + "countChoices": false, + "displayCount": null, + "hasNoneOfTheAbove": false, + "deselectEnabled": false + }, + "version": { + "major": 1, + "minor": 0 + } + } + } + }, + "answerArea": { + "calculator": false, + "chi2Table": false, + "periodicTable": false, + "tTable": false, + "zTable": false + }, + "itemDataVersion": { + "major": 0, + "minor": 1 + }, + "hints": [] +} From ef819ea959fbab0849724529538f9a9912173aa3 Mon Sep 17 00:00:00 2001 From: Mark Fitzgerald <13896410+mark-fitzgerald@users.noreply.github.com> Date: Tue, 3 Dec 2024 13:52:05 -0800 Subject: [PATCH 32/42] [Numeric Input] - Link tooltip content to input field for assistive technologies (#1891) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary: Currently, the format options tooltip for the input field is only available to sighted users. People using assistive technologies (like braille or screen readers) are unaware of any formatting options for their answers. This change will add an element adjacent to the input field that includes the content of the tooltip, and an association between the two so that assistive technologies can make use of the format options. Issue: LEMS-2458 ## Test plan: 1. Launch Storybook 1. Navigate to Perseus Editor => Editor => [Demo](http://localhost:6006/?path=/story/perseuseditor-editorpage--demo) 1. Add a Numeric Input widget 1. Configure the widget to have any number of format options ![Storybook - 1 Format Option](https://github.com/user-attachments/assets/a46af08c-cc6d-48a6-b20f-4b48c560023d) 1. Using the keyboard, tab to the close icon in the top right of the preview area ![Storybook - Close Icon](https://github.com/user-attachments/assets/a75b4447-4015-460f-8e91-76a71d46de6f) 1. Launch VoiceOver (or your preferred screen reader) 1. Tab to the input field in the preview 1. The screen reader should read the same content as what shows in the tooltip ![VoiceOver - 1 Format Option](https://github.com/user-attachments/assets/fa6bed95-5446-44f7-8cd5-7ec68b860495) 1. Multiple format options should be read with an "or" conjunction in the phrase ![VoiceOver - 3 Format Option](https://github.com/user-attachments/assets/16fc3ef9-bd53-4b65-a355-777919779600) ## Affected behavior: ### Before ![VoiceOver - Before](https://github.com/user-attachments/assets/12ee2a44-ef61-4d80-8120-600d4aa143f4) ### After ![VoiceOver - 1 Format Option](https://github.com/user-attachments/assets/911d1b53-9ea3-4758-bbd5-4a6f04aae901) Author: mark-fitzgerald Reviewers: SonicScrewdriver Required Reviewers: Approved By: SonicScrewdriver Checks: ✅ Publish npm snapshot (ubuntu-latest, 20.x), ✅ Lint, Typecheck, Format, and Test (ubuntu-latest, 20.x), ✅ Cypress (ubuntu-latest, 20.x), ✅ Check for .changeset entries for all changed files (ubuntu-latest, 20.x), ✅ Check builds for changes in size (ubuntu-latest, 20.x), ✅ Publish Storybook to Chromatic (ubuntu-latest, 20.x), ✅ gerald Pull Request URL: https://github.com/Khan/perseus/pull/1891 --- .changeset/odd-moons-remember.md | 5 +++ .../server-item-renderer.test.tsx.snap | 13 ++++++ .../src/components/input-with-examples.tsx | 23 +++++++++- .../perseus/src/components/text-input.tsx | 2 + packages/perseus/src/components/tooltip.tsx | 1 + .../multi-renderer.test.tsx.snap | 13 ++++++ .../graded-group-set-jipt.test.ts.snap | 18 ++++++++ .../graded-group-set.test.ts.snap | 6 +++ .../group/__snapshots__/group.test.tsx.snap | 12 +++++ .../__snapshots__/numeric-input.test.ts.snap | 32 +++++++++++++ .../numeric-input/numeric-input.test.ts | 45 +++++++++++++++++++ 11 files changed, 168 insertions(+), 2 deletions(-) create mode 100644 .changeset/odd-moons-remember.md diff --git a/.changeset/odd-moons-remember.md b/.changeset/odd-moons-remember.md new file mode 100644 index 0000000000..3ff9692b33 --- /dev/null +++ b/.changeset/odd-moons-remember.md @@ -0,0 +1,5 @@ +--- +"@khanacademy/perseus": patch +--- + +[Numeric Input] - Associate format options tooltip content with input field for assistive technologies 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 8e79ffdd65..a1c1828e6c 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 @@ -41,6 +41,7 @@ exports[`server item renderer should snapshot: initial render 1`] = `
      +