eli
eli

Reputation: 187

Menu Opened/Closed States Inherited in Children with Staggered Transitions in Tailwind/React

I'm struggling to figure out how to code a header/menu in my React/Tailwind app.

Here are the requirements:

What is the cleanest/most elegant approach to implement this with Tailwind?

I thought about separating Header/Navigation/NavigationItem into their own components, but the issue with this is that the Header needs to be aware of the NavigationItem components so that it can pass a isMenuOpen parameter to tell them to animate-in/out when the menu opens/closes.

It looks like this:

const App = () => {
  const [isMenuOpen, setIsMenuOpen] = useState(false)

  const handleMenuToggle = (isMenuOpen) => {
    setIsMenuOpen(isMenuOpen)
  }

  return (
    <Header onMenuToggle={handleMenuToggle}>
      <Navigation appearance="primary">
        <NavigationItem appearance="primary" index={0} isMenuOpen={isMenuOpen}>
          Menu Item #1
        </NavigationItem>
        <NavigationItem appearance="primary" index={1} isMenuOpen={isMenuOpen}>
          Menu Item #2
        </NavigationItem>
        <NavigationItem appearance="primary" index={2} isMenuOpen={isMenuOpen}>
          Menu Item #3
        </NavigationItem>
        <NavigationItem appearance="primary" index={3} isMenuOpen={isMenuOpen}>
          Menu Item #4
        </NavigationItem>
      </Navigation>
      <Navigation appearance="secondary">
        <NavigationItem appearance="secondary" index={4} isMenuOpen={isMenuOpen}>
          Facebook
        </NavigationItem>
        <NavigationItem appearance="secondary" index={5} isMenuOpen={isMenuOpen}>
          Twitter
        </NavigationItem>
      </Navigation>
    </Header>
  )
}

This isn't elegant and the components are too heavily tied to each other.

Another option was to do it purely in CSS with a class like .header.is-open .navigation-item {} but this feels like it goes against the Tailwind patterns.

Is there another way to do this?

Upvotes: 0

Views: 476

Answers (1)

Linda Paiste
Linda Paiste

Reputation: 42228

I thought about separating Header/Navigation/NavigationItem into their own components, but the issue with this is that the Header needs to be aware of the NavigationItem components so that it can pass a isMenuOpen parameter to tell them to animate-in/out when the menu opens/closes.

I don't see any reason why you can't pass isMenuOpen down as a prop. We can also clean up a lot of the duplication of similar JSX elements. The only difference from one NavigationItem to the next is the children content and the index, which increases by 1 each time.

We can create a MenuSection component that loops through items and applies the same props to all, incrementing the index.

const MenuSection = ({ appearance, isMenuOpen, startIndex = 0, menuItems }) => {
    return (
        <Navigation appearance={appearance}>
            {menuItems.map((node, i) => (
                <NavigationItem
                    key={i}
                    appearance={appearance}
                    index={i + startIndex}
                    isMenuOpen={isMenuOpen}
                >
                    {node}
                </NavigationItem>
            ))}
        </Navigation>
    );
};

This menuItems prop is an array of anything which can be used as the children property of the NavigationItem. So the elements can be a string or an element.

If we try to use this in App then it gets weird because we have to set the startIndex on the second MenuSection based on the count of items in the first. Does the index for the secondary navigation really need to continue from where the primary left off? If it does then we should extract one level deeper so that we can look at the length property of the first list.

const MenuPair = ({ isMenuOpen, primaryItems, secondaryItems }) => {
  return (
    <>
      <MenuSection
        isMenuOpen={isMenuOpen}
        appearance="primary"
        startIndex={0}
        menuItems={primaryItems}
      />
      <MenuSection
        isMenuOpen={isMenuOpen}
        appearance="secondary"
        startIndex={primaryItems.length}
        menuItems={secondaryItems}
      />
    </>
  );
};
const App = () => {
  const [isMenuOpen, setIsMenuOpen] = useState(false);

  const handleMenuToggle = (isMenuOpen) => {
    setIsMenuOpen(isMenuOpen);
  };

  return (
    <Header onMenuToggle={handleMenuToggle}>
      <MenuPair
        isMenuOpen={isMenuOpen}
        primaryItems={[
          "Menu Item #1",
          "Menu Item #2",
          "Menu Item #3",
          "Menu Item #4"
        ]}
        secondaryItems={["Facebook", "Twitter"]}
      />
    </Header>
  );
};

For this 6-item menu we haven't shortened the code by abstracting pieces into their own components, but at least we've ensured that the index property will always index properly without having to hard-code the numbers.

Upvotes: 0

Related Questions