Radial Menu Key Events

In this article, we add key events into the Radial Menu. The goal is to allow end-users to navigate the menu with the keyboard, once it has been opened, using the arrow keys. Additionally, end-users will be able to select panels with the enter key and close the menu with the escape key.

Setup

Instead of adding keyboard support as a plugin, I decided to incorporate it directly into the menu, as widgets should be navigable via the keyboard, as well as the mouse. Thus, there is no additional setup required.

How Its Done…

When the Radial Menu is opened, a keydown and keyup listener is attached to the document. The callback for the keydown event is:

_handleKeyDown: function(e) {
	var panels = this.get(panels),
		lastPanel = this._lastPanel,
		i = lastPanel ? lastPanel.getRadialIndex() : 0,
		n = panels.length,
		isValid = false,
		hoverClass = this.get(hoverClass),
		m, l=n%2;
	
	switch (e.keyCode) {
		case 38: // up
			if (0 != i) {
				isValid = true;
				if (n / 2 > i) {
					i -= 1;
				}
				else {
					i += 1;
				}
			}
		break;

		case 39: // right
			m = n / 4;

			if (m != i && ! (l && _isBetween(i, m-1, m+1))) {
				isValid = true;
				if (m >= i + 1 || n - m <= i) {
					i += 1;
				}
				else if (m <= i - 1) {
					i -= 1;
				}
			}
		break;

		case 40: // down
			m = n / 2;
				
			if (m != i && ! (l && _isBetween(i, m-1, m+1))) {
				isValid = true;
				if (m >= i + 1) {
					i += 1;
				}
				else if (m <= i - 1) {
					i -= 1;
				}
			}
		break;

		case 37: // left
			m = n / 4;

			if (n - m != i && ! (l && _isBetween(i, n-m-1, n-m+1))) {
				isValid = true;
				if (m < i && n - m >= i + 1) {
					i += 1;
				}
				else if (n - m <= i - 1 || i <= m) {
					i -= 1;
				}
			}
		break;

		case 13: // enter
			if (lastPanel) {
				e.target = lastPanel._node;
				this._handleClick(e);
			}
		break;

		case 27: // escape
			this.hide();
		break;
	}

	if (isValid) {
		if (this._timerKeyDown) {this._timerKeyDown.cancel();}

		if (0 > i){
			i = n - 1;
		}
		else if (n - 1 < i) {
			i = 0;
		}

		n = this.get(keyHoldTimeout);
		if (0 < n) {
			this._timerKeyDown = Y.later(n, this, this._handleKeyDown, e);
		}
		
		if (lastPanel) {lastPanel._node.removeClass(hoverClass);}
		lastPanel = panels[i];
		this._lastPanel = lastPanel;
		lastPanel._node.addClass(hoverClass);
		this._isKeyPressed = true;
	}
},

The keyup event listener simply stops the event timer started at the end of the keydown function:

_handleKeyUp: function(e) {
	if (this._timerKeyDown) {this._timerKeyDown.cancel();}
	this._isKeyPressed = false;
},

How It Works…

When an arrow key is detected, the position of the currently selected panel is fetched (or ZERO is used), and a calculation is made to determine whether the menu has additional panels in the desired direction available for selection. If there are additional panels, then the position is changed by one.

If there is a value for the new property keyHoldTimeout, then a timer is set to call the _handleKeyDown function again. This facilitates instances where end-users hold down an arrow key. The rest of the function handles caching the current panel and applying the hover class.

The enter key calls the click handler, and the escape key calls the hide function>

Theres More…

All events and pointers are attached when the menu is opened, and removed when the menu is closed, to remove impact on the page performance.

You may notice minor issues with the arrow navigation when using a small number of panels. You can use the arrow keys to reach all positions, but sometimes a position isn&rsquot;t reachable by all arrows that you think it should be (ie., you expect down and right arrows will get you to a position, but only the down arrow does). It did not seem to be a blocking issue, but if you have a solution, feel free to post it in the comments.

See also

The article, Radial Menu, where this widget was first introduced.
The test page, Radial Menu Test, where you can play with the animations yourself.