From 7c7e3e3697f443651566056df5f3e491e548c7ac Mon Sep 17 00:00:00 2001 From: Robert Knight Date: Thu, 22 Aug 2024 12:58:38 +0100 Subject: [PATCH] Add `useWarnOnPageUnload` hook This hook provides a convenient way to enable a browser warning if the user tries to close the tab while there are unsaved changes. This hook was copied verbatim from the hypothesis/lms repo. --- .../test/use-warn-on-page-unload-test.js | 62 +++++++++++++++++++ src/hooks/use-warn-on-page-unload.ts | 29 +++++++++ src/index.ts | 1 + 3 files changed, 92 insertions(+) create mode 100644 src/hooks/test/use-warn-on-page-unload-test.js create mode 100644 src/hooks/use-warn-on-page-unload.ts diff --git a/src/hooks/test/use-warn-on-page-unload-test.js b/src/hooks/test/use-warn-on-page-unload-test.js new file mode 100644 index 000000000..e68a944d6 --- /dev/null +++ b/src/hooks/test/use-warn-on-page-unload-test.js @@ -0,0 +1,62 @@ +import { mount } from 'enzyme'; +import { useState } from 'preact/hooks'; + +import { useWarnOnPageUnload } from '../use-warn-on-page-unload'; + +describe('useWarnOnPageUnload', () => { + let fakeWindow; + const FakeComponent = () => { + const [isUnsaved, setUnsaved] = useState(true); + + useWarnOnPageUnload(isUnsaved, fakeWindow); + + return ( + + ); + }; + const createComponent = () => mount(); + + const waitForBeforeUnloadEvent = () => { + const promise = new Promise(resolve => + fakeWindow.addEventListener('beforeunload', resolve), + ); + fakeWindow.dispatchEvent(new Event('beforeunload', { cancelable: true })); + + return promise; + }; + + beforeEach(() => { + fakeWindow = new EventTarget(); + }); + + it('registers event listener when unsaved data is true', async () => { + createComponent(); + + const event = await waitForBeforeUnloadEvent(); + + assert.isTrue(event.defaultPrevented); + assert.equal(event.returnValue, ''); + }); + + it('unregisters event listener when unsaved data is false', async () => { + const wrapper = createComponent(); + + wrapper.find('button').simulate('click'); + wrapper.update(); + + const event = await waitForBeforeUnloadEvent(); + + assert.isFalse(event.defaultPrevented); + }); + + it('unregisters event listener when component is unmounted', async () => { + const wrapper = createComponent(); + wrapper.unmount(); + + const event = await waitForBeforeUnloadEvent(); + + assert.isFalse(event.defaultPrevented); + }); +}); diff --git a/src/hooks/use-warn-on-page-unload.ts b/src/hooks/use-warn-on-page-unload.ts new file mode 100644 index 000000000..75c999695 --- /dev/null +++ b/src/hooks/use-warn-on-page-unload.ts @@ -0,0 +1,29 @@ +import { useEffect } from 'preact/hooks'; + +const noop = () => {}; + +/** + * Registers an event listener to window's 'beforeunload' if `hasUnsavedData` is true. + * It also unregisters the event if `hasUnsavedData` is false or the component is unmounted. + * + * This event listener makes the browser warn the user about potential unsaved changes, + * and gives the user the opportunity to cancel the page unload if desired. + * + * @link https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event + */ +export function useWarnOnPageUnload(hasUnsavedData: boolean, window_ = window) { + useEffect(() => { + if (!hasUnsavedData) { + return noop; + } + + const listener = (e: BeforeUnloadEvent) => { + e.preventDefault(); + e.returnValue = ''; + }; + + window_.addEventListener('beforeunload', listener); + + return () => window_.removeEventListener('beforeunload', listener); + }, [hasUnsavedData, window_]); +} diff --git a/src/index.ts b/src/index.ts index e0f8830ac..94fe39b9c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,6 +9,7 @@ export { useStableCallback } from './hooks/use-stable-callback'; export { useSyncedRef } from './hooks/use-synced-ref'; export { useToastMessages } from './hooks/use-toast-messages'; export { useValidateOnSubmit } from './hooks/use-validate-on-submit'; +export { useWarnOnPageUnload } from './hooks/use-warn-on-page-unload'; export type { ToastMessagesState, ToastMessageData,