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} diff --git a/tests/test_navlink.py b/tests/test_navlink.py new file mode 100644 index 00000000..89f9c305 --- /dev/null +++ b/tests/test_navlink.py @@ -0,0 +1,68 @@ +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, 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: + 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) + self_check_navlink(True, dash_duo, app) + +def test_002nl_navlink(dash_duo): + app = navlink_app(False) + self_check_navlink(False, dash_duo, app) + +def test_003nl_navlink(dash_duo): + app = navlink_app('exact') + self_check_navlink('exact', dash_duo, app) + +def test_004nl_navlink(dash_duo): + app = navlink_app('partial') + self_check_navlink('partial', dash_duo, app) +