Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 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
15 changes: 11 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# Change Log

# Unreleased

### Added
- Added `CheckboxCard` `CheckboxIndicator` `RadioCard` `RadioIndicator` components #486 by @deadkex

### Changed
- Expanded the `active` prop to support string values (`"exact"` and `"partial"`) in addition to `true`/`false`. #504 by @BSd3v
- `exact`: Marks the link as active only when `pathname` exactly matches `href`.
- `partial`: Marks the link as active when `pathname` starts with `href`, allowing for subpages.


# 1.0.0rc1

### Pre-release Highlights
Expand All @@ -8,10 +19,6 @@
- If you were using `dmc < 0.15.0`, please follow our [migration guide](https://www.dash-mantine-components.com/migration).
- ⚠️ **Important:** Apps using `dmc < 1.0.0` must pin `dash < 3` to avoid compatibility issues.

### Added

- Added `CheckboxCard` `CheckboxIndicator` `RadioCard` `RadioIndicator` components #486 by @deadkex

### Changed
- Updated to handle changes in Dash 3 #506 by @AnnMarieW:
- Removed `defaultProps` to be compatible with React 18.3
Expand Down
7 changes: 7 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

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
34 changes: 31 additions & 3 deletions src/ts/components/core/NavLink.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ 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';
import { setPersistence, getLoadingState } from "../../utils/dash3";


interface Props
extends BoxProps,
StylesApiProps,
Expand All @@ -23,8 +25,12 @@ 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;
/**
* Controls whether the link is styled as active (default: `false`).
* - `exact`: Active if `pathname` matches `href` exactly.
* - `partial`: Active if `pathname` starts with `href` (for subpages).
*/
active?: boolean | 'exact' | 'partial';
/** Key of `theme.colors` of any valid CSS color to control active styles, `theme.primaryColor` by default */
color?: MantineColor;
/** href */
Expand Down Expand Up @@ -64,8 +70,28 @@ const NavLink = ({
persistence_type,
setProps,
loading_state,
active = false,
...others
}: Props) => {
const [linkActive, setLinkActive] = useState(false);

const pathnameToActive = pathname => {
setLinkActive(
active === true ||
(active === 'exact' && pathname === href) ||
(active === 'partial' && pathname.startsWith(href))
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 still think startsWith is going to lead to unintended consequences, and partial should instead use

pathname === href || pathname.startsWith(href + '/')

To account for paths like:

/al
/al/subpage
/alex
/alex/subpage

If you use just startsWith, then href='/al' would match all of these, but you really only want to match the first two.

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.

if you do this

pathname === href || pathname.startsWith(href + '/')

and links looks like:

 dmc.NavLink(label="al", href="/al", active='partial'),
 dmc.NavLink(label="al2", href="/al/2", active='partial'),

Then if you click on the first link neither is active and when you click on the second link then only the /al page is active.

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.

@BSd3v suggested this and it works well 🙂

   (active === 'partial' && (pathname.startsWith(href + '/') || pathname == href))

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.

oh, I see that's what you had too @alexcjohnson 😊 I was just fixated on the href + '/' part and missed it.

);
};

useEffect(() => {
pathnameToActive(window.location.pathname);

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

const onChange = (state: boolean) => {
setProps({ opened: state });
Expand All @@ -91,6 +117,7 @@ const NavLink = ({
target={target}
onChange={onChange}
disabled={disabled}
active={linkActive}
{...others}
>
{children}
Expand All @@ -102,6 +129,7 @@ const NavLink = ({
disabled={disabled}
onChange={onChange}
onClick={increment}
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)