Creating an iPad-like dropdown menu in HTML5

[image]

I've been learning about the funky new stuff you can do using HTML5 lately, including doing a lot of Javascripting, and messing with Canvas and CSS3 transitions and thought I'd share some interesting things you can do that work on the iPad.

Well before the iPad and it's newer drop-down-with-arrow menu came out, I actually thought that dialog boxes with arrows were a great way to present options to a mobile/touch user. Check out my post from Sept. 2009 to see my mock-ups. Pretty similar to what the iPad's UI looks like now, hey? They're probably my favorite new UI feature.

As I'm developing web stuff and browsing them on the iPad, I thought it'd be nice for the option menus I create in HTML to look like the native ones. You can get it pretty close using rounded corners and an arrow png, or hacky css triangles, but as I was reading about drawing using the new Canvas object the other day, I thought, "hmmm, I bet that'd be really useful for creating new UI widgets." So I set about trying to re-create the iPad dropdown menu in HTML5.

You can check out the demo here.

There's lots of HTML5 Canvas and CSS3 Transitions tutorials out there, so I won't go into the details, but I figured I'd share some of the things I learned while experimenting. First, let me go over the basics: In order to make the example "canonical", I didn't use any libraries like JQuery, etc. It was nice to get "back to basics" actually, but in general I just use JQuery in all my projects as it makes things so much easier. Also, I'm not the world's greatest Javascript developer, so there might be better/more efficient ways of doing this stuff, so if you have any suggestions/corrections please send them my way.

Okay, here's the complete page dump:


<!DOCTYPE html> 
<html lang="en"> 
<head> 
    <meta charset="utf-8" /> 
    <title>HTML5 iPad Popup Menu</title>
<meta name="viewport" content="width=device-width, maximum-scale=1.0" />

<style>

body{
    font-family: sans-serif;
    padding: 10px;
}

a{
    text-decoration: none;
    color: inherit;
}

#page {
    text-align: center;
}

.shareme{
    display: inline-block;
    color: #fff;
    background: #333;
    padding: 10px;
}

#share_menu{
    position: absolute;
    left: -1000px;
    background-color: #fff;
    padding: 20px 0px 0px;
    background: -webkit-canvas(menu_background) no-repeat;
    -webkit-transition: opacity 300ms ease-out;
}

#share_menu a{
    display: block;
    margin: 7px;
    padding: 10px 20px;
    font-weight: bold;
    font-size: 20px;
    text-align: center;
    border-radius: 5px;
    background: -webkit-gradient(linear, 0% 50%, 0% 51%, from(#fff), to(#edeff3));
}

.hide { 
    opacity: 0; 
}

.show {
    opacity: 1;  
}

</style>
<script>

function initMenu(){

    var menu = document.getElementById('share_menu');

    drawMenuBackground(menu.offsetWidth, menu.offsetHeight);

    menu.addEventListener('webkitTransitionEnd', function(e){
        if(menu.getAttribute('class') == 'hide'){
            menu.style.setProperty('display','none');
        }
    });

    menu.setAttribute('class', 'hide');

}

function drawMenuBackground(rectWidth, rectHeight){

    var context = document.getCSSCanvasContext('2d', 'menu_background', rectWidth, rectHeight);    

    var arrowHeight = 20;
    var radius = 6;
    var lineWidth = 1;
    var pad = lineWidth/2;
    var xs = pad;
    var ys = pad + arrowHeight;
    var xe = rectWidth - pad;
    var ye = rectHeight - pad;

    var gradient = context.createLinearGradient(rectWidth/2, 0, rectWidth/2, arrowHeight * 2);
    gradient.addColorStop(0, '#eee'); 
    gradient.addColorStop(1, '#151d31'); 

    context.beginPath();

    context.lineJoin = 'miter';

    context.moveTo(xs + radius, ys);

    console.log(rectWidth);

    context.lineTo(rectWidth/2 - (arrowHeight + pad), ys);
    context.lineTo(rectWidth/2, pad);
    context.lineTo(rectWidth/2 + (arrowHeight + pad), ys);

    context.lineTo(xe - radius, ys);

    context.arcTo(xe, ys, xe, ys + radius, radius);

    context.lineTo(xe, ye - radius);
    context.arcTo(xe, ye, xe - radius, ye, radius);

    context.lineTo(xs + radius, ye);initMenu
    context.arcTo(xs, ye, xs, ye - radius, radius);

    context.lineTo(xs, ys + radius);
    context.arcTo(xs, ys, xs + radius, ys, radius);

    context.fillStyle = gradient;

    //context.fillStyle = '#000';
    context.globalAlpha = .95;
    context.fill();

    context.globalAlpha=1;

    context.strokeStyle = '#48484a';
    context.lineWidth = lineWidth;
    context.stroke();

}

function showMenu(el){

    var menu = document.getElementById('share_menu');

    menu.style.setProperty('display','block');    

    var targetLeft = el.offsetLeft;
    var targetBottom = el.offsetHeight;
    var targetWidth = el.offsetWidth;    
    var menuWidth = menu.offsetWidth;

    var menuLeft = targetLeft + (targetWidth/2) - (menuWidth/2);

    menu.style.setProperty('top', (targetBottom + 20) + 'px');
    menu.style.setProperty('left', menuLeft + 'px');

    menu.setAttribute('class', 'show');

    menu.onclick = function(e){
        if(e.target.tagName.toLowerCase() == 'a'){
            var type = e.target.innerHTML;
            var link = el.getAttribute('href');
            alert(type + 'ing ' + link);
            menu.setAttribute('class','hide');
        }
    }
}

</script>
</head> 
<body onload="initMenu();"> 
<div id="page">
    <div>
    <a class="shareme" href="http://www.russellbeattie.com/blog" onclick="showMenu(this);return false">Share Me</a>
    </div>
    <p>
    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras dictum scelerisque lectus nec porta. Sed sit amet mauris mauris. Nunc tempor scelerisque egestas. Suspendisse potenti. Integer tortor sapien, pharetra sed pretium a, suscipit nec sem. Vivamus eu tellus nec orci eleifend molestie in quis mauris. Quisque quis lorem vitae leo imperdiet sollicitudin ac ut quam. Duis lacinia aliquam magna, a vulputate orci consectetur eu. Suspendisse tincidunt nisl at tellus dapibus placerat. Nam et sem tortor, vitae pharetra ipsum. Quisque luctus egestas nunc, a fermentum lectus rhoncus accumsan. Quisque quis nunc augue, ut semper ligula.
    </p>
</div>

<div id="share_menu">
    <a href="#">Tweet</a>
    <a href="#">Facebook</a>
    <a href="#">Email</a>
 </div>

</body>
</html> 

Okay, here's a short overview of the page. First, I created a basic HTML5 layout, making sure to include the viewport instructions so that the iPad doesn't blow up the font and make the menu too huge. Then I have the basic CSS with a few -webkit based instructions that add transitions, borders and backgrounds. Then I have the Javascript which figures out where the menu is going to go, then draws the box-with-arrow outline of the menu by hand - including rounded corners with the toArc() methods and filled with a semi transparent background. Then I create the menu in a normal way using divs, anchors and some CSS to style it on top of the canvas background.

Here's some of the interesting things that I ran into:

* You can assign a canvas object as a element background. Rather than having to add a canvas tag on a page with a specific height and width, then positioning it absolutely with CSS, then shifting it to the back with a z-index, you can just name a virtual canvas, assign it to the background using CSS, then refer to it as the context to do your drawing. This is very cool, though I'm not sure how well known it is - many of the first tutorials I read about the canvas failed to mention it.

[Update: I tweaked the example I linked to so it works in Firefox 4 by adding the canvas element if the background canvas doesn't work.]

* There is a downside though to using the element's background - you can't add shadows easily. They'll have to be added using the canvas, and adjusted for the size of the element the canvas is assigned to, as the drawing won't go outside that area.

* CSS3 Transitions are very cool, but 'display' is not supported as a transitional property. What this means is that trying to mimic a super basic JQuery effect like "fadeOut()" isn't straight forward. You can transition a CSS property like opacity, but even at 0, the HTML object is still there on the page, it's just invisible. To get around this in the code above, I position the menu absolutely and shift it to the left -1000px to get rid of it after I fade it out, and then I hide it. Same thing for reverse. (Hiding an object removes it from the DOM, so in order to get it's width/height, it needs to be displayed at some point.)

* I still don't really understand the canvas arcTo() method. I just kept tweaking a "rounded corner rectangle" example I found until it did what I wanted. It *seems* straight forward, but it definitely wasn't.

* Also, the way lines are drawn on the canvas are just odd. You have to assume the pixel is being drawn from the middle of the line, which made it a pain to figure out how to give the arrow (triangle) a nice point. Again, I just played with the numbers until it looked right.

* Finally, one last thing I was surprised to discover is that once you've drawn on your canvas, from that point on the browser treats it like an image, not a vector graphic. So if you resize your page or just the canvas element (or the element it's a background of), it will stretch the graphic, not re-draw it! Basically, you'd have to listen for the resize event in Javascript, and then redraw the graphic if it changes, otherwise expect the drawing to be distorted.

Okay, that's all I've learned for now. Hope that helps someone out there who's playing with this stuff like me. The great thing about this though? Even though these are all cutting edge HTML5 technologies, the iPad supports it all really well (as do newer browser like Chrome), so I can immediately start to use it on pages I create for that platform. It's always nice to play with some new API that actually has users.

-Russ

< Previous         Next >