diff --git a/docs/components_page/components/__tests__/helpers.py b/docs/components_page/components/__tests__/helpers.py index ce70945ae..87c08350c 100644 --- a/docs/components_page/components/__tests__/helpers.py +++ b/docs/components_page/components/__tests__/helpers.py @@ -50,7 +50,7 @@ def rename_variable(snippet_path, suffix, variable, assign_op="="): for line in lines: if line.startswith(f"{variable} {assign_op}"): line = line.replace( - f"{variable} {assign_op}", f"{variable}_{suffix} {assign_op}" + f"{variable} {assign_op}", f"{variable}__{suffix} {assign_op}" ) new_lines.append(line) diff --git a/docs/components_page/components/__tests__/test_modal.py b/docs/components_page/components/__tests__/test_modal.py index 8cfefc5a4..e1c21684e 100644 --- a/docs/components_page/components/__tests__/test_modal.py +++ b/docs/components_page/components/__tests__/test_modal.py @@ -189,3 +189,60 @@ def check_modal_centered_callbacks(runner): lambda: len(runner.find_elements(".modal-content")) == 0, timeout=4, ) + + +# ------------------------------ + + +def test_r_modal_toggle(dashr): + r_app = load_r_app((HERE.parent / "modal" / "toggle.R"), "modal") + dashr.start_server(r_app) + check_modal_toggle_callbacks(dashr) + + +def test_jl_modal_toggle(dashjl): + jl_app = load_jl_app((HERE.parent / "modal" / "toggle.jl"), "modal") + dashjl.start_server(jl_app) + check_modal_toggle_callbacks(dashjl) + + +def check_modal_toggle_callbacks(runner): + runner.find_element("#open-toggle-modal").click() + wait.until( + lambda: len(runner.find_elements(".modal-content")) != 0, + timeout=4, + ) + wait.until( + lambda: len(runner.find_elements("#toggle-modal-1")) == 1, + timeout=4, + ) + wait.until( + lambda: len(runner.find_elements("#toggle-modal-2")) == 0, + timeout=4, + ) + + runner.find_element("#open-toggle-modal-2").click() + wait.until( + lambda: len(runner.find_elements("#toggle-modal-2")) == 1, + timeout=4, + ) + wait.until( + lambda: len(runner.find_elements("#toggle-modal-1")) == 0, + timeout=4, + ) + + runner.find_element("#open-toggle-modal-1").click() + wait.until( + lambda: len(runner.find_elements("#toggle-modal-1")) == 1, + timeout=4, + ) + wait.until( + lambda: len(runner.find_elements("#toggle-modal-2")) == 0, + timeout=4, + ) + + runner.find_elements(".modal-header > .btn-close")[0].click() + wait.until( + lambda: len(runner.find_elements(".modal-content")) == 0, + timeout=4, + ) diff --git a/docs/components_page/components/__tests__/test_snippets.py b/docs/components_page/components/__tests__/test_snippets.py index b39343fcd..24a8ec81b 100644 --- a/docs/components_page/components/__tests__/test_snippets.py +++ b/docs/components_page/components/__tests__/test_snippets.py @@ -73,7 +73,7 @@ def test_r_snippets(dash_thread_server, dashr_server, config): r_snippet = rename_variable( r_snippet_path, i, name, assign_op="<-" ) - python_r_compare.append((py_snippet, r_snippet, f"{name}_{i}")) + python_r_compare.append((py_snippet, r_snippet, f"{name}__{i}")) if python_r_compare: assert_layouts_equal( @@ -106,7 +106,7 @@ def test_jl_snippets(dash_thread_server, dashjl_server, config): if jl_snippet_path.exists(): jl_snippet = rename_variable(jl_snippet_path, i, name) - python_jl_compare.append((py_snippet, jl_snippet, f"{name}_{i}")) + python_jl_compare.append((py_snippet, jl_snippet, f"{name}__{i}")) if python_jl_compare: assert_layouts_equal( @@ -123,6 +123,7 @@ def test_jl_snippets(dash_thread_server, dashjl_server, config): def assert_layouts_equal( compare, runner, wrapper, port, py_runner, py_env, py_port ): + # Get python snippet layout app = py_source_to_app( PY_WRAPPER.format( diff --git a/docs/components_page/components/modal.md b/docs/components_page/components/modal.md index 65fdc4778..15b5a687a 100644 --- a/docs/components_page/components/modal.md +++ b/docs/components_page/components/modal.md @@ -47,6 +47,12 @@ To vertically center the modal on the page, set `centered=True`. {{example:components/modal/centered.py:modal}} +## Toggle between modals + +With some clever use of callbacks, you can also create modals that open other modals. + +{{example:components/modal/toggle.py:modal}} + {{apidoc:src/components/modal/Modal.js}} {{apidoc:src/components/modal/ModalHeader.js}} diff --git a/docs/components_page/components/modal/toggle.R b/docs/components_page/components/modal/toggle.R new file mode 100644 index 000000000..9fd9634d7 --- /dev/null +++ b/docs/components_page/components/modal/toggle.R @@ -0,0 +1,76 @@ +library(dashBootstrapComponents) +library(dashHtmlComponents) + +modal_1 <- dbcModal( + list( + dbcModalHeader(dbcModalTitle("Modal 1")), + dbcModalBody("This is the content of the first modal"), + dbcModalFooter( + dbcButton( + "Open Modal 2", + id = "open-toggle-modal-2", + class_name = "ms-auto", + n_clicks = 0 + ) + ) + ), + id = "toggle-modal-1", + is_open = FALSE +) + +modal_2 <- dbcModal( + list( + dbcModalHeader(dbcModalTitle("Modal 2")), + dbcModalBody("This is the second modal"), + dbcModalFooter( + dbcButton( + "Back to Modal 1", + id = "open-toggle-modal-1", + class_name = "ms-auto", + n_clicks = 0 + ) + ) + ), + id = "toggle-modal-2", + is_open = FALSE +) + + +modal <- htmlDiv( + list( + dbcButton("Open modal", id = "open-toggle-modal", n_clicks = 0), + modal_1, + modal_2 + ) +) + +app$callback( + output("toggle-modal-1", "is_open"), + list( + input("open-toggle-modal", "n_clicks"), + input("open-toggle-modal-1", "n_clicks"), + input("open-toggle-modal-2", "n_clicks"), + state("toggle-modal-1", "is_open") + ), + function(n0, n1, n2, is_open) { + if (n0 > 0 | n1 > 0 | n2 > 0) { + return(!is_open) + } + return(is_open) + } +) + +app$callback( + output("toggle-modal-2", "is_open"), + list( + input("open-toggle-modal-2", "n_clicks"), + input("open-toggle-modal-1", "n_clicks"), + state("toggle-modal-2", "is_open") + ), + function(n2, n1, is_open) { + if (n1 > 0 | n2 > 0) { + return(!is_open) + } + return(is_open) + } +) diff --git a/docs/components_page/components/modal/toggle.jl b/docs/components_page/components/modal/toggle.jl new file mode 100644 index 000000000..c035277e3 --- /dev/null +++ b/docs/components_page/components/modal/toggle.jl @@ -0,0 +1,63 @@ +using DashBootstrapComponents, DashHtmlComponents + +modal_1 = dbc_modal( + [ + dbc_modalheader(dbc_modaltitle("Modal 1")), + dbc_modalbody("This is the content of the first modal"), + dbc_modalfooter( + dbc_button( + "Open Modal 2", + id = "open-toggle-modal-2", + class_name = "ms-auto", + n_clicks = 0, + ), + ), + ], + id = "toggle-modal-1", + is_open = false, +) + +modal_2 = dbc_modal( + [ + dbc_modalheader(dbc_modaltitle("Modal 2")), + dbc_modalbody("This is the second modal"), + dbc_modalfooter( + dbc_button( + "Back to Modal 1", + id = "open-toggle-modal-1", + class_name = "ms-auto", + n_clicks = 0, + ), + ), + ], + id = "toggle-modal-2", + is_open = false, +) + + +modal = html_div([ + dbc_button("Open modal", id = "open-toggle-modal", n_clicks = 0), + modal_1, + modal_2, +]) + +callback!( + app, + Output("toggle-modal-1", "is_open"), + Input("open-toggle-modal", "n_clicks"), + Input("open-toggle-modal-1", "n_clicks"), + Input("open-toggle-modal-2", "n_clicks"), + State("toggle-modal-1", "is_open"), +) do n0, n1, n2, is_open + return n0 > 0 || n1 > 0 || n2 > 0 ? is_open == 0 : is_open +end; + +callback!( + app, + Output("toggle-modal-2", "is_open"), + Input("open-toggle-modal-2", "n_clicks"), + Input("open-toggle-modal-1", "n_clicks"), + State("toggle-modal-2", "is_open"), +) do n2, n1, is_open + return n1 > 0 || n2 > 0 ? is_open == 0 : is_open +end; diff --git a/docs/components_page/components/modal/toggle.py b/docs/components_page/components/modal/toggle.py new file mode 100644 index 000000000..d42db0cf6 --- /dev/null +++ b/docs/components_page/components/modal/toggle.py @@ -0,0 +1,75 @@ +import dash_bootstrap_components as dbc +import dash_html_components as html +from dash.dependencies import Input, Output, State + +modal_1 = dbc.Modal( + [ + dbc.ModalHeader(dbc.ModalTitle("Modal 1")), + dbc.ModalBody("This is the content of the first modal"), + dbc.ModalFooter( + dbc.Button( + "Open Modal 2", + id="open-toggle-modal-2", + class_name="ms-auto", + n_clicks=0, + ) + ), + ], + id="toggle-modal-1", + is_open=False, +) + +modal_2 = dbc.Modal( + [ + dbc.ModalHeader(dbc.ModalTitle("Modal 2")), + dbc.ModalBody("This is the second modal"), + dbc.ModalFooter( + dbc.Button( + "Back to Modal 1", + id="open-toggle-modal-1", + class_name="ms-auto", + n_clicks=0, + ) + ), + ], + id="toggle-modal-2", + is_open=False, +) + + +modal = html.Div( + [ + dbc.Button("Open modal", id="open-toggle-modal", n_clicks=0), + modal_1, + modal_2, + ] +) + + +@app.callback( + Output("toggle-modal-1", "is_open"), + [ + Input("open-toggle-modal", "n_clicks"), + Input("open-toggle-modal-1", "n_clicks"), + Input("open-toggle-modal-2", "n_clicks"), + ], + [State("toggle-modal-1", "is_open")], +) +def toggle_modal_1(n0, n1, n2, is_open): + if n0 or n1 or n2: + return not is_open + return is_open + + +@app.callback( + Output("toggle-modal-2", "is_open"), + [ + Input("open-toggle-modal-2", "n_clicks"), + Input("open-toggle-modal-1", "n_clicks"), + ], + [State("toggle-modal-2", "is_open")], +) +def toggle_modal_2(n2, n1, is_open): + if n1 or n2: + return not is_open + return is_open diff --git a/setup.cfg b/setup.cfg index c46a1fab3..2c85e5964 100644 --- a/setup.cfg +++ b/setup.cfg @@ -30,6 +30,7 @@ exclude = docs/components_page/components/modal/scrollable_source.py docs/components_page/components/modal/simple.py docs/components_page/components/modal/size.py + docs/components_page/components/modal/toggle.py docs/components_page/components/nav/navlink.py, docs/components_page/components/navbar/navbar.py, docs/components_page/components/offcanvas/backdrop.py,