diff --git a/src/components/Buttons/IconButtonWithTooltip.tsx b/src/components/Buttons/IconButtonWithTooltip.tsx index 51527916db..fd854f1e77 100644 --- a/src/components/Buttons/IconButtonWithTooltip.tsx +++ b/src/components/Buttons/IconButtonWithTooltip.tsx @@ -3,6 +3,7 @@ import { MouseEventHandler, ReactElement, ReactNode } from "react"; import { useTranslation } from "react-i18next"; interface IconButtonWithTooltipProps { + disabled?: boolean; icon: ReactElement; text?: ReactNode; textId?: string; @@ -27,7 +28,7 @@ export default function IconButtonWithTooltip( onClick={props.onClick} size={props.size || "medium"} id={props.buttonId} - disabled={!props.onClick} + disabled={props.disabled || !props.onClick} > {props.icon} diff --git a/src/components/DataEntry/DataEntryTable/EntryCellComponents/DeleteEntry.tsx b/src/components/DataEntry/DataEntryTable/EntryCellComponents/DeleteEntry.tsx index 5e6377311c..7ab950514c 100644 --- a/src/components/DataEntry/DataEntryTable/EntryCellComponents/DeleteEntry.tsx +++ b/src/components/DataEntry/DataEntryTable/EntryCellComponents/DeleteEntry.tsx @@ -12,6 +12,7 @@ interface DeleteEntryProps { // if no confirmId is specified, then there is no popup // and deletion will happen when the button is pressed confirmId?: string; + disabled?: boolean; wordId?: string; } @@ -34,6 +35,7 @@ export default function DeleteEntry(props: DeleteEntryProps): ReactElement { <> void | Promise; } @@ -18,11 +19,16 @@ export default function EntryNote(props: EntryNoteProps): ReactElement { <> t.palette.grey[700] }} /> + t.palette.grey[props.disabled ? 400 : 700] }} + /> ) : ( - t.palette.grey[700] }} /> + t.palette.grey[props.disabled ? 400 : 700] }} + /> ) } onClick={props.updateNote ? () => setNoteOpen(true) : undefined} diff --git a/src/components/DataEntry/DataEntryTable/RecentEntry.tsx b/src/components/DataEntry/DataEntryTable/RecentEntry.tsx index 95264f1f85..84419bc9d1 100644 --- a/src/components/DataEntry/DataEntryTable/RecentEntry.tsx +++ b/src/components/DataEntry/DataEntryTable/RecentEntry.tsx @@ -40,9 +40,19 @@ export function RecentEntry(props: RecentEntryProps): ReactElement { if (sense.glosses.length < 1) { sense.glosses.push(newGloss("", props.analysisLang.bcp47)); } + const [editing, setEditing] = useState(false); const [gloss, setGloss] = useState(firstGlossText(sense)); const [vernacular, setVernacular] = useState(props.entry.vernacular); + const updateGlossField = (gloss: string): void => { + setEditing(gloss !== firstGlossText(sense)); + setGloss(gloss); + }; + const updateVernField = (vern: string): void => { + setEditing(vern !== props.entry.vernacular); + setVernacular(vern); + }; + function conditionallyUpdateGloss(): void { if (firstGlossText(sense) !== gloss) { props.updateGloss(props.rowIndex, gloss); @@ -77,7 +87,7 @@ export function RecentEntry(props: RecentEntryProps): ReactElement { 1} - updateVernField={setVernacular} + updateVernField={updateVernField} onBlur={() => conditionallyUpdateVern()} handleEnter={() => { vernacular && props.focusNewEntry(); @@ -98,7 +108,7 @@ export function RecentEntry(props: RecentEntryProps): ReactElement { conditionallyUpdateGloss()} handleEnter={() => { gloss && props.focusNewEntry(); @@ -116,13 +126,12 @@ export function RecentEntry(props: RecentEntryProps): ReactElement { position: "relative", }} > - {!props.disabled && ( - - )} + - {!props.disabled && ( - { - props.delAudioFromWord(props.entry.id, fileName); - }} - replaceAudio={(audio) => - props.repAudioInWord(props.entry.id, audio) - } - uploadAudio={(file) => { - props.addAudioToWord(props.entry.id, file); - }} - /> - )} + { + props.delAudioFromWord(props.entry.id, fileName); + }} + replaceAudio={(audio) => props.repAudioInWord(props.entry.id, audio)} + uploadAudio={(file) => { + props.addAudioToWord(props.entry.id, file); + }} + /> - {!props.disabled && ( - - )} + ); diff --git a/src/components/DataEntry/DataEntryTable/tests/RecentEntry.test.tsx b/src/components/DataEntry/DataEntryTable/tests/RecentEntry.test.tsx index 1101939a44..3735826258 100644 --- a/src/components/DataEntry/DataEntryTable/tests/RecentEntry.test.tsx +++ b/src/components/DataEntry/DataEntryTable/tests/RecentEntry.test.tsx @@ -13,6 +13,7 @@ import "tests/reactI18nextMock"; import { Word } from "api/models"; import { defaultState } from "components/App/DefaultState"; import { + DeleteEntry, EntryNote, GlossWithSuggestions, VernWithSuggestions, @@ -21,6 +22,7 @@ import RecentEntry from "components/DataEntry/DataEntryTable/RecentEntry"; import { EditTextDialog } from "components/Dialogs"; import AudioPlayer from "components/Pronunciations/AudioPlayer"; import AudioRecorder from "components/Pronunciations/AudioRecorder"; +import PronunciationsBackend from "components/Pronunciations/PronunciationsBackend"; import theme from "types/theme"; import { newPronunciation, simpleWord } from "types/word"; import { newWritingSystem } from "types/writingSystem"; @@ -89,6 +91,34 @@ describe("ExistingEntry", () => { }); describe("vernacular", () => { + it("disables buttons if changing", async () => { + await renderWithWord(mockWord); + const vern = testHandle.findByType(VernWithSuggestions); + const note = testHandle.findByType(EntryNote); + const audio = testHandle.findByType(PronunciationsBackend); + const del = testHandle.findByType(DeleteEntry); + + expect(note.props.disabled).toBeFalsy(); + expect(audio.props.disabled).toBeFalsy(); + expect(del.props.disabled).toBeFalsy(); + + async function updateVern(text: string): Promise { + await act(async () => { + await vern.props.updateVernField(text); + }); + } + + await updateVern(mockText); + expect(note.props.disabled).toBeTruthy(); + expect(audio.props.disabled).toBeTruthy(); + expect(del.props.disabled).toBeTruthy(); + + await updateVern(mockVern); + expect(note.props.disabled).toBeFalsy(); + expect(audio.props.disabled).toBeFalsy(); + expect(del.props.disabled).toBeFalsy(); + }); + it("updates if changed", async () => { await renderWithWord(mockWord); testHandle = testHandle.findByType(VernWithSuggestions); @@ -102,11 +132,39 @@ describe("ExistingEntry", () => { await updateVernAndBlur(mockVern); expect(mockUpdateVern).toHaveBeenCalledTimes(0); await updateVernAndBlur(mockText); - expect(mockUpdateVern).toBeCalledWith(0, mockText); + expect(mockUpdateVern).toHaveBeenCalledWith(0, mockText); }); }); describe("gloss", () => { + it("disables buttons if changing", async () => { + await renderWithWord(mockWord); + const gloss = testHandle.findByType(GlossWithSuggestions); + const note = testHandle.findByType(EntryNote); + const audio = testHandle.findByType(PronunciationsBackend); + const del = testHandle.findByType(DeleteEntry); + + expect(note.props.disabled).toBeFalsy(); + expect(audio.props.disabled).toBeFalsy(); + expect(del.props.disabled).toBeFalsy(); + + async function updateGloss(text: string): Promise { + await act(async () => { + await gloss.props.updateGlossField(text); + }); + } + + await updateGloss(mockText); + expect(note.props.disabled).toBeTruthy(); + expect(audio.props.disabled).toBeTruthy(); + expect(del.props.disabled).toBeTruthy(); + + await updateGloss(mockGloss); + expect(note.props.disabled).toBeFalsy(); + expect(audio.props.disabled).toBeFalsy(); + expect(del.props.disabled).toBeFalsy(); + }); + it("updates if changed", async () => { await renderWithWord(mockWord); testHandle = testHandle.findByType(GlossWithSuggestions); @@ -120,7 +178,7 @@ describe("ExistingEntry", () => { await updateGlossAndBlur(mockGloss); expect(mockUpdateGloss).toHaveBeenCalledTimes(0); await updateGlossAndBlur(mockText); - expect(mockUpdateGloss).toBeCalledWith(0, mockText); + expect(mockUpdateGloss).toHaveBeenCalledWith(0, mockText); }); }); @@ -131,7 +189,7 @@ describe("ExistingEntry", () => { await act(async () => { testHandle.props.updateText(mockText); }); - expect(mockUpdateNote).toBeCalledWith(0, mockText); + expect(mockUpdateNote).toHaveBeenCalledWith(0, mockText); }); }); }); diff --git a/src/components/Pronunciations/AudioPlayer.tsx b/src/components/Pronunciations/AudioPlayer.tsx index 8222b8410f..74a2ff3a9f 100644 --- a/src/components/Pronunciations/AudioPlayer.tsx +++ b/src/components/Pronunciations/AudioPlayer.tsx @@ -10,7 +10,6 @@ import { Tooltip, } from "@mui/material"; import { - CSSProperties, MouseEvent, ReactElement, TouchEvent, @@ -32,11 +31,11 @@ import { import { PronunciationsStatus } from "components/Pronunciations/Redux/PronunciationsReduxTypes"; import { StoreState } from "types"; import { useAppDispatch, useAppSelector } from "types/hooks"; -import { themeColors } from "types/theme"; interface PlayerProps { audio: Pronunciation; deleteAudio?: (fileName: string) => void; + disabled?: boolean; onClick?: () => void; pronunciationUrl?: string; size?: "large" | "medium" | "small"; @@ -44,8 +43,6 @@ interface PlayerProps { warningTextId?: string; } -const iconStyle: CSSProperties = { color: themeColors.success }; - export default function AudioPlayer(props: PlayerProps): ReactElement { const isPlaying = useAppSelector( (state: StoreState) => @@ -178,6 +175,22 @@ export default function AudioPlayer(props: PlayerProps): ReactElement { ); } + const icon = isPlaying ? ( + + props.disabled ? t.palette.grey[400] : t.palette.success.main, + }} + /> + ) : ( + + props.disabled ? t.palette.grey[400] : t.palette.success.main, + }} + /> + ); + return ( <> - {isPlaying ? : } + {icon} - {isPlaying ? : } + {icon} {canChangeSpeaker && ( void; @@ -50,6 +51,7 @@ export default function AudioRecorder(props: RecorderProps): ReactElement { return ( {!props.playerOnly && !!props.uploadAudio && ( - + )} {audioButtons} @@ -60,6 +66,7 @@ function propsAreEqual( return false; } return ( + prev.disabled === next.disabled && prev.wordId === next.wordId && JSON.stringify(prev.audio) === JSON.stringify(next.audio) ); diff --git a/src/components/Pronunciations/RecorderIcon.tsx b/src/components/Pronunciations/RecorderIcon.tsx index 263c6dab7f..58526d3edb 100644 --- a/src/components/Pronunciations/RecorderIcon.tsx +++ b/src/components/Pronunciations/RecorderIcon.tsx @@ -16,6 +16,7 @@ export const recordButtonId = "recordingButton"; export const recordIconId = "recordingIcon"; interface RecorderIconProps { + disabled?: boolean; id: string; startRecording: () => void; stopRecording: () => void; @@ -61,6 +62,7 @@ export default function RecorderIcon(props: RecorderIconProps): ReactElement { + props.disabled + ? t.palette.grey[400] + : isRecording + ? themeColors.recordActive + : themeColors.recordIdle, }} /> diff --git a/src/components/Pronunciations/tests/AudioRecorder.test.tsx b/src/components/Pronunciations/tests/AudioRecorder.test.tsx index 546bca627f..82128d60a0 100644 --- a/src/components/Pronunciations/tests/AudioRecorder.test.tsx +++ b/src/components/Pronunciations/tests/AudioRecorder.test.tsx @@ -85,7 +85,7 @@ describe("Pronunciations", () => { ); }); const icon = testRenderer.root.findByProps({ id: recordIconId }); - expect(icon.props.sx.color).toEqual(themeColors.recordIdle); + expect(icon.props.sx.color({})).toEqual(themeColors.recordIdle); }); test("style depends on pronunciations state", () => { @@ -103,6 +103,6 @@ describe("Pronunciations", () => { ); }); const icon = testRenderer.root.findByProps({ id: recordIconId }); - expect(icon.props.sx.color).toEqual(themeColors.recordActive); + expect(icon.props.sx.color({})).toEqual(themeColors.recordActive); }); });