@dloizides/ui-nav 1.6.0 → 1.7.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 +42 -0
- package/README.md +30 -0
- package/dist/index.d.mts +130 -3
- package/dist/index.d.ts +130 -3
- package/dist/index.js +211 -47
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +183 -26
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -1
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { StyleSheet, Platform, TouchableOpacity, View, Text, useWindowDimensions, Pressable,
|
|
1
|
+
import { StyleSheet, Platform, TouchableOpacity, View, Text, useWindowDimensions, Pressable, ActivityIndicator, ScrollView } from 'react-native';
|
|
2
2
|
import { useUi } from '@dloizides/ui-feedback';
|
|
3
|
-
import { useState, useCallback, useMemo, useEffect, useRef } from 'react';
|
|
3
|
+
import React5, { useState, useCallback, useMemo, useEffect, useRef } from 'react';
|
|
4
4
|
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
5
|
+
import { ModalDropdown } from '@dloizides/ui-layout';
|
|
5
6
|
import { resolveAccessibleRoutes } from '@dloizides/auth-web';
|
|
6
7
|
|
|
7
8
|
// src/Sidebar.tsx
|
|
@@ -12,6 +13,7 @@ function isRouteActive(pathname, route) {
|
|
|
12
13
|
return pathname === route || pathname.startsWith(`${route}/`);
|
|
13
14
|
}
|
|
14
15
|
var ACTIVE_BORDER_RADIUS = 4;
|
|
16
|
+
var NAV_LINK_GAP = 4;
|
|
15
17
|
var navStyles = StyleSheet.create({
|
|
16
18
|
// --- Sidebar ---
|
|
17
19
|
sidebarContainer: {
|
|
@@ -74,6 +76,13 @@ var navBarStyles = StyleSheet.create({
|
|
|
74
76
|
rowGap: 4,
|
|
75
77
|
paddingVertical: 8
|
|
76
78
|
},
|
|
79
|
+
// EXPANDED: the row must NOT wrap — otherwise flexbox drops the right slot onto a 2nd line the
|
|
80
|
+
// moment brand+links+right overflow, instead of SHRINKING the (scrollable) links. `flex-shrink`
|
|
81
|
+
// only distributes negative space on a `nowrap` line; with `wrap` it wraps first and never shrinks.
|
|
82
|
+
// Collapsed keeps `wrap` so the stacked links/right (width:100%) can drop to their own lines.
|
|
83
|
+
innerNoWrap: {
|
|
84
|
+
flexWrap: "nowrap"
|
|
85
|
+
},
|
|
77
86
|
brand: { marginRight: 16, flexShrink: 0 },
|
|
78
87
|
toggle: {
|
|
79
88
|
marginLeft: "auto",
|
|
@@ -87,22 +96,26 @@ var navBarStyles = StyleSheet.create({
|
|
|
87
96
|
justifyContent: "center"
|
|
88
97
|
},
|
|
89
98
|
toggleGlyph: { fontSize: 20, fontWeight: "700" },
|
|
90
|
-
// Expanded links live
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
|
|
99
|
+
// Expanded links live on ONE ROW that never wraps: it is a shrinkable/growable flex child that
|
|
100
|
+
// fills the space between the pinned brand (left) and right slot, and CLIPS overflow. A measure
|
|
101
|
+
// pass (priority+) then keeps only the leading items that fit inline and collapses the rest behind
|
|
102
|
+
// the "More ▾" overflow trigger — so no item is ever cut off; the surplus moves into a dropdown.
|
|
103
|
+
// `overflow:'hidden'` only masks the single unmeasured first frame before the fit resolves.
|
|
104
|
+
linksRow: {
|
|
95
105
|
flexGrow: 1,
|
|
96
106
|
flexShrink: 1,
|
|
97
|
-
minWidth: 0
|
|
98
|
-
},
|
|
99
|
-
links: {
|
|
107
|
+
minWidth: 0,
|
|
100
108
|
flexDirection: "row",
|
|
101
109
|
alignItems: "center",
|
|
102
110
|
flexWrap: "nowrap",
|
|
103
|
-
columnGap:
|
|
104
|
-
|
|
111
|
+
columnGap: NAV_LINK_GAP,
|
|
112
|
+
overflow: "hidden"
|
|
105
113
|
},
|
|
114
|
+
// Each inline link + the overflow trigger sit in a natural-width cell that never shrinks, so their
|
|
115
|
+
// measured widths are their true content widths (what the priority+ fit computation reasons about).
|
|
116
|
+
linkCell: { flexShrink: 0 },
|
|
117
|
+
overflowTrigger: { columnGap: 4 },
|
|
118
|
+
overflowChevron: { fontSize: 12, fontWeight: "700" },
|
|
106
119
|
// Collapsed (below breakpoint): links drop onto their own full-width line,
|
|
107
120
|
// stacked vertically — mirrors the v1 `.gn-links` responsive behaviour.
|
|
108
121
|
linksStacked: {
|
|
@@ -348,7 +361,9 @@ var NAV_TEST_IDS = {
|
|
|
348
361
|
/** responsive hamburger toggle in the horizontal NavBar (shown below the breakpoint). */
|
|
349
362
|
navBarToggle: "navbar-toggle",
|
|
350
363
|
/** links container in the horizontal NavBar. */
|
|
351
|
-
navBarLinks: "navbar-links"
|
|
364
|
+
navBarLinks: "navbar-links",
|
|
365
|
+
/** the "More ▾" priority+ overflow trigger in the horizontal NavBar (shown when items spill). */
|
|
366
|
+
navBarOverflow: "navbar-overflow"
|
|
352
367
|
};
|
|
353
368
|
var APP_SHELL_SUFFIX = {
|
|
354
369
|
content: "-content",
|
|
@@ -563,6 +578,108 @@ var NavBarLink = ({
|
|
|
563
578
|
}
|
|
564
579
|
);
|
|
565
580
|
};
|
|
581
|
+
var TEXT_ON_PRIMARY3 = "#ffffff";
|
|
582
|
+
var NO_ACTIVE_ROUTE = "";
|
|
583
|
+
var NavOverflowMenu = ({
|
|
584
|
+
items,
|
|
585
|
+
pathname,
|
|
586
|
+
onNavigate,
|
|
587
|
+
colors,
|
|
588
|
+
label,
|
|
589
|
+
hint,
|
|
590
|
+
testID,
|
|
591
|
+
variant
|
|
592
|
+
}) => {
|
|
593
|
+
const options = useMemo(() => items.map((item) => ({ label: item.label, value: item.route })), [items]);
|
|
594
|
+
const activeRoute = useMemo(
|
|
595
|
+
() => items.find((item) => isRouteActive(pathname, item.route))?.route ?? NO_ACTIVE_ROUTE,
|
|
596
|
+
[items, pathname]
|
|
597
|
+
);
|
|
598
|
+
const optionTestID = useMemo(() => {
|
|
599
|
+
const byRoute = new Map(items.map((item) => [item.route, item.testID ?? item.key]));
|
|
600
|
+
return (route) => byRoute.get(route) ?? `${testID}-option-${route}`;
|
|
601
|
+
}, [items, testID]);
|
|
602
|
+
const hasActive = activeRoute !== NO_ACTIVE_ROUTE;
|
|
603
|
+
const textColor = hasActive ? TEXT_ON_PRIMARY3 : colors.rest;
|
|
604
|
+
const pillStyle = hasActive ? { backgroundColor: colors.activeBg } : void 0;
|
|
605
|
+
return /* @__PURE__ */ jsx(
|
|
606
|
+
ModalDropdown,
|
|
607
|
+
{
|
|
608
|
+
accessibilityHint: hint,
|
|
609
|
+
accessibilityLabel: label,
|
|
610
|
+
optionTestID,
|
|
611
|
+
options,
|
|
612
|
+
testID,
|
|
613
|
+
value: activeRoute,
|
|
614
|
+
variant,
|
|
615
|
+
onChange: onNavigate,
|
|
616
|
+
renderTrigger: ({ isOpen }) => /* @__PURE__ */ jsxs(View, { style: [navBarStyles.link, navBarStyles.overflowTrigger, pillStyle], children: [
|
|
617
|
+
/* @__PURE__ */ jsx(Text, { style: [navBarStyles.linkText, { color: textColor }], children: label }),
|
|
618
|
+
/* @__PURE__ */ jsx(Text, { style: [navBarStyles.overflowChevron, { color: textColor }], children: isOpen ? "\u25B4" : "\u25BE" })
|
|
619
|
+
] })
|
|
620
|
+
}
|
|
621
|
+
);
|
|
622
|
+
};
|
|
623
|
+
|
|
624
|
+
// src/computeVisibleCount.ts
|
|
625
|
+
function sum(values) {
|
|
626
|
+
return values.reduce((acc, value) => acc + value, 0);
|
|
627
|
+
}
|
|
628
|
+
function computeVisibleCount({ availableWidth, itemWidths, moreWidth, gap }) {
|
|
629
|
+
const total = itemWidths.length;
|
|
630
|
+
if (total === 0) return 0;
|
|
631
|
+
if (availableWidth <= 0 || itemWidths.some((width) => width <= 0)) return total;
|
|
632
|
+
const fullRowWidth = sum(itemWidths) + gap * (total - 1);
|
|
633
|
+
if (fullRowWidth <= availableWidth) return total;
|
|
634
|
+
const reservedForMore = moreWidth + gap;
|
|
635
|
+
let used = 0;
|
|
636
|
+
let count = 0;
|
|
637
|
+
for (let index = 0; index < total; index += 1) {
|
|
638
|
+
const width = itemWidths[index] ?? 0;
|
|
639
|
+
const withGap = used + (count > 0 ? gap : 0) + width;
|
|
640
|
+
if (withGap + reservedForMore > availableWidth) break;
|
|
641
|
+
used = withGap;
|
|
642
|
+
count += 1;
|
|
643
|
+
}
|
|
644
|
+
return count;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// src/useNavOverflow.ts
|
|
648
|
+
function resizeWidthBuffer(previous, count) {
|
|
649
|
+
const next = new Array(count).fill(0);
|
|
650
|
+
const shared = Math.min(previous.length, count);
|
|
651
|
+
for (let index = 0; index < shared; index += 1) next[index] = previous[index] ?? 0;
|
|
652
|
+
return next;
|
|
653
|
+
}
|
|
654
|
+
function useNavOverflow(itemCount, gap) {
|
|
655
|
+
const [availableWidth, setAvailableWidthState] = useState(0);
|
|
656
|
+
const [moreWidth, setMoreWidthState] = useState(0);
|
|
657
|
+
const [itemWidths, setItemWidths] = useState(() => new Array(itemCount).fill(0));
|
|
658
|
+
useEffect(() => {
|
|
659
|
+
setItemWidths((previous) => previous.length === itemCount ? previous : resizeWidthBuffer(previous, itemCount));
|
|
660
|
+
}, [itemCount]);
|
|
661
|
+
const setItemWidth = useCallback((index, width) => {
|
|
662
|
+
setItemWidths((previous) => {
|
|
663
|
+
if (index < 0 || index >= previous.length || previous[index] === width) return previous;
|
|
664
|
+
const next = previous.slice();
|
|
665
|
+
next[index] = width;
|
|
666
|
+
return next;
|
|
667
|
+
});
|
|
668
|
+
}, []);
|
|
669
|
+
const setAvailableWidth = useCallback(
|
|
670
|
+
(width) => setAvailableWidthState((previous) => previous === width ? previous : width),
|
|
671
|
+
[]
|
|
672
|
+
);
|
|
673
|
+
const setMoreWidth = useCallback(
|
|
674
|
+
(width) => setMoreWidthState((previous) => previous === width ? previous : width),
|
|
675
|
+
[]
|
|
676
|
+
);
|
|
677
|
+
const visibleCount = useMemo(
|
|
678
|
+
() => computeVisibleCount({ availableWidth, itemWidths, moreWidth, gap }),
|
|
679
|
+
[availableWidth, itemWidths, moreWidth, gap]
|
|
680
|
+
);
|
|
681
|
+
return { visibleCount, setAvailableWidth, setItemWidth, setMoreWidth };
|
|
682
|
+
}
|
|
566
683
|
var REDUCED_MOTION_QUERY = "(prefers-reduced-motion: reduce)";
|
|
567
684
|
function getReducedMotionQuery() {
|
|
568
685
|
if (typeof window === "undefined" || typeof window.matchMedia !== "function") return void 0;
|
|
@@ -599,6 +716,8 @@ var NavBar = ({
|
|
|
599
716
|
menuHint = "",
|
|
600
717
|
renderMenuIcon,
|
|
601
718
|
collapseBelow = DEFAULT_COLLAPSE_BELOW,
|
|
719
|
+
overflowLabel = "More",
|
|
720
|
+
overflowHint = "",
|
|
602
721
|
containerStyle
|
|
603
722
|
}) => {
|
|
604
723
|
const { theme } = useUi();
|
|
@@ -608,6 +727,7 @@ var NavBar = ({
|
|
|
608
727
|
const reducedMotion = useReducedMotion();
|
|
609
728
|
const [open, setOpen] = useState(false);
|
|
610
729
|
const [toggleFocused, setToggleFocused] = useState(false);
|
|
730
|
+
const { visibleCount, setAvailableWidth, setItemWidth, setMoreWidth } = useNavOverflow(items.length, NAV_LINK_GAP);
|
|
611
731
|
const collapsed = width < collapseBelow;
|
|
612
732
|
const showLinks = !collapsed || open;
|
|
613
733
|
const toggleMenu = useCallback(() => setOpen((v) => !v), []);
|
|
@@ -639,7 +759,7 @@ var NavBar = ({
|
|
|
639
759
|
{ backgroundColor: colors.surface, borderBottomColor: colors.border },
|
|
640
760
|
containerStyle
|
|
641
761
|
],
|
|
642
|
-
children: /* @__PURE__ */ jsxs(View, { style: navBarStyles.inner, children: [
|
|
762
|
+
children: /* @__PURE__ */ jsxs(View, { style: [navBarStyles.inner, collapsed ? null : navBarStyles.innerNoWrap], children: [
|
|
643
763
|
brand !== void 0 ? /* @__PURE__ */ jsx(View, { style: navBarStyles.brand, children: brand }) : null,
|
|
644
764
|
collapsed ? /* @__PURE__ */ jsx(
|
|
645
765
|
Pressable,
|
|
@@ -659,7 +779,7 @@ var NavBar = ({
|
|
|
659
779
|
}
|
|
660
780
|
) : null,
|
|
661
781
|
showLinks ? (() => {
|
|
662
|
-
const
|
|
782
|
+
const renderLink = (item) => /* @__PURE__ */ jsx(
|
|
663
783
|
NavBarLink,
|
|
664
784
|
{
|
|
665
785
|
collapsed,
|
|
@@ -670,19 +790,50 @@ var NavBar = ({
|
|
|
670
790
|
navigateHint,
|
|
671
791
|
reducedMotion,
|
|
672
792
|
onPress: handlePress
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
)
|
|
676
|
-
|
|
677
|
-
|
|
793
|
+
}
|
|
794
|
+
);
|
|
795
|
+
if (collapsed) {
|
|
796
|
+
return /* @__PURE__ */ jsx(View, { nativeID: LINKS_REGION_ID, style: navBarStyles.linksStacked, testID: NAV_TEST_IDS.navBarLinks, children: items.map((item) => /* @__PURE__ */ jsx(React5.Fragment, { children: renderLink(item) }, item.key)) });
|
|
797
|
+
}
|
|
798
|
+
const visibleItems = items.slice(0, visibleCount);
|
|
799
|
+
const overflowItems = items.slice(visibleCount);
|
|
800
|
+
return /* @__PURE__ */ jsxs(
|
|
801
|
+
View,
|
|
678
802
|
{
|
|
679
|
-
horizontal: true,
|
|
680
|
-
showsHorizontalScrollIndicator: false,
|
|
681
803
|
nativeID: LINKS_REGION_ID,
|
|
682
|
-
style: navBarStyles.
|
|
683
|
-
contentContainerStyle: navBarStyles.links,
|
|
804
|
+
style: navBarStyles.linksRow,
|
|
684
805
|
testID: NAV_TEST_IDS.navBarLinks,
|
|
685
|
-
|
|
806
|
+
onLayout: (event) => setAvailableWidth(event.nativeEvent.layout.width),
|
|
807
|
+
children: [
|
|
808
|
+
visibleItems.map((item, index) => /* @__PURE__ */ jsx(
|
|
809
|
+
View,
|
|
810
|
+
{
|
|
811
|
+
style: navBarStyles.linkCell,
|
|
812
|
+
onLayout: (event) => setItemWidth(index, event.nativeEvent.layout.width),
|
|
813
|
+
children: renderLink(item)
|
|
814
|
+
},
|
|
815
|
+
item.key
|
|
816
|
+
)),
|
|
817
|
+
overflowItems.length > 0 ? /* @__PURE__ */ jsx(
|
|
818
|
+
View,
|
|
819
|
+
{
|
|
820
|
+
style: navBarStyles.linkCell,
|
|
821
|
+
onLayout: (event) => setMoreWidth(event.nativeEvent.layout.width),
|
|
822
|
+
children: /* @__PURE__ */ jsx(
|
|
823
|
+
NavOverflowMenu,
|
|
824
|
+
{
|
|
825
|
+
colors: linkColors,
|
|
826
|
+
hint: overflowHint,
|
|
827
|
+
items: overflowItems,
|
|
828
|
+
label: overflowLabel,
|
|
829
|
+
pathname,
|
|
830
|
+
testID: NAV_TEST_IDS.navBarOverflow,
|
|
831
|
+
onNavigate: handlePress
|
|
832
|
+
}
|
|
833
|
+
)
|
|
834
|
+
}
|
|
835
|
+
) : null
|
|
836
|
+
]
|
|
686
837
|
}
|
|
687
838
|
);
|
|
688
839
|
})() : null,
|
|
@@ -691,6 +842,12 @@ var NavBar = ({
|
|
|
691
842
|
}
|
|
692
843
|
);
|
|
693
844
|
};
|
|
845
|
+
var Nav = (props) => {
|
|
846
|
+
if (props.orientation === "vertical") {
|
|
847
|
+
return /* @__PURE__ */ jsx(Sidebar, { ...props });
|
|
848
|
+
}
|
|
849
|
+
return /* @__PURE__ */ jsx(NavBar, { ...props });
|
|
850
|
+
};
|
|
694
851
|
var MOBILE_BREAKPOINT = 768;
|
|
695
852
|
var SCRIM_COLOR = "rgba(0, 0, 0, 0.5)";
|
|
696
853
|
var MENU_GLYPH2 = "\u2630";
|
|
@@ -928,6 +1085,6 @@ function accessibleNavItems(user, table, translate) {
|
|
|
928
1085
|
return roleRoutesToNavItems(resolveAccessibleRoutes(user, table), translate);
|
|
929
1086
|
}
|
|
930
1087
|
|
|
931
|
-
export { ACTIVE_BORDER_RADIUS, APP_SHELL_SUFFIX, AppShell, BASE_INDENT, CHEVRON_ICON_SIZE, NAV_ICON_SIZE, NAV_TEST_IDS, NavBar, NavExpandableItem, Sidebar, Topbar, accessibleNavItems, expandableStyles, isRouteActive, navStyles, resolveContentMaxWidth, roleRoutesToNavItems, useContentMaxWidth };
|
|
1088
|
+
export { ACTIVE_BORDER_RADIUS, APP_SHELL_SUFFIX, AppShell, BASE_INDENT, CHEVRON_ICON_SIZE, NAV_ICON_SIZE, NAV_LINK_GAP, NAV_TEST_IDS, Nav, NavBar, NavExpandableItem, NavOverflowMenu, Sidebar, Topbar, accessibleNavItems, expandableStyles, isRouteActive, navStyles, resolveContentMaxWidth, roleRoutesToNavItems, useContentMaxWidth };
|
|
932
1089
|
//# sourceMappingURL=index.mjs.map
|
|
933
1090
|
//# sourceMappingURL=index.mjs.map
|