Skip to content

Commit

Permalink
Merge pull request #1366 from cozy/feat/confirmOnExit
Browse files Browse the repository at this point in the history
feat: Adds a user-space modal for useConfirmExit
  • Loading branch information
edas authored Jan 24, 2020
2 parents ff2e3c6 + 87ef4eb commit 3e492af
Show file tree
Hide file tree
Showing 6 changed files with 350 additions and 70 deletions.
30 changes: 0 additions & 30 deletions react/hooks/useConfirmExit.js

This file was deleted.

40 changes: 0 additions & 40 deletions react/hooks/useConfirmExit.spec.js

This file was deleted.

151 changes: 151 additions & 0 deletions react/hooks/useConfirmExit/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import React, { useCallback, useRef, useState } from 'react'
import PropTypes from 'prop-types'

import useEventListener from '../useEventListener'
import Modal from '../../Modal'
import withLocales from '../../I18n/withLocales'

import en from './locales/en.json'
import fr from './locales/fr.json'

/**
* Confirmation modal
* @private
* @param {string} message - Confirmation message
* @param {string} title - Title of the modal
* @param {function} onConfirm - will be executed on confirmation
* @param {function} onCancel - will be executed on cancelation
*/
function ConfirmModal({ t, title, message, onConfirm, onCancel }) {
return (
<Modal
closable={false}
mobileFullscreen={false}
primaryAction={onConfirm}
primaryType="regular"
primaryText={t('useConfirmExit.leave')}
secondaryAction={onCancel}
secondaryType="secondary"
secondaryText={t('useConfirmExit.back')}
description={message || t('useConfirmExit.message')}
title={title || t('useConfirmExit.title')}
/>
)
}
ConfirmModal.PropTypes = {
message: PropTypes.string,
title: PropTypes.string,
onConfirm: PropTypes.func.isRequired,
onCancel: PropTypes.func.isRequired
}
const dictRequire = { en, fr }
const LocalizedConfirmModal = withLocales(dictRequire)(ConfirmModal)

/**
* Go to the requested destination (URL or function)
*
* @param {string|function} destination
*/
function go(destination) {
if (typeof destination === 'function') {
destination()
} else if (typeof destination === 'string') {
document.location = destination
} else {
throw new Error(`Unknown location where to jump to`)
}
}

function isActivated(activate) {
return typeof activate === 'function' ? activate() : activate
}

/**
* @typedef useConfirmExitResponse
* @property {function} requestToLeave - gets an URL or function,
* triggers a confirmation modal and redirect the browser to this URL
* or call this function if the user confirms.
* @property {function} exitConfirmationModal - React component
* that will show a confirmation modal when requested by requestToLeave
* and nothing otherwise
*/

/**
* When provided a message, will warn the user before exiting the page
*
* When the browser detects a page unload (go to another website or
* leave the window/tab), it will show a native popup asking for
* confirmation. This popup may show the `message` but will usually
* use a native message from the browser.
*
* If the user confirm he wants to leave, `onLeave` will be executed.
* This function may not be able to execute async code.
*
* @param {bool|function} activate - (return) falsy to deactivate the behaviour
* @param {string} message - Confirmation message
* @param {string} title - Title of the modal
* @param {function} onLeave - will be executed before returning
* @returns {useConfirmExitResponse}
*/
export default function useConfirmExit({
activate = true,
onLeave,
message,
title
}) {
// `onbeforeunload` event on the browser:
// Using a ref in order to have an event listener that does not
// need to be deregistered, recreated and registered again at each
// message or callback change. If not, the lag introduced by the
// useEffect inside useEventListener may create wrong behaviours
// for fast changing calls to useConfirmExit.
const state = useRef()
state.current = { message, onLeave, activate }
const beforeunload = useCallback(event => {
const activated = isActivated(state.current.activate)
activated && state.current.onLeave && state.current.onLeave()
const returnValue = activated ? state.current.message : null
if (returnValue) event.returnValue = returnValue
return returnValue
}, [])
useEventListener(window, 'beforeunload', beforeunload)

// contains an URL of function given to `requestToLeave`
// any truthy value will trigger the ExitConfirmationModal
const [modalDest, setModalDest] = useState(false)
const onCloseModalRequest = useCallback(() => {
setModalDest(false)
}, [setModalDest])

// call this with an URL or a function to trigger the ExitConfirmationModal
const requestToLeave = useCallback(
where => {
if (isActivated(state.current.activate)) {
setModalDest(() => where)
} else {
go(where)
}
},
[state, setModalDest]
)

// null when the modal is closed, a Modal otherwise
const onConfirm = useCallback(() => {
onLeave && onLeave()
go(modalDest)
}, [modalDest, onLeave])

// return the modal if opened
const modal = modalDest && (
<LocalizedConfirmModal
message={message}
title={title}
onCancel={onCloseModalRequest}
onConfirm={onConfirm}
/>
)
return {
requestToLeave,
exitConfirmationModal: modal
}
}
Loading

0 comments on commit 3e492af

Please sign in to comment.