How to Create A Bottom Drawer In ReactJS Using Ionic Framework Components and Gesture API

Aaron K Saunders - May 11 '20 - - Dev Community

Ionic Framework Gesture API makes it easy to create animations and effects in your mobile application. This is a walkthrough of a simple project where we are using the Ionic Framework Gesture API to implement a custom bottom-drawer component.

The Video

Lets Get Started

So the goal here is to have the drawer with only 10px displayed when
closed, which leaves room for button or handle to start drag

.bottom-drawer {
  position: absolute;
  right: 4px;
  left: 4px;
  bottom: -380px;
  height: 400px;
  border-radius: 30px;
}
Enter fullscreen mode Exit fullscreen mode

Set the class name for styling the drawer, bottom-drawer and then get reference to the element so we can attach the gestureAPI to the object. We are using the react-hooks useRef call to get the element.

The IonButton is styled a bit, but we are just using it as something the click to start the drag to open it is also used to toggle the state of the bottom drawer.

When clicked, the onClick handler calls a function toggleDrawer to open or close the menu based on it's current state.

<IonApp>
  <IonHeader>
    <IonToolbar />
  </IonHeader>
  <IonContent scrollY={false} className="ion-padding">

    <IonCard className="bottom-drawer" ref={drawerRef}>
      <div style={{ textAlign: "center" }}>
        <IonButton
          size="small"
          style={{ height: 10 }}
          onClick={toggleDrawer}
        />
      </div>
      <IonCardHeader>Bottom Drawer</IonCardHeader>
    </IonCard>
  </IonContent>
</IonApp>
Enter fullscreen mode Exit fullscreen mode

Getting The Element

Using react-hooks useRef to get the element, the value we actually need is the drawerRef.current.

As a note, the same can be accomplished by using the DOM query function document.getElementsByClassName

document.getElementsByClassName("bottom-drawer")
Enter fullscreen mode Exit fullscreen mode
const drawerRef = useRef();

... below in the render

<IonCard className="bottom-drawer" ref={drawerRef}>
</IonCard>
Enter fullscreen mode Exit fullscreen mode

Attaching The Gesture

We get the reference and use that value as the element to attach the gesture to; name it and then indicate we are focusing on the y-axis as the direction for this gesture.

Documentation for the Ionic Gesture API

 useEffect(() => {
    let c = drawerRef.current;
    const gesture = createGesture({
      el: c,
      gestureName: "my-swipe",
      direction: "y",
      onMove : (event)=> {},
      onEnd : (event)=> {}
  }, []);
Enter fullscreen mode Exit fullscreen mode

We are focusing on two of the handlers that are available with the Gesture API, onMove and onEnd.

With the onMove handler we detect that the DOM element is has received and event and is starting to move, we get the change in value, event.deltaY, from the event and reposition the element using translateY

We check if the user is dragging beyond the desired delta -300, and if so we stop repositioning the element because we don't want to open the bottom drawer beyond it's height.

To provide a better user experience, if the user has started to drag the element more than a delta of 20, we assume they want to close the bottom-drawer element so we will use some animation and reposition the element to it full closed position.

onMove: event => {
  if (event.deltaY < -300) return;

 // closing with a downward swipe
 if (event.deltaY > 20) {
   c.style.transform = "";
   c.dataset.open = "false";
   return;
 }
  c.style.transform = `translateY(${event.deltaY}px)`;
},
Enter fullscreen mode Exit fullscreen mode

To provide a better user experience, if the user has started to drag the element more than a delta of -30, we assume they want to open the bottom-drawer element so we will use some animate and reposition the element to it full open position.

onEnd: event => {
  c.style.transition = ".5s ease-out";

  if (event.deltaY < -30 && c.dataset.open != "true") {
    c.style.transform = `translateY(${-350}px) `;
    c.dataset.open = "true";
  }
}
Enter fullscreen mode Exit fullscreen mode

You noticed in the code above we have been using the dataset.open attribute on the element that we are manipulating. This custom attribute holds the state of the bottom-drawer.

Using Data Attributes in HTML

Yes you could have managed the state in the react application but I chose to do it this way.

Handling the Button Click

Since we now have the proper animations and delta thresholds figured out, we can use them as a response to a click event on a button to determine how to open or close the drawer.

And as mentioned in the last section we have access to the dataset.open attribute to let us know how to toggle the drawer open and closed based on the mouse click.

const toggleDrawer = () => {
  let c = drawerRef.current;
  if (c.dataset.open === "true") {
    c.style.transition = ".5s ease-out";
    c.style.transform = "";
    c.dataset.open = "false";
  } else {
    c.style.transition = ".5s ease-in";
    c.style.transform = `translateY(${-350}px) `;
    c.dataset.open = "true";
  }
};
Enter fullscreen mode Exit fullscreen mode

Conclusion

This is a simple example of the power of the new Gesture API in Ionic Framework. This BottomDrawer implementation works, but I am certain there are some tweaks to make it more robust and I am open to hearing some feedback.

Please take a look at the rest of the content I have posted on reactjs and Ionic Framework here on my Dev.To profile and also there are videos posted on my YouTube Channel

On CodeSandbox

Full Source Code

// App.js
import React, { useEffect, useRef } from "react";
import {
  IonApp,
  IonContent,
  IonButton,
  IonCard,
  IonHeader,
  IonToolbar,
  createGesture,
  IonCardHeader
} from "@ionic/react";

/* Core CSS required for Ionic components to work properly */
import "@ionic/react/css/core.css";

/* Basic CSS for apps built with Ionic */
import "@ionic/react/css/normalize.css";
import "@ionic/react/css/structure.css";
import "@ionic/react/css/typography.css";

/* Optional CSS utils that can be commented out */
import "@ionic/react/css/padding.css";
import "@ionic/react/css/float-elements.css";
import "@ionic/react/css/text-alignment.css";
import "@ionic/react/css/text-transformation.css";
import "@ionic/react/css/flex-utils.css";
import "@ionic/react/css/display.css";

import "/App.css";

const App = () => {
  const drawerRef = useRef();

  // when the page is loaded, we find the element that is the drawer
  // and attach the gesture to it's reference using react `useRef` hook
  useEffect(() => {
    let c = drawerRef.current;
    const gesture = createGesture({
      el: c,
      gestureName: "my-swipe",
      direction: "y",
      /**
       * when moving, we start to show more of the drawer
       */
      onMove: event => {
        if (event.deltaY < -300) return;

        // closing with a downward swipe
        if (event.deltaY > 20) {
          c.style.transform = "";
          c.dataset.open = "false";
          return;
        }

        c.style.transform = `translateY(${event.deltaY}px)`;
      },
      /**
       * when the moving is done, based on a specific delta in the movement; in this
       * case that value is -150, we determining the user wants to open the drawer.
       *
       * if not we just reset the drawer state to closed
       */
      onEnd: event => {
        c.style.transition = ".5s ease-out";

        if (event.deltaY < -30 && c.dataset.open !== "true") {
          c.style.transform = `translateY(${-350}px) `;
          c.dataset.open = "true";
          console.log("in on end");
        }
      }
    });

    // enable the gesture for the item
    gesture.enable(true);
  }, []);

  /**
   * this function is called when the button on the top of the drawer
   * is clicked.  We are using the data-set attributes on the element
   * to determine the state of the drawer.
   *
   * this could be done using react state if you like.
   */
  const toggleDrawer = () => {
    let c = drawerRef.current;
    if (c.dataset.open === "true") {
      c.style.transition = ".5s ease-out";
      c.style.transform = "";
      c.dataset.open = "false";
    } else {
      c.style.transition = ".5s ease-in";
      c.style.transform = `translateY(${-350}px) `;
      c.dataset.open = "true";
    }
  };

  return (
    <IonApp>
      <IonHeader>
        <IonToolbar />
      </IonHeader>
      <IonContent scrollY={false} className="ion-padding">
        <p>
          Sample project using Gesture API from Ionic Framework to create a
          bottom drawer
        </p>
        <ul>
          <li> Click button to open or close the drawer</li>
          <li> Drag to open or close</li>
        </ul>
        {/* 
    Set the class name for styling the drawer and then get reference
    so we can attach the gestureAPI to the object 
    */}
        <IonCard className="bottom-drawer" ref={drawerRef}>
          <div style={{ textAlign: "center" }}>
            <IonButton
              size="small"
              style={{ height: 10 }}
              onClick={toggleDrawer}
            />
          </div>
          <IonCardHeader>Bottom Drawer</IonCardHeader>
        </IonCard>
      </IonContent>
    </IonApp>
  );
};

export default App;
Enter fullscreen mode Exit fullscreen mode
/* App.css
so the goal here is to have the drawer with only 10px displayed when 
closed, which leaves room for button or handle to start drag
*/
.bottom-drawer {
  position: absolute;
  right: 4px;
  left: 4px;
  bottom: -380px;
  height: 400px;
  border-radius: 30px;
}

Enter fullscreen mode Exit fullscreen mode
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .