Skip to content

Commit a54e777

Browse files
authored
[NewEntry] Avoid focus while submission is underway (#4016)
Also, visually clear new entry while submitting
1 parent 74c0121 commit a54e777

File tree

3 files changed

+37
-33
lines changed

3 files changed

+37
-33
lines changed

src/backend/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,6 @@ export async function uploadAudio(
152152
wordId: string,
153153
file: FileWithSpeakerId
154154
): Promise<string> {
155-
console.info(file);
156155
const projectId = LocalStorage.getProjectId();
157156
const speakerId = file.speakerId ?? "";
158157
const params = { projectId, wordId, file };

src/components/DataEntry/DataEntryTable/NewEntry/index.tsx

Lines changed: 29 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -121,8 +121,9 @@ export default function NewEntry(props: NewEntryProps): ReactElement {
121121
resetNewEntry();
122122
setSubmitting(false);
123123
setVernOpen(false);
124-
focus(FocusTarget.Vernacular);
125-
}, [focus, resetNewEntry]);
124+
// Do not use focus(FocusTarget.Vernacular) here.
125+
// That allows typing atop the previous vernacular.
126+
}, [resetNewEntry]);
126127

127128
/** Reset when tree opens, except for the first time it is open. */
128129
useEffect(() => {
@@ -136,14 +137,15 @@ export default function NewEntry(props: NewEntryProps): ReactElement {
136137
}
137138
}, [isTreeOpen, resetState, wasTreeClosed]);
138139

139-
/** When the vern dialog is closed, focus needs to return to text fields.
140-
* The following sets a flag (shouldFocus) to be triggered by conditionalFocus(),
140+
/** When the vern dialog is closed or submission completed,
141+
* focus needs to return to text fields.
142+
* This sets a flag (shouldFocus) to be triggered by conditionalFocus(),
141143
* which is passed to each input component to call on update. */
142144
useEffect(() => {
143-
if (!vernOpen) {
145+
if (!submitting && !vernOpen) {
144146
setShouldFocus(selectedDup ? FocusTarget.Gloss : FocusTarget.Vernacular);
145147
}
146-
}, [selectedDup, vernOpen]);
148+
}, [selectedDup, submitting, vernOpen]);
147149

148150
/** This function is for a child input component to call on update
149151
* to move focus to itself, if shouldFocus says it should. */
@@ -159,42 +161,40 @@ export default function NewEntry(props: NewEntryProps): ReactElement {
159161
setVernOpen(!!openVernDialog);
160162
};
161163

162-
const addNewEntryAndReset = async (): Promise<void> => {
163-
// Prevent double-submission
164-
if (submitting) {
165-
return;
166-
}
167-
setSubmitting(true);
168-
await addNewEntry();
169-
resetState();
170-
};
171-
172164
const addOrUpdateWord = async (): Promise<void> => {
173165
if (suggestedDups.length) {
174166
// Case 1: Duplicate vern is typed
175167
if (!selectedDup) {
176-
// Case 1a: User hasn't made a selection
168+
// Case 1a: User hasn't made a selection (should never happen,
169+
// since submission is only triggered from the gloss field,
170+
// but left here in case that changes)
177171
setVernOpen(true);
172+
setSubmitting(false);
173+
return;
178174
} else if (selectedDup.id) {
179175
// Case 1b: User has selected an entry to modify
180176
await updateWordWithNewGloss();
181-
resetState();
182177
} else {
183178
// Case 1c: User has selected new entry
184-
await addNewEntryAndReset();
179+
await addNewEntry();
185180
}
186181
} else {
187182
// Case 2: New vern is typed
188-
await addNewEntryAndReset();
183+
await addNewEntry();
189184
}
185+
resetState();
190186
};
191187

192188
const handleGlossEnter = async (): Promise<void> => {
193189
// The user can never submit a new entry without a vernacular
194190
if (newVern) {
191+
// Blur to prevent double-submission or extending submitted gloss.
192+
glossInput.current?.blur();
193+
setSubmitting(true);
195194
await addOrUpdateWord();
195+
} else {
196+
focus(FocusTarget.Vernacular);
196197
}
197-
focus(FocusTarget.Vernacular);
198198
};
199199

200200
/** Clear the duplicate selection if user returns to the vernacular field. */
@@ -226,7 +226,7 @@ export default function NewEntry(props: NewEntryProps): ReactElement {
226226
<Grid2 size={4}>
227227
<VernWithSuggestions
228228
isNew
229-
vernacular={newVern}
229+
vernacular={submitting ? "" : newVern}
230230
vernInput={vernInput}
231231
updateVernField={(newValue: string, openDialog?: boolean) =>
232232
updateVernField(newValue, openDialog)
@@ -261,7 +261,7 @@ export default function NewEntry(props: NewEntryProps): ReactElement {
261261
<Grid2 size={4}>
262262
<GlossWithSuggestions
263263
isNew
264-
gloss={newGloss}
264+
gloss={submitting ? "" : newGloss}
265265
glossInput={glossInput}
266266
updateGlossField={setNewGloss}
267267
handleEnter={() => handleGlossEnter()}
@@ -276,15 +276,15 @@ export default function NewEntry(props: NewEntryProps): ReactElement {
276276
// note is not available if user selected to modify an existing entry
277277
<NoteButton
278278
buttonId={NewEntryId.ButtonNote}
279-
noteText={newNote}
279+
noteText={submitting ? "" : newNote}
280280
updateNote={setNewNote}
281281
/>
282282
)}
283283
</Grid2>
284284

285285
<Grid2 size={2}>
286286
<PronunciationsFrontend
287-
audio={newAudio}
287+
audio={submitting ? [] : newAudio}
288288
deleteAudio={delNewAudio}
289289
replaceAudio={repNewAudio}
290290
uploadAudio={addNewAudio}
@@ -295,7 +295,10 @@ export default function NewEntry(props: NewEntryProps): ReactElement {
295295
<Grid2 size={1}>
296296
<DeleteEntry
297297
buttonId={NewEntryId.ButtonDelete}
298-
removeEntry={() => resetState()}
298+
removeEntry={() => {
299+
resetState();
300+
focus(FocusTarget.Vernacular);
301+
}}
299302
/>
300303
</Grid2>
301304

src/components/DataEntry/DataEntryTable/NewEntry/tests/index.test.tsx

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,10 @@ const renderNewEntry = async (
6666
});
6767
};
6868

69-
/** Fire all Enter key events on the given element.
69+
/** Fire all Enter key events on document's active element, or body if none.
7070
* (For use with fake timers, since they don't play well with `userEvent`.) */
71-
const fireEnterOnElement = async (elem: Element): Promise<void> => {
71+
const fireEnterOnActiveElement = async (): Promise<void> => {
72+
const elem = document.activeElement ?? document.body;
7273
const enterOptions = { charCode: 13, code: "Enter", key: "Enter" };
7374
await act(async () => {
7475
fireEvent.keyDown(elem, enterOptions);
@@ -132,7 +133,8 @@ describe("NewEntry", () => {
132133
);
133134

134135
// Submit a new entry
135-
await fireEnterOnElement(getVernAndGlossFields().glossField);
136+
fireEvent.click(getVernAndGlossFields().glossField);
137+
await fireEnterOnActiveElement();
136138
expect(mockAddNewEntry).toHaveBeenCalledTimes(1);
137139
expect(mockResetNewEntry).not.toHaveBeenCalled();
138140

@@ -154,13 +156,13 @@ describe("NewEntry", () => {
154156
);
155157

156158
// Submit a new entry
157-
const { glossField } = getVernAndGlossFields();
158-
await fireEnterOnElement(glossField);
159+
fireEvent.click(getVernAndGlossFields().glossField);
160+
await fireEnterOnActiveElement();
159161
expect(mockAddNewEntry).toHaveBeenCalledTimes(1);
160162
expect(mockResetNewEntry).not.toHaveBeenCalled();
161163

162164
// Attempt a second submission before the first one completes
163-
await fireEnterOnElement(glossField);
165+
await fireEnterOnActiveElement();
164166
expect(mockAddNewEntry).toHaveBeenCalledTimes(1);
165167
expect(mockResetNewEntry).not.toHaveBeenCalled();
166168

0 commit comments

Comments
 (0)