-
Notifications
You must be signed in to change notification settings - Fork 136
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(lab): new component ConfirmationDialog (#286)
Commonly used to confirm actions such as deleting rows/objects from databases etc.
- Loading branch information
Showing
6 changed files
with
319 additions
and
0 deletions.
There are no files selected for viewing
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 |
---|---|---|
@@ -1 +1,2 @@ | ||
from .confirmation_dialog import ConfirmationDialog # noqa: F401 | ||
from .tabs import Tab, Tabs # noqa: F401 |
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,165 @@ | ||
from typing import Callable, List, Union, overload | ||
|
||
import reacton.ipyvuetify as v | ||
|
||
import solara | ||
|
||
|
||
@overload | ||
def ConfirmationDialog( | ||
open: solara.Reactive[bool], | ||
*, | ||
on_close: Union[None, Callable[[], None]] = None, | ||
content: Union[str, solara.Element] = "", | ||
title: str = "Confirm action", | ||
ok: Union[str, solara.Element] = "OK", | ||
on_ok: Callable[[], None] = lambda: None, | ||
cancel: Union[str, solara.Element] = "Cancel", | ||
on_cancel: Callable[[], None] = lambda: None, | ||
children: List[solara.Element] = [], | ||
max_width: Union[int, str] = 500, | ||
persistent: bool = False, | ||
): | ||
... | ||
|
||
|
||
# when open is a boolean, on_close should be given, otherwise a dialog can never be closed | ||
# TODO: copy this pattern to many other components | ||
@overload | ||
def ConfirmationDialog( | ||
open: bool, | ||
*, | ||
on_close: Callable[[], None], | ||
content: Union[str, solara.Element] = "", | ||
title: str = "Confirm action", | ||
ok: Union[str, solara.Element] = "OK", | ||
on_ok: Callable[[], None] = lambda: None, | ||
cancel: Union[str, solara.Element] = "Cancel", | ||
on_cancel: Callable[[], None] = lambda: None, | ||
children: List[solara.Element] = [], | ||
max_width: Union[int, str] = 500, | ||
persistent: bool = False, | ||
): | ||
... | ||
|
||
|
||
@solara.component | ||
def ConfirmationDialog( | ||
open: Union[solara.Reactive[bool], bool], | ||
*, | ||
on_close: Union[None, Callable[[], None]] = None, | ||
content: Union[str, solara.Element] = "", | ||
title: str = "Confirm action", | ||
ok: Union[str, solara.Element] = "OK", | ||
on_ok: Callable[[], None] = lambda: None, | ||
cancel: Union[str, solara.Element] = "Cancel", | ||
on_cancel: Callable[[], None] = lambda: None, | ||
children: List[solara.Element] = [], | ||
max_width: Union[int, str] = 500, | ||
persistent: bool = False, | ||
): | ||
"""A dialog used to confirm a user action. | ||
(*Note: [This component is experimental and its API may change in the future](/docs/lab).*) | ||
By default, has a title, a text explaining the | ||
decision to be made, and two buttons "OK" and "Cancel". | ||
## Basic examples | ||
```solara | ||
import solara | ||
open_delete_confirmation = solara.reactive(False) | ||
def delete_user(): | ||
# put your code to perform the action here | ||
print("User being deleted...") | ||
@solara.component | ||
def Page(): | ||
solara.Button(label="Delete user", on_click=lambda: open_delete_confirmation.set(True)) | ||
solara.lab.ConfirmationDialog(open_delete_confirmation, ok="Ok, Delete", on_ok=delete_user, content="Are you sure you want to delete this user?") | ||
``` | ||
## Arguments | ||
* `open`: Indicates whether the dialog is being shown or not. | ||
* `on_open`: Callback to call when the dialog opens of closes. | ||
* `content`: Message that is displayed. | ||
* `title`: Title of the dialog. | ||
* `ok`: If a string, this text will be displayed on the confirmation button (default is "OK"). If a Button, it will be used instead of the default button. | ||
* `on_ok`: Callback to be called when the OK button is clicked. | ||
* `cancel`: If a string, this text will be displayed on the cancellation button (default is "Cancel"). If a Button, it will be used instead of the default | ||
button. | ||
* `on_cancel`: Callback to be called when the Cancel button is clicked. When persistent is False, clicking outside of the element or pressing esc key will | ||
also trigger cancel. | ||
* `children`: Additional components that will be shown under the dialog message, but before the buttons. | ||
* `max_width`: Maximum width of the dialog window. | ||
* `persistent`: When False (the default), clicking outside of the element or pressing esc key will trigger cancel. | ||
""" | ||
|
||
def on_open(open_value): | ||
if not open_value: | ||
if on_close: | ||
on_close() | ||
|
||
open_reactive = solara.use_reactive(open, on_open) | ||
del open | ||
|
||
def close(): | ||
open_reactive.set(False) | ||
|
||
user_on_click_ok = None | ||
user_on_click_cancel = None | ||
|
||
def perform_ok(): | ||
if user_on_click_ok: | ||
user_on_click_ok() | ||
on_ok() | ||
close() | ||
|
||
def perform_cancel(): | ||
if user_on_click_cancel: | ||
user_on_click_cancel() | ||
on_cancel() | ||
close() | ||
|
||
def on_v_model(value): | ||
if not value: | ||
on_cancel() | ||
open_reactive.set(value) | ||
|
||
with v.Dialog( | ||
v_model=open_reactive.value, | ||
on_v_model=on_v_model, | ||
persistent=persistent, | ||
max_width=max_width, | ||
): | ||
with solara.v.Card(): | ||
solara.v.CardTitle(children=[title]) | ||
with solara.v.CardText(style_="min-height: 64px"): | ||
if isinstance(content, str): | ||
solara.Text(content) | ||
else: | ||
solara.display(content) | ||
if children: | ||
solara.display(*children) | ||
with solara.v.CardActions(class_="justify-end"): | ||
if isinstance(cancel, str): | ||
solara.Button(label=cancel, on_click=perform_cancel, text=True, classes=["grey--text", "text--darken-2"]) | ||
else: | ||
# the user may have passed in on_click already | ||
user_on_click_cancel = cancel.kwargs.get("on_click") | ||
# override or add our own on_click handler | ||
cancel.kwargs = {**cancel.kwargs, "on_click": perform_cancel} | ||
solara.display(cancel) | ||
|
||
# similar as cancel | ||
if isinstance(ok, str): | ||
solara.Button(label=ok, on_click=perform_ok, dark=True, color="primary", elevation=0) | ||
else: | ||
user_on_click_ok = ok.kwargs.get("on_click") | ||
ok.kwargs = {**ok.kwargs, "on_click": perform_ok} | ||
solara.display(ok) |
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 |
---|---|---|
|
@@ -118,6 +118,7 @@ | |
"pages": [ | ||
"tab", | ||
"tabs", | ||
"confirmation_dialog", | ||
], | ||
}, | ||
] | ||
|
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,55 @@ | ||
""" | ||
# ConfirmationDialog | ||
""" | ||
|
||
from typing import Union | ||
|
||
import solara | ||
from solara.lab.components.confirmation_dialog import ConfirmationDialog | ||
from solara.website.utils import apidoc | ||
|
||
title = "ConfirmationDialog" | ||
users = solara.reactive("Alice Bob Cindy Dirk Eve Fred".split()) | ||
user_to_be_deleted: solara.Reactive[Union[str, None]] = solara.reactive(None) | ||
|
||
|
||
def ask_to_delete_user(user): | ||
user_to_be_deleted.value = user | ||
|
||
|
||
def clear_user_to_be_deleted(): | ||
user_to_be_deleted.value = None | ||
|
||
|
||
def delete_user(): | ||
users.set([u for u in users.value if u != user_to_be_deleted.value]) | ||
clear_user_to_be_deleted() | ||
|
||
|
||
@solara.component | ||
def Page(): | ||
"""Create a list of users with a button to delete them. | ||
A confirmation dialog will pop up first before deletion.""" | ||
solara.Markdown("#### Users:") | ||
with solara.Column(style={"max-width": "300px"}): | ||
for user in users.value: | ||
with solara.Row(style={"align-items": "center"}): | ||
solara.Text(user) | ||
solara.v.Spacer() | ||
solara.Button(icon_name="mdi-delete", on_click=lambda user=user: ask_to_delete_user(user), icon=True) | ||
if not users.value: | ||
solara.Text("(no users left)") | ||
|
||
with ConfirmationDialog( | ||
user_to_be_deleted.value is not None, | ||
on_ok=delete_user, | ||
on_close=clear_user_to_be_deleted, | ||
ok="Ok, Delete", | ||
title="Delete user", | ||
): | ||
solara.Markdown(f"Are you sure you want to delete user **{user_to_be_deleted.value}**?") | ||
|
||
|
||
__doc__ += apidoc(solara.lab.components.confirmation_dialog.ConfirmationDialog.f) # type: ignore |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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,97 @@ | ||
from unittest.mock import MagicMock | ||
|
||
import ipyvuetify as vw | ||
|
||
import solara | ||
from solara.lab.components.confirmation_dialog import ConfirmationDialog | ||
|
||
|
||
def test_confirmation_dialog_ok(): | ||
is_open = solara.reactive(True) | ||
on_ok = MagicMock() | ||
on_close = MagicMock() | ||
el = ConfirmationDialog(is_open, on_ok=on_ok, on_close=on_close, content="Hello") | ||
_, rc = solara.render(el, handle_error=False) | ||
buttons = rc.find(vw.Btn) | ||
assert len(buttons) == 2 | ||
buttons[1].widget.click() | ||
assert on_ok.call_count == 1 # was OK button clicked? | ||
assert on_close.call_count == 1 # always triggered | ||
assert not is_open.value # is dialog closed? | ||
|
||
|
||
def test_confirmation_dialog_cancel(): | ||
is_open = solara.reactive(True) | ||
on_ok = MagicMock() | ||
on_close = MagicMock() | ||
el = ConfirmationDialog(is_open, on_ok=on_ok, on_close=on_close, content="Hello") | ||
_, rc = solara.render(el, handle_error=False) | ||
buttons = rc.find(vw.Btn) | ||
assert len(buttons) == 2 | ||
buttons[0].widget.click() | ||
assert on_ok.call_count == 0 # on_ok action should not have been executed | ||
assert on_close.call_count == 1 # always triggered | ||
assert not is_open.value # is dialog closed? | ||
|
||
|
||
def test_confirm_external_close(): | ||
# e.g. when persistent=False, clicking away from the dialog closes it | ||
is_open = solara.reactive(True) | ||
on_ok = MagicMock() | ||
on_cancel = MagicMock() | ||
on_close = MagicMock() | ||
el = ConfirmationDialog(is_open, on_ok=on_ok, on_cancel=on_cancel, on_close=on_close, content="Hello") | ||
_, rc = solara.render(el, handle_error=False) | ||
dialog = rc.find(vw.Dialog)[0].widget | ||
assert dialog.v_model | ||
dialog.v_model = False # trigger an external close, like escape or clicking away | ||
assert not is_open.value # is dialog closed? | ||
assert on_ok.call_count == 0 # on_ok action should not have been executed | ||
assert on_cancel.call_count == 1 # on_cancel action should not have been executed | ||
assert on_close.call_count == 1 # always triggered | ||
|
||
|
||
def test_confirmation_dialog_custom_button_no_onclick(): | ||
is_open = solara.reactive(True) | ||
on_ok = MagicMock() | ||
my_button = solara.Button(label="Not OK") # no on_click | ||
el = ConfirmationDialog(is_open, on_ok=on_ok, ok=my_button, content="Are you sure?") | ||
_, rc = solara.render(el, handle_error=False) | ||
buttons = rc.find(vw.Btn) | ||
assert len(buttons) == 2 | ||
assert buttons[0].widget.children == ["Cancel"] | ||
assert buttons[1].widget.children == ["Not OK"] | ||
buttons[1].widget.click() | ||
assert on_ok.call_count == 1 # should still be called | ||
|
||
|
||
def test_confirmation_dialog_custom_button_with_onclick(): | ||
is_open = solara.reactive(True) | ||
values = [] | ||
|
||
def on_ok(): | ||
values.append("on_ok") | ||
|
||
def on_cancel(): | ||
values.append("on_cancel") | ||
|
||
def on_click_ok(): | ||
values.append("on_click_ok") | ||
|
||
def on_click_cancel(): | ||
values.append("on_click_cancel") | ||
|
||
ok = solara.Button(label="Not OK", on_click=on_click_ok) | ||
cancel = solara.Button(label="Not Cancel", on_click=on_click_cancel) | ||
el = ConfirmationDialog(is_open, on_ok=on_ok, on_cancel=on_cancel, ok=ok, cancel=cancel, content="Are you sure?") | ||
_, rc = solara.render(el, handle_error=False) | ||
buttons = rc.find(vw.Btn) | ||
assert len(buttons) == 2 | ||
assert buttons[1].widget.children == ["Not OK"] | ||
assert buttons[0].widget.children == ["Not Cancel"] | ||
buttons[1].widget.click() | ||
assert values == ["on_click_ok", "on_ok"] # assert on_ok and on_click were both called, in that order | ||
values.clear() | ||
# now the same for cancel | ||
buttons[0].widget.click() | ||
assert values == ["on_click_cancel", "on_cancel"] |