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;
}
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>
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")
const drawerRef = useRef();
... below in the render
<IonCard className="bottom-drawer" ref={drawerRef}>
</IonCard>
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)=> {}
}, []);
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)`;
},
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";
}
}
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.
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";
}
};
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;
/* 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;
}