@dloizides/ui-nav 1.17.0 → 1.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -0
- package/dist/index.d.mts +75 -37
- package/dist/index.d.ts +75 -37
- package/dist/index.js +35 -6
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +35 -7
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.18.0
|
|
4
|
+
|
|
5
|
+
Root-cause fix for sidebar leaf co-activation. Behavioral/additive — no breaking API.
|
|
6
|
+
|
|
7
|
+
- **Fix (two sidebar leaves showed `aria-current="page"` at once).** On the live finreg
|
|
8
|
+
`/accounting/journals`, BOTH "Chart of Accounts" (route `/accounting`) and "Journals"
|
|
9
|
+
(route `/accounting/journals`) were marked active, because the active predicate was purely
|
|
10
|
+
prefix-based (`pathname === route || pathname.startsWith(route + '/')`) — a module-home leaf
|
|
11
|
+
routed at `/accounting` is a prefix of every `/accounting/*` sibling, so it co-activated on all
|
|
12
|
+
of them. (Second occurrence of this class; the first, CRM Contacts at `/crm`, was worked around
|
|
13
|
+
by moving the route.) Now resolution is **most-specific (longest) match wins**: among a nav
|
|
14
|
+
model's LEAF items, at most ONE is active for any path — the leaf whose route is the longest
|
|
15
|
+
prefix match. On `/accounting/journals` only Journals is active; on `/accounting` exactly only
|
|
16
|
+
Chart of Accounts is. For nav models with no parent/child route overlap (the common case)
|
|
17
|
+
behaviour is unchanged.
|
|
18
|
+
- **New export** `resolveActiveRoute(items, pathname)`: flattens the tree to its leaf routes and
|
|
19
|
+
returns the single longest matching route (honouring per-leaf `exact`), or `undefined`.
|
|
20
|
+
- `Sidebar` resolves the winning route once over the full leaf set and passes `activeRoute` to
|
|
21
|
+
each row; `NavExpandableItem` marks a leaf active iff `item.route === activeRoute` (falls back
|
|
22
|
+
to the per-item predicate when used standalone — backward compatible). Group HEADER expansion
|
|
23
|
+
(`hasActiveDescendant`) is a separate concern and is unchanged.
|
|
24
|
+
- The same winner-based resolution now backs `NavBar`, `PillNav`, `CollapsedRail` and
|
|
25
|
+
`NavOverflowMenu`, so the latent prefix co-activation cannot resurface in the horizontal navs.
|
|
26
|
+
- **Additive API**: new optional `NavItem.exact?: boolean` — a leaf active ONLY on its exact
|
|
27
|
+
path (never a nested one). A complementary escape hatch; the longest-match default is the real
|
|
28
|
+
fix, so no consumer needs to set it.
|
|
29
|
+
- `isRouteActive(pathname, route, exact?)` gains an optional third arg; the two-arg call is
|
|
30
|
+
unchanged.
|
|
31
|
+
|
|
3
32
|
## 1.17.0
|
|
4
33
|
|
|
5
34
|
Two live-defect fixes to the shared sidebar chrome. Behavioral/additive — no breaking API.
|
package/dist/index.d.mts
CHANGED
|
@@ -5,12 +5,71 @@ import { DropdownVariant } from '@dloizides/ui-layout';
|
|
|
5
5
|
import { resolveAccessibleRoutes, RoleRouteTable, RoleRoute } from '@dloizides/auth-web';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* only
|
|
8
|
+
* Public prop types for the `@dloizides/ui-nav` config-driven navigation shell.
|
|
9
|
+
*
|
|
10
|
+
* The rendering chrome (sidebar + topbar) was byte-identical across erevna-web
|
|
11
|
+
* and katalogos-web; only the *item data* (which routes, how they group) differs
|
|
12
|
+
* per app. So this package renders a caller-supplied `NavItem[]` — labels are
|
|
13
|
+
* pre-localized strings and icons are render slots, keeping the package free of
|
|
14
|
+
* any app's i18n helper, icon set, router, or store.
|
|
12
15
|
*/
|
|
13
|
-
|
|
16
|
+
|
|
17
|
+
/** One navigation entry. Labels are already localized by the caller. */
|
|
18
|
+
interface NavItem {
|
|
19
|
+
/** Stable key + default testID. */
|
|
20
|
+
key: string;
|
|
21
|
+
/** Localized display label. */
|
|
22
|
+
label: string;
|
|
23
|
+
/** Route/path this item navigates to (passed back to `onNavigate`). */
|
|
24
|
+
route: string;
|
|
25
|
+
/** Optional testID override (defaults to `key`). */
|
|
26
|
+
testID?: string;
|
|
27
|
+
/**
|
|
28
|
+
* Optional leading icon. Receives the resolved foreground colour and a size,
|
|
29
|
+
* so the app supplies its own icon component without this package importing
|
|
30
|
+
* an icon set.
|
|
31
|
+
*/
|
|
32
|
+
renderIcon?: (color: string, size: number) => React.ReactNode;
|
|
33
|
+
/** Optional nested items — rendered as an expandable section. */
|
|
34
|
+
children?: NavItem[];
|
|
35
|
+
/**
|
|
36
|
+
* Opt this leaf out of prefix matching: when `true` it is active ONLY when the
|
|
37
|
+
* pathname equals its route exactly (never for a nested `route/child` path). A
|
|
38
|
+
* complementary escape hatch — the default most-specific/longest-match resolution
|
|
39
|
+
* (see `resolveActiveRoute`) already prevents a module-home leaf like `/accounting`
|
|
40
|
+
* from co-activating with `/accounting/journals`, so most consumers never need
|
|
41
|
+
* this. Reach for it only when a leaf must NEVER light up on any descendant path
|
|
42
|
+
* even when no longer sibling route exists.
|
|
43
|
+
*/
|
|
44
|
+
exact?: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Active-route matching for the nav shells.
|
|
49
|
+
*
|
|
50
|
+
* `isRouteActive` is the low-level per-route predicate (prefix-based, ported
|
|
51
|
+
* verbatim from the twin app sidebars): a route is active when the current
|
|
52
|
+
* pathname equals it or is nested under it (`/foo` matches `/foo` and `/foo/bar`,
|
|
53
|
+
* but `/` only matches `/`). With `exact`, only an exact path matches.
|
|
54
|
+
*
|
|
55
|
+
* `resolveActiveRoute` is the CROSS-ITEM resolver that fixes the sidebar
|
|
56
|
+
* co-activation bug: because a module-home leaf (`/accounting`) is a prefix of
|
|
57
|
+
* every sibling (`/accounting/journals`), the naive per-item predicate lights up
|
|
58
|
+
* BOTH on `/accounting/journals`. `resolveActiveRoute` instead returns the ONE
|
|
59
|
+
* most-specific (longest) leaf route that matches — so at most one leaf is ever
|
|
60
|
+
* active, the longest wins on nested paths, and the module-home leaf is active
|
|
61
|
+
* only on its own exact path. For a nav model with no parent/child route overlap
|
|
62
|
+
* (the common case) this is identical to the old behaviour.
|
|
63
|
+
*/
|
|
64
|
+
|
|
65
|
+
declare function isRouteActive(pathname: string, route: string, exact?: boolean): boolean;
|
|
66
|
+
/**
|
|
67
|
+
* The single active leaf route for `pathname`: the LONGEST leaf route that
|
|
68
|
+
* matches (most-specific wins). Returns `undefined` when no leaf matches. Marking
|
|
69
|
+
* a leaf active iff `item.route === resolveActiveRoute(items, pathname)` guarantees
|
|
70
|
+
* at most one active leaf — the whack-a-mole prefix co-activation fix.
|
|
71
|
+
*/
|
|
72
|
+
declare function resolveActiveRoute(items: NavItem[], pathname: string): string | undefined;
|
|
14
73
|
|
|
15
74
|
/**
|
|
16
75
|
* SidebarSearch — the persistent, DESKTOP inline search field that lives at the
|
|
@@ -51,36 +110,6 @@ interface SidebarSearchProps {
|
|
|
51
110
|
/** The inline sidebar search field (desktop) with a clear button. */
|
|
52
111
|
declare const SidebarSearch: ({ value, onChangeText, onClear, labels, testID, }: SidebarSearchProps) => React.ReactElement;
|
|
53
112
|
|
|
54
|
-
/**
|
|
55
|
-
* Public prop types for the `@dloizides/ui-nav` config-driven navigation shell.
|
|
56
|
-
*
|
|
57
|
-
* The rendering chrome (sidebar + topbar) was byte-identical across erevna-web
|
|
58
|
-
* and katalogos-web; only the *item data* (which routes, how they group) differs
|
|
59
|
-
* per app. So this package renders a caller-supplied `NavItem[]` — labels are
|
|
60
|
-
* pre-localized strings and icons are render slots, keeping the package free of
|
|
61
|
-
* any app's i18n helper, icon set, router, or store.
|
|
62
|
-
*/
|
|
63
|
-
|
|
64
|
-
/** One navigation entry. Labels are already localized by the caller. */
|
|
65
|
-
interface NavItem {
|
|
66
|
-
/** Stable key + default testID. */
|
|
67
|
-
key: string;
|
|
68
|
-
/** Localized display label. */
|
|
69
|
-
label: string;
|
|
70
|
-
/** Route/path this item navigates to (passed back to `onNavigate`). */
|
|
71
|
-
route: string;
|
|
72
|
-
/** Optional testID override (defaults to `key`). */
|
|
73
|
-
testID?: string;
|
|
74
|
-
/**
|
|
75
|
-
* Optional leading icon. Receives the resolved foreground colour and a size,
|
|
76
|
-
* so the app supplies its own icon component without this package importing
|
|
77
|
-
* an icon set.
|
|
78
|
-
*/
|
|
79
|
-
renderIcon?: (color: string, size: number) => React.ReactNode;
|
|
80
|
-
/** Optional nested items — rendered as an expandable section. */
|
|
81
|
-
children?: NavItem[];
|
|
82
|
-
}
|
|
83
|
-
|
|
84
113
|
/**
|
|
85
114
|
* Sidebar — the config-driven left navigation shell promoted from the
|
|
86
115
|
* byte-identical erevna-web / katalogos-web `Sidebar`. It renders a caller
|
|
@@ -1080,6 +1109,15 @@ interface NavExpandableItemProps {
|
|
|
1080
1109
|
item: NavItem;
|
|
1081
1110
|
pathname: string;
|
|
1082
1111
|
onNavigate: (route: string) => void;
|
|
1112
|
+
/**
|
|
1113
|
+
* The single winning active leaf route for the whole nav model, resolved once by
|
|
1114
|
+
* the parent via `resolveActiveRoute` (most-specific/longest match wins). A leaf
|
|
1115
|
+
* is active iff `item.route === activeRoute`, so at most one leaf ever carries
|
|
1116
|
+
* `aria-current="page"` — the fix for `/accounting` co-activating with
|
|
1117
|
+
* `/accounting/journals`. When omitted (standalone use) each leaf falls back to
|
|
1118
|
+
* the per-item prefix predicate, preserving the pre-1.18 behaviour.
|
|
1119
|
+
*/
|
|
1120
|
+
activeRoute?: string;
|
|
1083
1121
|
/** a11y hint for a leaf item, given its label. */
|
|
1084
1122
|
navigateHint: (label: string) => string;
|
|
1085
1123
|
/** a11y hint shown when the section is collapsed (press expands). */
|
|
@@ -1094,7 +1132,7 @@ interface NavExpandableItemProps {
|
|
|
1094
1132
|
renderChevron?: (expanded: boolean, color: string, size: number) => React.ReactNode;
|
|
1095
1133
|
depth?: number;
|
|
1096
1134
|
}
|
|
1097
|
-
declare const NavExpandableItem: ({ item, pathname, onNavigate, navigateHint, expandHint, collapseHint, renderChevron, depth, }: NavExpandableItemProps) => React.ReactElement;
|
|
1135
|
+
declare const NavExpandableItem: ({ item, pathname, onNavigate, navigateHint, expandHint, collapseHint, renderChevron, activeRoute, depth, }: NavExpandableItemProps) => React.ReactElement;
|
|
1098
1136
|
|
|
1099
1137
|
/**
|
|
1100
1138
|
* Pure matching logic for the {@link CommandPalette}. Kept framework-free so the
|
|
@@ -1489,4 +1527,4 @@ declare const NAV_ICON_SIZE = 14;
|
|
|
1489
1527
|
/** Chevron icon size for expandable sections. */
|
|
1490
1528
|
declare const CHEVRON_ICON_SIZE = 12;
|
|
1491
1529
|
|
|
1492
|
-
export { ACTIVE_BORDER_RADIUS, ACTIVE_TINT_OPACITY, APP_SHELL_SUFFIX, type AccountPlan, type AccountState, AppShell, type AppShellProps, type AppShellWidth, BASE_INDENT, CHEVRON_ICON_SIZE, CollapsedRail, type CollapsedRailProps, type CollapsedRailRange, type CommandItem, CommandPalette, type CommandPaletteLabels, type CommandPaletteProps, CommandPaletteTrigger, type CommandPaletteTriggerProps, DEFAULT_COLLAPSED_RAIL_MAX, DEFAULT_SEARCH_BREAKPOINT, DarkModeControl, type DarkModeControlProps, type DarkModeOption, type DarkModeVariant, HOVER_TINT_OPACITY, NAV_ICON_SIZE, NAV_LINK_GAP, NAV_ROW_RADIUS, NAV_TEST_IDS, Nav, NavBar, type NavBarProps, NavExpandableItem, type NavExpandableItemProps, type NavItem, type NavOrientation, NavOverflowMenu, type NavOverflowMenuProps, type NavProps, NavShell, type NavShellCollapsedRail, type NavShellLayout, type NavShellProps, type NavShellSideRail, type NavShellTopBar, type NavUser, PillNav, type PillNavProps, RAIL_FULL_BREAKPOINT, type RailMode, type ResolveRailModeArgs, type SearchAffordance, type ShellMessage, Sidebar, SidebarChevron, type SidebarChevronProps, type SidebarPaletteTriggerConfig, type SidebarProps, SidebarSearch, type SidebarSearchConfig, type SidebarSearchLabels, type SidebarSearchProps, Topbar, type TopbarAction, type TopbarProps, type TopbarUser, accessibleNavItems, collapsedRailStyles, darkModeStyles, expandableStyles, filterCommands, filterNavItems, isFilterActive, isRouteActive, navStyles, pillNavStyles, resolveContentMaxWidth, resolveRailMode, resolveSearchAffordance, roleRoutesToNavItems, searchTokens, useCommandPaletteHotkey, useContentMaxWidth };
|
|
1530
|
+
export { ACTIVE_BORDER_RADIUS, ACTIVE_TINT_OPACITY, APP_SHELL_SUFFIX, type AccountPlan, type AccountState, AppShell, type AppShellProps, type AppShellWidth, BASE_INDENT, CHEVRON_ICON_SIZE, CollapsedRail, type CollapsedRailProps, type CollapsedRailRange, type CommandItem, CommandPalette, type CommandPaletteLabels, type CommandPaletteProps, CommandPaletteTrigger, type CommandPaletteTriggerProps, DEFAULT_COLLAPSED_RAIL_MAX, DEFAULT_SEARCH_BREAKPOINT, DarkModeControl, type DarkModeControlProps, type DarkModeOption, type DarkModeVariant, HOVER_TINT_OPACITY, NAV_ICON_SIZE, NAV_LINK_GAP, NAV_ROW_RADIUS, NAV_TEST_IDS, Nav, NavBar, type NavBarProps, NavExpandableItem, type NavExpandableItemProps, type NavItem, type NavOrientation, NavOverflowMenu, type NavOverflowMenuProps, type NavProps, NavShell, type NavShellCollapsedRail, type NavShellLayout, type NavShellProps, type NavShellSideRail, type NavShellTopBar, type NavUser, PillNav, type PillNavProps, RAIL_FULL_BREAKPOINT, type RailMode, type ResolveRailModeArgs, type SearchAffordance, type ShellMessage, Sidebar, SidebarChevron, type SidebarChevronProps, type SidebarPaletteTriggerConfig, type SidebarProps, SidebarSearch, type SidebarSearchConfig, type SidebarSearchLabels, type SidebarSearchProps, Topbar, type TopbarAction, type TopbarProps, type TopbarUser, accessibleNavItems, collapsedRailStyles, darkModeStyles, expandableStyles, filterCommands, filterNavItems, isFilterActive, isRouteActive, navStyles, pillNavStyles, resolveActiveRoute, resolveContentMaxWidth, resolveRailMode, resolveSearchAffordance, roleRoutesToNavItems, searchTokens, useCommandPaletteHotkey, useContentMaxWidth };
|
package/dist/index.d.ts
CHANGED
|
@@ -5,12 +5,71 @@ import { DropdownVariant } from '@dloizides/ui-layout';
|
|
|
5
5
|
import { resolveAccessibleRoutes, RoleRouteTable, RoleRoute } from '@dloizides/auth-web';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* only
|
|
8
|
+
* Public prop types for the `@dloizides/ui-nav` config-driven navigation shell.
|
|
9
|
+
*
|
|
10
|
+
* The rendering chrome (sidebar + topbar) was byte-identical across erevna-web
|
|
11
|
+
* and katalogos-web; only the *item data* (which routes, how they group) differs
|
|
12
|
+
* per app. So this package renders a caller-supplied `NavItem[]` — labels are
|
|
13
|
+
* pre-localized strings and icons are render slots, keeping the package free of
|
|
14
|
+
* any app's i18n helper, icon set, router, or store.
|
|
12
15
|
*/
|
|
13
|
-
|
|
16
|
+
|
|
17
|
+
/** One navigation entry. Labels are already localized by the caller. */
|
|
18
|
+
interface NavItem {
|
|
19
|
+
/** Stable key + default testID. */
|
|
20
|
+
key: string;
|
|
21
|
+
/** Localized display label. */
|
|
22
|
+
label: string;
|
|
23
|
+
/** Route/path this item navigates to (passed back to `onNavigate`). */
|
|
24
|
+
route: string;
|
|
25
|
+
/** Optional testID override (defaults to `key`). */
|
|
26
|
+
testID?: string;
|
|
27
|
+
/**
|
|
28
|
+
* Optional leading icon. Receives the resolved foreground colour and a size,
|
|
29
|
+
* so the app supplies its own icon component without this package importing
|
|
30
|
+
* an icon set.
|
|
31
|
+
*/
|
|
32
|
+
renderIcon?: (color: string, size: number) => React.ReactNode;
|
|
33
|
+
/** Optional nested items — rendered as an expandable section. */
|
|
34
|
+
children?: NavItem[];
|
|
35
|
+
/**
|
|
36
|
+
* Opt this leaf out of prefix matching: when `true` it is active ONLY when the
|
|
37
|
+
* pathname equals its route exactly (never for a nested `route/child` path). A
|
|
38
|
+
* complementary escape hatch — the default most-specific/longest-match resolution
|
|
39
|
+
* (see `resolveActiveRoute`) already prevents a module-home leaf like `/accounting`
|
|
40
|
+
* from co-activating with `/accounting/journals`, so most consumers never need
|
|
41
|
+
* this. Reach for it only when a leaf must NEVER light up on any descendant path
|
|
42
|
+
* even when no longer sibling route exists.
|
|
43
|
+
*/
|
|
44
|
+
exact?: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Active-route matching for the nav shells.
|
|
49
|
+
*
|
|
50
|
+
* `isRouteActive` is the low-level per-route predicate (prefix-based, ported
|
|
51
|
+
* verbatim from the twin app sidebars): a route is active when the current
|
|
52
|
+
* pathname equals it or is nested under it (`/foo` matches `/foo` and `/foo/bar`,
|
|
53
|
+
* but `/` only matches `/`). With `exact`, only an exact path matches.
|
|
54
|
+
*
|
|
55
|
+
* `resolveActiveRoute` is the CROSS-ITEM resolver that fixes the sidebar
|
|
56
|
+
* co-activation bug: because a module-home leaf (`/accounting`) is a prefix of
|
|
57
|
+
* every sibling (`/accounting/journals`), the naive per-item predicate lights up
|
|
58
|
+
* BOTH on `/accounting/journals`. `resolveActiveRoute` instead returns the ONE
|
|
59
|
+
* most-specific (longest) leaf route that matches — so at most one leaf is ever
|
|
60
|
+
* active, the longest wins on nested paths, and the module-home leaf is active
|
|
61
|
+
* only on its own exact path. For a nav model with no parent/child route overlap
|
|
62
|
+
* (the common case) this is identical to the old behaviour.
|
|
63
|
+
*/
|
|
64
|
+
|
|
65
|
+
declare function isRouteActive(pathname: string, route: string, exact?: boolean): boolean;
|
|
66
|
+
/**
|
|
67
|
+
* The single active leaf route for `pathname`: the LONGEST leaf route that
|
|
68
|
+
* matches (most-specific wins). Returns `undefined` when no leaf matches. Marking
|
|
69
|
+
* a leaf active iff `item.route === resolveActiveRoute(items, pathname)` guarantees
|
|
70
|
+
* at most one active leaf — the whack-a-mole prefix co-activation fix.
|
|
71
|
+
*/
|
|
72
|
+
declare function resolveActiveRoute(items: NavItem[], pathname: string): string | undefined;
|
|
14
73
|
|
|
15
74
|
/**
|
|
16
75
|
* SidebarSearch — the persistent, DESKTOP inline search field that lives at the
|
|
@@ -51,36 +110,6 @@ interface SidebarSearchProps {
|
|
|
51
110
|
/** The inline sidebar search field (desktop) with a clear button. */
|
|
52
111
|
declare const SidebarSearch: ({ value, onChangeText, onClear, labels, testID, }: SidebarSearchProps) => React.ReactElement;
|
|
53
112
|
|
|
54
|
-
/**
|
|
55
|
-
* Public prop types for the `@dloizides/ui-nav` config-driven navigation shell.
|
|
56
|
-
*
|
|
57
|
-
* The rendering chrome (sidebar + topbar) was byte-identical across erevna-web
|
|
58
|
-
* and katalogos-web; only the *item data* (which routes, how they group) differs
|
|
59
|
-
* per app. So this package renders a caller-supplied `NavItem[]` — labels are
|
|
60
|
-
* pre-localized strings and icons are render slots, keeping the package free of
|
|
61
|
-
* any app's i18n helper, icon set, router, or store.
|
|
62
|
-
*/
|
|
63
|
-
|
|
64
|
-
/** One navigation entry. Labels are already localized by the caller. */
|
|
65
|
-
interface NavItem {
|
|
66
|
-
/** Stable key + default testID. */
|
|
67
|
-
key: string;
|
|
68
|
-
/** Localized display label. */
|
|
69
|
-
label: string;
|
|
70
|
-
/** Route/path this item navigates to (passed back to `onNavigate`). */
|
|
71
|
-
route: string;
|
|
72
|
-
/** Optional testID override (defaults to `key`). */
|
|
73
|
-
testID?: string;
|
|
74
|
-
/**
|
|
75
|
-
* Optional leading icon. Receives the resolved foreground colour and a size,
|
|
76
|
-
* so the app supplies its own icon component without this package importing
|
|
77
|
-
* an icon set.
|
|
78
|
-
*/
|
|
79
|
-
renderIcon?: (color: string, size: number) => React.ReactNode;
|
|
80
|
-
/** Optional nested items — rendered as an expandable section. */
|
|
81
|
-
children?: NavItem[];
|
|
82
|
-
}
|
|
83
|
-
|
|
84
113
|
/**
|
|
85
114
|
* Sidebar — the config-driven left navigation shell promoted from the
|
|
86
115
|
* byte-identical erevna-web / katalogos-web `Sidebar`. It renders a caller
|
|
@@ -1080,6 +1109,15 @@ interface NavExpandableItemProps {
|
|
|
1080
1109
|
item: NavItem;
|
|
1081
1110
|
pathname: string;
|
|
1082
1111
|
onNavigate: (route: string) => void;
|
|
1112
|
+
/**
|
|
1113
|
+
* The single winning active leaf route for the whole nav model, resolved once by
|
|
1114
|
+
* the parent via `resolveActiveRoute` (most-specific/longest match wins). A leaf
|
|
1115
|
+
* is active iff `item.route === activeRoute`, so at most one leaf ever carries
|
|
1116
|
+
* `aria-current="page"` — the fix for `/accounting` co-activating with
|
|
1117
|
+
* `/accounting/journals`. When omitted (standalone use) each leaf falls back to
|
|
1118
|
+
* the per-item prefix predicate, preserving the pre-1.18 behaviour.
|
|
1119
|
+
*/
|
|
1120
|
+
activeRoute?: string;
|
|
1083
1121
|
/** a11y hint for a leaf item, given its label. */
|
|
1084
1122
|
navigateHint: (label: string) => string;
|
|
1085
1123
|
/** a11y hint shown when the section is collapsed (press expands). */
|
|
@@ -1094,7 +1132,7 @@ interface NavExpandableItemProps {
|
|
|
1094
1132
|
renderChevron?: (expanded: boolean, color: string, size: number) => React.ReactNode;
|
|
1095
1133
|
depth?: number;
|
|
1096
1134
|
}
|
|
1097
|
-
declare const NavExpandableItem: ({ item, pathname, onNavigate, navigateHint, expandHint, collapseHint, renderChevron, depth, }: NavExpandableItemProps) => React.ReactElement;
|
|
1135
|
+
declare const NavExpandableItem: ({ item, pathname, onNavigate, navigateHint, expandHint, collapseHint, renderChevron, activeRoute, depth, }: NavExpandableItemProps) => React.ReactElement;
|
|
1098
1136
|
|
|
1099
1137
|
/**
|
|
1100
1138
|
* Pure matching logic for the {@link CommandPalette}. Kept framework-free so the
|
|
@@ -1489,4 +1527,4 @@ declare const NAV_ICON_SIZE = 14;
|
|
|
1489
1527
|
/** Chevron icon size for expandable sections. */
|
|
1490
1528
|
declare const CHEVRON_ICON_SIZE = 12;
|
|
1491
1529
|
|
|
1492
|
-
export { ACTIVE_BORDER_RADIUS, ACTIVE_TINT_OPACITY, APP_SHELL_SUFFIX, type AccountPlan, type AccountState, AppShell, type AppShellProps, type AppShellWidth, BASE_INDENT, CHEVRON_ICON_SIZE, CollapsedRail, type CollapsedRailProps, type CollapsedRailRange, type CommandItem, CommandPalette, type CommandPaletteLabels, type CommandPaletteProps, CommandPaletteTrigger, type CommandPaletteTriggerProps, DEFAULT_COLLAPSED_RAIL_MAX, DEFAULT_SEARCH_BREAKPOINT, DarkModeControl, type DarkModeControlProps, type DarkModeOption, type DarkModeVariant, HOVER_TINT_OPACITY, NAV_ICON_SIZE, NAV_LINK_GAP, NAV_ROW_RADIUS, NAV_TEST_IDS, Nav, NavBar, type NavBarProps, NavExpandableItem, type NavExpandableItemProps, type NavItem, type NavOrientation, NavOverflowMenu, type NavOverflowMenuProps, type NavProps, NavShell, type NavShellCollapsedRail, type NavShellLayout, type NavShellProps, type NavShellSideRail, type NavShellTopBar, type NavUser, PillNav, type PillNavProps, RAIL_FULL_BREAKPOINT, type RailMode, type ResolveRailModeArgs, type SearchAffordance, type ShellMessage, Sidebar, SidebarChevron, type SidebarChevronProps, type SidebarPaletteTriggerConfig, type SidebarProps, SidebarSearch, type SidebarSearchConfig, type SidebarSearchLabels, type SidebarSearchProps, Topbar, type TopbarAction, type TopbarProps, type TopbarUser, accessibleNavItems, collapsedRailStyles, darkModeStyles, expandableStyles, filterCommands, filterNavItems, isFilterActive, isRouteActive, navStyles, pillNavStyles, resolveContentMaxWidth, resolveRailMode, resolveSearchAffordance, roleRoutesToNavItems, searchTokens, useCommandPaletteHotkey, useContentMaxWidth };
|
|
1530
|
+
export { ACTIVE_BORDER_RADIUS, ACTIVE_TINT_OPACITY, APP_SHELL_SUFFIX, type AccountPlan, type AccountState, AppShell, type AppShellProps, type AppShellWidth, BASE_INDENT, CHEVRON_ICON_SIZE, CollapsedRail, type CollapsedRailProps, type CollapsedRailRange, type CommandItem, CommandPalette, type CommandPaletteLabels, type CommandPaletteProps, CommandPaletteTrigger, type CommandPaletteTriggerProps, DEFAULT_COLLAPSED_RAIL_MAX, DEFAULT_SEARCH_BREAKPOINT, DarkModeControl, type DarkModeControlProps, type DarkModeOption, type DarkModeVariant, HOVER_TINT_OPACITY, NAV_ICON_SIZE, NAV_LINK_GAP, NAV_ROW_RADIUS, NAV_TEST_IDS, Nav, NavBar, type NavBarProps, NavExpandableItem, type NavExpandableItemProps, type NavItem, type NavOrientation, NavOverflowMenu, type NavOverflowMenuProps, type NavProps, NavShell, type NavShellCollapsedRail, type NavShellLayout, type NavShellProps, type NavShellSideRail, type NavShellTopBar, type NavUser, PillNav, type PillNavProps, RAIL_FULL_BREAKPOINT, type RailMode, type ResolveRailModeArgs, type SearchAffordance, type ShellMessage, Sidebar, SidebarChevron, type SidebarChevronProps, type SidebarPaletteTriggerConfig, type SidebarProps, SidebarSearch, type SidebarSearchConfig, type SidebarSearchLabels, type SidebarSearchProps, Topbar, type TopbarAction, type TopbarProps, type TopbarUser, accessibleNavItems, collapsedRailStyles, darkModeStyles, expandableStyles, filterCommands, filterNavItems, isFilterActive, isRouteActive, navStyles, pillNavStyles, resolveActiveRoute, resolveContentMaxWidth, resolveRailMode, resolveSearchAffordance, roleRoutesToNavItems, searchTokens, useCommandPaletteHotkey, useContentMaxWidth };
|
package/dist/index.js
CHANGED
|
@@ -128,10 +128,31 @@ function isFilterActive(query) {
|
|
|
128
128
|
}
|
|
129
129
|
|
|
130
130
|
// src/isRouteActive.ts
|
|
131
|
-
function isRouteActive(pathname, route) {
|
|
131
|
+
function isRouteActive(pathname, route, exact = false) {
|
|
132
|
+
if (exact) return pathname === route;
|
|
132
133
|
if (route === "/") return pathname === "/";
|
|
133
134
|
return pathname === route || pathname.startsWith(`${route}/`);
|
|
134
135
|
}
|
|
136
|
+
function leafRoutes(items) {
|
|
137
|
+
return items.flatMap((item) => {
|
|
138
|
+
const children = item.children ?? [];
|
|
139
|
+
const isGroup = children.length > 0;
|
|
140
|
+
return isGroup ? leafRoutes(children) : [{ route: item.route, exact: item.exact === true }];
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
function resolveActiveRoute(items, pathname) {
|
|
144
|
+
let winner;
|
|
145
|
+
let winnerLength = -1;
|
|
146
|
+
for (const leaf of leafRoutes(items)) {
|
|
147
|
+
const matches = isRouteActive(pathname, leaf.route, leaf.exact);
|
|
148
|
+
const isLonger = leaf.route.length > winnerLength;
|
|
149
|
+
if (matches && isLonger) {
|
|
150
|
+
winner = leaf.route;
|
|
151
|
+
winnerLength = leaf.route.length;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return winner;
|
|
155
|
+
}
|
|
135
156
|
var ACTIVE_BORDER_RADIUS = 4;
|
|
136
157
|
var ACTIVE_ACCENT_WIDTH = 3;
|
|
137
158
|
var NAV_ROW_RADIUS = 8;
|
|
@@ -509,6 +530,7 @@ var NavExpandableItem = ({
|
|
|
509
530
|
expandHint,
|
|
510
531
|
collapseHint,
|
|
511
532
|
renderChevron,
|
|
533
|
+
activeRoute,
|
|
512
534
|
depth = 0
|
|
513
535
|
}) => {
|
|
514
536
|
const [expanded, setExpanded] = React.useState(() => hasActiveDescendant(item, pathname));
|
|
@@ -522,7 +544,7 @@ var NavExpandableItem = ({
|
|
|
522
544
|
const onHoverOut = React.useCallback(() => setHovered(false), []);
|
|
523
545
|
const indent = depth * BASE_INDENT;
|
|
524
546
|
const hasChildren = Array.isArray(item.children) && item.children.length > 0;
|
|
525
|
-
const isActive = isRouteActive(pathname, item.route);
|
|
547
|
+
const isActive = activeRoute !== void 0 ? item.route === activeRoute : isRouteActive(pathname, item.route, item.exact === true);
|
|
526
548
|
const isSectionHeader = hasChildren && depth === 0;
|
|
527
549
|
const activeAccentStyle = React.useMemo(
|
|
528
550
|
() => ({ borderLeftWidth: ACTIVE_ACCENT_WIDTH, borderLeftColor: primaryColor }),
|
|
@@ -610,6 +632,7 @@ var NavExpandableItem = ({
|
|
|
610
632
|
/* @__PURE__ */ jsxRuntime.jsx(uiMotion.Collapse, { open: expanded, children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: expandableStyles.childrenContainer, children: item.children?.map((child) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
611
633
|
NavExpandableItem,
|
|
612
634
|
{
|
|
635
|
+
activeRoute,
|
|
613
636
|
collapseHint,
|
|
614
637
|
depth: depth + 1,
|
|
615
638
|
expandHint,
|
|
@@ -760,6 +783,7 @@ var Sidebar = ({
|
|
|
760
783
|
const showPaletteTrigger = paletteTrigger !== void 0 && !isInlineAffordance;
|
|
761
784
|
const visibleItems = showInlineSearch ? filterNavItems(items, query) : items;
|
|
762
785
|
const showEmpty = showInlineSearch && isFilterActive(query) && visibleItems.length === 0;
|
|
786
|
+
const activeRoute = resolveActiveRoute(items, pathname);
|
|
763
787
|
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
764
788
|
reactNative.View,
|
|
765
789
|
{
|
|
@@ -797,6 +821,7 @@ var Sidebar = ({
|
|
|
797
821
|
showEmpty && search !== void 0 ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [navStyles.sidebarEmpty, { color: colors.textSecondary }], testID: "sidebar-search-empty", children: search.emptyText }) : visibleItems.map((item) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
798
822
|
NavExpandableItem,
|
|
799
823
|
{
|
|
824
|
+
activeRoute,
|
|
800
825
|
collapseHint,
|
|
801
826
|
expandHint,
|
|
802
827
|
item,
|
|
@@ -1061,7 +1086,7 @@ var NavOverflowMenu = ({
|
|
|
1061
1086
|
}) => {
|
|
1062
1087
|
const options = React.useMemo(() => items.map((item) => ({ label: item.label, value: item.route })), [items]);
|
|
1063
1088
|
const activeRoute = React.useMemo(
|
|
1064
|
-
() => items
|
|
1089
|
+
() => resolveActiveRoute(items, pathname) ?? NO_ACTIVE_ROUTE,
|
|
1065
1090
|
[items, pathname]
|
|
1066
1091
|
);
|
|
1067
1092
|
const optionTestID = React.useMemo(() => {
|
|
@@ -1194,6 +1219,7 @@ var NavBarInner = ({
|
|
|
1194
1219
|
},
|
|
1195
1220
|
[onNavigate]
|
|
1196
1221
|
);
|
|
1222
|
+
const activeRoute = resolveActiveRoute(items, pathname);
|
|
1197
1223
|
const linkColors = React.useMemo(
|
|
1198
1224
|
() => ({
|
|
1199
1225
|
rest: colors.textSecondary,
|
|
@@ -1241,7 +1267,7 @@ var NavBarInner = ({
|
|
|
1241
1267
|
collapsed,
|
|
1242
1268
|
colors: linkColors,
|
|
1243
1269
|
iconSize: NAV_ICON_SIZE,
|
|
1244
|
-
isActive:
|
|
1270
|
+
isActive: item.route === activeRoute,
|
|
1245
1271
|
item,
|
|
1246
1272
|
navigateHint,
|
|
1247
1273
|
reducedMotion,
|
|
@@ -1603,6 +1629,7 @@ var CollapsedRail = ({
|
|
|
1603
1629
|
const { theme } = uiFeedback.useUi();
|
|
1604
1630
|
const colors = theme.colors;
|
|
1605
1631
|
const primaryColor = theme.palette.primary["500"];
|
|
1632
|
+
const activeRoute = resolveActiveRoute(items, pathname);
|
|
1606
1633
|
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1607
1634
|
reactNative.View,
|
|
1608
1635
|
{
|
|
@@ -1617,7 +1644,7 @@ var CollapsedRail = ({
|
|
|
1617
1644
|
children: [
|
|
1618
1645
|
header !== void 0 ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: collapsedRailStyles.slot, children: renderTextSlot(header, { color: colors.text }) }) : null,
|
|
1619
1646
|
items.map((item) => {
|
|
1620
|
-
const active =
|
|
1647
|
+
const active = item.route === activeRoute;
|
|
1621
1648
|
const iconColor = active ? primaryColor : colors.textSecondary;
|
|
1622
1649
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1623
1650
|
FocusableTouchable,
|
|
@@ -1745,6 +1772,7 @@ var PillNav = ({
|
|
|
1745
1772
|
const colors = theme.colors;
|
|
1746
1773
|
const primaryColor = theme.palette.primary["500"];
|
|
1747
1774
|
if (items.length < minItems) return null;
|
|
1775
|
+
const activeRoute = resolveActiveRoute(items, pathname);
|
|
1748
1776
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1749
1777
|
reactNative.View,
|
|
1750
1778
|
{
|
|
@@ -1753,7 +1781,7 @@ var PillNav = ({
|
|
|
1753
1781
|
role: "navigation",
|
|
1754
1782
|
style: [pillNavStyles.container, containerStyle],
|
|
1755
1783
|
children: items.map((item) => {
|
|
1756
|
-
const active =
|
|
1784
|
+
const active = item.route === activeRoute;
|
|
1757
1785
|
const textColor = active ? TEXT_ON_PRIMARY4 : colors.textSecondary;
|
|
1758
1786
|
const pillStyle = active ? { backgroundColor: primaryColor } : { backgroundColor: colors.surfaceElevated };
|
|
1759
1787
|
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
@@ -2049,6 +2077,7 @@ exports.isFilterActive = isFilterActive;
|
|
|
2049
2077
|
exports.isRouteActive = isRouteActive;
|
|
2050
2078
|
exports.navStyles = navStyles;
|
|
2051
2079
|
exports.pillNavStyles = pillNavStyles;
|
|
2080
|
+
exports.resolveActiveRoute = resolveActiveRoute;
|
|
2052
2081
|
exports.resolveContentMaxWidth = resolveContentMaxWidth;
|
|
2053
2082
|
exports.resolveRailMode = resolveRailMode;
|
|
2054
2083
|
exports.resolveSearchAffordance = resolveSearchAffordance;
|