Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@

- Added `RichTextEditor` and `TypographyStylesProvider` components #530 by @emilhe

### Changed

- Complex components such as highlightcode, stepper are now rendered by the dash ecosystem when using dash 3+. Dash 2 falls back on `dash-extensions-js` to render via `React.createElement` (by @emilhe). This enables the use of these components in callbacks as triggers. #531 @BSd3v
Comment thread
BSd3v marked this conversation as resolved.
Outdated

# 1.0.0

Expand Down
3 changes: 2 additions & 1 deletion requires-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ pytest<8.1.0
wheel
selenium<4.3.0
black
build
build
dash-iconify
4 changes: 2 additions & 2 deletions src/ts/components/core/stepper/Stepper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { DashBaseProps } from "props/dash";
import { StylesApiProps } from "props/styles";
import { omit } from "ramda";
import React, { useState } from "react";
import { getChildLayout, getLoadingState, newRenderDashComponents } from "../../../utils/dash3";
import { getChildLayout, getLoadingState, newRenderDashComponents, getContextPath } from "../../../utils/dash3";

interface Props extends BoxProps, DashBaseProps, StylesApiProps {
/** Index of the active step */
Expand Down Expand Up @@ -80,7 +80,7 @@ const Stepper = ({ setProps, loading_state, active, children, ...others }: Props
"icon",
"progressIcon",
"completedIcon",
], childProps.componentPath
], getContextPath()
);

return (
Expand Down
212 changes: 212 additions & 0 deletions tests/test_optional_components.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
from dash import Dash, html, Output, Input, State, _dash_renderer, clientside_callback
import dash_mantine_components as dmc
from dash_iconify import DashIconify
from dash.testing.wait import until
import time

_dash_renderer._set_react_version("18.2.0")


def test_001oc_optional_components(dash_duo):
app = Dash(__name__)

min_step = 0
max_step = 3
active = 0

layout = html.Div(
[
dmc.Stepper(
id="stepper",
active=active,
children=[
dmc.StepperStep(
label="Step 1",
description=html.Div(dmc.Button(id='test_button', children='test', disabled=True)),
children="Step 1",
),
dmc.StepperStep(
label="Step 2",
description="Verify email",
children="Step 2",
),
dmc.StepperStep(
label="Step 3",
description="Get full access",
children="Step 3",
),
dmc.StepperCompleted(
children=dmc.Text(
"Completed, click back button to get to previous step",
ta="center",
)
),
],
),
dmc.Group(
justify="center",
mt="xl",
children=[
dmc.Button("Back", id="previous-button", variant="default"),
dmc.Button("Next step", id="next-button"),
],
),
html.Div(id='output')
]
)

app.layout = dmc.MantineProvider(layout)

clientside_callback("""(n)=> {
if (n) {
return `clicked ${n} times`
}
}""",
Output('output', 'children'),
Input('test_button', 'n_clicks'),
prevent_initial_call=True,
suppress_callback_exceptions=True)

clientside_callback(
"""(a) => {
return a !== 0
}""",
Output('test_button', 'disabled'),
Input('stepper', 'active'),
suppress_callback_exceptions=True
)

clientside_callback(
"""(n, active) => n ? Math.max(active - 1, 0) : active""",
Output("stepper", "active", allow_duplicate=True),
Input("previous-button", "n_clicks"),
State("stepper", "active"),
prevent_initial_call=True,
)

clientside_callback(
"""(n, active) => n ? Math.min(active + 1, 2) : active""",
Output("stepper", "active", allow_duplicate=True),
Input("next-button", "n_clicks"),
State("stepper", "active"),
prevent_initial_call=True,
)

dash_duo.start_server(app)

# Wait for the app to load
dash_duo.wait_for_text_to_equal("#test_button", "test")
assert dash_duo.find_element('#test_button').get_attribute('disabled') is None

step_buttons = dash_duo.find_elements("button.mantine-Stepper-step")
for i, btn in enumerate(step_buttons):
btn.click()
dash_duo.wait_for_text_to_equal("div.mantine-Stepper-content", f"Step {i + 1}")

dash_duo.find_element("#previous-button").click()
dash_duo.wait_for_text_to_equal("div.mantine-Stepper-content", "Step 2")

assert dash_duo.find_element('#test_button').get_attribute('disabled') == 'true'


dash_duo.find_element("#previous-button").click()
dash_duo.wait_for_text_to_equal("div.mantine-Stepper-content", "Step 1")

until(lambda: dash_duo.find_element('#test_button').get_attribute('disabled') is None, timeout=3)

for i in range(5):
dash_duo.find_element('#test_button').click()
dash_duo.wait_for_text_to_equal("#output", f"clicked {i+1} times")

assert dash_duo.get_logs() == []

def test_002oc_optional_components(dash_duo):
## tests async rendering and also compares after callbacks to the icon
styles_css = """
.dmc-api-demo-root {
border: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
padding: var(--mantine-spacing-xs) var(--mantine-spacing-sm);
border-radius: var(--mantine-radius-md);
font-weight: 500;
cursor: pointer;

&[data-checked] {
background-color: var(--mantine-color-blue-filled);
border-color: var(--mantine-color-blue-filled);
color: var(--mantine-color-white);
}
}"""

demo_py = """
import dash_mantine_components as dmc

dmc.Checkbox(
classNames={"root": "dmc-api-demo-root"},
label="Checkbox button",
w=180
)"""


code = [
{
"fileName": "demo.py",
"code": demo_py,
"language": "python",
"icon": DashIconify(icon="vscode-icons:file-type-reactts", width=20, id='test-0-icon'),
},
{
"fileName": "styles.css",
"code":styles_css,
"language": "css",
"icon": DashIconify(icon="vscode-icons:file-type-css", width=20, id='test-1-icon'),
},
]


app = Dash(external_stylesheets=dmc.styles.ALL)


component = dmc.CodeHighlightTabs(
code=code,
withExpandButton=True,
expandCodeLabel="Show full code",
collapseCodeLabel="Show less",
defaultExpanded=False,
maxCollapsedHeight=100,
m="lg"
)


app.layout = dmc.MantineProvider(
[component,
dmc.Button(id='test-0', children='Change Python Icon'),
dmc.Button(id='test-1', children='Change CSS Icon')],
id="mantine-provider",
forceColorScheme="light",
)

for i in range(2):
clientside_callback(
"""(n) => {
return 'dashicons:money-alt'
}""",
Output(f'test-{i}-icon', 'icon'),
Input(f'test-{i}', 'n_clicks'),
prevent_initial_call=True,
suppress_callback_exceptions=True
)

dash_duo.start_server(app)

icons = dash_duo.find_elements('svg.iconify')

assert len(icons) == 2

for i, x in enumerate(icons):
old_html = x.get_attribute('innerHTML')
dash_duo.find_element(f'#test-{i}').click()
if i:
until(lambda: dash_duo.find_element(f'svg.iconify:nth-child({i})').get_attribute('innerHTML') != old_html, timeout=3)
else:
until(lambda: dash_duo.find_element(f'svg.iconify:first-child').get_attribute('innerHTML') != old_html,
timeout=3)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:nth-child is 1-indexed, so should all of this be just :nth-child({i + 1})? Moreover, to be sure this test is really showing that the same element has changed, old_html and new should use the same selector, rather than old_html coming from the enumerate and new getting a new selector, something like:

for i in range(len(icons)):
    icon_selector = f'svg.iconify:nth-child({i + 1})'
    old_html = dash_duo.find_element(icon_selector).get_attribute('innerHTML')
    dash_duo.find_element(f'#test-{i}').click()
    until(lambda: dash_duo.find_element(icon_selector).get_attribute('innerHTML') != old_html, timeout=3)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not valid. I tried it...

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

>  document.querySelector('div:first-child')===document.querySelector('div:nth-child(1)')
<- true

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nth-child(2) isnt found for some reason, though it should be.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Of course, dash iconify doesnt pass the id to the component...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alexcjohnson what about what I just used? I wrapped the icons in divs and used that to target the components.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regardless as written this test is looking at the first one in both until calls, which is clearly not what you want. So one way or another we need to get the right elements, and get them in a symmetric way so it’s clear it’s the same element before and after

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I updated the test, that's what I was referring to. 🙂

Might be able to do without the time.sleep.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's nice and clear 😎