diff --git a/src/components/Buttons/NoteButton.tsx b/src/components/Buttons/NoteButton.tsx index 1b8bc4948c..74cffa7e9c 100644 --- a/src/components/Buttons/NoteButton.tsx +++ b/src/components/Buttons/NoteButton.tsx @@ -11,6 +11,7 @@ interface NoteButtonProps { /** If `noteText` is empty and `updateNote` defined, * the button will have default add-note hover text. */ noteText: string; + onExited?: () => void; updateNote?: (newText: string) => void | Promise; } @@ -18,6 +19,18 @@ interface NoteButtonProps { export default function NoteButton(props: NoteButtonProps): ReactElement { const [noteOpen, setNoteOpen] = useState(false); + const handleOpen = (): void => { + setNoteOpen(true); + + if (props.onExited) { + // Allow custom focus handling after dialog closes + if (document.activeElement instanceof HTMLElement) { + // Blur the button to prevent it from receiving focus when dialog closes + document.activeElement.blur(); + } + } + }; + return ( <> ) } - onClick={props.updateNote ? () => setNoteOpen(true) : undefined} + onClick={props.updateNote ? handleOpen : undefined} side="top" size="small" text={props.noteText || undefined} @@ -46,6 +59,7 @@ export default function NoteButton(props: NoteButtonProps): ReactElement { text={props.noteText} titleId={"addWords.addNote"} close={() => setNoteOpen(false)} + onExited={props.onExited} updateText={props.updateNote ?? (() => {})} buttonIdCancel="note-edit-cancel" buttonIdClear="note-edit-clear" diff --git a/src/components/DataEntry/DataEntryTable/NewEntry/index.tsx b/src/components/DataEntry/DataEntryTable/NewEntry/index.tsx index bb79f5dc5a..fd650353a9 100644 --- a/src/components/DataEntry/DataEntryTable/NewEntry/index.tsx +++ b/src/components/DataEntry/DataEntryTable/NewEntry/index.tsx @@ -277,6 +277,7 @@ export default function NewEntry(props: NewEntryProps): ReactElement { focus(FocusTarget.Gloss)} updateNote={setNewNote} /> )} diff --git a/src/components/DataEntry/DataEntryTable/NewEntry/tests/index.test.tsx b/src/components/DataEntry/DataEntryTable/NewEntry/tests/index.test.tsx index 5810af6106..12953c14db 100644 --- a/src/components/DataEntry/DataEntryTable/NewEntry/tests/index.test.tsx +++ b/src/components/DataEntry/DataEntryTable/NewEntry/tests/index.test.tsx @@ -1,4 +1,11 @@ -import { act, fireEvent, render, screen } from "@testing-library/react"; +import "@testing-library/jest-dom"; +import { + act, + fireEvent, + render, + screen, + waitFor, +} from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { createRef } from "react"; import { Provider } from "react-redux"; @@ -11,13 +18,14 @@ import { newWritingSystem } from "types/writingSystem"; jest.mock("components/DataEntry/utilities.ts", () => ({ ...jest.requireActual("components/DataEntry/utilities.ts"), - focusInput: jest.fn(), + focusInput: () => mockFocusInput(), })); jest.mock("components/Pronunciations/PronunciationsFrontend", () => jest.fn()); const mockAddNewAudio = jest.fn(); const mockAddNewEntry = jest.fn(); const mockDelNewAudio = jest.fn(); +const mockFocusInput = jest.fn(); const mockSetNewGloss = jest.fn(); const mockSetNewNote = jest.fn(); const mockSetNewVern = jest.fn(); @@ -79,7 +87,7 @@ const fireEnterOnActiveElement = async (): Promise => { }; beforeEach(() => { - jest.resetAllMocks(); + jest.clearAllMocks(); }); afterEach(() => { @@ -173,4 +181,31 @@ describe("NewEntry", () => { expect(mockAddNewEntry).toHaveBeenCalledTimes(1); expect(mockResetNewEntry).toHaveBeenCalledTimes(1); }); + + it("returns focus to gloss after closing note dialog", async () => { + await renderNewEntry(); + mockFocusInput.mockClear(); + + // Click the note button to open the dialog + await userEvent.click(screen.getByTestId(NewEntryId.ButtonNote)); + expect(mockFocusInput).not.toHaveBeenCalled(); + + // Cancel and verify that focusInput was called after transition completes + await userEvent.click(screen.getByText(new RegExp("cancel"))); + await waitFor(() => expect(mockFocusInput).toHaveBeenCalled()); + }); + + it("returns focus to gloss after confirming note", async () => { + await renderNewEntry(); + mockFocusInput.mockClear(); + + // Click the note button to open the dialog and type a note + await userEvent.click(screen.getByTestId(NewEntryId.ButtonNote)); + await userEvent.type(document.activeElement!, "note text"); + expect(mockFocusInput).not.toHaveBeenCalled(); + + // Confirm and verify that focusInput was called after transition completes + await userEvent.click(screen.getByText(new RegExp("confirm"))); + await waitFor(() => expect(mockFocusInput).toHaveBeenCalled()); + }); }); diff --git a/src/components/Dialogs/EditTextDialog.tsx b/src/components/Dialogs/EditTextDialog.tsx index 5ca977ed1b..4c59bd1703 100644 --- a/src/components/Dialogs/EditTextDialog.tsx +++ b/src/components/Dialogs/EditTextDialog.tsx @@ -24,6 +24,7 @@ interface EditTextDialogProps { text: string; titleId: string; close: () => void; + onExited?: () => void; updateText: (newText: string) => void | Promise; buttonIdCancel?: string; buttonIdClear?: string; @@ -90,6 +91,7 @@ export default function EditTextDialog(