In the first part of this Draggable journey, we discussed how to include scripts, investigated the ThrowPropsPlugin, including the requirements to jump start our project in hopes of taking it to eleven! Now, get ready to make an off-canvas menu system which reacts to keyboard and touch.
The Demo
The full demo that we’ll be building and discussing for the remainder of this tutorial is also available on CodePen.
I encourage you to test this for yourself across as many devices as possible, especially keyboard navigation. Each interaction–whether touch, keyboard or mouse–has been accounted for, but as you’ll find in our current landscape you can’t detect a touchscreen and at times trying to do so even results in false positives.
The Setup
Using the markup from part I we’ll begin by adding a container div
for structural purposes along with correlating classes for CSS and JavaScript hooks.
<div class="app"> <header class="dragaebel-lcontainer" role="banner"> <a href="/" class="js-draglogo">…</a> <a href="#menu" class="dragaebel-toggle js-dragtoggle" id="menu-button">…</a> <nav class="dragaebel-nav js-dragnav" id="menu" role="navigation">…</nav> </header> <main role="main"> <div class="dragaebel-lcontainer js-dragsurface"></div> </main> </div>
Classes that begin with the ”js” prefix signify that these classes only appear in JavaScript; removing them would hinder functionality. They’re never used in CSS, helping to isolate the focus of concerns. The surrounding container will help to control scrolling behavior which is discussed in the upcoming CSS section.
Accessibility
With the the foundation in place it’s time to add a layer of ARIA on top to lend semantic meaning to screen readers and keyboard users.
<nav aria-hidden="true">…</nav>
Since the menu will be hidden by default the aria-hidden
attribute is labeled true
and will be updated accordingly depending on the menu’s state; false
for open, true
for closed. Here’s an explanation of the attribute aria-hidden
per the W3C specification:
Indicates that the element and all of its descendants are not visible or perceivable to any user as implemented by the author. […] Authors MUST set aria-hidden=”true” on content that is not displayed, regardless of the mechanism used to hide it. This allows assistive technologies or user agents to properly skip hidden elements in the document. ~W3C WAI-ARIA Spec
Authors should be careful what content they hide, making this attribute a separate discussion outside the scope of this article. For those curious, the specification defines the attribute in further length and is somewhat grokkable; something I don’t usually say that often about specification jargon.
The CSS
Our CSS is where the magic really begins. Let’s take the important parts from the demo that bear meaning and break it down.
body { // scroll fix height: 100%; overflow: hidden; // end scroll fix } .app { // scroll fix overflow-y: scroll; height: 100vh; // end scroll fix } .dragaebel-nav { height: 100vh; overflow-y: auto; position: fixed; top: 0; right: 0; }
Setting the body height to 100% allows the container to stretch the entire viewport, but it’s also playing a more important part; allowing us to hide its overflow.
The overflow scroll fix helps to control how the primary container and navigation behave when either one contains overflowing content. For example, If the container is scrolled—or the menu—the other will not scroll when the user reaches the end of the intially scrolled element. It’s a weird behavior, not typically discussed, but makes for a better user experience.
Viewport Units
Viewport units are really powerful and play a vital role in how the primary container holds overflowing content. Viewport units have wonderful support across browsers these days and I highly suggest you start using them. I’ve used vh units on the nav, but I could have used a percentage instead. During development it was discovered that div.app
must use vh
units since percentage won’t allow for the overflowing content to maintain typical scrolling behavior; the content results in being clipped. Overflow is set to scroll
in preparation in case the menu items exceeed the height of the menu or the height of the viewport becomes narrow.
// Allow nav to open when JS fails .no-js .dragaebel-nav:target { margin-right: 0; } .dragaebel-nav { margin-right: -180px; width: 180px; }
The .no-js .nav:target
provides access to our menu regardless if JavaScript fails or is turned off, hence the reason we added the ID value to the href
attribute of the menu trigger.
The primary navigation is moved to the right via a negative margin which is also the same as the nav’s width. For the sake of brevity I’m writing Vanilla CSS, but I’m sure you could write something fancier in a pre-processor of your choice.
The JavaScript
JavaScript is the last stop of this draggable menu journey, but before we write one line of JS we’ll need to write a module pattern setup.
var dragaebelMenu = (function() { function doSomething() {…} return { init: function() {…} } })(); dragaebelMenu.init(); // start it!
Variables
For the configuration setup we’ll define some variables for future reference.
var dragaebelMenu = (function() { var container = document.querySelectorAll('.js-dragsurface')[0], nav = document.querySelectorAll('.js-dragnav')[0], nav_trigger = document.querySelectorAll('.js-dragtoggle')[0], logo = document.querySelectorAll('.js-draglogo')[0], gs_targets = [ container, nav, logo, nav_trigger ], closed_nav = nav.offsetWidth + getScrollBarWidth(); })();
Most of these variables are simply grabbing DOM
elements, with the exception of the last two that define our GreenSock targets plus the width of the navigation menu. The utility function getScrollBarWidth()
(outside our discussion today) retrieves the width of the scroll bar so we can position the nav just beyond the width of the bar itself in order to see it when the menu opens. The targets are what we move when the menu opens in order to allow adjacent content to be pushed.
Methods
To keep things short I’ll only discuss methods that are extremely important to the functionality of the menu behavior. Everything else that you’ll see in the demo not discussed here is the “sugar on top” stuff that makes the menu even more powerful.
function menu(duration) { container._gsTransform.x === -closed_nav ? TweenMax.to(gs_targets, duration, { x: 0, ease: Linear.easeIn }) : TweenMax.to(gs_targets, duration, { x: -closed_nav, ease: Linear.easeOut }); }
The menu
function detects whether the container’s x coordinate equals the closed nav state. If so it sets the targets back to their starting position, otherwise sets them to their open position.
function isOpen() { return container._gsTransform.x < 0; }
This is a utility function to check the menu’s state. This will return 0
if the menu is closed, or a negative value if it’s open.
function updateNav(event) { TweenMax.set([nav, logo, nav_trigger], { x: container._gsTransform.x }); }
This is another utility function which sets the target’s x coordinate inside the array parameter of the .set()
method to the container’s x position everytime the onDrag
or onThrowUpdate
event happens. This is part of the Draggable
object instance.
function enableSelect() { container.onselectstart = null; // Fires when the object is being selected. TweenMax.set(container, { userSelect: 'text' }); } function disableSelect() { TweenMax.set(container, { userSelect: 'none' }); } function isSelecting() { // window.getSelection: Returns a Selection object representing // the range of text selected by the user or the current position // of the caret. return !!window.getSelection().toString().length; }
These functions help to determine if someone is really selecting text in order to enable / disbale selection capabilities when someone drags across the screen. This is not the most ideal behavior for mouse events, but again, as we already mentioned, you can’t detect a touch screen.
Draggable Instance
Draggable.create([targets], {options})
As we discussed in the previous tutorial about Draggable, this will create the instance of the Draggable object and target the DOM
objects of our choice that can be passed as as an array.
Draggable.create([container], { type: 'x', dragClickables: false, throwProps: true, dragResistance: 0.025, edgeResistance: 0.99999999, maxDuration: 0.25, throwResistance: 2000, cursor: 'resize', allowEventDefault: true, bounds: {…}, onDrag: updateNav, onDragEnd: function(event) {…}, liveSnap: function(value) {…}, onPress: function(event) {…}, onClick: function(event) {…}, onThrowUpdate: function() {…} });
This is our entire Draggable instance and the properties used. The actual demo code contains comments I’ve left in order to understand and gain a better persepective on what each one is responsible for. I encourage you to look into the demo code and even challenge you to deconstruct the why and how.
So Long, But Not Goodbye
This is the end to our GreenSock journey and I hope you learned a ton along the way. A special thanks to Jack and Carl at GreenSock, along with the entire GreenSock community, for their incredible help throughout this series. Last, but not least, a huge thanks to you, the reader for reaching the end of this series; congrats! I hope this series helped gain a better look into the powerful benefits and capabilites of an awesome JavaScript animation library. Build awesome things and stay creative!