diff --git a/src/sidebar/components/Annotation/AnnotationEditor.tsx b/src/sidebar/components/Annotation/AnnotationEditor.tsx index 1eccfc1095c..6c6fa38c9dc 100644 --- a/src/sidebar/components/Annotation/AnnotationEditor.tsx +++ b/src/sidebar/components/Annotation/AnnotationEditor.tsx @@ -28,6 +28,7 @@ import { useSidebarStore } from '../../store'; import type { Draft } from '../../store/modules/drafts'; import MarkdownEditor from '../MarkdownEditor'; import TagEditor from '../TagEditor'; +import { useUnsavedChanges } from '../hooks/unsaved-changes'; import AnnotationLicense from './AnnotationLicense'; import AnnotationPublishControl from './AnnotationPublishControl'; @@ -75,6 +76,12 @@ function AnnotationEditor({ const text = draft.text; const isEmpty = !text && !tags.length; + // Warn user if they try to close the tab while there is an open, non-empty + // draft. + // + // WARNING: This does not work in all browsers. See hook docs for details. + useUnsavedChanges(!isEmpty); + const onEditTags = useCallback( (tags: string[]) => { store.createDraft(draft.annotation, { ...draft, tags }); diff --git a/src/sidebar/components/Annotation/test/AnnotationEditor-test.js b/src/sidebar/components/Annotation/test/AnnotationEditor-test.js index 6f54cd8dd88..9eb2155ab1a 100644 --- a/src/sidebar/components/Annotation/test/AnnotationEditor-test.js +++ b/src/sidebar/components/Annotation/test/AnnotationEditor-test.js @@ -18,6 +18,7 @@ describe('AnnotationEditor', () => { let fakeSettings; let fakeToastMessenger; let fakeGroupsService; + let fakeUseUnsavedChanges; let fakeStore; @@ -74,8 +75,13 @@ describe('AnnotationEditor', () => { defaultAuthority: sinon.stub().returns(''), }; + fakeUseUnsavedChanges = sinon.stub(); + $imports.$mock(mockImportedComponents()); $imports.$mock({ + '../hooks/unsaved-changes': { + useUnsavedChanges: fakeUseUnsavedChanges, + }, '../../store': { useSidebarStore: () => fakeStore }, '../../helpers/theme': { applyTheme: fakeApplyTheme }, }); @@ -279,6 +285,19 @@ describe('AnnotationEditor', () => { assert.notCalled(fakeAnnotationsService.save); }); + it('warns if user closes tab while there is a non-empty draft', () => { + // If the draft is empty, the warning is disabled. + const wrapper = createComponent(); + assert.calledWith(fakeUseUnsavedChanges, false); + + // If the draft changes to non-empty, the warning is enabled. + fakeUseUnsavedChanges.resetHistory(); + const draft = fixtures.defaultDraft(); + draft.text = 'something is here'; + wrapper.setProps({ draft }); + assert.calledWith(fakeUseUnsavedChanges, true); + }); + describe('handling publish options', () => { it('sets the publish control to disabled if the annotation is empty', () => { // default draft has no tags or text diff --git a/src/sidebar/components/hooks/test/unsaved-changes-test.js b/src/sidebar/components/hooks/test/unsaved-changes-test.js new file mode 100644 index 00000000000..93112520cef --- /dev/null +++ b/src/sidebar/components/hooks/test/unsaved-changes-test.js @@ -0,0 +1,62 @@ +import { mount } from '@hypothesis/frontend-testing'; + +import { useUnsavedChanges, hasUnsavedChanges } from '../unsaved-changes'; + +function TestUseUnsavedChanges({ unsaved, fakeWindow }) { + useUnsavedChanges(unsaved, fakeWindow); + return
; +} + +describe('useUnsavedChanges', () => { + let fakeWindow; + + function dispatchBeforeUnload() { + const event = new Event('beforeunload', { cancelable: true }); + fakeWindow.dispatchEvent(event); + return event; + } + + function createWidget(unsaved) { + return mount( +