-
-
Notifications
You must be signed in to change notification settings - Fork 144
Confirmation modal #211
Confirmation modal #211
Changes from 13 commits
425c811
75eafb5
60ae0c2
eb8adce
ab3ae1f
49253d6
a95d578
02e5d34
91926b5
86d86e2
497bc73
f8a3b38
d4adc7a
aac2115
ac90a45
93ed6a7
727521a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
import PropTypes from 'prop-types'; | ||
import {Component} from 'react'; | ||
|
||
/** | ||
* ConfirmDialog is used to display the browser's native "confirm" modal, | ||
* with an optional message and two buttons ("OK" and "Cancel"). | ||
* This ConfirmDialog can be used in conjunction with buttons when the user | ||
* is performing an action that should require an extra step of verification. | ||
*/ | ||
export default class ConfirmDialog extends Component { | ||
|
||
constructor(props) { | ||
super(props); | ||
} | ||
|
||
componentDidUpdate() { | ||
const { displayed, message, setProps, cancel_n_clicks, submit_n_clicks, n_clicks } = this.props; | ||
|
||
if (displayed) { | ||
new Promise(resolve => resolve(window.confirm(message))).then(result => setProps({ | ||
n_clicks: n_clicks + 1, | ||
n_clicks_timestamp: Date.now(), | ||
cancel_n_clicks: !result ? cancel_n_clicks + 1 : cancel_n_clicks, | ||
submit_n_clicks: result ? submit_n_clicks + 1: submit_n_clicks, | ||
displayed: false, | ||
})); | ||
} | ||
} | ||
|
||
render() { | ||
return null; | ||
} | ||
} | ||
|
||
ConfirmDialog.defaultProps = { | ||
n_clicks: 0, | ||
n_clicks_timestamp: -1, | ||
submit_n_clicks: 0, | ||
cancel_n_clicks: 0, | ||
}; | ||
|
||
ConfirmDialog.propTypes = { | ||
id: PropTypes.string, | ||
|
||
/** | ||
* Message to show in the popup. | ||
*/ | ||
message: PropTypes.string, | ||
|
||
/** | ||
* Number of times the modal was submited or canceled. | ||
*/ | ||
n_clicks: PropTypes.number, | ||
/** | ||
* Last timestamp the popup was clicked. | ||
*/ | ||
n_clicks_timestamp: PropTypes.number, | ||
/** | ||
* Number of times the submit was clicked | ||
*/ | ||
submit_n_clicks: PropTypes.number, | ||
/** | ||
* Number of times the popup was canceled. | ||
*/ | ||
cancel_n_clicks: PropTypes.number, | ||
/** | ||
* Set to true to send the ConfirmDialog. | ||
*/ | ||
displayed: PropTypes.bool, | ||
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. I think that I thought that
Does that work? Or am I missing a functional difference between 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. Send_confirm is for activating the modal, it get sets to false after fire, displayed was for telling if the confirmation was currently showing. I just changed for just displayed and it works the same. 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. I think a good rule to keep in mind here and with most functional/React style coding is that prop names should not be verbs, especially not imperative ones like |
||
|
||
/** | ||
* Dash-assigned callback that gets fired when the value changes. | ||
*/ | ||
setProps: PropTypes.func | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
import React from 'react'; | ||
import PropTypes from 'prop-types'; | ||
|
||
import ConfirmDialog from './ConfirmDialog.react' | ||
|
||
|
||
|
||
/** | ||
* Wrap children onClick to send a confirmation dialog. | ||
* You can add a button directly as a children: | ||
* `dcc.ConfirmDialogProvider(html.Button('click me', id='btn'), id='confirm')` | ||
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. Our users probably won't know what
|
||
*/ | ||
export default class ConfirmDialogProvider extends React.Component { | ||
render() { | ||
const { id, setProps, children } = this.props; | ||
|
||
// Will lose the previous onClick of the child | ||
const wrapClick = (child) => React.cloneElement(child, {onClick: () => | ||
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. Nice, this seems like a good solution. |
||
{ | ||
setProps({ | ||
displayed: true | ||
}); | ||
} | ||
}); | ||
|
||
const realChild = children.props | ||
? children.props.children | ||
: children.map(e => e.props.children); | ||
|
||
return ( | ||
<div id={id}> | ||
{ | ||
realChild && realChild.length | ||
? realChild.map(wrapClick) | ||
: wrapClick(realChild) | ||
} | ||
<ConfirmDialog {...this.props}/> | ||
</div> | ||
) | ||
} | ||
}; | ||
|
||
ConfirmDialogProvider.defaultProps = { | ||
n_clicks: 0, | ||
n_clicks_timestamp: -1, | ||
submit_n_clicks: 0, | ||
cancel_n_clicks: 0, | ||
}; | ||
|
||
ConfirmDialogProvider.propTypes = { | ||
id: PropTypes.string, | ||
|
||
/** | ||
* Message to show in the popup. | ||
*/ | ||
message: PropTypes.string, | ||
|
||
/** | ||
* Number of times the modal was submited or canceled. | ||
*/ | ||
n_clicks: PropTypes.number, | ||
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. I'm still wondering if there is a use case for this property given that we have 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. If I add the timestamps for submit and cancel, I don't see one as I was only using it to know which one was clicked in the test and it wasn't a real use case. I will remove it. |
||
/** | ||
* Last timestamp the popup was clicked. | ||
*/ | ||
n_clicks_timestamp: PropTypes.number, | ||
/** | ||
* Number of times the submit was clicked | ||
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.
|
||
*/ | ||
submit_n_clicks: PropTypes.number, | ||
/** | ||
* Number of times the popup was canceled. | ||
*/ | ||
cancel_n_clicks: PropTypes.number, | ||
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. Can we add |
||
/** | ||
* Is the modal currently displayed. | ||
*/ | ||
displayed: PropTypes.bool, | ||
|
||
/** | ||
* Dash-assigned callback that gets fired when the value changes. | ||
*/ | ||
setProps: PropTypes.func, | ||
children: PropTypes.any, | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,6 +5,7 @@ | |
import os | ||
import sys | ||
import time | ||
import multiprocessing | ||
import pandas as pd | ||
|
||
import dash | ||
|
@@ -15,8 +16,9 @@ | |
from selenium import webdriver | ||
from selenium.webdriver.common.keys import Keys | ||
from selenium.common.exceptions import InvalidElementStateException | ||
import time | ||
|
||
from textwrap import dedent | ||
|
||
try: | ||
from urlparse import urlparse | ||
except ImportError: | ||
|
@@ -366,7 +368,6 @@ def test_gallery(self): | |
|
||
self.snapshot('gallery - text input') | ||
|
||
|
||
def test_location_link(self): | ||
app = dash.Dash(__name__) | ||
|
||
|
@@ -528,3 +529,66 @@ def update_graph(n_clicks): | |
button.click() | ||
time.sleep(2) | ||
self.snapshot('candlestick - 2 click') | ||
|
||
def _test_confirm(self, app, test_name): | ||
count = multiprocessing.Value('i', 0) | ||
|
||
@app.callback(Output('confirmed', 'children'), | ||
[Input('confirm', 'n_clicks'), | ||
Input('confirm', 'submit_n_clicks'), | ||
Input('confirm', 'cancel_n_clicks')]) | ||
def _on_confirmed(n_clicks, submit_n_clicks, cancel_n_clicks): | ||
if not n_clicks: | ||
return '' | ||
count.value = n_clicks | ||
if n_clicks == 1: | ||
self.assertEqual(1, submit_n_clicks) | ||
return 'confirmed' | ||
elif n_clicks == 2: | ||
self.assertEqual(1, cancel_n_clicks) | ||
return 'canceled' | ||
|
||
self.startServer(app) | ||
self.snapshot(test_name + ' -> initial') | ||
button = self.wait_for_element_by_css_selector('#button') | ||
|
||
button.click() | ||
time.sleep(1) | ||
self.driver.switch_to.alert.accept() | ||
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. 🎉 very nice! |
||
self.wait_for_text_to_equal('#confirmed', 'confirmed') | ||
self.snapshot(test_name + ' -> confirmed') | ||
|
||
button.click() | ||
time.sleep(0.5) | ||
self.driver.switch_to.alert.dismiss() | ||
time.sleep(0.5) | ||
self.wait_for_text_to_equal('#confirmed', 'canceled') | ||
self.snapshot(test_name + ' -> canceled') | ||
|
||
self.assertEqual(2, count.value, 'Expected 2 callback but got ' + str(count.value)) | ||
|
||
def test_confirm(self): | ||
app = dash.Dash(__name__) | ||
|
||
app.layout = html.Div([ | ||
html.Button(id='button', children='Send confirm', n_clicks=0), | ||
dcc.ConfirmDialog(id='confirm', message='Please confirm.'), | ||
html.Div(id='confirmed') | ||
]) | ||
|
||
@app.callback(Output('confirm', 'displayed'), [Input('button', 'n_clicks')]) | ||
def on_click_confirm(n_clicks): | ||
if n_clicks: | ||
return True | ||
|
||
self._test_confirm(app, 'ConfirmDialog') | ||
|
||
def test_confirm_dialog_provider(self): | ||
app = dash.Dash(__name__) | ||
|
||
app.layout = html.Div([ | ||
dcc.ConfirmDialogProvider(html.Button('click me', id='button'), id='confirm', message='Please confirm.'), | ||
html.Div(id='confirmed') | ||
]) | ||
|
||
self._test_confirm(app, 'ConfirmDialogProvider') |
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.
Given that we have
submit_n_clicks
andcancel_n_clicks
, is there a use case for the generaln_clicks
?