@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/dist/index.mjs CHANGED
@@ -1,7 +1,8 @@
1
- import { StyleSheet, Platform, TouchableOpacity, View, Text, useWindowDimensions, Pressable, ScrollView, ActivityIndicator } from 'react-native';
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 inside a horizontal ScrollView so the bar stays ONE ROW no matter how many
98
- // items an app passes: they never wrap onto a second line any overflow scrolls instead, with the
99
- // brand (left) and right slot pinned. `linksScroll` is the ScrollView's own box (a shrinkable,
100
- // growable flex child that may collapse to 0 and scroll); `links` is its non-wrapping content row.
101
- linksScroll: {
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: 4,
111
- rowGap: 4
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 links = items.map((item) => /* @__PURE__ */ jsx(
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
- item.key
682
- ));
683
- return collapsed ? /* @__PURE__ */ jsx(View, { nativeID: LINKS_REGION_ID, style: navBarStyles.linksStacked, testID: NAV_TEST_IDS.navBarLinks, children: links }) : /* @__PURE__ */ jsx(
684
- ScrollView,
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.linksScroll,
690
- contentContainerStyle: navBarStyles.links,
804
+ style: navBarStyles.linksRow,
691
805
  testID: NAV_TEST_IDS.navBarLinks,
692
- children: links
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