Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions src/components/LanguagePicker/LanguagePicker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import {
LanguagePicker as MLP,
languagePickerStrings_en,
} from "mui-language-picker";
import { ReactElement } from "react";

type LanguagePickerProps = Omit<React.ComponentProps<typeof MLP>, "t">;

/** A wrapped `mui-language-picker` component with the localization strings preset. */
export default function LanguagePicker(
props: LanguagePickerProps
): ReactElement {
return <MLP {...props} t={languagePickerStrings_en} />;
}
8 changes: 8 additions & 0 deletions src/components/LanguagePicker/index.tsx
Original file line number Diff line number Diff line change
@@ -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;
75 changes: 40 additions & 35 deletions src/components/ProjectScreen/CreateProject.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
SelectChangeEvent,
Typography,
} from "@mui/material";
import { LanguagePicker, languagePickerStrings_en } from "mui-language-picker";
import {
type ChangeEvent,
type FormEvent,
Expand All @@ -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,
Expand Down Expand Up @@ -276,41 +276,46 @@ export default function CreateProject(): ReactElement {
)}
</div>

{/* Vernacular language picker */}
<Typography style={{ marginTop: theme.spacing(1) }}>
{t(CreateProjectTextId.LangVernacular)}
</Typography>
{vernLangSelect()}
{(vernLangIsOther || !vernLangOptions.length) && (
<LanguagePicker
value={vernLang.bcp47}
setCode={setVernBcp47}
name={vernLang.name}
setName={setVernName}
font={vernLang.font}
setFont={setVernFont}
setDir={setVernRtl}
t={languagePickerStrings_en}
/>
)}
{/* Don't render language pickers until project creation begins. */}
{!!(name || languageData || vernLang.name || analysisLang.name) && (
<>
{/* Vernacular language picker */}
<Typography sx={{ marginTop: 1 }} variant="h6">
{t(CreateProjectTextId.LangVernacular)}
</Typography>
{vernLangSelect()}
{(vernLangIsOther || !vernLangOptions.length) && (
<LanguagePicker
value={vernLang.bcp47}
setCode={setVernBcp47}
name={vernLang.name}
setName={setVernName}
font={vernLang.font}
setFont={setVernFont}
setDir={setVernRtl}
/>
)}

{/* Analysis language picker */}
<Typography style={{ marginTop: theme.spacing(1) }}>
{t(CreateProjectTextId.LangAnalysis)}
</Typography>
{languageData ? (
t(CreateProjectTextId.LangAnalysisInfo)
) : (
<LanguagePicker
value={analysisLang.bcp47}
setCode={setAnalysisBcp47}
name={analysisLang.name}
setName={setAnalysisName}
font={analysisLang.font}
setFont={setAnalysisFont}
setDir={setAnalysisRtl}
t={languagePickerStrings_en}
/>
{/* Analysis language picker */}
<Typography sx={{ marginTop: 1 }} variant="h6">
{t(CreateProjectTextId.LangAnalysis)}
</Typography>
{languageData ? (
<Typography>
{t(CreateProjectTextId.LangAnalysisInfo)}
</Typography>
) : (
<LanguagePicker
value={analysisLang.bcp47}
setCode={setAnalysisBcp47}
name={analysisLang.name}
setName={setAnalysisName}
font={analysisLang.font}
setFont={setAnalysisFont}
setDir={setAnalysisRtl}
/>
)}
</>
)}

{/* Form submission button */}
Expand Down
86 changes: 67 additions & 19 deletions src/components/ProjectScreen/tests/CreateProject.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}) => (
<MockLP
data-testid={mockLangPickerId}
onChange={(e) => props.setCode(e.target.value)}
onChange={(e) => {
props.setCode(e.target.value);
props.setName(e.target.value);
}}
/>
),
}));
Expand Down Expand Up @@ -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();
Expand All @@ -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.
Expand All @@ -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();

Expand Down
3 changes: 1 addition & 2 deletions src/components/ProjectSettings/ProjectLanguages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,14 @@ 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";

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";
Expand Down Expand Up @@ -213,7 +213,6 @@ export default function ProjectLanguages(
rtl: rtl || undefined,
}))
}
t={languagePickerStrings_en}
/>

<IconButton
Expand Down
Loading