From d1bc1f9627e54924b6cd9a1f1f7e61aeca9b141c Mon Sep 17 00:00:00 2001 From: BSd3v <82055130+BSd3v@users.noreply.github.com> Date: Wed, 5 Feb 2025 12:06:12 -0500 Subject: [PATCH 1/3] adding support for navlink to set active based upon page navigation automatically: - options for `active` are now `boolean | 'partial' | 'exact'` - partial will care if the pathname starts with the href - exact will display if the pathname is an exact match - true / false will disable the matching and set true and false respectively --- package.json | 1 + src/ts/components/core/NavLink.tsx | 31 +++++++++++++++++++++++++++--- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 7d30e80f..a5bb9f88 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "@mantine/notifications": "7.16.2", "@mantine/nprogress": "7.16.2", "@mantine/spotlight": "7.16.2", + "@plotly/dash-component-plugins": "^1.2.0", "dayjs": "^1.11.10", "embla-carousel-auto-scroll": "^8.4.0", "embla-carousel-autoplay": "^8.4.0", diff --git a/src/ts/components/core/NavLink.tsx b/src/ts/components/core/NavLink.tsx index e36d4ed2..7e2ae49d 100644 --- a/src/ts/components/core/NavLink.tsx +++ b/src/ts/components/core/NavLink.tsx @@ -6,8 +6,9 @@ import { import { BoxProps } from "props/box"; import { DashBaseProps, PersistenceProps } from "props/dash"; import { StylesApiProps } from "props/styles"; -import React, { MouseEvent } from "react"; +import React, { MouseEvent, useState, useEffect } from "react"; import { TargetProps, onClick } from "../../utils/anchor"; +import {History} from '@plotly/dash-component-plugins'; interface Props extends BoxProps, @@ -22,8 +23,11 @@ interface Props leftSection?: React.ReactNode; /** Section displayed on the right side of the label */ rightSection?: React.ReactNode; - /** Determines whether the link should have active styles, `false` by default */ - active?: boolean; + /** Determines whether the link should have active styles, `false` by default, + mimics NavLink behaviour from dash-bootstrap-components + exact will match the pathname exactly, whereas partial will only be concerned about the startsWith + */ + active?: boolean | 'partial' | 'exact'; /** Key of `theme.colors` of any valid CSS color to control active styles, `theme.primaryColor` by default */ color?: MantineColor; /** href */ @@ -52,6 +56,7 @@ interface Props /** NavLink */ const NavLink = (props: Props) => { + const [linkActive, setLinkActive] = useState(false); const { disabled, href, @@ -64,9 +69,28 @@ const NavLink = (props: Props) => { persistence_type, setProps, loading_state, + active, ...others } = props; + const pathnameToActive = pathname => { + setLinkActive( + active === true || + (active === 'exact' && pathname === href) || + (active === 'partial' && pathname.startsWith(href)) + ); + }; + + useEffect(() => { + pathnameToActive(window.location.pathname); + + if (typeof active === 'string') { + History.onChange(() => { + pathnameToActive(window.location.pathname); + }); + } + }, [active]); + const onChange = (state: boolean) => { setProps({ opened: state }); }; @@ -93,6 +117,7 @@ const NavLink = (props: Props) => { target={target} onChange={onChange} disabled={disabled} + active={linkActive} {...others} > {children} From 8eea3c1454554cc33ee42e57a600a5127eeb7dde Mon Sep 17 00:00:00 2001 From: BSd3v <82055130+BSd3v@users.noreply.github.com> Date: Wed, 5 Feb 2025 12:54:00 -0500 Subject: [PATCH 2/3] adding tests for navlink --- tests/test_navlink.py | 84 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 tests/test_navlink.py diff --git a/tests/test_navlink.py b/tests/test_navlink.py new file mode 100644 index 00000000..667e5207 --- /dev/null +++ b/tests/test_navlink.py @@ -0,0 +1,84 @@ +import dash +from dash import Dash, html, Output, Input, _dash_renderer, ALL, State +import dash_mantine_components as dmc + +_dash_renderer._set_react_version("18.2.0") + + +def navlink_app(active): + app = Dash(__name__, use_pages=True, pages_folder='') + + app.layout = dmc.MantineProvider([ + html.Div( + [ + dmc.NavLink(label=f"link-{x}", id=f"link-{x}", href=f"/link-{x}", active=active, + w=300) + for x in range(30) + ] + ), + dash.page_container + ]) + return app + +def self_check_navlink(active, dash_duo): + els = dash_duo.find_elements('a[data-active]') + if isinstance(active, bool): + if active: + assert len(els) == 30 + assert len(dash_duo.find_elements('a:not([data-active])')) == 0 + else: + assert len(els) == 0 + assert len(dash_duo.find_elements('a:not([data-active])')) == 30 + return + else: + assert len(els) == 0 + for t in [1, 5, 10, 12, 13, 20, 22]: + dash_duo.find_element(f'#link-{t}').click() + els = dash_duo.find_elements('a[data-active]') + if active == 'exact': + assert len(els) == 1 + else: + if len(str(t)) > 1: + assert len(els) == 2 + else: + assert len(els) == 1 + for el in els: + assert el.get_attribute('id') in f"link-{t}" + + +def test_001nl_navlink(dash_duo): + + app = navlink_app(True) + dash_duo.start_server(app) + + # Wait for the app to load + dash_duo.wait_for_element("#link-0") + self_check_navlink(True, dash_duo) + +def test_002nl_navlink(dash_duo): + + app = navlink_app(False) + dash_duo.start_server(app) + + # Wait for the app to load + dash_duo.wait_for_element("#link-0") + self_check_navlink(False, dash_duo) + +def test_003nl_navlink(dash_duo): + + app = navlink_app('exact') + dash_duo.start_server(app) + + # Wait for the app to load + dash_duo.wait_for_element("#link-0") + self_check_navlink('exact', dash_duo) + +def test_004nl_navlink(dash_duo): + + app = navlink_app('partial') + dash_duo.start_server(app) + + # Wait for the app to load + dash_duo.wait_for_element("#link-0") + self_check_navlink('partial', dash_duo) + From b063be78f55bcf039b29000d9db54620bd065387 Mon Sep 17 00:00:00 2001 From: BSd3v <82055130+BSd3v@users.noreply.github.com> Date: Wed, 5 Feb 2025 12:58:44 -0500 Subject: [PATCH 3/3] DRY principle for test --- tests/test_navlink.py | 34 +++++++++------------------------- 1 file changed, 9 insertions(+), 25 deletions(-) diff --git a/tests/test_navlink.py b/tests/test_navlink.py index 667e5207..89f9c305 100644 --- a/tests/test_navlink.py +++ b/tests/test_navlink.py @@ -20,7 +20,11 @@ def navlink_app(active): ]) return app -def self_check_navlink(active, dash_duo): +def self_check_navlink(active, dash_duo, app): + dash_duo.start_server(app) + + # Wait for the app to load + dash_duo.wait_for_element("#link-0") els = dash_duo.find_elements('a[data-active]') if isinstance(active, bool): if active: @@ -47,38 +51,18 @@ def self_check_navlink(active, dash_duo): def test_001nl_navlink(dash_duo): - app = navlink_app(True) - dash_duo.start_server(app) - - # Wait for the app to load - dash_duo.wait_for_element("#link-0") - self_check_navlink(True, dash_duo) + self_check_navlink(True, dash_duo, app) def test_002nl_navlink(dash_duo): - app = navlink_app(False) - dash_duo.start_server(app) - - # Wait for the app to load - dash_duo.wait_for_element("#link-0") - self_check_navlink(False, dash_duo) + self_check_navlink(False, dash_duo, app) def test_003nl_navlink(dash_duo): - app = navlink_app('exact') - dash_duo.start_server(app) - - # Wait for the app to load - dash_duo.wait_for_element("#link-0") - self_check_navlink('exact', dash_duo) + self_check_navlink('exact', dash_duo, app) def test_004nl_navlink(dash_duo): - app = navlink_app('partial') - dash_duo.start_server(app) - - # Wait for the app to load - dash_duo.wait_for_element("#link-0") - self_check_navlink('partial', dash_duo) + self_check_navlink('partial', dash_duo, app)