@dloizides/ui-nav 1.6.1 → 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 +203 -46
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +175 -25
- 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: {
|
|
@@ -94,22 +96,26 @@ var navBarStyles = StyleSheet.create({
|
|
|
94
96
|
justifyContent: "center"
|
|
95
97
|
},
|
|
96
98
|
toggleGlyph: { fontSize: 20, fontWeight: "700" },
|
|
97
|
-
// Expanded links live
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
|
|
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: {
|
|
102
105
|
flexGrow: 1,
|
|
103
106
|
flexShrink: 1,
|
|
104
|
-
minWidth: 0
|
|
105
|
-
},
|
|
106
|
-
links: {
|
|
107
|
+
minWidth: 0,
|
|
107
108
|
flexDirection: "row",
|
|
108
109
|
alignItems: "center",
|
|
109
110
|
flexWrap: "nowrap",
|
|
110
|
-
columnGap:
|
|
111
|
-
|
|
111
|
+
columnGap: NAV_LINK_GAP,
|
|
112
|
+
overflow: "hidden"
|
|
112
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" },
|
|
113
119
|
// Collapsed (below breakpoint): links drop onto their own full-width line,
|
|
114
120
|
// stacked vertically — mirrors the v1 `.gn-links` responsive behaviour.
|
|
115
121
|
linksStacked: {
|
|
@@ -355,7 +361,9 @@ var NAV_TEST_IDS = {
|
|
|
355
361
|
/** responsive hamburger toggle in the horizontal NavBar (shown below the breakpoint). */
|
|
356
362
|
navBarToggle: "navbar-toggle",
|
|
357
363
|
/** links container in the horizontal NavBar. */
|
|
358
|
-
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"
|
|
359
367
|
};
|
|
360
368
|
var APP_SHELL_SUFFIX = {
|
|
361
369
|
content: "-content",
|
|
@@ -570,6 +578,108 @@ var NavBarLink = ({
|
|
|
570
578
|
}
|
|
571
579
|
);
|
|
572
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
|
+
}
|
|
573
683
|
var REDUCED_MOTION_QUERY = "(prefers-reduced-motion: reduce)";
|
|
574
684
|
function getReducedMotionQuery() {
|
|
575
685
|
if (typeof window === "undefined" || typeof window.matchMedia !== "function") return void 0;
|
|
@@ -606,6 +716,8 @@ var NavBar = ({
|
|
|
606
716
|
menuHint = "",
|
|
607
717
|
renderMenuIcon,
|
|
608
718
|
collapseBelow = DEFAULT_COLLAPSE_BELOW,
|
|
719
|
+
overflowLabel = "More",
|
|
720
|
+
overflowHint = "",
|
|
609
721
|
containerStyle
|
|
610
722
|
}) => {
|
|
611
723
|
const { theme } = useUi();
|
|
@@ -615,6 +727,7 @@ var NavBar = ({
|
|
|
615
727
|
const reducedMotion = useReducedMotion();
|
|
616
728
|
const [open, setOpen] = useState(false);
|
|
617
729
|
const [toggleFocused, setToggleFocused] = useState(false);
|
|
730
|
+
const { visibleCount, setAvailableWidth, setItemWidth, setMoreWidth } = useNavOverflow(items.length, NAV_LINK_GAP);
|
|
618
731
|
const collapsed = width < collapseBelow;
|
|
619
732
|
const showLinks = !collapsed || open;
|
|
620
733
|
const toggleMenu = useCallback(() => setOpen((v) => !v), []);
|
|
@@ -666,7 +779,7 @@ var NavBar = ({
|
|
|
666
779
|
}
|
|
667
780
|
) : null,
|
|
668
781
|
showLinks ? (() => {
|
|
669
|
-
const
|
|
782
|
+
const renderLink = (item) => /* @__PURE__ */ jsx(
|
|
670
783
|
NavBarLink,
|
|
671
784
|
{
|
|
672
785
|
collapsed,
|
|
@@ -677,19 +790,50 @@ var NavBar = ({
|
|
|
677
790
|
navigateHint,
|
|
678
791
|
reducedMotion,
|
|
679
792
|
onPress: handlePress
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
)
|
|
683
|
-
|
|
684
|
-
|
|
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,
|
|
685
802
|
{
|
|
686
|
-
horizontal: true,
|
|
687
|
-
showsHorizontalScrollIndicator: false,
|
|
688
803
|
nativeID: LINKS_REGION_ID,
|
|
689
|
-
style: navBarStyles.
|
|
690
|
-
contentContainerStyle: navBarStyles.links,
|
|
804
|
+
style: navBarStyles.linksRow,
|
|
691
805
|
testID: NAV_TEST_IDS.navBarLinks,
|
|
692
|
-
|
|
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
|
+
]
|
|
693
837
|
}
|
|
694
838
|
);
|
|
695
839
|
})() : null,
|
|
@@ -698,6 +842,12 @@ var NavBar = ({
|
|
|
698
842
|
}
|
|
699
843
|
);
|
|
700
844
|
};
|
|
845
|
+
var Nav = (props) => {
|
|
846
|
+
if (props.orientation === "vertical") {
|
|
847
|
+
return /* @__PURE__ */ jsx(Sidebar, { ...props });
|
|
848
|
+
}
|
|
849
|
+
return /* @__PURE__ */ jsx(NavBar, { ...props });
|
|
850
|
+
};
|
|
701
851
|
var MOBILE_BREAKPOINT = 768;
|
|
702
852
|
var SCRIM_COLOR = "rgba(0, 0, 0, 0.5)";
|
|
703
853
|
var MENU_GLYPH2 = "\u2630";
|
|
@@ -935,6 +1085,6 @@ function accessibleNavItems(user, table, translate) {
|
|
|
935
1085
|
return roleRoutesToNavItems(resolveAccessibleRoutes(user, table), translate);
|
|
936
1086
|
}
|
|
937
1087
|
|
|
938
|
-
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 };
|
|
939
1089
|
//# sourceMappingURL=index.mjs.map
|
|
940
1090
|
//# sourceMappingURL=index.mjs.map
|