Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Animate the expanding/collapsing of sub-items in navigation #10

Open
hrishikesh-k opened this issue Aug 1, 2020 · 4 comments
Open

Animate the expanding/collapsing of sub-items in navigation #10

hrishikesh-k opened this issue Aug 1, 2020 · 4 comments

Comments

@hrishikesh-k
Copy link

hrishikesh-k commented Aug 1, 2020

Hello.

I've been trying to animate the opening/closing of sub-items in the navigation. I've got successful in animating the expand, but, I don't know how I should animate the collapse.

Here's what I did to animate the expand:

  1. In my src\gatsby-theme-document\components\LeftSidebar\NavItem.js, I wrapped the sub-items inside a <div> with overflow-y: hidden like this:
<div style = {{overflowY: "hidden"}}>
  {
    hasChildren && !isCollapsed && (
      <NavItemChild>
        {
          item.items.map(child => (
            <StyledNavItem key = {child.url}>
              <NavItemLink to = {child.url} activeClassName = "is-active">
                {child.title}
              </NavItemLink>
            </StyledNavItem>))
        }
      </NavItemChild>)
  }
</div>
  1. In the same file, I added the following styles to NavItemChild (the const in the end of the file):
animation-name: animExpand;
animation-duration: 0.25s;
animation-timing-function: var(--ease-in-out-quad);
position: relative
  1. Lastly, in src\gatsby-theme-document\styles\global.js, I added the animation:
@keyframes animExpand
  {
    0%
      {
        top: -100px;
      }
    100%
      {
        top: 0px
      }
  }

So, yeah, this works perfectly while expanding, but, the collapsing is still instant. How can I animate that too?

Here's the demo of what I've done:

Demo

@hrishikesh-k
Copy link
Author

I've now made use of JavaScript animations. This is the complete src\gatsby-theme-document\components\LeftSidebar\NavItem.js:

/* modified */
/* eslint-disable no-sequences */
import React, {useContext} from 'react';
import {Link} from 'gatsby';
import styled from '@emotion/styled';
import {GlobalDispatchContext, GlobalStateContext} from '../../context/GlobalContextProvider';
import ButtonCollapse from '../ButtonCollapse';
const NavItem = ({item}) =>
  {
    const state = useContext(GlobalStateContext);
    const dispatch = useContext(GlobalDispatchContext);
    const isCollapsed = state.collapsed[item.url];
    const hasChildren = item.items && item.items.length > 0;
    function expandList ()
      {
        var elem1 = document.getElementById("navChild");
        var pos1 = -100;
        var id1 = setInterval(frame1, 1);
        function frame1()
          {
            if (pos1 === 0)
              {
                clearInterval(id1);
              } else
                {
                  pos1++;
                  elem1.style.top = pos1 + 'px';
                }
          }
      }
    function collapseList ()
      {
        var elem2 = document.getElementById("navChild");
        var pos2 = 0;
        var id2 = setInterval(frame2, 1);
        function frame2()
          {
            if (pos2 === -100)
              {
                clearInterval(id2);
              } else
                {
                  pos2--;
                  elem2.style.top = pos2 + 'px';
                }
          }
      }
    return (
      <StyledNavItem>
        <NavItemLink to = {item.url} activeClassName = "is-active">
          {item.title}
        </NavItemLink>
          {
            hasChildren && (
              <ButtonCollapse isCollapsed = {isCollapsed} onClick =
                {
                  () => isCollapsed ? (
                  <>
                    {
                      expandList(),
                      dispatch({type: 'TOGGLE_NAV_COLLAPSED', url: item.url})
                    }
                  </>) : (
                    <>
                      {
                        collapseList(),
                        setTimeout(() =>
                          {
                            dispatch({type: 'TOGGLE_NAV_COLLAPSED', url: item.url})
                          }, 250)
                      }
                    </>)
                }/>)
          }
        <div style = {{overflowY: "hidden"}}>
          {
            hasChildren && (
              <NavItemChild id = "navChild">
                {
                  item.items.map(child => (
                    <StyledNavItem key = {child.url}>
                      <NavItemLink to = {child.url} activeClassName = "is-active">
                        {child.title}
                      </NavItemLink>
                    </StyledNavItem>))
                }
              </NavItemChild>)
          }
        </div>
      </StyledNavItem>);
  };
const StyledNavItem = styled.li `position: relative; display: block; padding: 0; margin: 0.2rem 0; width: 100%; list-style: none`;
const NavItemLink = styled(Link) `display: block; padding: 0.5rem 1.8rem 0.5rem 1.2rem; width: 100%; color: ${p => p.theme.colors.text}; font-weight: 600; text-decoration: none; transition: color ${p => p.theme.transition}; &:hover, &:focus, &.is-active {color: ${p => p.theme.colors.primary}}`;
const NavItemChild = styled.ul `margin: 0.5rem 0 0.5rem 1.2rem; padding: 0; border-left: 1px solid ${p => p.theme.colors.text}; list-style: none; & > li {margin: 0}; position: relative`;
export default React.memo(NavItem);

This is definitely not the best way to do it, but, at my skill level, even this took my entire day and a lot of efforts. All suggestions to make it better even slighter are welcome, but, till then, I'm using it like this.

@hrishikesh-k
Copy link
Author

I didn't consider the use case of adding more items to the list and now that I try with that, it doesn't work. The subsequent items don't move.

@hrishikesh-k hrishikesh-k reopened this Aug 2, 2020
@hrishikesh-k
Copy link
Author

After a lot of reading, I have reached till here:

/* modified */
/* eslint-disable no-sequences */
import React from 'react';
import {Link} from 'gatsby';
import styled from '@emotion/styled';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'
import {faCaretRight} from '@fortawesome/free-solid-svg-icons'
const NavItem = ({item}) =>
  {
    var isCollapsed = false
    const hasChildren = item.items && item.items.length > 0;
    let buttonRef = React.createRef();
    let listRef = React.createRef();
    function expandList ()
      {
        listRef.current.style.display = 'block';
        var pos1 = -100;
        var rot1 = 90;
        var id1a = setInterval(frame1a, 1);
        var id1b = setInterval(frame1b, 1);
        function frame1a()
          {
            if (pos1 === 0)
              {
                clearInterval(id1a);
              } else
                {
                  pos1++;
                  listRef.current.style.top = pos1 + 'px';
                }
          }
        function frame1b()
          {
            if (rot1 === -90)
              {
                clearInterval(id1b);
              } else
                {
                  rot1--;
                  buttonRef.current.style.transform = 'rotate(' + rot1 + 'deg)';
                }
          }
      }
    function collapseList ()
      {
        var pos2 = 0;
        var rot2 = -90;
        var id2a = setInterval(frame2a, 1);
        var id2b = setInterval(frame2b, 1)
        function frame2a()
          {
            if (pos2 === -100)
              {
                clearInterval(id2a);
                listRef.current.style.display = 'none';
              } else
                {
                  pos2--;
                  listRef.current.style.top = pos2 + 'px';
                  
                }
          }
        function frame2b()
          {
            if (rot2 === 90)
              {
                clearInterval(id2b);
              } else
                {
                  rot2++;
                  buttonRef.current.style.transform = 'rotate(' + rot2 + 'deg)';
                }
          }
      }
    return (
      <StyledNavItem>
        <NavItemLink to = {item.url} activeClassName = "is-active">
          {item.title}
        </NavItemLink>
          {
            hasChildren && (
              <StyledButton ref = {buttonRef}>
                <FontAwesomeIcon icon = {faCaretRight} size = "1.75x" fixedWidth onClick =
                  {
                    () => isCollapsed ? (
                    <>
                      {
                        expandList(),
                        isCollapsed = false
                      }
                    </>) : (
                      <>
                        {
                          collapseList(),
                          isCollapsed = true
                        }
                      </>)
                  }/>
                </StyledButton>)
          }
        <div style = {{overflowY: "hidden"}}>
          {
            hasChildren && (
              <NavItemChild ref = {listRef}>
                {
                  item.items.map(child => (
                    <StyledNavItem key = {child.url}>
                      <NavItemLink to = {child.url} activeClassName = "is-active">
                        {child.title}
                      </NavItemLink>
                    </StyledNavItem>))
                }
              </NavItemChild>)
          }
        </div>
      </StyledNavItem>);
  };
const StyledNavItem = styled.li `position: relative; display: block; padding: 0; margin: 0.2rem 0; width: 100%; list-style: none`;
const NavItemLink = styled(Link) `display: block; padding: 0.5rem 1.8rem 0.5rem 1.2rem; width: 100%; color: ${p => p.theme.colors.text}; font-weight: 600; text-decoration: none; transition: color ${p => p.theme.transition}; &:hover, &:focus, &.is-active {color: ${p => p.theme.colors.primary}}`;
const StyledButton = styled.button `position: absolute; top: 0; right: 0; padding: 0 0.8rem; height: 37px; background: none; border: 0; color: ${p => p.theme.colors.text}; cursor: pointer; font-size: 1rem; outline: none; transform: rotate(-90deg)`;
const NavItemChild = styled.ul `margin: 0.5rem 0 0.5rem 1.2rem; padding: 0; border-left: 1px solid ${p => p.theme.colors.text}; list-style: none; & > li {margin: 0}; position: relative`;
export default React.memo(NavItem);

I've got rid of the ButtonCollapse, GlobalDispatchContext, GlobalStateContext and instead used a simple variable to trigger expand and collapse. Also, I switched to use React refs instead of the document.getElementById("navChild"); in my previous snippet.

The last things left to fix in this is the animation sync of the rotation of arrow and position of the list 'jump' that's caused when the next navChild move up or down. Here's a screen-recording of my progress:

ScreenRecorderProject1

@hrishikesh-k
Copy link
Author

The above code doesn't save the state when navigating in pages. I'm now giving up on this as it was the end of my skill level, unless some skilled and kind soul decides to help.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant