Skip to content

Commit e658c44

Browse files
[NewEntry] Return focus to gloss after note interaction (#4037)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Danny Rorabaugh <imnasnainaec@gmail.com>
1 parent b5a0303 commit e658c44

File tree

4 files changed

+56
-4
lines changed

4 files changed

+56
-4
lines changed

src/components/Buttons/NoteButton.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,26 @@ interface NoteButtonProps {
1111
/** If `noteText` is empty and `updateNote` defined,
1212
* the button will have default add-note hover text. */
1313
noteText: string;
14+
onExited?: () => void;
1415
updateNote?: (newText: string) => void | Promise<void>;
1516
}
1617

1718
/** A note adding/editing/viewing button */
1819
export default function NoteButton(props: NoteButtonProps): ReactElement {
1920
const [noteOpen, setNoteOpen] = useState<boolean>(false);
2021

22+
const handleOpen = (): void => {
23+
setNoteOpen(true);
24+
25+
if (props.onExited) {
26+
// Allow custom focus handling after dialog closes
27+
if (document.activeElement instanceof HTMLElement) {
28+
// Blur the button to prevent it from receiving focus when dialog closes
29+
document.activeElement.blur();
30+
}
31+
}
32+
};
33+
2134
return (
2235
<>
2336
<IconButtonWithTooltip
@@ -35,7 +48,7 @@ export default function NoteButton(props: NoteButtonProps): ReactElement {
3548
/>
3649
)
3750
}
38-
onClick={props.updateNote ? () => setNoteOpen(true) : undefined}
51+
onClick={props.updateNote ? handleOpen : undefined}
3952
side="top"
4053
size="small"
4154
text={props.noteText || undefined}
@@ -46,6 +59,7 @@ export default function NoteButton(props: NoteButtonProps): ReactElement {
4659
text={props.noteText}
4760
titleId={"addWords.addNote"}
4861
close={() => setNoteOpen(false)}
62+
onExited={props.onExited}
4963
updateText={props.updateNote ?? (() => {})}
5064
buttonIdCancel="note-edit-cancel"
5165
buttonIdClear="note-edit-clear"

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,7 @@ export default function NewEntry(props: NewEntryProps): ReactElement {
277277
<NoteButton
278278
buttonId={NewEntryId.ButtonNote}
279279
noteText={submitting ? "" : newNote}
280+
onExited={() => focus(FocusTarget.Gloss)}
280281
updateNote={setNewNote}
281282
/>
282283
)}

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

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
import { act, fireEvent, render, screen } from "@testing-library/react";
1+
import "@testing-library/jest-dom";
2+
import {
3+
act,
4+
fireEvent,
5+
render,
6+
screen,
7+
waitFor,
8+
} from "@testing-library/react";
29
import userEvent from "@testing-library/user-event";
310
import { createRef } from "react";
411
import { Provider } from "react-redux";
@@ -11,13 +18,14 @@ import { newWritingSystem } from "types/writingSystem";
1118

1219
jest.mock("components/DataEntry/utilities.ts", () => ({
1320
...jest.requireActual("components/DataEntry/utilities.ts"),
14-
focusInput: jest.fn(),
21+
focusInput: () => mockFocusInput(),
1522
}));
1623
jest.mock("components/Pronunciations/PronunciationsFrontend", () => jest.fn());
1724

1825
const mockAddNewAudio = jest.fn();
1926
const mockAddNewEntry = jest.fn();
2027
const mockDelNewAudio = jest.fn();
28+
const mockFocusInput = jest.fn();
2129
const mockSetNewGloss = jest.fn();
2230
const mockSetNewNote = jest.fn();
2331
const mockSetNewVern = jest.fn();
@@ -79,7 +87,7 @@ const fireEnterOnActiveElement = async (): Promise<void> => {
7987
};
8088

8189
beforeEach(() => {
82-
jest.resetAllMocks();
90+
jest.clearAllMocks();
8391
});
8492

8593
afterEach(() => {
@@ -173,4 +181,31 @@ describe("NewEntry", () => {
173181
expect(mockAddNewEntry).toHaveBeenCalledTimes(1);
174182
expect(mockResetNewEntry).toHaveBeenCalledTimes(1);
175183
});
184+
185+
it("returns focus to gloss after closing note dialog", async () => {
186+
await renderNewEntry();
187+
mockFocusInput.mockClear();
188+
189+
// Click the note button to open the dialog
190+
await userEvent.click(screen.getByTestId(NewEntryId.ButtonNote));
191+
expect(mockFocusInput).not.toHaveBeenCalled();
192+
193+
// Cancel and verify that focusInput was called after transition completes
194+
await userEvent.click(screen.getByText(new RegExp("cancel")));
195+
await waitFor(() => expect(mockFocusInput).toHaveBeenCalled());
196+
});
197+
198+
it("returns focus to gloss after confirming note", async () => {
199+
await renderNewEntry();
200+
mockFocusInput.mockClear();
201+
202+
// Click the note button to open the dialog and type a note
203+
await userEvent.click(screen.getByTestId(NewEntryId.ButtonNote));
204+
await userEvent.type(document.activeElement!, "note text");
205+
expect(mockFocusInput).not.toHaveBeenCalled();
206+
207+
// Confirm and verify that focusInput was called after transition completes
208+
await userEvent.click(screen.getByText(new RegExp("confirm")));
209+
await waitFor(() => expect(mockFocusInput).toHaveBeenCalled());
210+
});
176211
});

src/components/Dialogs/EditTextDialog.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ interface EditTextDialogProps {
2424
text: string;
2525
titleId: string;
2626
close: () => void;
27+
onExited?: () => void;
2728
updateText: (newText: string) => void | Promise<void>;
2829
buttonIdCancel?: string;
2930
buttonIdClear?: string;
@@ -90,6 +91,7 @@ export default function EditTextDialog(
9091
<Dialog
9192
open={props.open}
9293
onClose={escapeClose}
94+
slotProps={{ transition: { onExited: props.onExited } }}
9395
aria-labelledby="alert-dialog-title"
9496
aria-describedby="alert-dialog-description"
9597
>

0 commit comments

Comments
 (0)