@dloizides/ui-nav 1.4.0 → 1.5.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,6 +1,6 @@
1
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 } from 'react';
3
+ import { useState, useCallback, useMemo, useEffect, useRef } from 'react';
4
4
  import { jsxs, jsx } from 'react/jsx-runtime';
5
5
  import { resolveAccessibleRoutes } from '@dloizides/auth-web';
6
6
 
@@ -346,6 +346,12 @@ var APP_SHELL_SUFFIX = {
346
346
  nav: "-nav",
347
347
  /** The persistent left rail of the back-office layout (only when `sidebar` is supplied). */
348
348
  sidebar: "-sidebar",
349
+ /** Mobile hamburger that opens the nav drawer (back-office layout, below the breakpoint). */
350
+ menuToggle: "-menu-toggle",
351
+ /** The mobile nav drawer panel (holds the `sidebar` slot when opened on a narrow viewport). */
352
+ drawer: "-drawer",
353
+ /** The mobile nav drawer scrim (tap to close). */
354
+ scrim: "-drawer-scrim",
349
355
  banner: "-banner",
350
356
  pending: "-pending",
351
357
  loading: "-loading",
@@ -669,6 +675,82 @@ var NavBar = ({
669
675
  }
670
676
  );
671
677
  };
678
+ var MOBILE_BREAKPOINT = 768;
679
+ var SCRIM_COLOR = "rgba(0, 0, 0, 0.5)";
680
+ var MENU_GLYPH2 = "\u2630";
681
+ var MENU_MIN_TARGET = 44;
682
+ var MENU_GLYPH_FONT_SIZE = 20;
683
+ var MENU_TOGGLE_PADDING = 12;
684
+ var DRAWER_Z_INDEX = 50;
685
+ var NAV_ACTIVATABLE_SELECTOR = 'a, [role="button"], [role="link"]';
686
+ var DEFAULT_DRAWER_LABELS = {
687
+ openLabel: "Menu",
688
+ openHint: "Open the navigation menu",
689
+ closeLabel: "Close menu",
690
+ closeHint: "Close the navigation menu"
691
+ };
692
+ var styles = StyleSheet.create({
693
+ overlay: { ...StyleSheet.absoluteFillObject, zIndex: DRAWER_Z_INDEX },
694
+ scrim: { ...StyleSheet.absoluteFillObject, backgroundColor: SCRIM_COLOR },
695
+ panel: { position: "absolute", top: 0, bottom: 0, left: 0 },
696
+ menuToggle: {
697
+ minWidth: MENU_MIN_TARGET,
698
+ minHeight: MENU_MIN_TARGET,
699
+ alignItems: "center",
700
+ justifyContent: "center",
701
+ paddingHorizontal: MENU_TOGGLE_PADDING
702
+ },
703
+ menuToggleGlyph: { fontSize: MENU_GLYPH_FONT_SIZE, fontWeight: "700" }
704
+ });
705
+ var MenuToggle = ({ label, hint, onOpen, testID }) => {
706
+ const { theme } = useUi();
707
+ return /* @__PURE__ */ jsx(
708
+ FocusableTouchable,
709
+ {
710
+ accessibilityHint: hint,
711
+ accessibilityLabel: label,
712
+ ringColor: theme.palette.primary["500"],
713
+ style: styles.menuToggle,
714
+ testID,
715
+ onPress: onOpen,
716
+ children: /* @__PURE__ */ jsx(Text, { style: [styles.menuToggleGlyph, { color: theme.colors.text }], children: MENU_GLYPH2 })
717
+ }
718
+ );
719
+ };
720
+ var MobileDrawer = ({
721
+ sidebar,
722
+ labels,
723
+ onClose,
724
+ drawerTestID,
725
+ scrimTestID
726
+ }) => {
727
+ const panelRef = useRef(null);
728
+ useEffect(() => {
729
+ const node = panelRef.current;
730
+ if (node === null || typeof node.addEventListener !== "function") return void 0;
731
+ const handleClick = (event) => {
732
+ const target = event.target;
733
+ const onNavItem = target !== null && target.closest(NAV_ACTIVATABLE_SELECTOR) !== null;
734
+ if (onNavItem) onClose();
735
+ };
736
+ node.addEventListener("click", handleClick);
737
+ return () => node.removeEventListener("click", handleClick);
738
+ }, [onClose]);
739
+ return /* @__PURE__ */ jsxs(View, { style: styles.overlay, children: [
740
+ /* @__PURE__ */ jsx(
741
+ Pressable,
742
+ {
743
+ accessibilityHint: labels.closeHint,
744
+ accessibilityLabel: labels.closeLabel,
745
+ accessibilityRole: "button",
746
+ style: styles.scrim,
747
+ testID: scrimTestID,
748
+ onPress: onClose
749
+ }
750
+ ),
751
+ /* @__PURE__ */ jsx(View, { ref: panelRef, "aria-modal": true, role: "dialog", style: styles.panel, testID: drawerTestID, children: sidebar })
752
+ ] });
753
+ };
672
754
  function resolveContentMaxWidth(width, viewport) {
673
755
  if (width === "full") return "full";
674
756
  const wideMinViewport = width.wideMinViewport ?? Number.POSITIVE_INFINITY;
@@ -686,11 +768,14 @@ var CARD_BORDER_WIDTH = 1;
686
768
  var CARD_TITLE_FONT_SIZE = 16;
687
769
  var CARD_MESSAGE_FONT_SIZE = 14;
688
770
  var CARD_TITLE_MARGIN_BOTTOM = 8;
689
- var styles = StyleSheet.create({
771
+ var styles2 = StyleSheet.create({
690
772
  root: { flex: 1 },
691
773
  centerFill: { flex: 1, justifyContent: "center", alignItems: "center" },
692
774
  scroll: { flex: 1 },
693
775
  scrollContent: { flexGrow: 1 },
776
+ // Mobile back-office header: the hamburger sits inline before the header slot.
777
+ headerRow: { flexDirection: "row", alignItems: "center" },
778
+ headerFill: { flex: 1 },
694
779
  // Back-office layout: a persistent left rail beside the scrolling content column.
695
780
  bodyRow: { flex: 1, flexDirection: "row" },
696
781
  columnFull: { flex: 1 },
@@ -716,11 +801,11 @@ function MessageCard({
716
801
  return /* @__PURE__ */ jsxs(
717
802
  View,
718
803
  {
719
- style: [styles.card, { backgroundColor: colors.surface, borderColor: accentColor }],
804
+ style: [styles2.card, { backgroundColor: colors.surface, borderColor: accentColor }],
720
805
  testID,
721
806
  children: [
722
- /* @__PURE__ */ jsx(Text, { style: [styles.cardTitle, { color: accentColor }], children: message.titleText }),
723
- /* @__PURE__ */ jsx(Text, { style: [styles.cardMessage, { color: colors.textSecondary }], children: message.messageText })
807
+ /* @__PURE__ */ jsx(Text, { style: [styles2.cardTitle, { color: accentColor }], children: message.titleText }),
808
+ /* @__PURE__ */ jsx(Text, { style: [styles2.cardMessage, { color: colors.textSecondary }], children: message.messageText })
724
809
  ]
725
810
  }
726
811
  );
@@ -734,13 +819,14 @@ function useContentBody(state, children, testID) {
734
819
  if (state?.error)
735
820
  return /* @__PURE__ */ jsx(MessageCard, { accentColor: errorColor, message: state.error, testID: `${testID}${APP_SHELL_SUFFIX.error}` });
736
821
  if (state?.loading === true)
737
- return /* @__PURE__ */ jsx(View, { style: styles.centerFill, testID: `${testID}${APP_SHELL_SUFFIX.loading}`, children: /* @__PURE__ */ jsx(ActivityIndicator, { color: primary, size: "large" }) });
822
+ return /* @__PURE__ */ jsx(View, { style: styles2.centerFill, testID: `${testID}${APP_SHELL_SUFFIX.loading}`, children: /* @__PURE__ */ jsx(ActivityIndicator, { color: primary, size: "large" }) });
738
823
  return children;
739
824
  }
740
825
  var AppShell = ({
741
826
  header,
742
827
  nav,
743
828
  sidebar,
829
+ mobileMenu,
744
830
  banner,
745
831
  width = "full",
746
832
  contentPadding = DEFAULT_CONTENT_PADDING,
@@ -754,32 +840,65 @@ var AppShell = ({
754
840
  const primary = theme.palette.primary["500"];
755
841
  const maxWidth = useContentMaxWidth(width);
756
842
  const body = useContentBody(state, children, testID);
843
+ const { width: viewportWidth } = useWindowDimensions();
844
+ const isNarrow = viewportWidth < MOBILE_BREAKPOINT;
845
+ const useDrawer = sidebar !== void 0 && isNarrow;
846
+ const [drawerOpen, setDrawerOpen] = useState(false);
847
+ const openDrawer = useCallback(() => setDrawerOpen(true), []);
848
+ const closeDrawer = useCallback(() => setDrawerOpen(false), []);
849
+ useEffect(() => {
850
+ if (!isNarrow && drawerOpen) setDrawerOpen(false);
851
+ }, [isNarrow, drawerOpen]);
852
+ const drawerLabels = { ...DEFAULT_DRAWER_LABELS, ...mobileMenu };
757
853
  const isUnauthenticated = gate !== void 0 && !gate.pending && !gate.authenticated;
758
854
  useEffect(() => {
759
855
  if (isUnauthenticated && gate !== void 0) gate.onRedirect();
760
856
  }, [isUnauthenticated, gate]);
761
857
  if (gate?.pending === true)
762
- return /* @__PURE__ */ jsx(View, { style: [styles.root, styles.centerFill, { backgroundColor: theme.colors.background }], testID: `${testID}${APP_SHELL_SUFFIX.pending}`, children: /* @__PURE__ */ jsx(ActivityIndicator, { color: primary, size: "large" }) });
858
+ return /* @__PURE__ */ jsx(View, { style: [styles2.root, styles2.centerFill, { backgroundColor: theme.colors.background }], testID: `${testID}${APP_SHELL_SUFFIX.pending}`, children: /* @__PURE__ */ jsx(ActivityIndicator, { color: primary, size: "large" }) });
763
859
  if (isUnauthenticated) return null;
764
- const columnStyle = maxWidth === "full" ? styles.columnFull : [styles.columnCapped, { maxWidth }];
765
- const navInnerStyle = chromeAlignment === "content" && maxWidth !== "full" ? [styles.chromeCapped, { maxWidth, paddingHorizontal: contentPadding }] : void 0;
860
+ const columnStyle = maxWidth === "full" ? styles2.columnFull : [styles2.columnCapped, { maxWidth }];
861
+ const navInnerStyle = chromeAlignment === "content" && maxWidth !== "full" ? [styles2.chromeCapped, { maxWidth, paddingHorizontal: contentPadding }] : void 0;
766
862
  const scroller = /* @__PURE__ */ jsx(
767
863
  ScrollView,
768
864
  {
769
- contentContainerStyle: [styles.scrollContent, { padding: contentPadding }],
770
- style: styles.scroll,
865
+ contentContainerStyle: [styles2.scrollContent, { padding: contentPadding }],
866
+ style: styles2.scroll,
771
867
  testID: `${testID}${APP_SHELL_SUFFIX.content}`,
772
868
  children: /* @__PURE__ */ jsx(View, { style: columnStyle, children: body })
773
869
  }
774
870
  );
775
- return /* @__PURE__ */ jsxs(View, { style: [styles.root, { backgroundColor: theme.colors.background }], testID, children: [
776
- /* @__PURE__ */ jsx(View, { testID: `${testID}${APP_SHELL_SUFFIX.header}`, children: header }),
871
+ const headerRegion = useDrawer ? /* @__PURE__ */ jsxs(View, { style: styles2.headerRow, testID: `${testID}${APP_SHELL_SUFFIX.header}`, children: [
872
+ /* @__PURE__ */ jsx(
873
+ MenuToggle,
874
+ {
875
+ hint: drawerLabels.openHint,
876
+ label: drawerLabels.openLabel,
877
+ testID: `${testID}${APP_SHELL_SUFFIX.menuToggle}`,
878
+ onOpen: openDrawer
879
+ }
880
+ ),
881
+ /* @__PURE__ */ jsx(View, { style: styles2.headerFill, children: header })
882
+ ] }) : /* @__PURE__ */ jsx(View, { testID: `${testID}${APP_SHELL_SUFFIX.header}`, children: header });
883
+ const showRail = sidebar !== void 0 && !isNarrow;
884
+ return /* @__PURE__ */ jsxs(View, { style: [styles2.root, { backgroundColor: theme.colors.background }], testID, children: [
885
+ headerRegion,
777
886
  nav !== void 0 ? /* @__PURE__ */ jsx(View, { testID: `${testID}${APP_SHELL_SUFFIX.nav}`, children: navInnerStyle !== void 0 ? /* @__PURE__ */ jsx(View, { style: navInnerStyle, children: nav }) : nav }) : null,
778
887
  banner !== void 0 ? /* @__PURE__ */ jsx(View, { testID: `${testID}${APP_SHELL_SUFFIX.banner}`, children: banner }) : null,
779
- sidebar !== void 0 ? /* @__PURE__ */ jsxs(View, { style: styles.bodyRow, children: [
888
+ showRail ? /* @__PURE__ */ jsxs(View, { style: styles2.bodyRow, children: [
780
889
  /* @__PURE__ */ jsx(View, { testID: `${testID}${APP_SHELL_SUFFIX.sidebar}`, children: sidebar }),
781
890
  scroller
782
- ] }) : scroller
891
+ ] }) : scroller,
892
+ useDrawer && drawerOpen ? /* @__PURE__ */ jsx(
893
+ MobileDrawer,
894
+ {
895
+ drawerTestID: `${testID}${APP_SHELL_SUFFIX.drawer}`,
896
+ labels: drawerLabels,
897
+ scrimTestID: `${testID}${APP_SHELL_SUFFIX.scrim}`,
898
+ sidebar,
899
+ onClose: closeDrawer
900
+ }
901
+ ) : null
783
902
  ] });
784
903
  };
785
904
  function roleRoutesToNavItems(routes, translate) {