skip to Main Content

I am making a React Native application in which I have menus and submenus.

Menus and submenus structure:

let arr = [
  {
    name: 'Header 1',
    routeName: 'Home',
    child: [
      { name: 'Header 1 - Submenu 1', child: [], routeName: 'Home' },
      {
        name: 'Header 1 - Submenu 2',
        child: [],
        routeName: 'NotificationScreen',
      },
      { name: 'Header 1 - Submenu 3', child: [], routeName: 'Home' },
    ],
  },
  {
    name: 'Header 2',
    routeName: 'NotificationScreen',
    child: [
      {
        name: 'Header 2 - Submenu 1',
        child: [],
        routeName: 'NotificationScreen',
      },
      { name: 'Header 2 - Submenu 2', child: [], routeName: 'Home' },
      {
        name: 'Header 2 - Submenu 3',
        child: [],
        routeName: 'NotificationScreen',
      },
    ],
  },
];

Render of Menu’s and Submenu’s:

<TouchableOpacity style={styles.item} onPress={onPress} activeOpacity={1}>
  <View style={styles.row}>
    <Text style={{ paddingRight: 20 }}>{name}</Text>
    {child.length ? <Text>{open ? 'close' : 'open'}</Text> : null}
  </View>
  {open &&
    child.map((x: any, i: any) => {
      if (x.child.length) {
        return (
          <Item
            key={x}
            active={childActive}
            i={i}
            setActive={setChildActive}
            child={x.child}
          />
        );
      }
      return (
        <TouchableOpacity
          key={x}
          style={styles.subItem}
          onPress={() => {
            handleRouteChange(x.routeName);
          }}>
          <Text>
            {name} - submenu - {i + 1}
          </Text>
        </TouchableOpacity>
      );
    })}
</TouchableOpacity>

Working example: https://snack.expo.dev/@manirajmurugan/custom-header-title-component

Here I am in the need to make a breadcrumb like structure on the screen page as per the navigation done by user through menu and submenus.

Current Scenario:

-> If user clicks on Header 1 and then select submenu Header 1 – Submenu 1, then the user will be redirected to Home Screen.

Expected Scenario:

-> Requirement here is that I am in the need to display a breadcrumb for this screen like,

Header 1 > Header 1 - Submenu 1

On click of the Header 1 in this breadcrumb, user will be redirected to the respective routeName given in the object.

Kindly help me to generate breadcrumb for the navigation done in menu for the respective screen’s.

Thanks in advance.

Edit:

In real app menu and submenu will be like,

enter image description here

Here if user click’s on Stored Data configuration under Unit Data Management,

then the expected breadcrumb result would be,

enter image description here

2

Answers


  1. For readability, I started by moving all of the components into their own separate files. I also used a FlatList instead of a ScrollView for the recursive SubMenuItem rendering.

    I first broke the breadcrumb down into three components. The first, MenuItem, which would always be visible in the Header. When pressed, it would reveals a dropdown menu, which would reveal the SubMenu, MenuItem’s children. Pressing SubMenu would reveal its children, if it had any, or would trigger navigation, if a routeName was provided. Demo

    Login or Signup to reply.
  2. Create a piece of state to hold the crumbs. It will be an array that will hold three values: the screen you are currently on, the section title press, and the section item pressed. Handling the last two will be done by the SectionList and to keep track of the current screen you can add a screenListener to the StackNavigator that will listen for all navigation changes on all screens.

    import React, { useState, createContext } from 'react';
    import { View, StyleSheet, useWindowDimensions } from 'react-native';
    import { NavigationContainer } from '@react-navigation/native';
    import { createNativeStackNavigator } from '@react-navigation/native-stack';
    import Header from './screens/Header';
    import HomeScreen from './screens/Home';
    import SettingsScreen from './screens/Settings';
    const Stack = createNativeStackNavigator();
    
    import { ICON_SIZE, headerHeight } from './Constants';
    const initialScreen = 'Settings';
    
    export default function App() {
      // this will have at most 3 items:
      //   the current screen
      //   the section header
      //   the section.data[index]
      const [crumbNavigation, setCrumbNavigation] = useState([initialScreen]);
      const { width, height } = useWindowDimensions();
      const screenOptions = {
        // pass crumbNavigation and its setter to header
        headerTitle: (props) => (
          <Header
            {...props}
            width={width}
            height={headerHeight}
            crumbNavigation={crumbNavigation}
            setCrumbNavigation={setCrumbNavigation}
          />
        ),
        headerStyle: {
          height: headerHeight,
          width: width,
        },
        // since there will be a bread crumb navigator
        // theres no need for back button
        headerBackVisible: false,
        headerLeft: () => null,
      };
      return (
        <NavigationContainer>
          <Stack.Navigator
            initialRouteName={initialScreen}
            screenOptions={screenOptions}
            screenListeners={{
              state: (nav) => {
                console.log(nav.type);
                const routes = nav.data.state.routes;
                const lastRoute = routes[routes.length - 1]?.name;
                if (lastRoute)
                  setCrumbNavigation((prev) => {
                    prev[0] = lastRoute;
                    return prev;
                  });
              },
            }}>
            <Stack.Screen name="Home" component={HomeScreen} />
            <Stack.Screen name="Settings" component={SettingsScreen} />
          </Stack.Navigator>
        </NavigationContainer>
      );
    }
    

    Now that current screen is handled, you can move on to deal with getting the SectionTitle, which the SectionHeader and MenuItem components handles. If a section data array’s length is zero, pressing it will trigger the parent component’s onItemPress with the section as an argument, if not, then pressing it will reveal its children

    import { useState } from 'react';
    import { TouchableOpacity, Text, View, StyleSheet } from 'react-native';
    import TouchableMaterialIcon from './TouchableMaterialIcon';
    import { ICON_SIZE } from '../Constants';
    export default function SectionHeader({ section, toggleSectionMenu, onPress }) {
      return (
        <TouchableOpacity
          style={[
            styles.headerContainer,
            section.isOpen && styles.highlightedTitleContainer,
          ]}
          onPress={()=>{
            if(section.data.length > 0)
              toggleSectionMenu(section.id)
            else
              onPress(section.title)
          }}
          >
          <View style={{ flex: 1 }}>
            <Text
              styles={[
                styles.titleText,
                section.isOpen && styles.highlightedTitleText,
              ]}>
              {section.title}
            </Text>
          </View>
          {section.data.length > 0 && (
            <TouchableMaterialIcon
              name={section.isOpen ? 'keyboard-arrow-up' : 'keyboard-arrow-down'}
              size={ICON_SIZE * 0.6}
              onPress={() => toggleSectionMenu(section.id)}
            />
          )}
        </TouchableOpacity>
      );
    }
    const styles = StyleSheet.create({
      headerContainer: {
        width: '100%',
        flexDirection: 'row',
        // justifyContent:'center',
        alignItems: 'center',
        borderBottomWidth: StyleSheet.hairlineWidth,
        borderBottomColor: '#eee',
        paddingVertical: 5,
      },
      titleText: {
        fontSize: 16,
      },
      highlightedTitleText: {
        fontWeight: 'bold',
      },
      highlightedTitleContainer: {
        backgroundColor: '#edf',
      },
    });
    

    The SectionItem is pretty straightforward. When visible, its a button that when pressed will trigger its grandparent’s onItemPressed with both the section title and the item’s title

    import { View, StyleSheet, Text, TouchableOpacity } from 'react-native';
    
    export default function MenuItem({ section, index, item, onPress }) {
      const isLastItem = index == section.data.length - 1;
      if (section.isOpen)
        return (
          <TouchableOpacity
            style={[styles.itemContainer, isLastItem && styles.lastItem]}
            onPress={() => onPress(section.title, item)}>
            <Text>{item}</Text>
          </TouchableOpacity>
        );
    }
    const styles = StyleSheet.create({
      itemContainer: {
        marginRight: 5,
        padding: 5,
      },
      lastItem: {
        borderBottomWidth: StyleSheet.hairlineWidth,
        borderBottomColor: '#eee',
      },
    });
    

    ButtonWithMenu contains a button that when pressed, renders the SectionList directly beneath it. It accepts an onItemPress prop which will be passed on into the SectionList:

    import React, { useState } from 'react';
    import {
      Text,
      View,
      StyleSheet,
      SectionList,
      useWindowDimensions,
      Modal,
      TouchableWithoutFeedback,
    } from 'react-native';
    
    import TouchableMaterialIcon from './TouchableMaterialIcon';
    import MenuItem from './MenuItem';
    import SectionHeader from './SectionHeader';
    import useLayout from '../hooks/useLayout';
    
    export default function DropDownButtonMenu({
      name,
      size,
      color,
      onItemPress,
      buttonStyle,
      containerStyle,
      menu,
      onMenuAction,
    }) {
      const [openMenu, setOpenMenu] = useState(false);
      const [buttonLayout, onButtonLayout] = useLayout();
      // add additional state to menu
      const [menuItems, setMenuItems] = useState(
        menu?.map((section, i) => {
          section.isOpened = false;
          section.id = `section-item-${i}`;
          return section;
        }) || []
      );
      const { width, height } = useWindowDimensions();
      const handleButtonPress = () => {
        setOpenMenu((prev) => !prev);
      };
      const toggleSectionMenu = (sectionId) => {    
        // deep clone menuItems
        const newData = JSON.parse(JSON.stringify(menuItems));
        const index = newData.findIndex((section) => section.id == sectionId);
        if (index < 0) {
          console.log('Section id not found');
          return;
        }
        newData[index].isOpen = !newData[index].isOpen;
        setMenuItems(newData);
      };
      const handleItemPress = (sectionTitle,itemTitle)=>{
        onItemPress(sectionTitle,itemTitle)
        setOpenMenu(false)
      }
      return (
        <View style={[containerStyle, { zIndex: 1 }]}>
          <TouchableMaterialIcon
            style={buttonStyle}
            name={name}
            size={size}
            color={color}
            onLayout={onButtonLayout}
            onPress={handleButtonPress}
            disabled={menuItems.length < 1}
          />
          {/* 
          wrap modal in touchable so that pressing outside modal will close it
          tried to use absolute positioned view but no matter the zIndex but
          the screen contents would always appear on top
          */}
          <TouchableWithoutFeedback onPress={() => setOpenMenu(false)}>
            <Modal transparent visible={openMenu}>
              <View
                style={[
                  styles.menu,
                  {
                    top: buttonLayout.y + buttonLayout.height,
                    left: buttonLayout.left,
                    minWidth: 150,
                    maxHeight: height * 0.6,
                  },
                ]}>
                <SectionList
                  sections={menuItems}
                  renderItem={(props) => <MenuItem {...props} onPress={handleItemPress} />}
                  renderSectionHeader={(props) => (
                    <SectionHeader
                      {...props}
                      toggleSectionMenu={toggleSectionMenu}
                      onPress={handleItemPress}
                    />
                  )}
                />
              </View>
            </Modal>
          </TouchableWithoutFeedback>
        </View>
      );
    }
    
    const styles = StyleSheet.create({
      menu: {
        position: 'absolute',
        backgroundColor: 'white',
        padding: 10,
        zIndex: 10,
      },
    });
    

    Now in the Header component the only thing that is left to do is create a list for the SectionList to render and then update the crumb state with onItemPress:

    import { useContext } from 'react';
    import { View, StyleSheet, Text, useWindowDimensions } from 'react-native';
    import ButtonWithMenu from '../components/ButtonWithMenu';
    import { LinearGradient } from 'expo-linear-gradient';
    import { ICON_SIZE } from '../Constants';
    
    export default function Header({width,height,crumbNavigation,setCrumbNavigation}) {
      return (
        <View style={{flex:1,height, width}}>
          <View style={styles.navbar}>
            
            <ButtonWithMenu
              containerStyle={[styles.navbarButton,{marginLeft:ICON_SIZE}]}
              name="home"
              size={ICON_SIZE}
              color="black"
            />
            <ButtonWithMenu
              containerStyle={styles.navbarButton}
              name="close"
              size={ICON_SIZE}
              color="black"
              onItemPress={(sectionTitle, itemTitle) =>
                setCrumbNavigation((prev) => {
                  const newCrumbNav = [prev[0]];
                  if (sectionTitle) newCrumbNav.push(sectionTitle);
                  if (itemTitle) newCrumbNav.push(itemTitle);
                  return newCrumbNav;
                })
              }
              menu={[
                { title: 'Disconnect from Unit', data: [] },
                { title: 'Monitor Unit', data: ['This Thing', 'That Thing'] },
                { title: 'Unit Settings', data: ['This Thing', 'That Thing'] },
                {
                  title: 'Unit Data Management',
                  data: ['Stored Data Configuration', 'Stored Data Snapshot'],
                },
                { title: 'Information', data: ['This Thing', 'That Thing'] },
                { title: 'Help', data: ['This Thing', 'That Thing'] },
              ]}
            />
          </View>
          <LinearGradient
            colors={['#902337', '#21458d']}
            style={styles.colorBar}
            start={{ x: 1, y: 1 }}
            locations={[0.5, 0.8]}
          />
          <View style={styles.crumbs}>
            <Text style={styles.crumbScreen} numberOfLines={1}>
              {crumbNavigation[0]} {crumbNavigation.length > 1 && ' > '}
              <Text style={styles.crumbTitle}>
                {crumbNavigation.slice(1).join(' : ')}
              </Text>
            </Text>
          </View>
        </View>
      );
    }
    
    const styles = StyleSheet.create({
      navbar: {
        flexDirection: 'row',
      },
      colorBar: {
        height: 5,
        width: '100%',
      },
    
      navbarButton: {
        // marginHorizontal: 10,
        paddingHorizontal: ICON_SIZE * 0.25,
      },
      crumbs: {
        overflow:'hidden',
        flexWrap:'none'
      },
      crumbScreen: {
        fontWeight: 'bold',
      },
      crumbTitle: {
        color: '#21458d',
        fontWeight: '400',
      },
    });
    

    Demo

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search