Posted on by & filed under keyboard shortcuts, Safari Flow.

As you may have heard on Twitter, we recently added tons of keyboard shortcuts to Safari Flow. While developing this feature, my goal was to be able to navigate the entire app without picking up my hands from the keyboard. Let us know what you think, but I think it’s wonderful.

To try out the shortcuts, first log into Safari Flow. Once you’re in, you can see a full list of shortcuts from most pages by pressing the ? (Shift + /) key.

screenshot of keyboard shortcuts

Get info on keyboard shortcuts with ?

While I like being able to more easily navigate the reading interface (for example, you can open the table of contents by typing t), my favorite shortcuts are related to navigating through cards and quickly adding them to my queue, removing them from my recommendations, or going directly to a chapter. To try out these shortcuts, use your arrow keys to navigate the cards.

animation of a user cycling through cards

Use the arrow keys to select cards

Any of the arrow keys focus the first card on the page.1 Once a selected card receives focus, you can cycle focus through its individual elements using the Tab key.2 Pressing Enter on a focused element is the same as clicking it.

animation of a user cycling through through individual links within a card

Use tab to cycle through selected card elements

Once you’ve focused on a card there are special shortcuts you can use: q to add the chapter to your queue, d to remove the card from your recommendations, Shift + Enter to go directly to the chapter, and Escape to remove the selection from the card.

How we did it

Changing the tabindex of an HTML element to -1 allows you to set its focus and change it’s tab precedence. Once I read about this on the Twitter blog, I knew that implementing it for our cards would be the bee’s knees.

Walking the DOM

Selecting cards is a simple matter of tracking it’s index in an unordered list (with JavaScript) and then changing it’s attributes. A selected card should have these attributes: a selection HTML class for targeting purposes, -1 tabindex, and focus.

On most pages (Queue, Recommended, and Recent), the cards are stacked inside an unordered list, which makes traversing them easy. The homepage is a bit different—each category of cards is wrapped inside a section HTML5 tag, so you need to know when to “jump” to the next or previous row. This can be accomplished by looking at the :first-child and :last-child of the list items.

Selecting the card above or below it is a bit trickier because they are responsive. In order to jump to the card directly above it or below it within an unordered list, you need to to know how many cards are in the row. If there are three cards, jumping three cards above will take you to the card directly above it in the next row. This is tracked by putting our responsive CSS breakpoints into variables, checking them against the width of the viewport, and storing their index in a “jump” variable.

  var $cards = $('.js-bitlist'),
    $first = $('.js-bitlist:first > li.card:first-child'),
    isHomepage = $('body').hasClass('homepage');

  selectNextCard = function () {
    selectCard('next');
  };

  selectPrevCard = function () {
    selectCard('prev');
  };

  selectUpCard = function () {
    selectCard('up');
  };

  selectDownCard = function () {
    selectCard('down');
  };

   selectCard = function (direction) {
    var $selected = $cards.find('.selected-card.card'),
      prev = direction === 'prev',
      next = direction === 'next',
      up = direction === 'up',
      down = direction === 'down',
      goPrevRow = prev && $selected.is(':first-child'),
      goNextRow = next && $selected.is(':nth-child(3)'),
      width = $('body').outerWidth(true),
      oneAcross = width < 768,
      twoAcross = width > 768 && width < 1280,
      threeAcross = width > 1280 && width < 1600,
      fourAcross = width > 1600,
      cardsInRow = [oneAcross, twoAcross, threeAcross, fourAcross],
      index = $selected.index() + 1;

    _.each(cardsInRow, function (row, i) {
      if (row) {
        this.jump = i + 1;
      }
    });

    if ($cards.length) {
      if (!$selected.length) {
        $first.attr('tabindex', -1)
          .addClass('selected-card')
          .trigger('focus');
      }
      else {
        if (isHomepage) {
          if (!(next || prev)) {
            if (goPrevRow || up) {
              $cards = $selected.parents('section').prev().find('ul');
              removeSelection($selected);

              if (goPrevRow) {
                selectNewCard($cards, 3);
              }
              else {
                selectNewCard($cards, index);
              }
            }
            else if (goNextRow || down) {
              $cards = $selected.parents('section').next().find('ul');
              removeSelection($selected);

              if (goNextRow) {
                selectNewCard($cards, 1);
              }
              else {
                selectNewCard($cards, index);
              }
            }
          }
          else {
            $cards = $selected.parents('ul');
            removeSelection($selected);

            if (next) {
              selectNewCard($cards, index + 1);
            }
            else {
              selectNewCard($cards, index - 1);
            }
          }
        }
        else {
          removeSelection($selected);

          if (next) {
            selectNewCard($cards, index + 1);
          }
          else if (prev) {
            selectNewCard($cards, index - 1);
          }
          else if (up) {
            selectNewCard($cards, index - this.jump);
          }
          else {
            selectNewCard($cards, index + this.jump);
          }
        }
      }
    }
  };

  removeSelection = function ($selected) {
    $selected.removeClass('selected-card')
      .removeAttr('tabIndex');
  };

  selectNewCard = function ($cards, index) {
    $cards.find('li.card:nth-child(' + index + ')')
      .attr('tabIndex', -1)
      .addClass('selected-card')
      .trigger('focus');
  };

Finally, once you’ve set focus on a card, thereby allowing the user to cycle that focus through that card’s individual elements with the Tab key, you’ll just need to style all of its :focus elements.

1. Applies only to Queue, Recent, Recommended, and Home pages.

2. If you’re using the Safari web browser, you must first enable Tab key navigation.

Tags:

Comments are closed.