How to change a slide out drawer in react grid to position fixed

Viewed 627

I am trying to create a slide out drawer exactly like the one present in JIRA.

Here is what JIRA looks like (notice how sidebar opens and pushes content to the side):

Jira 1

Here is what I have managed to create in my code:

My code

The sliding effect and shelf looks similar enough and I am happy with it. However, there is one feature that Jira has that I find quite interesting and would like to recreate in my code base. When the sidebar is closed, and the user hovers over the sliver of sidebar that remains, the sidebar opens up as if the button to open had been clicked, and it then closes when the mouse leaves the sidebar. What is really interesting to me is that the grid format does not change and the sidebar appears to not move the other content on the page which makes me believe it somehow switches to a fixed position just during this mouse over event. I am not sure how to achieve this effect and my code is currently not working. Any help would be greatly appreciated!

Here is the desired effect (notice how sidebar opens and DOES NOT push content to the side, which is different than actually clicking the button. See first gif example):

Jira Final

EDIT: Here is the effect for mousing in to the sidebar and the button becoming visible while in the sidebar:

Edit Image

Here is what I have tried so far:

(SideBar Component)

import React, { useState, useRef } from 'react';
import { Link } from 'react-router-dom';
import './RenewalSidebar.css';

function RenewalSidebar({ worksheetID }) {
    const [sidebar, setSidebar] = useState(false);
    const [cursorInSidebar, setCursorInSidebar] = useState(false);
    const showSidebar = () => setSidebar(!sidebar);
    const sidebarRef = useRef(null);

    return (
        <nav
            ref={sidebarRef}
            className={sidebar ? 'sidebar' : 'sidebar inactive'}
            onMouseLeave={() => setCursorInSidebar(false)}
            onMouseOver={(e) => {
                if (e.target === sidebarRef.current || sidebarRef.current.contains(e.target)) {
                    console.log(`Hey I'm in the sidebar`);
                    setCursorInSidebar(true);
                }
            }}
            data-sidebarhover={cursorInSidebar}
            data-sidebaropen={!sidebar}
        >
            <div
                role="button"
                tabIndex={0}
                className="hamburger"
                type="button"
                onClick={showSidebar}
                data-open={!sidebar}
                data-hover={cursorInSidebar}
                onMouseEnter={() => setCursorInSidebar(true)}
                onMouseLeave={() => setCursorInSidebar(false)}
            >
                {sidebar ? '>' : '<'}
            </div>

            <div className="sidebar-content">
                <ul onClick={showSidebar} data-open={!sidebar}>
                    <li>
                        <Link to={`/`}>Test</Link>
                    </li>
                    <li>
                        <Link to={`/`}>Test</Link>
                    </li>
                    <li>
                        <Link to={`/`}>Test</Link>
                    </li>
                </ul>
            </div>
        </nav>
    );
}

export default RenewalSidebar;

Here is my css for the top level grid container I am using:

.RenewalsDetailContainer {
    display: grid;
    grid-template-columns: max-content 1fr;
    justify-items: center;
}

And here is my css for the Sidebar component:

.sidebar {
    position: relative;
    /* top: 20vh;
    left: -300px; */
    width: 20px;
    height: 100%;
    /* border-radius: 0px 5px 5px 0px; */
    background-color: aliceblue;
    z-index: 0;
    transition: width 200ms ease-out;
    box-shadow: inset -10px 0 25px -25px #888;
    padding: 0 10px;
}

.sidebar.inactive {
    width: 300px;
    z-index: 6000;
}

.sidebar ul {
    margin: 0;
    padding: 0;
}

.sidebar ul[data-open='false'] {
    display: none;
}

.sidebar li a {
    display: block;
    border: none;
    background-color: white;
    padding: 14px 28px;
    font-size: 16px;
    margin: 10px 10px 10px 10px;
    width: auto;
    text-align: center;
}

/* .sidebar[data-sidebaropen='false'][data-sidebarhover='true'] {
    z-index: -9000;
} */

.sidebar[data-sidebaropen='false'][data-sidebarhover='true'] .sidebar-content {
    position: fixed;
    width: 300px;
    top: 0;
    left: 0;
    background-color: purple;
}

.hamburger {
    color: rgb(107, 119, 140);
    background-color: rgb(255, 255, 255);
    height: 24px;
    border: none;
    outline: 0;
    width: 24px;
    top: 32px;
    position: absolute;
    right: 0px;
    font-size: 15px;
    font-weight: bold;
    border-radius: 50%;
    transform: translateX(50%);
    display: grid;
    place-items: center;
    transition: all 500ms linear, color 100ms linear, opacity 350ms cubic-bezier(0.2, 0, 0, 1);
    box-shadow: rgb(9 30 66 / 8%) 0px 0px 0px 1px, rgb(9 30 66 / 8%) 0px 2px 4px 1px;
}

.hamburger:hover {
    background-color: #4c9aff;
    color: white;
    box-shadow: none;
    cursor: pointer;
}

.hamburger[data-open='true'] {
    opacity: 0;
}

.hamburger[data-open='true']:hover {
    opacity: 1;
}

.hamburger[data-open='true'][data-hover='true'] {
    opacity: 1;
}

.hamburger:after,
.hamburger:before,
.hamburger div {
    background-color: #0076c7;
    /* margin: 7px 0; */
    border-radius: 3px;
    content: '';
    display: block;
    transition: all 300ms ease-in-out;
}

.sidebar-content {
    position: relative;
    width: 100%;
    height: 100%;
}

.sidebar-content .sidebar.active .hamburger div {
    transform: scale(0);
}

And finally...here is a Code sandbox link I have created to help you test the code. Click me!

2 Answers

I have made few changes to accommodate this.

  1. Nesting of sidebar and button will raise conflict between onMouseEnter events, you won't be able to click on the button. (Moved the button outside nav)
  2. Position absolute does not push the content. And you can use react inline styles like this : style={{position: cursorInSidebar && !sidebar ? "absolute" : "relative"}}
  3. Added isOpen to track if the sidebar is open. Either by button click or mouse hover.
  4. onClick events should set cursorInSidebar.
const SideBar = () => {
  const [sidebar, setSidebar] = useState(false);
  const [cursorInSidebar, setCursorInSidebar] = useState(false);

  const showSidebar = (isOpen) => {
    setSidebar(!isOpen);
    setCursorInSidebar(false);
  };

  const sidebarRef = useRef(null);

  const enterSideBar = () => {
    setCursorInSidebar(true);
  };

  const leaveSideBar = () => {
    setCursorInSidebar(false);
  };

  const isOpen = sidebar || cursorInSidebar;
  return (
    <div className="divSid">
      <div
        role="button"
        tabIndex={0}
        className="hamburger"
        type="button"
        onClick={() => showSidebar(isOpen)}
        data-open={isOpen}
      >
        {isOpen ? "<" : ">"}
      </div>
      <nav
        ref={sidebarRef}
        style={{
          position: cursorInSidebar && !sidebar ? "absolute" : "relative"
        }}
        className={isOpen ? "sidebar inactive" : "sidebar"}
        data-sidebaropen={isOpen}
        onMouseEnter={enterSideBar}
        onMouseLeave={leaveSideBar}
      >
        <div className="sidebar-content">
          <ul onClick={() => showSidebar(isOpen)} data-open={isOpen}>
            <li>Test</li>
            <li>Test</li>
            <li>Test</li>
          </ul>
        </div>
      </nav>
    </div>
  );
};

While moving button outside nav, alignment of components will break. Here is the working code.

I believe I have addressed all of your feature requests in this updated CodeSandbox:

https://codesandbox.io/s/jira-inspired-menu-bnyd8

The crux of the solution was to decouple the SideBar content from the mechanics of how that column is propped open, depending on UI state. It required a bit of nesting and absolute positioning to get the job done.

The content width of the SideBar is measured with getBoundingClientRect() when the panel is opened up, and then that value is stored in state. That value is then used to control the width of the SideBar container when it is open, as well as the shim width (which is what actually pushes the main content to the side when the SideBar is fully opened) and hamburger x coordinate.

Relevant code for my solution is in SideBar.js and SideBar.css in the CodeSandbox link above.


Preview of UI in action:

enter image description here

Related