Improving the Hamburger Menu with mouse hover feedback and proper keyboard handling

Last time, we saw how to build a simple Hamburger Menu for navigation in a Universal Windows Platform App starting with Windows 10.

In this post, I will highlight a few modifications that we need to make in order to enhance the appearance of the menu and in order to properly handle keyboard navigation.

Keyboard navigation

A Universal Windows Platform (UWP) App runs on a potentially a large number of devices, including high-end desktop computers. Therefore, it is important to allow the use of the application through proper handling of the keyboard.

Many features come built in the controls available in XAML. For instance, using the Space key allows one to press a button. So it works all right with the Hamburger button and the Radio Buttons.

In my example, however, I would like the Enter key to be used to select a particular option. And I would like the Arrow keys to be used to select amongst the available options.

Open up the MainPage.xaml and add a handler for the KeyDown event:

<Page ... xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" KeyDown="Shell_KeyDown">

    <SplitView x:Name="NavigationPane">
    ...
</Page>

Then, in MainPage.xaml.cs and type in the following code:

///
<summary>
/// Default keyboard focus movement for any unhandled keyboarding
/// </summary>

/// <param name="sender"></param>
/// <param name="e"></param>
private void Shell_KeyDown(object sender, KeyRoutedEventArgs e)
{
    FocusNavigationDirection direction = FocusNavigationDirection.None;

    switch (e.Key)
    {
        // both Space and Enter will trigger navigation

        case Windows.System.VirtualKey.Space:
        case Windows.System.VirtualKey.Enter:
            {
                var control = FocusManager.GetFocusedElement() as Control;
                var option = control as RadioButton;
                if (option != null)
                {
                    var automation = new RadioButtonAutomationPeer(option);
                    automation.Select();
                }
            }
            return;

        // otherwise, find next focusable element in the appropriate direction

        case Windows.System.VirtualKey.Left:
        case Windows.System.VirtualKey.GamepadDPadLeft:
        case Windows.System.VirtualKey.GamepadLeftThumbstickLeft:
        case Windows.System.VirtualKey.NavigationLeft:
            direction = FocusNavigationDirection.Left;
            break;
        case Windows.System.VirtualKey.Right:
        case Windows.System.VirtualKey.GamepadDPadRight:
        case Windows.System.VirtualKey.GamepadLeftThumbstickRight:
        case Windows.System.VirtualKey.NavigationRight:
            direction = FocusNavigationDirection.Right;
            break;

        case Windows.System.VirtualKey.Up:
        case Windows.System.VirtualKey.GamepadDPadUp:
        case Windows.System.VirtualKey.GamepadLeftThumbstickUp:
        case Windows.System.VirtualKey.NavigationUp:
            direction = FocusNavigationDirection.Up;
            break;

        case Windows.System.VirtualKey.Down:
        case Windows.System.VirtualKey.GamepadDPadDown:
        case Windows.System.VirtualKey.GamepadLeftThumbstickDown:
        case Windows.System.VirtualKey.NavigationDown:
            direction = FocusNavigationDirection.Down;
            break;
    }

    if (direction != FocusNavigationDirection.None)
    {
        var control = FocusManager.FindNextFocusableElement(direction) as Control;
        if (control != null)
        {
            control.Focus(FocusState.Programmatic);
            e.Handled = true;
        }
    }
}

The code is divided into two parts.

The first part handles the Space and Enter key so that it selects the currently selected button. In order to do that, one has to use the RadioButtonAutomationPeer class to programmatically select the associated RadioButton. That way, when you press Enter, the RadioButton is selected and this, in turn, triggers navigation to the appropriate page.

The second part of the code handles the Arrow keys and equivalent keys coming from controllers and game pads. This searches for and set the focus to the next focusable element available in the given direction. For instance, if the currently selected option is the Option 1 RadioButton, a press on the Down key will find the Option 2 RadioButton. That way, selecting a given control using the Arrow keys is very simple.

The last modification we need to make is to fix the incorrect display of the focus rectangle when the SplitView is in Compact DisplayMode. Because the Compact display mode clips the content of the navigation pane, the result is somewhat unexpected:

In MainPage.xaml.cs modify the implementation of the HamburgerButton_Click event handler and add a call to a method that resizes the controls hosted on the SplitView pane:

private void HamburgerButton_Click(object sender, RoutedEventArgs e)
{
    NavigationPane.IsPaneOpen = !NavigationPane.IsPaneOpen;

    ResizeOptions();
}

///
<summary>
/// Fix the clipped focus rectangle when SplitView DisplayMode is Compact
/// </summary>

private void ResizeOptions()
{
    // calculate the actual width of the navigation pane

    var width = NavigationPane.CompactPaneLength;
    if (NavigationPane.IsPaneOpen)
        width = NavigationPane.OpenPaneLength;

    // change the width of all control in the navigation pane

    HamburgerButton.Width = width;

    foreach (var option in NavigationMenu.Children)
    {
        var radioButton = (option as RadioButton);
        if (radioButton != null)
            radioButton.Width = width;
    }
}

Mouse Hover feedback

In to improve the feedback to the user as to whether a particular control can be selected or pressed, one has to change the color of the control when the mouse hovers above it. Because we already have some styles for the HamburgerButton and the navigation RadioButton controls, it is very easy to add this feature.

A quick look into the default generic.xaml file that ships with the SDK alllows one to copy and paste the default Template for a given control. This file is located in the following folder:

For instance, the default Button template defines the following Common States such as Normal, PointOver Pressed and Disabled. Likewize, the default RadioButton Template defines the same Common States (because it is also a Button) as well as the following CheckedStates such as Checked, Unchecked and Indeterminate.

We are only interested in the PointerOver and Pressed states for the HamburgerButton and the RadioButton controls.

So, open up Themes\Generic.xaml and add the following states in both styles MenuItemButtonStyle and NavigationButtonStyle:

<VisualStateManager.VisualStateGroups>
    <VisualStateGroup x:Name="Common">
        <VisualState x:Name="Normal" />
        <VisualState x:Name="PointerOver">
            <Storyboard>
                <ObjectAnimationUsingKeyFrames
                    Storyboard.TargetName="RootGrid"
                    Storyboard.TargetProperty="Background">
                    <DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SystemControlHighlightListLowBrush}" />
                </ObjectAnimationUsingKeyFrames>
            </Storyboard>
        </VisualState>
        <VisualState x:Name="Pressed">
            <Storyboard>
                <ObjectAnimationUsingKeyFrames
                    Storyboard.TargetName="RootGrid"
                    Storyboard.TargetProperty="Background">
                    <DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SystemControlHighlightListMediumBrush}" />
                </ObjectAnimationUsingKeyFrames>
            </Storyboard>
        </VisualState>
        <VisualState x:Name="Disabled" />
    </VisualStateGroup>
</VisualStateManager.VisualStateGroups>

This entry was posted in UWP and tagged , , . Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s