diff --git a/src/fragmentarium/ui/edition/TransliterationForm.test.tsx b/src/fragmentarium/ui/edition/TransliterationForm.test.tsx index 82a1f5f13..b781a2edd 100644 --- a/src/fragmentarium/ui/edition/TransliterationForm.test.tsx +++ b/src/fragmentarium/ui/edition/TransliterationForm.test.tsx @@ -1,31 +1,52 @@ import React from 'react' -import { render, screen } from '@testing-library/react' -import { changeValueByLabel, submitFormByTestId } from 'test-support/utils' +import { fireEvent, render, screen } from '@testing-library/react' +import { submitFormByTestId } from 'test-support/utils' import { Promise } from 'bluebird' import TransliterationForm from './TransliterationForm' +import { act } from 'react-dom/test-utils' +import userEvent from '@testing-library/user-event' const transliteration = 'line1\nline2' const notes = 'notes' const introduction = 'introduction' +let addEventListenerSpy let updateEdition -beforeEach(() => { +beforeEach(async () => { + jest.restoreAllMocks() + addEventListenerSpy = jest.spyOn(window, 'addEventListener') updateEdition = jest.fn() updateEdition.mockReturnValue(Promise.resolve()) - render( - - ) + act(() => { + render( + + ) + }) }) -test('Submitting the form calls updateEdition', () => { +it('Updates transliteration on change', async () => { + const newTransliteration = 'line1\nline2\nnew line' + const transliterationEditor = screen.getAllByRole('textbox')[0] + fireEvent.click(transliterationEditor) + await userEvent.click(transliterationEditor) + await userEvent.paste(transliterationEditor, newTransliteration) + act(() => { + fireEvent.change(transliterationEditor, { + target: { value: newTransliteration }, + }) + }) + expect(transliterationEditor).toHaveValue(newTransliteration) +}) + +it('Submitting the form calls updateEdition', () => { submitFormByTestId(screen, 'transliteration-form') expect(updateEdition).toHaveBeenCalledWith( transliteration, @@ -34,18 +55,31 @@ test('Submitting the form calls updateEdition', () => { ) }) -xit('Updates transliteration on change', () => { +it('Displays warning before closing when unsaved', async () => { const newTransliteration = 'line1\nline2\nnew line' - changeValueByLabel(screen, 'Transliteration', newTransliteration) - - expect(screen.getByLabelText('Transliteration')).toHaveValue( - newTransliteration + window.confirm = jest.fn(() => true) + const beforeUnloadEvent = new Event('beforeunload', { cancelable: true }) + const transliterationEditor = screen.getAllByRole('textbox')[0] + fireEvent.click(transliterationEditor) + await userEvent.click(transliterationEditor) + await userEvent.paste(transliterationEditor, newTransliteration) + act(() => { + fireEvent.change(transliterationEditor, { + target: { value: newTransliteration }, + }) + }) + expect(transliterationEditor).toHaveValue(newTransliteration) + window.dispatchEvent(beforeUnloadEvent) + expect(addEventListenerSpy).toHaveBeenCalledWith( + 'beforeunload', + expect.any(Function) + ) + const mockEvent = { returnValue: '' } + const beforeUnloadHandler = addEventListenerSpy.mock.calls.find( + (call) => call[0] === 'beforeunload' + )[1] + beforeUnloadHandler(mockEvent) + expect(mockEvent.returnValue).toBe( + 'You have unsaved changes. Are you sure you want to leave?' ) -}) - -xit('Updates introduction on change', () => { - const newIntroduction = 'Introduction\n\nintroduction continued' - changeValueByLabel(screen, 'Introduction', newIntroduction) - - expect(screen.getByLabelText('Introduction')).toHaveValue(newIntroduction) }) diff --git a/src/fragmentarium/ui/edition/TransliterationForm.tsx b/src/fragmentarium/ui/edition/TransliterationForm.tsx index cc80c68c1..441c480d4 100644 --- a/src/fragmentarium/ui/edition/TransliterationForm.tsx +++ b/src/fragmentarium/ui/edition/TransliterationForm.tsx @@ -1,4 +1,4 @@ -import React, { Component, FormEvent } from 'react' +import React, { useState, useEffect, FormEvent, useCallback } from 'react' import { FormGroup, FormLabel, @@ -25,159 +25,221 @@ type Props = { notes: string, introduction: string ) => Promise - disabled: boolean + disabled?: boolean } -type State = { + +type FormData = { transliteration: string notes: string introduction: string error: Error | null - disabled: boolean + disabled?: boolean } -class TransliterationForm extends Component { - static readonly defaultProps = { - disabled: false, - } - private readonly formId: string - private updatePromise: Promise - - constructor(props: Props) { - super(props) - this.formId = _.uniqueId('TransliterationForm-') - this.state = { - transliteration: this.props.transliteration, - notes: this.props.notes, - introduction: this.props.introduction, - error: null, - disabled: false, - } - this.updatePromise = Promise.resolve() +const handleBeforeUnload = ( + event: BeforeUnloadEvent, + hasChanges: () => boolean +): string | void => { + if (hasChanges()) { + const confirmationMessage = + 'You have unsaved changes. Are you sure you want to leave?' + event.returnValue = confirmationMessage + return confirmationMessage } +} - componentWillUnmount(): void { - this.updatePromise.cancel() +const runBeforeUnloadEvent = ({ + hasChanges, + updatePromise, +}: { + hasChanges: () => boolean + updatePromise: Promise +}) => { + const _handleBeforeEvent = (event) => handleBeforeUnload(event, hasChanges) + if (hasChanges()) { + window.addEventListener('beforeunload', _handleBeforeEvent) + } else { + window.removeEventListener('beforeunload', _handleBeforeEvent) } - - get hasChanges(): boolean { - const transliterationChanged = - this.state.transliteration !== this.props.transliteration - const notesChanged = this.state.notes !== this.props.notes - const introductionChanged = - this.state.introduction !== this.props.introduction - return transliterationChanged || notesChanged || introductionChanged + return () => { + window.removeEventListener('beforeunload', _handleBeforeEvent) + updatePromise.cancel() } +} - update = (property: string) => (value: string): void => { - this.setState({ - ...this.state, +const SubmitButton = ({ + propsDisabled, + hasChanges, + formId, +}: { + propsDisabled?: boolean + hasChanges: boolean + formId: string +}) => ( + +) + +const getFormGroup = ({ + name, + key, + value, + formId, + propsDisabled, + update, + formData, +}: { + name: 'transliteration' | 'notes' | 'introduction' + key: number + value: string + formId: string + propsDisabled?: boolean + update: (property: keyof FormData) => (value: string) => void + formData: FormData +}): JSX.Element => { + return ( + + {_.capitalize(name)}{' '} + {name === 'transliteration' && } + + + ) +} + +const fields: Array<'transliteration' | 'notes' | 'introduction'> = [ + 'transliteration', + 'notes', + 'introduction', +] + +const TransliterationForm: React.FC = ({ + transliteration, + notes, + introduction, + updateEdition, + disabled: propsDisabled, +}): JSX.Element => { + const formId = _.uniqueId('TransliterationForm-') + const [formData, setFormData] = useState({ + transliteration, + notes, + introduction, + error: null, + disabled: false, + }) + const [updatePromise, setUpdatePromise] = useState(Promise.resolve()) + const update = (property: keyof FormData) => (value: string) => { + setFormData({ + ...formData, [property]: value, }) } - onTemplate = (template: string): void => { - this.setState({ - ...this.state, + const onTemplate = (template: string) => { + setFormData({ + ...formData, transliteration: template, }) } - submit = (event: FormEvent): void => { + const submit = (event: FormEvent) => { event.preventDefault() - this.setState({ - ...this.state, - error: null, - }) - this.updatePromise = this.props - .updateEdition( - this.state.transliteration, - this.state.notes, - this.state.introduction - ) + setFormData({ ...formData, error: null }) + const promise = updateEdition( + formData.transliteration, + formData.notes, + formData.introduction + ) .then((fragment) => { - this.setState({ - ...this.state, + setFormData({ + ...formData, transliteration: fragment.atf, notes: fragment.notes.text, introduction: fragment.introduction.text, }) }) - .catch((error) => - this.setState({ - ...this.state, - error: error, - }) - ) + .catch((error) => { + setFormData({ ...formData, error }) + }) + setUpdatePromise(promise) } - SubmitButton = (): JSX.Element => ( - + const hasChanges = useCallback( + (): boolean => + formData.transliteration !== transliteration || + formData.notes !== notes || + formData.introduction !== introduction, + [formData, transliteration, notes, introduction] ) - render(): JSX.Element { - return ( - - - - -
- - Transliteration{' '} - - - - - Notes{' '} - - - - Introduction{' '} - - -
-
- -
- - - - - - - - - - -
- ) - } + useEffect(() => { + return runBeforeUnloadEvent({ hasChanges, updatePromise }) + }, [ + formData, + transliteration, + notes, + introduction, + updatePromise, + hasChanges, + ]) + + const formGroups = fields.map( + (name, key: number): JSX.Element => + getFormGroup({ + name, + key, + value: formData[name], + formId, + propsDisabled, + update, + formData, + }) + ) + + return ( + + + + +
+ {formGroups} +
+
+ +
+ + + + + + + + + + +
+ ) } export default TransliterationForm