Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
42 changes: 39 additions & 3 deletions src/ts/components/core/NavLink.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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' | 'hyperlink';
/** Key of `theme.colors` of any valid CSS color to control active styles, `theme.primaryColor` by default */
color?: MantineColor;
/** href */
Expand Down Expand Up @@ -52,6 +56,7 @@ interface Props

/** NavLink */
const NavLink = (props: Props) => {
const [linkActive, setLinkActive] = useState(false);
const {
disabled,
href,
Expand All @@ -64,9 +69,39 @@ 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)) ||
(active === 'hyperlink' && pathname.endsWith(href))
);
};

const parsePath = (location) => {
if (active === 'hyperlink') {
pathnameToActive(location.href)
}
else {
pathnameToActive(location.pathname)
}
}
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.

I like the endsWith option so it can pick up query stings and # for scrolling to a position on a page. What do you think about using different variable names to make it more clear?

    setLinkActive(
        active === true ||
        (active === 'exact-path' && location.pathname === href) ||  
        (active === 'starts-with-path' && location.pathname.startsWith(href)) ||  
        (active === 'ends-with-url' && fullUrl.endsWith(href)) 
    );
    ```

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.

Sure, I think that would be fine.

Was more taking the lead from dbc on it.


useEffect(() => {
parsePath(window.location);

if (typeof active === 'string') {
History.onChange(() => {
parsePath(window.location);
;
});
}
}, [active]);

const onChange = (state: boolean) => {
setProps({ opened: state });
};
Expand All @@ -93,6 +128,7 @@ const NavLink = (props: Props) => {
target={target}
onChange={onChange}
disabled={disabled}
active={linkActive}
{...others}
>
{children}
Expand Down
68 changes: 68 additions & 0 deletions tests/test_navlink.py
Original file line number Diff line number Diff line change
@@ -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)