@dloizides/ui-nav 1.3.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
 
@@ -344,6 +344,14 @@ var APP_SHELL_SUFFIX = {
344
344
  content: "-content",
345
345
  header: "-header",
346
346
  nav: "-nav",
347
+ /** The persistent left rail of the back-office layout (only when `sidebar` is supplied). */
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",
347
355
  banner: "-banner",
348
356
  pending: "-pending",
349
357
  loading: "-loading",
@@ -667,6 +675,82 @@ var NavBar = ({
667
675
  }
668
676
  );
669
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
+ };
670
754
  function resolveContentMaxWidth(width, viewport) {
671
755
  if (width === "full") return "full";
672
756
  const wideMinViewport = width.wideMinViewport ?? Number.POSITIVE_INFINITY;
@@ -684,11 +768,16 @@ var CARD_BORDER_WIDTH = 1;
684
768
  var CARD_TITLE_FONT_SIZE = 16;
685
769
  var CARD_MESSAGE_FONT_SIZE = 14;
686
770
  var CARD_TITLE_MARGIN_BOTTOM = 8;
687
- var styles = StyleSheet.create({
771
+ var styles2 = StyleSheet.create({
688
772
  root: { flex: 1 },
689
773
  centerFill: { flex: 1, justifyContent: "center", alignItems: "center" },
690
774
  scroll: { flex: 1 },
691
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 },
779
+ // Back-office layout: a persistent left rail beside the scrolling content column.
780
+ bodyRow: { flex: 1, flexDirection: "row" },
692
781
  columnFull: { flex: 1 },
693
782
  columnCapped: { flex: 1, width: "100%", alignSelf: "center" },
694
783
  // Non-flex variant used to cap the nav strip's inner content to the content
@@ -712,11 +801,11 @@ function MessageCard({
712
801
  return /* @__PURE__ */ jsxs(
713
802
  View,
714
803
  {
715
- style: [styles.card, { backgroundColor: colors.surface, borderColor: accentColor }],
804
+ style: [styles2.card, { backgroundColor: colors.surface, borderColor: accentColor }],
716
805
  testID,
717
806
  children: [
718
- /* @__PURE__ */ jsx(Text, { style: [styles.cardTitle, { color: accentColor }], children: message.titleText }),
719
- /* @__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 })
720
809
  ]
721
810
  }
722
811
  );
@@ -730,12 +819,14 @@ function useContentBody(state, children, testID) {
730
819
  if (state?.error)
731
820
  return /* @__PURE__ */ jsx(MessageCard, { accentColor: errorColor, message: state.error, testID: `${testID}${APP_SHELL_SUFFIX.error}` });
732
821
  if (state?.loading === true)
733
- 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" }) });
734
823
  return children;
735
824
  }
736
825
  var AppShell = ({
737
826
  header,
738
827
  nav,
828
+ sidebar,
829
+ mobileMenu,
739
830
  banner,
740
831
  width = "full",
741
832
  contentPadding = DEFAULT_CONTENT_PADDING,
@@ -749,28 +840,65 @@ var AppShell = ({
749
840
  const primary = theme.palette.primary["500"];
750
841
  const maxWidth = useContentMaxWidth(width);
751
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 };
752
853
  const isUnauthenticated = gate !== void 0 && !gate.pending && !gate.authenticated;
753
854
  useEffect(() => {
754
855
  if (isUnauthenticated && gate !== void 0) gate.onRedirect();
755
856
  }, [isUnauthenticated, gate]);
756
857
  if (gate?.pending === true)
757
- 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" }) });
758
859
  if (isUnauthenticated) return null;
759
- const columnStyle = maxWidth === "full" ? styles.columnFull : [styles.columnCapped, { maxWidth }];
760
- const navInnerStyle = chromeAlignment === "content" && maxWidth !== "full" ? [styles.chromeCapped, { maxWidth, paddingHorizontal: contentPadding }] : void 0;
761
- return /* @__PURE__ */ jsxs(View, { style: [styles.root, { backgroundColor: theme.colors.background }], testID, children: [
762
- /* @__PURE__ */ jsx(View, { testID: `${testID}${APP_SHELL_SUFFIX.header}`, children: header }),
860
+ const columnStyle = maxWidth === "full" ? styles2.columnFull : [styles2.columnCapped, { maxWidth }];
861
+ const navInnerStyle = chromeAlignment === "content" && maxWidth !== "full" ? [styles2.chromeCapped, { maxWidth, paddingHorizontal: contentPadding }] : void 0;
862
+ const scroller = /* @__PURE__ */ jsx(
863
+ ScrollView,
864
+ {
865
+ contentContainerStyle: [styles2.scrollContent, { padding: contentPadding }],
866
+ style: styles2.scroll,
867
+ testID: `${testID}${APP_SHELL_SUFFIX.content}`,
868
+ children: /* @__PURE__ */ jsx(View, { style: columnStyle, children: body })
869
+ }
870
+ );
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,
763
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,
764
887
  banner !== void 0 ? /* @__PURE__ */ jsx(View, { testID: `${testID}${APP_SHELL_SUFFIX.banner}`, children: banner }) : null,
765
- /* @__PURE__ */ jsx(
766
- ScrollView,
888
+ showRail ? /* @__PURE__ */ jsxs(View, { style: styles2.bodyRow, children: [
889
+ /* @__PURE__ */ jsx(View, { testID: `${testID}${APP_SHELL_SUFFIX.sidebar}`, children: sidebar }),
890
+ scroller
891
+ ] }) : scroller,
892
+ useDrawer && drawerOpen ? /* @__PURE__ */ jsx(
893
+ MobileDrawer,
767
894
  {
768
- contentContainerStyle: [styles.scrollContent, { padding: contentPadding }],
769
- style: styles.scroll,
770
- testID: `${testID}${APP_SHELL_SUFFIX.content}`,
771
- children: /* @__PURE__ */ jsx(View, { style: columnStyle, children: body })
895
+ drawerTestID: `${testID}${APP_SHELL_SUFFIX.drawer}`,
896
+ labels: drawerLabels,
897
+ scrimTestID: `${testID}${APP_SHELL_SUFFIX.scrim}`,
898
+ sidebar,
899
+ onClose: closeDrawer
772
900
  }
773
- )
901
+ ) : null
774
902
  ] });
775
903
  };
776
904
  function roleRoutesToNavItems(routes, translate) {