diff --git a/src/components/DataEntry/DataEntryHeader.tsx b/src/components/DataEntry/DataEntryHeader.tsx index 3c7bf6d4d5..715f79bb04 100644 --- a/src/components/DataEntry/DataEntryHeader.tsx +++ b/src/components/DataEntry/DataEntryHeader.tsx @@ -5,7 +5,6 @@ import { useTranslation } from "react-i18next"; import { Key } from "ts-key-enum"; import { SemanticDomainFull } from "api/models"; -import theme from "types/theme"; interface DataEntryHeaderProps { domain: SemanticDomainFull; @@ -23,11 +22,7 @@ export default function DataEntryHeader( const { t } = useTranslation(); return ( - + {t("addWords.domainTitle", { val1: domain.name, val2: domain.id })} {domain.description} @@ -35,11 +30,11 @@ export default function DataEntryHeader( props.setQuestionVisibility(!props.questionsVisible)} - icon={} - checkedIcon={} + icon={} + checkedIcon={} checked={props.questionsVisible} color="primary" - style={{ paddingTop: "8px" }} + sx={{ pt: "11px" }} disabled={!domain.questions.length} onKeyDown={(e) => { if (e.key === Key.Enter) { diff --git a/src/components/DataEntry/DataEntryTable/NewEntry/VernDialog.tsx b/src/components/DataEntry/DataEntryTable/NewEntry/VernDialog.tsx index 2d9d1f4577..3f7f2564dc 100644 --- a/src/components/DataEntry/DataEntryTable/NewEntry/VernDialog.tsx +++ b/src/components/DataEntry/DataEntryTable/NewEntry/VernDialog.tsx @@ -1,7 +1,7 @@ import { Dialog, DialogContent, - Grid, + Grid2, ListItemText, MenuList, Typography, @@ -117,7 +117,7 @@ export function VernList(props: VernListProps): ReactElement { props.onSelect(word.id, sense?.guid)} - sx={isSense ? { marginInlineStart: theme.spacing(4) } : conditionalGrey} + sx={isSense ? { marginInlineStart: 4 } : conditionalGrey} > props.onSelect(word.id, "")} - sx={{ marginInlineStart: theme.spacing(4) }} + sx={{ marginInlineStart: 4 }} > @@ -214,51 +214,38 @@ interface DialogListItemTextProps { const DialogListItemText = (props: DialogListItemTextProps): ReactElement => { return ( - - - - {props.text} - - + + {props.text} + + {!!props.word && ( <> {props.showGlosses && ( - - - + )} {props.showDefinitions && ( - - - - )} - {props.showPartOfSpeech && ( - - - - )} - {props.showDomain && ( - - - + )} + {props.showPartOfSpeech && } + {props.showDomain && } )} - + ); }; diff --git a/src/components/DataEntry/DataEntryTable/NewEntry/index.tsx b/src/components/DataEntry/DataEntryTable/NewEntry/index.tsx index 62740c7325..acc3912d07 100644 --- a/src/components/DataEntry/DataEntryTable/NewEntry/index.tsx +++ b/src/components/DataEntry/DataEntryTable/NewEntry/index.tsx @@ -1,6 +1,5 @@ -import { AutocompleteCloseReason, Grid, Typography } from "@mui/material"; +import { AutocompleteCloseReason, Grid2, Typography } from "@mui/material"; import { - CSSProperties, ReactElement, RefObject, useCallback, @@ -22,7 +21,6 @@ import VernDialog from "components/DataEntry/DataEntryTable/NewEntry/VernDialog" import { focusInput } from "components/DataEntry/utilities"; import PronunciationsFrontend from "components/Pronunciations/PronunciationsFrontend"; import { type StoreState } from "rootRedux/types"; -import theme from "types/theme"; import { FileWithSpeakerId } from "types/word"; export enum NewEntryId { @@ -38,11 +36,6 @@ export enum FocusTarget { Vernacular, } -const gridItemStyle = (spacing: number): CSSProperties => ({ - paddingInline: theme.spacing(spacing), - position: "relative", -}); - interface NewEntryProps { analysisLang: WritingSystem; vernacularLang: WritingSystem; @@ -224,44 +217,48 @@ export default function NewEntry(props: NewEntryProps): ReactElement { }; return ( - - - - - updateVernField(newValue, openDialog) + + + + updateVernField(newValue, openDialog) + } + onBlur={() => setVernOpen(true)} + onClose={(_, reason: AutocompleteCloseReason) => { + // Handle if the user fully types an identical vernacular to a suggestion + // and selects it from the Autocomplete. This should open the dialog. + if (reason === "selectOption") { + // User pressed Enter or Left Click on an item. + setVernOpen(true); } - onBlur={() => setVernOpen(true)} - onClose={(_, reason: AutocompleteCloseReason) => { - // Handle if the user fully types an identical vernacular to a suggestion - // and selects it from the Autocomplete. This should open the dialog. - if (reason === "selectOption") { - // User pressed Enter or Left Click on an item. - setVernOpen(true); - } - }} - onFocus={handleOnVernFocus} - suggestedVerns={suggestedVerns} - // To prevent unintentional no-gloss or wrong-gloss submissions - // and to simplify interactions with Autocomplete and with the dialogs: - // if Enter is pressed from the vern field, move focus to gloss field. - handleEnter={() => focus(FocusTarget.Gloss)} - vernacularLang={vernacularLang} - textFieldId={NewEntryId.TextFieldVern} - onUpdate={() => conditionalFocus(FocusTarget.Vernacular)} - /> - - - - + }} + onFocus={handleOnVernFocus} + suggestedVerns={suggestedVerns} + // To prevent unintentional no-gloss or wrong-gloss submissions + // and to simplify interactions with Autocomplete and with the dialogs: + // if Enter is pressed from the vern field, move focus to gloss field. + handleEnter={() => focus(FocusTarget.Gloss)} + vernacularLang={vernacularLang} + textFieldId={NewEntryId.TextFieldVern} + onUpdate={() => conditionalFocus(FocusTarget.Vernacular)} + /> + + + + conditionalFocus(FocusTarget.Gloss)} /> - - + + + {!selectedDup?.id && ( // note is not available if user selected to modify an existing entry )} - - + + + focus(FocusTarget.Gloss)} /> - - + + + resetState()} /> - + + - + ); } function EnterGrid(): ReactElement { const { t } = useTranslation(); return ( - + {t("addWords.pressEnter")} - + ); } diff --git a/src/components/DataEntry/DataEntryTable/RecentEntry.tsx b/src/components/DataEntry/DataEntryTable/RecentEntry.tsx index 1cbcc67d2b..a3636b66dd 100644 --- a/src/components/DataEntry/DataEntryTable/RecentEntry.tsx +++ b/src/components/DataEntry/DataEntryTable/RecentEntry.tsx @@ -1,4 +1,4 @@ -import { Grid } from "@mui/material"; +import { Grid2 } from "@mui/material"; import { ReactElement, memo, useState } from "react"; import { Pronunciation, Word, WritingSystem } from "api/models"; @@ -9,7 +9,6 @@ import { VernWithSuggestions, } from "components/DataEntry/DataEntryTable/EntryCellComponents"; import PronunciationsBackend from "components/Pronunciations/PronunciationsBackend"; -import theme from "types/theme"; import { FileWithSpeakerId, newGloss } from "types/word"; import { firstGlossText } from "utilities/wordUtilities"; @@ -76,12 +75,13 @@ export function RecentEntry(props: RecentEntryProps): ReactElement { props.updateNote(props.rowIndex, noteText); return ( - - + + 1} @@ -93,12 +93,9 @@ export function RecentEntry(props: RecentEntryProps): ReactElement { vernacularLang={props.vernacularLang} textFieldId={`${idAffix}-${props.rowIndex}-vernacular`} /> - - + + + - - + + + - - + + + - - + + + - - + + ); } diff --git a/src/components/DataEntry/DataEntryTable/index.tsx b/src/components/DataEntry/DataEntryTable/index.tsx index 0a119b4c50..f1811b7fb7 100644 --- a/src/components/DataEntry/DataEntryTable/index.tsx +++ b/src/components/DataEntry/DataEntryTable/index.tsx @@ -1,9 +1,8 @@ import { ExitToApp, List as ListIcon } from "@mui/icons-material"; -import { Button, Grid, Typography } from "@mui/material"; +import { Button, Grid2, Typography } from "@mui/material"; import { enqueueSnackbar } from "notistack"; import { FormEvent, - Fragment, ReactElement, useCallback, useContext, @@ -36,7 +35,6 @@ import { uploadFileFromPronunciation } from "components/Pronunciations/utilities import { useAppSelector } from "rootRedux/hooks"; import { type StoreState } from "rootRedux/types"; import { type Hash } from "types/hash"; -import theme from "types/theme"; import { FileWithSpeakerId, newGloss, @@ -1017,38 +1015,23 @@ export default function DataEntryTable( return (
) => e?.preventDefault()}> - - - - + + + {t("addWords.vernacular")} - - - + + + + {t("addWords.glosses")} - + {state.recentWords.map((wordAccess, index) => ( - - + ))} - + - - + - - + {props.hasDrawerButton ? ( - ) : ( - +
)} - - + - - + + ); } diff --git a/src/components/DataEntry/ExistingDataTable/ImmutableExistingData.tsx b/src/components/DataEntry/ExistingDataTable/ImmutableExistingData.tsx index 259c8ff10e..db63b86496 100644 --- a/src/components/DataEntry/ExistingDataTable/ImmutableExistingData.tsx +++ b/src/components/DataEntry/ExistingDataTable/ImmutableExistingData.tsx @@ -1,11 +1,11 @@ -import { Grid } from "@mui/material"; -import { CSSProperties, ReactElement } from "react"; +import { Grid2, SxProps } from "@mui/material"; +import { ReactElement } from "react"; import { Gloss } from "api/models"; import { TypographyWithFont } from "utilities/fontComponents"; /** Style with a top dotted line if the index isn't 0. */ -function TopStyle(index: number, style?: "solid" | "dotted"): CSSProperties { +function BorderTopSx(index: number, style?: "solid" | "dotted"): SxProps { return index ? { borderTopStyle: style ?? "solid", borderTopWidth: 1 } : {}; } @@ -19,34 +19,28 @@ interface ImmutableExistingDataProps { export default function ImmutableExistingData( props: ImmutableExistingDataProps ): ReactElement { + const { glosses, index, vernacular } = props; + return ( - - + + - {props.vernacular} + {vernacular} - - - {props.glosses.map((g, i) => ( + + + {glosses.map((g, i) => ( {g.def} ))} - - + + ); } diff --git a/src/components/DataEntry/ExistingDataTable/index.tsx b/src/components/DataEntry/ExistingDataTable/index.tsx index b82930feb5..a24914aacb 100644 --- a/src/components/DataEntry/ExistingDataTable/index.tsx +++ b/src/components/DataEntry/ExistingDataTable/index.tsx @@ -1,4 +1,4 @@ -import { Drawer, Grid, List, SxProps } from "@mui/material"; +import { Drawer, Grid2, List, SxProps } from "@mui/material"; import { Fragment, ReactElement } from "react"; import { SemanticDomain } from "api/models"; @@ -34,7 +34,7 @@ export default function ExistingDataTable( const closeDrawer = (): void => props.toggleDrawer(false); const list = (): ReactElement => ( - + {props.domainWords.map((w, i) => ( {list()} @@ -66,9 +66,9 @@ export default function ExistingDataTable( }; const renderSidePanel = (): ReactElement => ( - + {list()} - + ); return props.typeDrawer ? renderDrawer() : renderSidePanel(); diff --git a/src/components/DataEntry/index.tsx b/src/components/DataEntry/index.tsx index e12c109a00..73697d4361 100644 --- a/src/components/DataEntry/index.tsx +++ b/src/components/DataEntry/index.tsx @@ -1,4 +1,4 @@ -import { Dialog, Divider, Grid, Paper } from "@mui/material"; +import { Dialog, Divider, Paper, Stack, useMediaQuery } from "@mui/material"; import { ReactElement, useCallback, @@ -19,19 +19,11 @@ import { closeTree, openTree } from "components/TreeView/Redux/TreeViewActions"; import { useAppDispatch, useAppSelector } from "rootRedux/hooks"; import { type StoreState } from "rootRedux/types"; import { newSemanticDomain } from "types/semanticDomain"; -import theme from "types/theme"; import { DomainWord } from "types/word"; import { useWindowSize } from "utilities/useWindowSize"; -export const smallScreenThreshold = 960; export const treeViewDialogId = "tree-view-dialog"; -const paperStyle = { - marginInline: "auto", - maxWidth: 800, - padding: theme.spacing(2), -}; - /** * Allows users to add words to a project, add senses to an existing word, * and add the current semantic domain to an existing sense. @@ -52,20 +44,18 @@ export default function DataEntry(): ReactElement { const { id, lang, name } = currentDomain; - /* This ref is for a container of both the and , - * in order to check its height and update the height of the . - * Attach to the because the parent won't shrink to its content, - * but will match the height of its neighbor in . */ + /* This ref is for the container of both the and , + * in order to check its height and update the height of the . */ const dataEntryRef = useRef(null); const [domain, setDomain] = useState(newSemanticDomain(id, name, lang)); const [domainWords, setDomainWords] = useState([]); const [drawerOpen, setDrawerOpen] = useState(false); const [height, setHeight] = useState(); - const [isSmallScreen, setIsSmallScreen] = useState(false); const [questionsVisible, setQuestionsVisible] = useState(false); - const { windowWidth } = useWindowSize(); + const isSmallScreen = useMediaQuery((th) => th.breakpoints.down("md")); + const { windowHeight } = useWindowSize(); const updateHeight = useCallback(() => { setHeight(dataEntryRef.current?.clientHeight); @@ -76,11 +66,6 @@ export default function DataEntry(): ReactElement { dispatch(openTree()); }, [dispatch]); - // When window width changes, check if there's space for the sidebar. - useLayoutEffect(() => { - setIsSmallScreen(windowWidth < smallScreenThreshold); - }, [windowWidth]); - // When domain changes, fetch full domain details. useEffect(() => { const customDomain = customDomains.find( @@ -100,7 +85,7 @@ export default function DataEntry(): ReactElement { // Recalculate height if something changed that might affect it. useEffect(() => { updateHeight(); - }, [domain, questionsVisible, updateHeight, windowWidth]); + }, [domain, questionsVisible, updateHeight, windowHeight]); const returnControlToCaller = useCallback(async () => { setDomainWords( @@ -112,15 +97,21 @@ export default function DataEntry(): ReactElement { return ( <> {!open && !!domain.guid && ( - - - + + {/* Domain data entry */} + + } spacing={2}> - + 0} hideQuestions={() => setQuestionsVisible(false)} @@ -130,8 +121,10 @@ export default function DataEntry(): ReactElement { showExistingData={() => setDrawerOpen(true)} updateHeight={updateHeight} /> - - + + + + {/* Existing domain entries */} - + )} + diff --git a/src/components/DataEntry/tests/index.test.tsx b/src/components/DataEntry/tests/index.test.tsx index b02035d176..48d0cb5a7e 100644 --- a/src/components/DataEntry/tests/index.test.tsx +++ b/src/components/DataEntry/tests/index.test.tsx @@ -1,13 +1,15 @@ +import { ThemeProvider } from "@mui/material"; import { act, render, screen } from "@testing-library/react"; import { Provider } from "react-redux"; import createMockStore from "redux-mock-store"; -import DataEntry, { smallScreenThreshold } from "components/DataEntry"; +import DataEntry from "components/DataEntry"; import { openTree } from "components/TreeView/Redux/TreeViewActions"; import { TreeViewState } from "components/TreeView/Redux/TreeViewReduxTypes"; import { defaultState } from "rootRedux/types"; import { newSemanticDomainTreeNode } from "types/semanticDomain"; -import * as useWindowSize from "utilities/useWindowSize"; +import theme from "types/theme"; +import { setMatchMedia } from "utilities/testingLibraryUtilities"; jest.mock("backend", () => ({ getSemanticDomainFull: (...args: any[]) => mockGetSemanticDomainFull(...args), @@ -36,11 +38,7 @@ const mockDomain = newSemanticDomainTreeNode("mockId", "mockName", "mockLang"); const mockGetSemanticDomainFull = jest.fn(); const mockStore = createMockStore(); -function spyOnUseWindowSize(windowWidth: number): jest.SpyInstance { - return jest - .spyOn(useWindowSize, "useWindowSize") - .mockReturnValue({ windowWidth, windowHeight: 1 }); -} +const mdWidth = 900; beforeEach(() => { jest.clearAllMocks(); @@ -72,28 +70,26 @@ describe("DataEntry", () => { }); it("renders on a small screen", async () => { - await renderDataEntry( - { currentDomain: mockDomain }, - smallScreenThreshold - 1 - ); + await renderDataEntry({ currentDomain: mockDomain }, mdWidth - 1); }); }); async function renderDataEntry( treeViewState: Partial, - windowWidth = smallScreenThreshold + 1 + width = mdWidth ): Promise { - spyOnUseWindowSize(windowWidth); + // Required (along with a `ThemeProvider`) for `useMediaQuery` to work + setMatchMedia(width); + + treeViewState = { ...defaultState.treeViewState, ...treeViewState }; + await act(async () => { render( - - - + + + + + ); }); }