Как сделать навигационную клавиатуру доступной?

Я искал многоуровневое вертикальное меню с возможностью использовать родительские ссылки в течение очень долгого времени. Наконец, я нашел navgoco jQuery, который я использую в своем проекте. Я очень люблю этот плагин. Единственная проблема, которую я имею, - то, что это не доступно через клавиатуру. Есть ли способ это исправить?

https://jsfiddle.net/webIra7/xs4mr7at/7/

(function($) {

 "use strict";

 /**
  * Plugin Constructor. Every menu must have a unique id which will either
  * be the actual id attribute or its index in the page.
  *
  * @param {Element} el
  * @param {Object} options
  * @param {Integer} idx
  * @returns {Object} Plugin Instance
  */
 var Plugin = function(el, options, idx) {
  this.el = el;
  this.$el = $(el);
  this.options = options;
  this.uuid = this.$el.attr('id') ? this.$el.attr('id') : idx;
  this.state = {};
  this.init();
  return this;
 };

 /**
  * Plugin methods
  */
 Plugin.prototype = {
  /**
   * Load cookie, assign a unique data-index attribute to
   * all sub-menus and show|hide them according to cookie
   * or based on the parent open class. Find all parent li > a
   * links add the carent if it's on and attach the event click
   * to them.
   */
  init: function() {
   var self = this;
   self._load();
   self.$el.find('ul').each(function(idx) {
    var sub = $(this);
    sub.attr('data-index', idx);
    if (self.options.save && self.state.hasOwnProperty(idx)) {
     sub.parent().addClass(self.options.openClass);
     sub.show();
    } else if (sub.parent().hasClass(self.options.openClass)) {
     sub.show();
     self.state[idx] = 1;
    } else {
     sub.hide();
    }
   });

   var caret = $('<span></span>').prepend(self.options.caretHtml);
   var links = self.$el.find("li > a");
   self._trigger(caret, false);
   self._trigger(links, true);
   self.$el.find("li:has(ul) > a").prepend(caret);
  },
  /**
   * Add the main event trigger to toggle menu items to the given sources
   * @param {Element} sources
   * @param {Boolean} isLink
   */
  _trigger: function(sources, isLink) {
   var self = this;
   sources.on('click', function(event) {
    event.stopPropagation();
    var sub = isLink ? $(this).next() : $(this).parent().next();
    var isAnchor = false;
    if (isLink) {
     var href = $(this).attr('href');
     isAnchor = href === undefined || href === '' || href === '#';
    }
    sub = sub.length > 0 ? sub : false;
    self.options.onClickBefore.call(this, event, sub);

    if (!isLink || sub && isAnchor) {
     event.preventDefault();
     self._toggle(sub, sub.is(":hidden"));
     self._save();
    } else if (self.options.accordion) {
     var allowed = self.state = self._parents($(this));
     self.$el.find('ul').filter(':visible').each(function() {
      var sub = $(this),
       idx = sub.attr('data-index');

      if (!allowed.hasOwnProperty(idx)) {
       self._toggle(sub, false);
      }
     });
     self._save();
    }
    self.options.onClickAfter.call(this, event, sub);
   });
  },
  /**
   * Accepts a JQuery Element and a boolean flag. If flag is false it removes the `open` css
   * class from the parent li and slides up the sub-menu. If flag is open it adds the `open`
   * css class to the parent li and slides down the menu. If accordion mode is on all
   * sub-menus except the direct parent tree will close. Internally an object with the menus
   * states is maintained for later save duty.
   *
   * @param {Element} sub
   * @param {Boolean} open
   */
  _toggle: function(sub, open) {
   var self = this,
    idx = sub.attr('data-index'),
    parent = sub.parent();

   self.options.onToggleBefore.call(this, sub, open);
   if (open) {
    parent.addClass(self.options.openClass);
    sub.slideDown(self.options.slide);
    self.state[idx] = 1;

    if (self.options.accordion) {
     var allowed = self.state = self._parents(sub);
     allowed[idx] = self.state[idx] = 1;

     self.$el.find('ul').filter(':visible').each(function() {
      var sub = $(this),
       idx = sub.attr('data-index');

      if (!allowed.hasOwnProperty(idx)) {
       self._toggle(sub, false);
      }
     });
    }
   } else {
    parent.removeClass(self.options.openClass);
    sub.slideUp(self.options.slide);
    self.state[idx] = 0;
   }
   self.options.onToggleAfter.call(this, sub, open);
  },
  /**
   * Returns all parents of a sub-menu. When obj is true It returns an object with indexes for
   * keys and the elements as values, if obj is false the object is filled with the value `1`.
   *
   * @since v0.1.2
   * @param {Element} sub
   * @param {Boolean} obj
   * @returns {Object}
   */
  _parents: function(sub, obj) {
   var result = {},
    parent = sub.parent(),
    parents = parent.parents('ul');

   parents.each(function() {
    var par = $(this),
     idx = par.attr('data-index');

    if (!idx) {
     return false;
    }
    result[idx] = obj ? par : 1;
   });
   return result;
  },
  /**
   * If `save` option is on the internal object that keeps track of the sub-menus states is
   * saved with a cookie. For size reasons only the open sub-menus indexes are stored.   *
   */
  _save: function() {
   if (this.options.save) {
    var save = {};
    for (var key in this.state) {
     if (this.state[key] === 1) {
      save[key] = 1;
     }
    }
    cookie[this.uuid] = this.state = save;
    $.cookie(this.options.cookie.name, JSON.stringify(cookie), this.options.cookie);
   }
  },
  /**
   * If `save` option is on it reads the cookie data. The cookie contains data for all
   * navgoco menus so the read happens only once and stored in the global `cookie` var.
   */
  _load: function() {
   if (this.options.save) {
    if (cookie === null) {
     var data = $.cookie(this.options.cookie.name);
     cookie = (data) ? JSON.parse(data) : {};
    }
    this.state = cookie.hasOwnProperty(this.uuid) ? cookie[this.uuid] : {};
   }
  },
  /**
   * Public method toggle to manually show|hide sub-menus. If no indexes are provided all
   * items will be toggled. You can pass sub-menus indexes as regular params. eg:
   * navgoco('toggle', true, 1, 2, 3, 4, 5);
   *
   * Since v0.1.2 it will also open parents when providing sub-menu indexes.
   *
   * @param {Boolean} open
   */
  toggle: function(open) {
   var self = this,
    length = arguments.length;

   if (length <= 1) {
    self.$el.find('ul').each(function() {
     var sub = $(this);
     self._toggle(sub, open);
    });
   } else {
    var idx,
     list = {},
     args = Array.prototype.slice.call(arguments, 1);
    length--;

    for (var i = 0; i < length; i++) {
     idx = args[i];
     var sub = self.$el.find('ul[data-index="' + idx + '"]').first();
     if (sub) {
      list[idx] = sub;
      if (open) {
       var parents = self._parents(sub, true);
       for (var pIdx in parents) {
        if (!list.hasOwnProperty(pIdx)) {
         list[pIdx] = parents[pIdx];
        }
       }
      }
     }
    }

    for (idx in list) {
     self._toggle(list[idx], open);
    }
   }
   self._save();
  },
  /**
   * Removes instance from JQuery data cache and unbinds events.
   */
  destroy: function() {
   $.removeData(this.$el);
   this.$el.find("li:has(ul) > a").unbind('click');
   this.$el.find("li:has(ul) > a > span").unbind('click');
  }
 };

 /**
  * A JQuery plugin wrapper for navgoco. It prevents from multiple instances and also handles
  * public methods calls. If we attempt to call a public method on an element that doesn't have
  * a navgoco instance, one will be created for it with the default options.
  *
  * @param {Object|String} options
  */
 $.fn.navgoco = function(options) {
  if (typeof options === 'string' && options.charAt(0) !== '_' && options !== 'init') {
   var callback = true,
    args = Array.prototype.slice.call(arguments, 1);
  } else {
   options = $.extend({}, $.fn.navgoco.defaults, options || {});
   if (!$.cookie) {
    options.save = false;
   }
  }
  return this.each(function(idx) {
   var $this = $(this),
    obj = $this.data('navgoco');

   if (!obj) {
    obj = new Plugin(this, callback ? $.fn.navgoco.defaults : options, idx);
    $this.data('navgoco', obj);
   }
   if (callback) {
    obj[options].apply(obj, args);
   }
  });
 };
 /**
  * Global var holding all navgoco menus open states
  *
  * @type {Object}
  */
 var cookie = null;

 /**
  * Default navgoco options
  *
  * @type {Object}
  */
 $.fn.navgoco.defaults = {
  caretHtml: '',
  accordion: false,
  openClass: 'open',
  save: true,
  cookie: {
   name: 'navgoco',
   expires: false,
   path: '/'
  },
  slide: {
   duration: 400,
   easing: 'swing'
  },
  onClickBefore: $.noop,
  onClickAfter: $.noop,
  onToggleBefore: $.noop,
  onToggleAfter: $.noop
 };
  $(document).ready(function() {
        $('.nav').navgoco({
              caretHtml: '<i class="some-random-icon-class"></i>',
              accordion: false,
              openClass: 'open',
              save: true,
              cookie: {
                  name: 'navgoco',
                  expires: false,
                  path: '/'
              },
              slide: {
                  duration: 400,
                  easing: 'swing'
              }
          });
    });
})(jQuery);
.nav, .nav ul, .nav li {
    list-style: none;
}

ul.nav {width: 250px;}

.nav ul {
    padding: 0;
    margin: 0 0 0 18px;
}

.nav {
    padding: 4px;
    margin: 0px;
}

.nav > li {
    margin: 4px 0;
}

.nav > li li {
    margin: 2px 0;
}

.nav a {
    color: #333;
    display: block;
    outline: none;
    -webkit-border-radius: 4px;
    -moz-border-radius: 4px;
    border-radius: 4px;
    text-decoration: none;
}

.nav li > a > span {
    float: right;
    font-size: 19px;
    font-weight: bolder;
}

.nav li > a:hover > span {
    color: #fff;
}

.nav li > a > span:after {
    content: '\25be';
}
.nav li.open > a > span:after {
    content: '\25b4';
}

.nav a:hover, .nav li.active > a {
    background-color: #5D5D5D;
    color: #f5f5f5;
}

.nav > li.active > a  {
    background-color: #4D90FE;
}

.nav li a {
    font-size: 12px;
    line-height: 18px;
    padding: 2px 10px;
}

.nav > li > a {
    font-size: 14px;
    line-height: 20px;
    padding: 4px 10px;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<ul class="nav">
    <li><a href="https://github.com/tefra/navgoco/blob/master/README.md">Item 1</a>
        <ul>
            <li><a href="#">1.1 Submenu</a></li>
            <li><a href="#">1.2 Submenu</a></li>
            <li><a href="#">1.3 Submenu</a></li>
        </ul>
    </li>
</ul>

1 ответ

Он уже доступен с клавиатуры, но на самом деле он не виден пользователю.

Вы должны добавить немного CSS, чтобы сделать фокус видимым. Например:

.nav a:focus {
    background-color: #ccc;
}

В вашем примере скрипки вы также можете удалить tabindex от ul.nav, Это предотвращает появление прямоугольника фокуса на ul элемент, который не желателен.

Другие вопросы по тегам