[Enterprise Search] Shared unsaved changes prompt component (#92277)

This commit is contained in:
Jason Stoltzfus 2021-02-23 12:55:42 -05:00 committed by GitHub
parent 0cc3fa61ce
commit e1166999e0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 158 additions and 0 deletions

View file

@ -0,0 +1,8 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export { UnsavedChangesPrompt } from './unsaved_changes_prompt';

View file

@ -0,0 +1,109 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
jest.mock('react-router-dom', () => ({
Prompt: () => null,
}));
import { Prompt } from 'react-router-dom';
import { shallow, mount, ReactWrapper } from 'enzyme';
import { UnsavedChangesPrompt } from './unsaved_changes_prompt';
describe('UnsavedChangesPrompt', () => {
let addEventListenerSpy: jest.SpyInstance;
let removeEventListenerSpy: jest.SpyInstance;
beforeAll(() => {
addEventListenerSpy = jest.spyOn(window, 'addEventListener').mockImplementation(() => true);
removeEventListenerSpy = jest
.spyOn(window, 'removeEventListener')
.mockImplementation(() => true);
});
afterAll(() => {
addEventListenerSpy.mockRestore();
removeEventListenerSpy.mockRestore();
});
it('renders a React Router Prompt, which will show users a confirmation message when navigating within the SPA if hasUnsavedChanges is true', () => {
const wrapper = shallow(<UnsavedChangesPrompt hasUnsavedChanges />);
const prompt = wrapper.find(Prompt);
expect(prompt.exists()).toBe(true);
expect(prompt.prop('when')).toBe(true);
expect(prompt.prop('message')).toBe(
'Your changes have not been saved. Are you sure you want to leave?'
);
});
it('the message text of the prompt can be customized', () => {
const wrapper = shallow(
<UnsavedChangesPrompt hasUnsavedChanges messageText="Some custom text" />
);
expect(wrapper.find(Prompt).prop('message')).toBe('Some custom text');
});
describe('external navigation', () => {
let wrapper: ReactWrapper;
const getAddBeforeUnloadEventCalls = () =>
addEventListenerSpy.mock.calls.filter((call) => call[0] === 'beforeunload');
const getRemoveBeforeUnloadEventCalls = () =>
removeEventListenerSpy.mock.calls.filter((call) => call[0] === 'beforeunload');
const getLastRegisteredBeforeUnloadEventHandler = () => {
const calls = getAddBeforeUnloadEventCalls();
return calls[calls.length - 1][1];
};
beforeAll(() => {
wrapper = mount(<UnsavedChangesPrompt hasUnsavedChanges />);
});
it('sets up a handler for the beforeunload event', () => {
const calls = getAddBeforeUnloadEventCalls();
expect(calls.length).toBe(1);
});
it('that handler will show users a confirmation message when navigating outside the SPA if hasUnsavedChanges is true', () => {
const handler = getLastRegisteredBeforeUnloadEventHandler();
const event = { returnValue: null, preventDefault: jest.fn() };
handler(event);
expect(event.returnValue).toEqual('');
expect(event.preventDefault).toHaveBeenCalled();
});
it('will not register a new handler if there is a re-render and hasUnsavedChanges is still true', () => {
wrapper.setProps({ hasUnsavedChanges: true, messageText: 'custom message text' });
const calls = getAddBeforeUnloadEventCalls();
expect(calls.length).toBe(1);
});
it('when the hasUnsavedChanges prop changes to false, it will deregister the old handler and create a new one, which will not show users a confirmation', () => {
const initialHandler = getLastRegisteredBeforeUnloadEventHandler();
wrapper.setProps({ hasUnsavedChanges: false });
// The old handler is unregistered
const unregisterCalls = getRemoveBeforeUnloadEventCalls();
expect(unregisterCalls.length).toBe(1);
expect(unregisterCalls[0][1]).toBe(initialHandler);
// The new handler is registered
const calls = getAddBeforeUnloadEventCalls();
expect(calls.length).toBe(2);
const newHandler = getLastRegisteredBeforeUnloadEventHandler();
// The new handler does not show a confirmation message
const event = { returnValue: null, preventDefault: jest.fn() };
newHandler(event);
expect(event.returnValue).toEqual(null);
expect(event.preventDefault).not.toHaveBeenCalled();
});
});
});

View file

@ -0,0 +1,41 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { useEffect } from 'react';
import { Prompt } from 'react-router-dom';
import { i18n } from '@kbn/i18n';
const DEFAULT_MESSAGE_TEXT = i18n.translate('xpack.enterpriseSearch.shared.unsavedChangesMessage', {
defaultMessage: 'Your changes have not been saved. Are you sure you want to leave?',
});
interface Props {
hasUnsavedChanges: boolean;
messageText?: string;
}
export const UnsavedChangesPrompt: React.FC<Props> = ({
hasUnsavedChanges,
messageText = DEFAULT_MESSAGE_TEXT,
}) => {
useEffect(() => {
const handler = (event: BeforeUnloadEvent) => {
if (hasUnsavedChanges) {
// These 2 lines of code are the recommendation from MDN for triggering a browser prompt for confirming
// whether or not a user wants to leave the current site.
event.preventDefault();
event.returnValue = '';
}
};
// Adding this handler will prompt users if they are navigating to a new page, outside of the Kibana SPA
window.addEventListener('beforeunload', handler);
return () => window.removeEventListener('beforeunload', handler);
}, [hasUnsavedChanges]);
// Adding this Prompt will prompt users if they are navigating to a new page, within the Kibana SPA
return <Prompt when={hasUnsavedChanges} message={messageText} />;
};