diff --git a/src/components/LanguagePicker/LanguagePicker.tsx b/src/components/LanguagePicker/LanguagePicker.tsx new file mode 100644 index 0000000000..77a6ef7488 --- /dev/null +++ b/src/components/LanguagePicker/LanguagePicker.tsx @@ -0,0 +1,14 @@ +import { + LanguagePicker as MLP, + languagePickerStrings_en, +} from "mui-language-picker"; +import { ReactElement } from "react"; + +type LanguagePickerProps = Omit, "t">; + +/** A wrapped `mui-language-picker` component with the localization strings preset. */ +export default function LanguagePicker( + props: LanguagePickerProps +): ReactElement { + return ; +} diff --git a/src/components/LanguagePicker/index.tsx b/src/components/LanguagePicker/index.tsx new file mode 100644 index 0000000000..fe90b4f856 --- /dev/null +++ b/src/components/LanguagePicker/index.tsx @@ -0,0 +1,8 @@ +import loadable from "@loadable/component"; + +/** A lazy-loaded Language Picker, because it's a 5 MB dependency. */ +const LoadableLanguagePicker = loadable( + () => import("components/LanguagePicker/LanguagePicker") +); + +export default LoadableLanguagePicker; diff --git a/src/components/ProjectScreen/CreateProject.tsx b/src/components/ProjectScreen/CreateProject.tsx index 97891b284e..dc6320085e 100644 --- a/src/components/ProjectScreen/CreateProject.tsx +++ b/src/components/ProjectScreen/CreateProject.tsx @@ -9,7 +9,6 @@ import { SelectChangeEvent, Typography, } from "@mui/material"; -import { LanguagePicker, languagePickerStrings_en } from "mui-language-picker"; import { type ChangeEvent, type FormEvent, @@ -24,6 +23,7 @@ import { type WritingSystem } from "api/models"; import { projectDuplicateCheck, uploadLiftAndGetWritingSystems } from "backend"; import FileInputButton from "components/Buttons/FileInputButton"; import LoadingDoneButton from "components/Buttons/LoadingDoneButton"; +import LanguagePicker from "components/LanguagePicker"; import { asyncCreateProject, asyncFinishProject, @@ -276,41 +276,46 @@ export default function CreateProject(): ReactElement { )} - {/* Vernacular language picker */} - - {t(CreateProjectTextId.LangVernacular)} - - {vernLangSelect()} - {(vernLangIsOther || !vernLangOptions.length) && ( - - )} + {/* Don't render language pickers until project creation begins. */} + {!!(name || languageData || vernLang.name || analysisLang.name) && ( + <> + {/* Vernacular language picker */} + + {t(CreateProjectTextId.LangVernacular)} + + {vernLangSelect()} + {(vernLangIsOther || !vernLangOptions.length) && ( + + )} - {/* Analysis language picker */} - - {t(CreateProjectTextId.LangAnalysis)} - - {languageData ? ( - t(CreateProjectTextId.LangAnalysisInfo) - ) : ( - + {/* Analysis language picker */} + + {t(CreateProjectTextId.LangAnalysis)} + + {languageData ? ( + + {t(CreateProjectTextId.LangAnalysisInfo)} + + ) : ( + + )} + )} {/* Form submission button */} diff --git a/src/components/ProjectScreen/tests/CreateProject.test.tsx b/src/components/ProjectScreen/tests/CreateProject.test.tsx index 2f19da363d..b99823f191 100644 --- a/src/components/ProjectScreen/tests/CreateProject.test.tsx +++ b/src/components/ProjectScreen/tests/CreateProject.test.tsx @@ -12,13 +12,19 @@ import CreateProject, { import { defaultState } from "rootRedux/types"; import { newWritingSystem } from "types/writingSystem"; -jest.mock("mui-language-picker", () => ({ - ...jest.requireActual("mui-language-picker"), - /** Mocked with Input that triggers the `setCode` prop when typed in. */ - LanguagePicker: (props: { setCode: (code: string) => void }) => ( +jest.mock("components/LanguagePicker", () => ({ + __esModule: true, + /** Mocked with Input that triggers the `setCode`, `setName` props when typed in. */ + default: (props: { + setCode: (code: string) => void; + setName: (name: string) => void; + }) => ( props.setCode(e.target.value)} + onChange={(e) => { + props.setCode(e.target.value); + props.setName(e.target.value); + }} /> ), })); @@ -59,11 +65,48 @@ beforeEach(async () => { }); describe("CreateProject", () => { + it("enables language pickers when name nonempty", async () => { + // No language pickers by default. + expect(screen.queryByTestId(mockLangPickerId)).toBeNull(); + const nameInput = screen.getByRole("textbox"); + + // Typing in name shows both language pickers. + await userEvent.type(nameInput, "non-empty-name"); + expect(screen.getAllByTestId(mockLangPickerId)).toHaveLength(2); + + // Clearing name hides both language pickers. + await userEvent.clear(nameInput); + expect(screen.queryByTestId(mockLangPickerId)).toBeNull(); + }); + + it("keeps language pickers when vernacular language nonempty", async () => { + // No language pickers by default. + expect(screen.queryByTestId(mockLangPickerId)).toBeNull(); + + // Language pickers don't hide if vernacular language is filled. + const nameInput = screen.getByRole("textbox"); + await userEvent.type(nameInput, "non-empty-name"); + await userEvent.type(screen.getAllByTestId(mockLangPickerId)[0], "lang"); + await userEvent.clear(nameInput); + expect(screen.getAllByTestId(mockLangPickerId)).toHaveLength(2); + }); + + it("keeps language pickers when analysis language nonempty", async () => { + // No language pickers by default. + expect(screen.queryByTestId(mockLangPickerId)).toBeNull(); + + // Language pickers don't hide if analysis language is filled. + const nameInput = screen.getByRole("textbox"); + await userEvent.type(nameInput, "non-empty-name"); + await userEvent.type(screen.getAllByTestId(mockLangPickerId)[1], "lang"); + await userEvent.clear(nameInput); + expect(screen.getAllByTestId(mockLangPickerId)).toHaveLength(2); + }); + it("errors on taken name", async () => { // Input project name and vernacular language. - const [nameInput, vernInput] = screen.getAllByRole("textbox"); - await userEvent.type(nameInput, "non-empty-name"); - await userEvent.type(vernInput, "non-empty-code"); + await userEvent.type(screen.getByRole("textbox"), "non-empty-name"); + await userEvent.type(screen.getAllByTestId(mockLangPickerId)[0], "lang"); // Error appears when duplicate name submitted. expect(screen.queryByText(CreateProjectTextId.NameTaken)).toBeNull(); @@ -74,21 +117,21 @@ describe("CreateProject", () => { expect(screen.queryByText(CreateProjectTextId.NameTaken)).toBeTruthy(); }); - it("disables submit button when empty name or empty vern lang bcp code", async () => { + it("disables submit button when name or vernacular language is empty", async () => { const button = screen.getByRole("button", { name: CreateProjectTextId.Create, }); - const [nameInput, vernInput] = screen.getAllByRole("textbox"); // Start with empty name and vern language: button disabled. expect(button).toBeDisabled(); // Add name but still no vern language: button still disabled. + const nameInput = screen.getByRole("textbox"); await userEvent.type(nameInput, "non-empty-name"); expect(button).toBeDisabled(); // Also add a vern language: button enabled. - await userEvent.type(vernInput, "non-empty-code"); + await userEvent.type(screen.getAllByTestId(mockLangPickerId)[0], "lang"); expect(button).toBeEnabled(); // Change name to whitespace: button disabled again. @@ -97,22 +140,27 @@ describe("CreateProject", () => { expect(button).toBeDisabled(); }); - it("disables language picker(s) when file selected", async () => { - // Both vernacular and analysis lang pickers available by default. - expect(screen.queryAllByTestId(mockLangPickerId)).toHaveLength(2); + it("enables 1 language picker when file selected without writing systems", async () => { + // No language pickers by default. + expect(screen.queryByTestId(mockLangPickerId)).toBeNull(); - // File with no writing systems only disables analysis lang picker. + // File with no writing systems only enables vernacular lang picker. mockUploadLiftAndGetWritingSystems.mockResolvedValueOnce([]); await userEvent.click(screen.getByText(CreateProjectTextId.UploadBrowse)); - expect(screen.queryAllByTestId(mockLangPickerId)).toHaveLength(1); + expect(screen.getByTestId(mockLangPickerId)).toBeTruthy(); + }); + + it("enables 0 language pickers when file selected with writing systems", async () => { + // No language pickers by default. + expect(screen.queryByTestId(mockLangPickerId)).toBeNull(); - // File with writing systems disables both lang pickers. + // File with writing systems enables no lang pickers. mockUploadLiftAndGetWritingSystems.mockResolvedValueOnce(mockLangs); await userEvent.click(screen.getByText(CreateProjectTextId.UploadBrowse)); - expect(screen.queryAllByTestId(mockLangPickerId)).toHaveLength(0); + expect(screen.queryByTestId(mockLangPickerId)).toBeNull(); }); - it("offers vern langs when file has some", async () => { + it("offers vernacular languages when file has some", async () => { // No vern combobox selector by default. expect(screen.queryByRole("combobox")).toBeNull(); diff --git a/src/components/ProjectSettings/ProjectLanguages.tsx b/src/components/ProjectSettings/ProjectLanguages.tsx index 2d267f180f..6db1a252a8 100644 --- a/src/components/ProjectSettings/ProjectLanguages.tsx +++ b/src/components/ProjectSettings/ProjectLanguages.tsx @@ -17,7 +17,6 @@ import { Stack, Typography, } from "@mui/material"; -import { LanguagePicker, languagePickerStrings_en } from "mui-language-picker"; import { Fragment, type ReactElement, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { toast } from "react-toastify"; @@ -25,6 +24,7 @@ import { toast } from "react-toastify"; import { type WritingSystem } from "api/models"; import { getFrontierWords } from "backend"; import IconButtonWithTooltip from "components/Buttons/IconButtonWithTooltip"; +import LanguagePicker from "components/LanguagePicker"; import { type ProjectSettingProps } from "components/ProjectSettings/ProjectSettingsTypes"; import theme from "types/theme"; import { newWritingSystem, semDomWritingSystems } from "types/writingSystem"; @@ -213,7 +213,6 @@ export default function ProjectLanguages( rtl: rtl || undefined, })) } - t={languagePickerStrings_en} />