DEVELOPERS BLOG

Developing Cross-Platform UIs for the Enterprise (Part 2): Lessons Learned from the PantherUI

HOW-TO / 06.11.14 / myjing

LessonsLearned

In my previous blog post, “Introducing the PantherUI Carousel”, I wrote about how you can use virtual space to create responsive apps for different platforms and screen sizes. So for this post, I wanted to share some of those cross-platform “gotchas” with some tips.

1. Touch Support

If you’re planning on building an app that runs on mobile and desktop, you could get away with just using a click event for finger taps and mouse clicks, but the user experience will be slower on most mobile browsers due to a 300 ms delay on click events. Whereas, a touchstart event has no delay. Another reason for using touch is it gives you the ability to handle gestures like swiping.

Especially, if you use a tiny library like Hammer.js that provides support for advanced multi-touch gestures like pinch and rotate. If you’re dealing with a legacy desktop app, you could use a polyfill library like FastClick that doesn’t require changes to your existing code. Or if you prefer to do this manually like I did, you can dynamically bind your events based on whether your browser supports touch or not:

var touch = ('ontouchstart' in window) || window.DocumentTouch && document instanceof DocumentTouch;
if (touch === true) {
    addTouchListeners(); // touchstart, touchmove, touchend
}
else {
    addMouseListeners(); // mousedown, mousemove, mouseup
}

I chose not to use a library because I wanted the PantherUI to be a standalone library with no dependencies. So the choice really depends on your needs.

Touchend not firing

Another thing to remember is that touch events have quirks on some devices. Originally in the PantherUI Carousel, you could swipe left or right anywhere on the screen to rotate the panels. But when I tested this on an Android device, I found the carousels weren’t turning. After debugging the touch events, I discovered that the touchend event wasn’t firing. Apparently, this was due to a known issue on Android.

preventDefault

The workaround was to call preventDefault() on either the touchstart or the first touchmove event. Unfortunately, using preventDefault cancels native scrolling for overflow content, which means if you want that behavior, you’ll have to implement it yourself. Although, there are libraries like iScroll that I could’ve used, I knew that the BlackBerry 10 browser had hardware-accelerated scrolling so I wanted to keep that default behavior.

Touch target

So the simplest solution was to bind the touch event to a non-touch area like the panel’s title bar. This area was big enough for swiping left or right, and the touch events with preventDefault didn’t conflict with scrolling or clicking links. Also, by moving the touch target outside the content area ensured that it wouldn’t conflict with components like geographic maps, where a user would normally pan or zoom using gestures.

Figure 1: Title Bar

Figure 1: Title Bar

Event delegation

Problem solved, right? Not quite. Moving the listener to the title bars meant I’d have to attach a listener to every panel, which isn’t a big deal if we only have a few panels. But once we start dynamically adding dozens of panels or removing them, it becomes more complex and can affect performance. So I decided to use event delegation to attach one listener to the carousel element (parent) that would handle all the panels (children) and execute only when the target is a title bar:

elem.addEventListener('touchmove', function (ev) {
    if (ev.target.className == 'panther-Carousel-panelTitle') {
        ev.preventDefault(); // need this for Android
        Panther.Carousel.inputmove(c, ev.changedTouches[0].clientX, ev.changedTouches[0].clientY, ev.timeStamp);
    }
});

minDragDistance

So delegating the events greatly simplified the code, but I noticed the touch target was very sensitive. If you just tapped the title bar with a slight jitter, even a 1px movement would cause the carousel to rotate when you just meant to tap on the maximize or restore button. To solve that issue, I added some logic to check if the distance met the minimum threshold for the drag distance (i.e., 16px) before performing the actual turn:

inputend: function (c, x, y, timestamp) {
    var minDragDistance = 16; // px
    var diff = Math.abs(Panther.Carousel.pos.x.end - Panther.Carousel.pos.x.start);

    if (diff < minDragDistance) {
        return;
    }

    // left
    if (Panther.Carousel.pos.x.end < Panther.Carousel.pos.x.start) {
        c.rotation += c.theta * 1 * -1;
    }
    // right
    else {
        c.rotation += c.theta * -1 * -1;
    }

    c.transform();
}

“Rubber band” bounce effect

Another platform-specific feature that I ran into was the “rubber band” bounce effect in iOS. If you swiped on a slight angle, it would cause the whole page to scroll up or down and bounce back even though the page had no overflow content. Although it’s a neat effect, it’s not the behavior we want in our carousel-based app.

To fix that issue, I could’ve used preventDefault on touchstart or touchmove for the entire body, but that would cancel all the default behaviors like scrolling and clicking on links. Although I could’ve added logic to detect and handle all those things manually, it wasn’t worth the effort. Two simpler solutions were to: (1) use buttons instead of gestures to turn the carousel, or (2) for PhoneGap apps, disable the webviewbounce setting in the config.xml:


preference name="webviewbounce" value="false" />
preference name="DisallowOverscroll" value="true" />

Another option is to use to a library like AppScroll to fix this website dragging issue.


2. CSS 3D Transforms

preserve-3d

Although CSS3 features are available in all the modern browsers, 3D Transforms are not supported in IE 8 or 9, and only has partial support in IE 10 and 11 (see caniuse.com/#feat=transforms3d). Unfortunately, this is due to the lack of support for the preserve-3dproperty on nested elements. For the PantherUI Carousel, this means all the panels are flattened on top of each other:

Figure 2: Carousel in IE 11

Figure 2: Carousel in IE 11

So if you need to support IE 10 or 11, you can wait for Microsoft to add it (see IE Web Platform Status), or the workaround is to manually apply the parent element’s transform to each of the child elements in addition to the child element’s normal transform. This is something that may be added to the Carousel in a future update, but the more likely option will be to create a fallback to a 2D Carousel like Swipe JS to support older browsers.

overflow-scrolling

Another issue that I found was that 3D Transforms can affect the position of the scrollbar if hardware-accelerated scrolling is enabled in some browsers:

Figure 3: Scrollbar Offset

Figure 3: Scrollbar Offset

This doesn’t affect every panel so you could leave it in, but I chose to comment out the CSS until it’s fixed in the affected browsers:

.panther-Carousel-carousel figure {
    /* -webkit-overflow-scrolling: touch; */
}


3. Cross-browser

Another tip worth mentioning is understanding browser differences, and the differences between certain JavaScript properties that may seem alike, but are subtley different.

target vs srcElement

For cross-browser support with events, you should use W3C’s target property because srcElement is a Microsoft standard that is not supported in Firefox. However, if you need to support IE browsers before IE 9, you’ll need to use srcElement.

childNodes vs children

When you’re accessing the DOM, be aware that children is a property of an Element. Only Elements have children, and these children are all of type Element. Whereas, childNodes is a property of Node, and childNodes can contain any node.

Besides the type differences, what this means is that childNodes includes text nodes that may contain empty whitespace. Most of the time, you’ll want to use children because you don’t want to loop over TextNodes or comments in your DOM manipulation.

for (i = 0; i < this.panelCount; i++) {
    panel = this.element.children[i];
    panel.style.opacity = 1;
}

But if you’re looping over the contents of an XML feed, where the order is unknown, it may be useful to use childNodes so you can inspect each node:

var rss, i, ii, cn;
for (i = 0, ii = xhr.responseXML.childNodes.length; i < ii; i++) {
    cn = xhr.responseXML.childNodes[i];
    if (cn.nodeName == 'rss') {
        rss = cn;
        break;
    }
}

Also, if you’re comparing the length, childNodes will return more stuff:

[xml-stylesheet, xml-stylesheet, rss, item: function]
0: xml-stylesheet
1: xml-stylesheet
2: rss
length: 3

Whereas, children will return just the elements.

"rss, item: function, namedItem: function]
0: rss
length: 1


Final Thoughts

If you’re interested in a more complete list of cross-browser best practices, check out Modern.IE’s 20 Tips. If you have others, please feel free to add them in the comments section below.

The final lesson I wanted to share is to use the 3D Carousel sparingly. Just because it’s cool and you can load it with “unlimited” content, be practical and know your audience — the content in each carousel panel should be related. For example, letting a user swipe between financial reports makes it easy to compare revenue numbers from one year to the next.

Since the PantherUI Carousel is a work-in-progress, some planned features are:

  • Providing a 2D fallback for older browsers.
  • Adding a 1:1 swipe gesture so the carousel turns with your finger, which is useful for peeking and going back.
  • Adding a more natural swipe gesture for vertical-turning carousels without conflicting with vertical overflow scrolling.
  • Adding tilt support for turning a carousel using the accelerometer.

If you have other suggestions, please feel free to submit an issue to the project.

Hopefully, that gives you some tips you can use in your own cross-browser development projects. In Part 3, I’ll introduce some Lightweight UI Frameworks

About myjing