-
Notifications
You must be signed in to change notification settings - Fork 197
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add Confirm Dialog component #5361
Merged
Merged
Changes from all commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
bab05b3
Add Confirm Dialog Component
fjorgemota 4c66840
Add tests for the Confirm Dialog Component
fjorgemota 743e5fd
Add hook useConfirmDialogProps to help using ConfirmDialog component …
fjorgemota 5ac3f2d
Add tests for the hook useConfirmDialogProps
fjorgemota d822f6e
Add entry-point for the confirm-dialog folder
fjorgemota 2eaf528
Move buttons to the right side and add margin to the message
fjorgemota 0550301
Add period on documentation in the confirm dialog
fjorgemota 0a0741b
Rename parameter registerListener to shouldRegisterListener in useCon…
fjorgemota b63c695
Remove title's default value of the ConfirmDialog component
fjorgemota 9aaea09
Add props to set custom text for confirm and cancel buttons
fjorgemota b108cd0
ALlow to set additional props when calling confirm from useConfirmDia…
fjorgemota 23c3667
Merge branch 'add/confirm-dialog' of github.com:Automattic/sensei int…
fjorgemota 8f046ba
Remove title parameter in favour of newProps to pass additional props…
fjorgemota 8369814
Add documentation for the function returned by useConfirmDialogProps
fjorgemota 13ac0cb
Separe buttons on the Confirm Dialog with a little margin (of 8px)
fjorgemota File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
87 changes: 87 additions & 0 deletions
87
assets/blocks/editor-components/confirm-dialog/confirm-dialog.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
/** | ||
* WordPress dependencies | ||
*/ | ||
import { Modal, Button } from '@wordpress/components'; | ||
import { __ } from '@wordpress/i18n'; | ||
import { useEffect } from '@wordpress/element'; | ||
import { ENTER } from '@wordpress/keycodes'; | ||
|
||
/** | ||
* Controlled Component that shows a modal containing a confirm dialog. Inspired by Gutenberg's experimental | ||
* Confirm Dialog. | ||
* | ||
* @see https://developer.wordpress.org/block-editor/reference-guides/components/confirm-dialog/ | ||
* @param {Object} props Component props. | ||
* @param {boolean} props.isOpen Determines if the confirm dialog is open or not. | ||
* @param {string} props.title Title for the confirm dialog. | ||
* @param {string} props.children Content for the confirm dialog, can be any React component. | ||
* @param {Function} props.onConfirm Callback called when the user click on "OK" or press Enter with the modal open. | ||
* @param {Function} props.onCancel Callback called when the user click on "Cancel" or press ESC with the modal open. | ||
* @param {string} props.confirmButtonText Optional custom text to display as the confirmation button's label. | ||
* @param {string} props.cancelButtonText Optional custom text to display as the cancellation button's label. | ||
*/ | ||
const ConfirmDialog = ( { | ||
isOpen = false, | ||
title, | ||
children, | ||
onConfirm, | ||
onCancel, | ||
cancelButtonText = __( 'Cancel', 'sensei-lms' ), | ||
confirmButtonText = __( 'OK', 'sensei-lms' ), | ||
} ) => { | ||
useConfirmOnEnter( isOpen, onConfirm ); | ||
if ( ! isOpen ) { | ||
return null; | ||
} | ||
return ( | ||
<Modal | ||
title={ title } | ||
onRequestClose={ onCancel } | ||
shouldCloseOnClickOutside={ false } | ||
className="sensei-confirm-dialog" | ||
> | ||
<div className="sensei-confirm-dialog__message">{ children }</div> | ||
<div className="sensei-confirm-dialog__button-container"> | ||
<Button | ||
variant="tertiary" | ||
onClick={ onCancel } | ||
className="sensei-confirm-dialog__button" | ||
> | ||
{ cancelButtonText } | ||
</Button> | ||
<Button | ||
variant="primary" | ||
onClick={ onConfirm } | ||
className="sensei-confirm-dialog__button" | ||
> | ||
{ confirmButtonText } | ||
</Button> | ||
</div> | ||
</Modal> | ||
); | ||
}; | ||
|
||
/** | ||
* Calls onConfirm when registerListener is true and the user press ENTER. | ||
* | ||
* @param {boolean} shouldRegisterListener If the listener should be set up or not. | ||
* @param {Function} fn The callback to call when the user press ENTER, if registerListener is true. | ||
*/ | ||
const useConfirmOnEnter = ( shouldRegisterListener, fn ) => { | ||
useEffect( () => { | ||
if ( ! shouldRegisterListener ) { | ||
return; | ||
} | ||
const callback = ( event ) => { | ||
if ( event.keyCode === ENTER && ! event.defaultPrevented ) { | ||
event.preventDefault(); | ||
fn(); | ||
} | ||
}; | ||
document.body.addEventListener( 'keydown', callback, false ); | ||
return () => | ||
document.body.removeEventListener( 'keydown', callback, false ); | ||
}, [ shouldRegisterListener, fn ] ); | ||
}; | ||
|
||
export default ConfirmDialog; |
12 changes: 12 additions & 0 deletions
12
assets/blocks/editor-components/confirm-dialog/confirm-dialog.scss
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
.sensei-confirm-dialog { | ||
&__message { | ||
margin: 1em; | ||
} | ||
&__button-container { | ||
display: flex; | ||
justify-content: end; | ||
} | ||
&__button { | ||
margin-left: 8px; | ||
} | ||
} |
93 changes: 93 additions & 0 deletions
93
assets/blocks/editor-components/confirm-dialog/confirm-dialog.test.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
/** | ||
* WordPress dependencies | ||
*/ | ||
import { ENTER } from '@wordpress/keycodes'; | ||
|
||
/** | ||
* External dependencies | ||
*/ | ||
import { render, fireEvent } from '@testing-library/react'; | ||
|
||
/** | ||
* Internal dependencies | ||
*/ | ||
import ConfirmDialog from './confirm-dialog'; | ||
|
||
describe( '<ConfirmDialog />', () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Great tests! |
||
it( 'Should not render if the dialog is not open', () => { | ||
const { queryByText } = render( | ||
<ConfirmDialog isOpen={ false } title="Hey Title"> | ||
Hey Content | ||
</ConfirmDialog> | ||
); | ||
|
||
expect( queryByText( 'Hey Title' ) ).toBeFalsy(); | ||
expect( queryByText( 'Hey Content' ) ).toBeFalsy(); | ||
} ); | ||
|
||
it( 'Should render modal if the dialog is open', () => { | ||
const { queryByText } = render( | ||
<ConfirmDialog isOpen={ true } title="Hey Title"> | ||
Hey Content | ||
</ConfirmDialog> | ||
); | ||
|
||
expect( queryByText( 'Hey Title' ) ).toBeTruthy(); | ||
expect( queryByText( 'Hey Content' ) ).toBeTruthy(); | ||
} ); | ||
|
||
it( 'Should cancel the modal if click on Cancel button', () => { | ||
const onCancel = jest.fn(); | ||
const { queryByText } = render( | ||
<ConfirmDialog | ||
isOpen={ true } | ||
title="Hey Title" | ||
onCancel={ onCancel } | ||
> | ||
Hey Content | ||
</ConfirmDialog> | ||
); | ||
|
||
expect( onCancel ).not.toHaveBeenCalled(); | ||
fireEvent.click( queryByText( 'Cancel' ) ); | ||
expect( onCancel ).toHaveBeenCalled(); | ||
} ); | ||
|
||
it( 'Should confirm the modal if click on OK button', () => { | ||
const onConfirm = jest.fn(); | ||
const { queryByText } = render( | ||
<ConfirmDialog | ||
isOpen={ true } | ||
title="Hey Title" | ||
onConfirm={ onConfirm } | ||
> | ||
Hey Content | ||
</ConfirmDialog> | ||
); | ||
|
||
expect( onConfirm ).not.toHaveBeenCalled(); | ||
fireEvent.click( queryByText( 'OK' ) ); | ||
expect( onConfirm ).toHaveBeenCalled(); | ||
} ); | ||
|
||
it( 'Should confirm the modal if the user press ENTER', () => { | ||
const onConfirm = jest.fn(); | ||
render( | ||
<ConfirmDialog | ||
isOpen={ true } | ||
title="Hey Title" | ||
onConfirm={ onConfirm } | ||
> | ||
Hey Content | ||
</ConfirmDialog> | ||
); | ||
|
||
expect( onConfirm ).not.toHaveBeenCalled(); | ||
fireEvent.keyDown( document.body, { | ||
key: 'Enter', | ||
code: 'Enter', | ||
keyCode: ENTER, | ||
} ); | ||
expect( onConfirm ).toHaveBeenCalled(); | ||
} ); | ||
} ); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
/** | ||
* Internal dependencies | ||
*/ | ||
export { default as ConfirmDialog } from './confirm-dialog'; | ||
export { default as useConfirmDialogProps } from './use-confirm-dialog-props'; |
45 changes: 45 additions & 0 deletions
45
assets/blocks/editor-components/confirm-dialog/use-confirm-dialog-props.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
/** | ||
* WordPress dependencies | ||
*/ | ||
import { useState } from '@wordpress/element'; | ||
|
||
/** | ||
* Hook that returns the props for the component ConfirmDialog, with an additional async function that mimics the | ||
* synchronous native confirm() API. Loosely inspired by react-confirm HOC. | ||
* | ||
* @see https://github.com/haradakunihiko/react-confirm | ||
* @return {Array} The first item is the props to pass to ConfirmDialog, the second one is the async function to call to | ||
* trigger ConfirmDialog. | ||
*/ | ||
export const useConfirmDialogProps = () => { | ||
const [ props, setProps ] = useState( { isOpen: false } ); | ||
/** | ||
* Shows the ConfirmDialog component and returns a boolean with the result asynchronously. | ||
* | ||
* @param {string} text Text of the Confirm Dialog. | ||
* @param {Object} newProps Additional properties to use on the ConfirmDialog. | ||
* @param {string} newProps.title Title of the Confirm Dialog. | ||
* @param {string} newProps.cancelButtonText Text of the Cancel button on the Confirm Dialog. | ||
* @param {string} newProps.okButtonText Text of the Ok button on the Confirm Dialog. | ||
* @return {Promise<boolean>} true if the user clicked the OK button or pressed Enter. false if the user clicked the | ||
* Cancel button or pressed ESC. | ||
*/ | ||
const confirm = ( text, newProps = {} ) => { | ||
return new Promise( ( resolve ) => { | ||
const callback = ( value ) => () => { | ||
resolve( value ); | ||
setProps( { isOpen: false } ); | ||
}; | ||
setProps( { | ||
...newProps, | ||
isOpen: true, | ||
children: text, | ||
onConfirm: callback( true ), | ||
onCancel: callback( false ), | ||
} ); | ||
} ); | ||
}; | ||
return [ props, confirm ]; | ||
}; | ||
|
||
export default useConfirmDialogProps; |
68 changes: 68 additions & 0 deletions
68
assets/blocks/editor-components/confirm-dialog/use-confirm-dialog-props.test.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
/** | ||
* External dependencies | ||
*/ | ||
import { renderHook, act } from '@testing-library/react-hooks'; | ||
|
||
/** | ||
* Internal dependencies | ||
*/ | ||
import useConfirmDialogProps from './use-confirm-dialog-props'; | ||
|
||
describe( 'useConfirmDialogProps()', () => { | ||
it( 'Should return isOpen as false by default', () => { | ||
const { result } = renderHook( () => useConfirmDialogProps() ); | ||
const [ props ] = result.current; | ||
expect( props.isOpen ).toBe( false ); | ||
} ); | ||
|
||
it( 'Should set Confirm Dialog props when calling confirm', () => { | ||
const { result } = renderHook( () => useConfirmDialogProps() ); | ||
let [ props, confirm ] = result.current; | ||
expect( props.isOpen ).toBe( false ); | ||
act( () => { | ||
confirm( 'Hey Content', { title: 'Hey Title' } ); | ||
} ); | ||
[ props, confirm ] = result.current; | ||
expect( props.isOpen ).toBe( true ); | ||
expect( props.title ).toBe( 'Hey Title' ); | ||
expect( props.children ).toBe( 'Hey Content' ); | ||
expect( props.onConfirm ).toBeInstanceOf( Function ); | ||
expect( props.onCancel ).toBeInstanceOf( Function ); | ||
} ); | ||
|
||
it( 'confirm should return true when onConfirm is called', async () => { | ||
const { result } = renderHook( () => useConfirmDialogProps() ); | ||
let [ props, confirm ] = result.current; | ||
expect( props.isOpen ).toBe( false ); | ||
const confirmResponse = act( () => | ||
expect( | ||
confirm( 'Hey Content', { title: 'Hey Title' } ) | ||
).resolves.toBe( true ) | ||
); | ||
[ props, confirm ] = result.current; | ||
expect( props.isOpen ).toBe( true ); | ||
act( () => props.onConfirm() ); | ||
// We need to verify AFTER calling the props.on* callback, otherwise, the promise won't be resolved yet. | ||
await confirmResponse; | ||
[ props, confirm ] = result.current; | ||
expect( props.isOpen ).toBe( false ); | ||
} ); | ||
|
||
it( 'confirm should return false when onCancel is called', async () => { | ||
const { result } = renderHook( () => useConfirmDialogProps() ); | ||
let [ props, confirm ] = result.current; | ||
expect( props.isOpen ).toBe( false ); | ||
const confirmResponse = act( () => | ||
expect( | ||
confirm( 'Hey Content', { title: 'Hey Title' } ) | ||
).resolves.toBe( false ) | ||
); | ||
[ props, confirm ] = result.current; | ||
expect( props.isOpen ).toBe( true ); | ||
act( () => props.onCancel() ); | ||
// We need to verify AFTER calling the props.on* callback, otherwise, the promise won't be resolved yet. | ||
await confirmResponse; | ||
[ props, confirm ] = result.current; | ||
expect( props.isOpen ).toBe( false ); | ||
} ); | ||
} ); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice code separation here! 😀