Improve Horizon nav sidebar

- Make menu responsive (hides on smaller screens, e,g, 1/2 laptop screen,
  tablet, mobile)
- Add aria parameters for accessibility
- Show current focus when navigating (accessibility)
- Remove the blue outline when clicking links in Chrome
- Makes menu less hideous

Change-Id: I1cdfa079f0b371d1afddefa67d8a21e93abde9ee
Implements: blueprint navigation-improvements
Closes-Bug: 1315488
Closes-Bug: 1628274
This commit is contained in:
Rob Cresswell 2016-05-16 15:34:34 +01:00
parent c46b5014c8
commit 837587fe73
12 changed files with 171 additions and 84 deletions

View File

@ -35,14 +35,15 @@ horizon.addInitFunction(horizon.selenium.init = function() {
});
horizon.selenium.initSideBarHelpers = function() {
var $activeEntry = $('li.openstack-dashboard.active > ul.panel-collapse.in');
var dashboardLoc = 'li.openstack-dashboard';
var groupLoc = 'li.nav-header.panel';
var $activeEntry = $('.openstack-dashboard-active > ul.panel-collapse.in');
var dashboardLoc = '.openstack-dashboard';
var groupLoc = 'li.openstack-panel-group';
var activeCls = horizon.selenium.ACTIVE_CLS;
var $activeDashboard = $activeEntry.closest(dashboardLoc).toggleClass(activeCls);
var $activeGroup = $activeEntry.find(
'li.nav-header.panel > ul.panel-collapse.in').closest(groupLoc).toggleClass(activeCls);
'li.openstack-panel-group > ul.panel-collapse.in'
).closest(groupLoc).toggleClass(activeCls);
function toggleActiveDashboard($dashboard) {
if ($activeDashboard) {

View File

@ -0,0 +1,29 @@
/*
* (c) Copyright 2015 Hewlett-Packard Development Company, L.P.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
$(document).ready(function () {
'use strict';
var $sidenav = $('#sidebar');
var $mask = $(document.createElement('div'))
.prop('id', 'sidebar-mask')
.appendTo($('#content_body'));
// Hamburger Happiness !!!
$(document).on('click', '#sidebar-toggle', function () {
$mask.toggleClass('on-screen');
$sidenav.toggleClass('on-screen');
});
});

View File

@ -3,11 +3,11 @@
<ul id="sidebar-accordion" class="nav nav-pills nav-stacked">
{% for dashboard, panel_info in components %}
{% if user|has_permissions:dashboard %}
<li class="panel openstack-dashboard{% if current.slug == dashboard.slug %} active{% endif %}">
<li class="panel openstack-dashboard">
<a data-toggle="collapse"
data-parent="#sidebar-accordion"
data-target="#sidebar-accordion-{{ dashboard.slug }}"
href="javascript:;"
aria-controls="sidebar-accordion-{{ dashboard.slug }}"
{% if current.slug != dashboard.slug %}
class="collapsed"
{% endif %}>
@ -20,34 +20,30 @@
{% with panels|has_permissions_on_list:user as filtered_panels %}
{% if filtered_panels %}
{% if group.name %}
<li class="nav-header panel">
<li class="panel openstack-panel-group">
<a data-toggle="collapse"
data-parent="#sidebar-accordion-{{ dashboard.slug }}"
data-target="#sidebar-accordion-{{ dashboard.slug }}-{{ group.slug }}"
href="javascript:;"
aria-controls="sidebar-accordion-{{ dashboard.slug }}-{{ group.slug }}"
{% if current.slug == dashboard.slug and current_panel_group != group.slug %}class="collapsed"
{% elif current.slug != dashboard.slug and forloop.counter0 != 0 %}class="collapsed"{% endif %}>
<span class="nav-header-title">
{{ group.name }}
<span class="openstack-toggle fa pull-right"></span>
</span>
{{ group.name }}
<span class="openstack-toggle fa pull-right"></span>
</a>
<ul id="sidebar-accordion-{{ dashboard.slug }}-{{ group.slug }}"
class="nav collapse panel-collapse
{% endif %}
<div id="sidebar-accordion-{{ dashboard.slug }}-{{ group.slug }}"
class="list-group collapse
{% if current.slug == dashboard.slug and current_panel_group == group.slug %} in
{% elif current.slug != dashboard.slug and forloop.counter0 == 0 %} in{% endif %}">
{% endif %}
{% for panel in filtered_panels %}
<li class="panel openstack-panel{% if current.slug == dashboard.slug and current_panel == panel.slug %} active{% endif %}">
<a class="openstack-spin" href="{{ panel.get_absolute_url }}"
<a class="openstack-spin list-group-item openstack-panel {% if current.slug == dashboard.slug and current_panel == panel.slug %}active{% endif %}" href="{{ panel.get_absolute_url }}"
target="_self"
tabindex="{{ forloop.counter }}" >
{{ panel.name }}
</a>
</li>
{% endfor %}
</div>
{% if group.name %}
</ul>
</li>
{% endif %}
{% endif %}

View File

@ -1,5 +1,5 @@
{% load branding horizon i18n %}
<div id='sidebar'>
<nav id='sidebar'>
{% horizon_nav %}
</div>
</nav>

View File

@ -13,20 +13,20 @@ $navbar-border-size: 1px !default;
$navbar-true-height: $navbar-height + $navbar-border-size !default;
#main_content {
height: 100%;
min-width: $main-content-min-width;
padding-top: $navbar-true-height;
}
#sidebar {
position: fixed;
float: left;
}
#content_body {
width: 100%;
padding-left: $sidebar-width;
// Always show the side nav on larger screens
@media(min-width: $screen-sm-min) {
#content_body {
padding-left: $sidebar-width;
}
}
.page-header {
margin-top: $padding-base-horizontal;
}
}

View File

@ -1,63 +1,110 @@
/*
* This file defines the styling for side navigation in Horizon, which uses
* nested panels to utilise Bootstraps native JS handling for accordion menus
* in panels. However, Bootstrap does *not* natively support nested panels
* in its markup; to work around this, we remove the panel styling and inherit
* list group styling.
*/
#sidebar-accordion {
width: $sidebar-width;
}
#sidebar-mask {
background-color: rgba(0, 0, 0, 0.5);
height: 100%;
left: 0;
position: fixed;
top: 0;
visibility: hidden;
opacity: 0;
width: 100%;
z-index: 2;
transition: all 0.3s ease 0s;
@media (max-width: $screen-sm-min) {
&.on-screen {
visibility: visible;
opacity: 1;
}
}
}
#sidebar {
min-width: $sidebar-width;
z-index: 0;
width: $sidebar-width;
// Make sure the side nav is always shown at larger screen sizes,
// regardless of previous state
@media (min-width: $screen-sm-min) {
display: block;
}
@media (max-width: $screen-sm-min) {
transition: all 0.3s ease 0s;
position: fixed;
top: $navbar-height;
bottom: 0;
z-index: 3;
left: -$sidebar-width;
overflow-y: auto;
overflow-x: hidden;
&.on-screen {
left: 0;
}
}
// Sets the arrow toggles for each dashboard list
[data-toggle="collapse"] {
.openstack-toggle.fa {
line-height: $line-height-computed;
width: $line-height-computed;
height: $line-height-computed;
text-align: center;
@include transition(transform 0.3s ease 0s);
@extend .fa-chevron-down;
.openstack-toggle.fa {
line-height: $line-height-computed;
text-align: center;
@include transition(transform 0.3s ease 0s);
@extend .fa-chevron-down;
}
// Rotate the arrow toggle for closed panels
.collapsed > .openstack-toggle.fa {
@include rotate(-90deg);
}
// Remove panel default styling for the side nav only
.panel {
margin: 0;
border-radius: 0;
border: 0;
box-shadow: none;
cursor: pointer;
.list-group-item {
border-radius: 0;
border: 0;
}
&.collapsed {
.openstack-toggle.fa {
@include rotate(-90deg);
// Use the list group styling for consistency. We use panels in the markup
// for accordion, but style should be list-group.
> a {
color: $list-group-link-color;
background: $list-group-bg;
&:hover {
background: $list-group-hover-bg;
}
}
}
// Styles for the Dashboard Names
.openstack-dashboard {
& > a {
border-radius: $border-radius-base $border-radius-base 0 0;
}
// Remove Chromes glowing blue border for focus. This should not affect
// accessibility, as the tabs already have a focus effect.
a:focus {
outline: 0;
}
// Styles for the Panel Names
// Center align panel groups
.openstack-panel-group {
text-align: center;
}
// Right align panels
.openstack-panel {
& > a {
text-align: right;
color: $text-color;
}
&.active > a {
color: $brand-primary;
font-weight: bold;
}
}
.panel {
border: none;
border-radius: 0;
@include box-shadow(none);
margin-bottom: 0;
text-align: right;
}
}
// Bootstrap 3 removed nav headers, lets add them back
.nav .nav-header > a > .nav-header-title {
font-size: $font-size-base;
font-weight: bold;
text-transform: uppercase;
text-align: center;
width: 100%;
display: inline-block;
&:hover {
background-color: transparent;
}
}

View File

@ -7,6 +7,7 @@
<head>
<meta content='IE=edge' http-equiv='X-UA-Compatible' />
<meta content='text/html; charset=utf-8' http-equiv='Content-Type' />
<meta name="viewport" content="width=device-width, initial-scale=1">
{% include "horizon/_custom_meta.html" %}
<title>{% block title %}{% endblock %} - {% site_branding %}</title>
{% comment %} Load CSS sheets before Javascript {% endcomment %}

View File

@ -5,13 +5,21 @@
<div class="container-fluid">
<!-- Brand and toggle get grouped for better mobile display -->
<div class="navbar-header">
{% include "header/_brand.html" %}
<button id="sidebar-toggle" type="button" class="navbar-toggle pull-left">
<span class="sr-only">{% trans "Toggle navigation" %}</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar-collapse">
<span class="sr-only">{% trans "Toggle navigation" %}</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
{% include "header/_brand.html" %}
</div>
<!-- Collect the nav links, forms, and other content for toggling -->

View File

@ -50,6 +50,7 @@
<script src='{{ STATIC_URL }}horizon/js/horizon.d3barchart.js'></script>
<script src='{{ STATIC_URL }}horizon/js/horizon.firewalls.js'></script>
<script src='{{ STATIC_URL }}horizon/js/horizon.volumes.js'></script>
<script src='{{ STATIC_URL }}horizon/js/horizon.sidebar.js'></script>
{% for file in HORIZON_CONFIG.js_files %}
<script src='{{ STATIC_URL }}{{ file }}'></script>

View File

@ -30,15 +30,15 @@ class NavigationAccordionRegion(baseregion.BaseRegion):
return self._get_element(*self._project_bar_locator)
_first_level_item_selected_locator = (
by.By.CSS_SELECTOR, 'li.openstack-dashboard.selenium-active > a')
by.By.CSS_SELECTOR, '.openstack-dashboard-active.selenium-active > a')
_second_level_item_selected_locator = (
by.By.CSS_SELECTOR, 'li.nav-header.selenium-active > a')
by.By.CSS_SELECTOR, 'li.openstack-panel-group.selenium-active > a')
_first_level_item_xpath_template = (
"//li[contains(concat('', @class, ''), 'openstack-dashboard') "
"and contains(., '%s')]/a")
_second_level_item_xpath_template = (
"//li[contains(concat('', @class, ''), 'nav-header') "
"//li[contains(concat('', @class, ''), 'openstack-panel-group') "
"and contains(., '%s')]/a")
_third_level_item_xpath_template = (
".//li[contains(concat('', @class, ''), 'openstack-panel') and "

View File

@ -52,6 +52,10 @@
.openstack-panel > a {
padding: $padding-small-horizontal $font-size-h4 $padding-small-horizontal ($font-size-h1 - $padding-small-horizontal);
text-align: left;
&.active {
font-weight: bold;
}
}
.openstack-toggle {

View File

@ -4,7 +4,7 @@
<ul id="sidebar-drawer" class="nav nav-pills nav-stacked">
{% for dashboard, panel_info in components %}
{% if user|has_permissions:dashboard %}
<li class="openstack-dashboard{% if current.slug == dashboard.slug %} active{% endif %}">
<li class="openstack-dashboard">
<a data-toggle="collapse"
data-parent="#sidebar-drawer"
data-target="#sidebar-drawer-{{ dashboard.slug }}"
@ -39,8 +39,8 @@
{% elif current.slug != dashboard.slug and forloop.counter0 == 0 %} in{% endif %}">
{% endif %}
{% for panel in filtered_panels %}
<li class="openstack-panel{% if current.slug == dashboard.slug and current_panel == panel.slug %} active{% endif %}">
<a class="openstack-spin" href="{{ panel.get_absolute_url }}"
<li class="openstack-panel">
<a class="openstack-spin {% if current.slug == dashboard.slug and current_panel == panel.slug %}active{% endif %}" href="{{ panel.get_absolute_url }}"
target="_self"
tabindex="{{ forloop.counter }}" >
{{ panel.name }}