diff --git a/CHANGELOG.md b/CHANGELOG.md index e0fcd49690..379c34f3af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ All notable changes to `dash` will be documented in this file. This project adheres to [Semantic Versioning](https://semver.org/). +## [unreleased] + +## Fixed +- [#3264](https://github.com/plotly/dash/pull/3264) Fixed an issue where moving components inside of children would not update the `setProps` path, leading to hashes being incorrect + ## [3.0.2] - 2025-04-01 ## Changed diff --git a/dash/dash-renderer/src/wrapper/DashWrapper.tsx b/dash/dash-renderer/src/wrapper/DashWrapper.tsx index 82bee5b0ba..370c385ba5 100644 --- a/dash/dash-renderer/src/wrapper/DashWrapper.tsx +++ b/dash/dash-renderer/src/wrapper/DashWrapper.tsx @@ -65,6 +65,7 @@ function DashWrapper({ const dispatch = useDispatch(); const memoizedKeys: MutableRefObject = useRef({}); const newRender = useRef(false); + const renderedPath = useRef(componentPath); let renderComponent: any = null; let renderComponentProps: any = null; let renderH: any = null; @@ -90,6 +91,7 @@ function DashWrapper({ } else { newRender.current = false; } + renderedPath.current = componentPath; }, [_newRender]); const setProps = (newProps: UpdatePropsPayload) => { @@ -101,7 +103,10 @@ function DashWrapper({ dispatch((dispatch, getState) => { const currentState = getState(); const {graphs} = currentState; - const oldLayout = getComponentLayout(componentPath, currentState); + const oldLayout = getComponentLayout( + renderedPath.current, + currentState + ); if (!oldLayout) return; const {props: oldProps} = oldLayout; if (!oldProps) return; @@ -144,7 +149,7 @@ function DashWrapper({ dispatch( updateProps({ props: changedProps, - itempath: componentPath, + itempath: renderedPath.current, renderType: 'internal' }) ); diff --git a/tests/integration/renderer/test_children_reorder.py b/tests/integration/renderer/test_children_reorder.py new file mode 100644 index 0000000000..3e92c5befe --- /dev/null +++ b/tests/integration/renderer/test_children_reorder.py @@ -0,0 +1,89 @@ +from dash import Dash, Input, Output, html, dcc, State, ALL + + +class Section: + def __init__(self, idx): + self.idx = idx + self.options = ["A", "B", "C"] + + @property + def section_id(self): + return {"type": "section-container", "id": self.idx} + + @property + def dropdown_id(self): + return {"type": "dropdown", "id": self.idx} + + @property + def swap_btn_id(self): + return {"type": "swap-btn", "id": self.idx} + + def get_layout(self) -> html.Div: + layout = html.Div( + id=self.section_id, + children=[ + html.H1(f"I am section {self.idx}"), + html.Button( + "SWAP", + id=self.swap_btn_id, + n_clicks=0, + className=f"swap_button_{self.idx}", + ), + dcc.Dropdown( + self.options, + id=self.dropdown_id, + multi=True, + value=[], + className=f"dropdown_{self.idx}", + ), + ], + ) + return layout + + +def test_roc001_reorder_children(dash_duo): + app = Dash() + + app.layout = html.Div( + id="main-app", children=[*[Section(idx=i).get_layout() for i in range(2)]] + ) + + @app.callback( + Output("main-app", "children"), + Input({"type": "swap-btn", "id": ALL}, "n_clicks"), + State("main-app", "children"), + prevent_initial_call=True, + ) + def swap_button_action(n_clicks, children): + if any(n > 0 for n in n_clicks): + return children[::-1] + + dash_duo.start_server(app) + + for i in range(2): + dash_duo.wait_for_text_to_equal("h1", f"I am section {i}") + dash_duo.wait_for_text_to_equal( + f".dropdown_{i} .Select-multi-value-wrapper", "Select..." + ) + dash_duo.find_element(f".dropdown_{i}").click() + dash_duo.find_element(f".dropdown_{i} .VirtualizedSelectOption").click() + dash_duo.wait_for_text_to_equal( + f".dropdown_{i} .Select-multi-value-wrapper", "×A\n " + ) + dash_duo.find_element(f".dropdown_{i}").click() + dash_duo.find_element(f".dropdown_{i} .VirtualizedSelectOption").click() + dash_duo.wait_for_text_to_equal( + f".dropdown_{i} .Select-multi-value-wrapper", "×A\n ×B\n " + ) + dash_duo.find_element(f".dropdown_{i}").click() + dash_duo.find_element(f".dropdown_{i} .VirtualizedSelectOption").click() + dash_duo.wait_for_text_to_equal( + f".dropdown_{i} .Select-multi-value-wrapper", "×A\n ×B\n ×C\n " + ) + dash_duo.find_element(f".swap_button_{i}").click() + dash_duo.wait_for_text_to_equal( + f".dropdown_{0} .Select-multi-value-wrapper", "×A\n ×B\n ×C\n " + ) + dash_duo.wait_for_text_to_equal( + f".dropdown_{1} .Select-multi-value-wrapper", "×A\n ×B\n ×C\n " + )