/js-spatial-navigation

A javascript-based implementation of Spatial Navigation.

Primary LanguageJavaScriptMozilla Public License 2.0MPL-2.0

JavaScript SpatialNavigation

A javascript-based implementation of Spatial Navigation.

Examples

Basic usage

<head>
  <script src="https://luke-chang.github.io/js-spatial-navigation/spatial_navigation.js"></script>
  <script>
    window.addEventListener('load', function() {
      // Initialize
      SpatialNavigation.init();

      // Define navigable elements (anchors and elements with "focusable" class).
      SpatialNavigation.add({
        selector: 'a, .focusable'
      });

      // Make the *currently existing* navigable elements focusable.
      SpatialNavigation.makeFocusable();

      // Focus the first navigable element.
      SpatialNavigation.focus();
    });
  </script>
  <style>
    /* Add style to the focused elements */
    :focus {
      outline: 2px solid red;
    }
  </style>
</head>
<body>
  <a href="#">Link 1</a>
  <a href="#">Link 2</a>
  <div class="focusable">Div 1</div>
  <div class="focusable">Div 2</div>
</body>

Integrate jQuery

Although SpatialNavigation is a standalone (pure-javascript-based) library, it can work perfectly with jQuery.

<script src="https://code.jquery.com/jquery-2.2.1.min.js"></script>
<script>
  $.getScript('https://luke-chang.github.io/js-spatial-navigation/spatial_navigation.js', function() {
    $('a, .focusable')
      .SpatialNavigation()
      .focus(function() { $(this).css('outline', '2px solid red'); })
      .blur(function() { $(this).css('outline', ''); })
      .first()
      .focus();
  });
</script>

More Demonstrations

Documentation

API Reference

SpatialNavigation.init()

Initializes SpatialNavigation and binds event listeners to the global object. It is a synchronous function, so you don't need to await ready state. Calling init() more than once is possible since SpatialNavigation internally prevents it from reiterating the initialization.

Note: It should be called before using any other methods of SpatialNavigation!

SpatialNavigation.uninit()

Uninitializes SpatialNavigation, resets the variable state and unbinds the event listeners.

SpatialNavigation.clear()

Resets the variable state without unbinding the event listeners.

SpatialNavigation.add([sectionId], config)

Adds a section to SpatialNavigation with its own configuration. The config doesn't have to contain all the properties. Those omitted will inherit global ones automatically.

A section is a conceptual scope to define a set of elements no matter where they are in DOM structure. You can group elements based on their functions or behaviors (e.g. main, menu, dialog, etc.) into a section.

Giving a sectionId to a section enables you to refer to it in other methods but is not required. SpatialNavigation allows you to set it by config.id alternatively, yet it is not allowed in set().

SpatialNavigation.remove(sectionId)

  • sectionId: String

Removes the section with the specified sectionId from SpatialNavigation. Elements defined in this section will not be navigated anymore.

SpatialNavigation.set([sectionId], config)

Updates the config of the section with the specified sectionId. If sectionId is omitted, the global configuration will be updated.

Omitted properties in config will not affect the original one, which was set by add(), so only properties that you want to update need to be listed. In other words, if you want to delete any previously added properties, you have to explicitly assign undefined to those properties in the config.

SpatialNavigation.disable(sectionId)

  • sectionId: String

Disables the section with the specified sectionId temporarily. Elements defined in this section will become unnavigable until enable() is called.

SpatialNavigation.enable(sectionId)

  • sectionId: String

Enables the section with the specified sectionId. Elements defined in this section, on which if disable() was called earlier, will become navigable again.

SpatialNavigation.pause()

Makes SpatialNavigation pause until resume() is called. During its pause, SpatialNavigation stops to react to key events and will not trigger any custom events.

SpatialNavigation.resume()

Resumes SpatialNavigation, so it can react to key events and trigger events which paused because of pause().

SpatialNavigation.focus([sectionId/selector], [silent])

  • sectionId/selector: (optional) String / Selector (without @ syntax)
  • silent: (optional) Boolean

Focuses the section with the specified sectionId or the first element that matches selector.

If the first argument matches any of the existing sectionId, it will be regarded as a sectionId. Otherwise, it will be treated as selector instead. If omitted, the default section, which is set by setDefaultSection(), will be the substitution.

Setting silent to true lets you focus an element without triggering any custom events, but note that it does not stop native focus and blur events.

SpatialNavigation.move(direction, [selector])

  • direction: 'left', 'right', 'up' or 'down'
  • selector: (optional) Selector (without @ syntax)

Moves the focus to the given direction based on the rule of SpatialNavigation. The first element matching selector is regarded as the origin. If selector is omitted, SpatialNavigation will move the focus based on the currently focused element.

SpatialNavigation.makeFocusable([sectionId])

  • sectionId: (optional) String

A helper to add tabindex="-1" to elements defined in the specified section to make them focusable. If sectionId is omitted, it applies to all sections.

Note: It won't affect elements which have been focusable already or have not been appended to DOM tree yet.

SpatialNavigation.setDefaultSection([sectionId])

  • sectionId: (optional) String

Assigns the specified section to be the default section. It will be used as a substitution in certain methods, of which if sectionId is omitted.

Calling this method without the argument can reset the default section to undefined.

Configuration

Configuration is a plain object with configurable properties.

There are two kinds of the configuration: global and per-section. If you call set(config) without specifying sectionId, it will apply to the global one, from which the omitted per-section properties will inherit automatically.

Following is an example with default values.

{
  selector: '',
  straightOnly: false,
  straightOverlapThreshold: 0.5,
  rememberSource: false,
  disabled: false,
  defaultElement: '',
  enterTo: '',
  leaveFor: null,
  restrict: 'self-first',
  tabIndexIgnoreList: 'a, input, select, textarea, button, iframe, [contentEditable=true]',
  navigableFilter: null
}

selector

Elements matching selector are regarded as navigable elements in SpatialNavigation. However, hidden or disabled elements are ignored as they can not be focused in any way.

straightOnly

  • Type: Boolean
  • Default: false

When it is true, only elements in the straight (vertical or horizontal) direction will be navigated. i.e. SpatialNavigation ignores elements in the oblique directions.

straightOverlapThreshold

  • Type: Number in the range [0, 1]
  • Default: 0.5

This threshold is used to determine whether an element is considered in the straight (vertical or horizontal) directions. Valid number is between 0 to 1.0.

Setting it to 0.3 means that an element is counted in the straight directions only if it overlaps the straight area at least 0.3x of its total area.

rememberSource

  • Type: Boolean
  • Default: false

When it is true, the previously focused element will have higher priority to be chosen as the next candidate.

disabled

  • Type: Boolean
  • Default: false

When it is true, elements defined in this section are unnavigable. This property is modified by disable() and enable() as well.

defaultElement

  • Type: Selector (without @ syntax)
  • Default: ''

When a section is specified to be the next focused target, e.g. focus('some-section-id') is called, the first navigable element matching defaultElement within this section will be chosen first.

enterTo

  • Type: '', 'last-focused' or 'default-element'
  • Default: ''

If the focus comes from another section, you can define which element in this section should be focused first.

'last-focused' indicates the last focused element before we left this section last time. If this section has never been focused yet, the default element (if any) will be chosen next.

'default-element' indicates the element defined in defaultElement.

'' (empty string) implies following the original rule without any change.

leaveFor

  • Type: null or PlainObject
  • Default: null

This property specifies which element should be focused next when a user presses the corresponding arrow key and intends to leave the current section.

It should be a PlainObject consists of four properties: 'left', 'right', 'up' and 'down'. Each property should be a Selector. Any of these properties can be omitted, and SpatialNavigation will follow the original rule to navigate.

Note: Assigning an empty string to any of these properties makes SpatialNavigation go nowhere at that direction.

restrict

  • Type: 'self-first', 'self-only' or 'none'
  • Default: 'self-first'

'self-first' implies that elements within the same section will have higher priority to be chosen as the next candidate.

'self-only' implies that elements in the other sections will never be navigated by arrow keys. (However, you can always focus them by calling focus() manually.)

'none' implies no restriction.

tabIndexIgnoreList

  • Type: String
  • Default: 'a, input, select, textarea, button, iframe, [contentEditable=true]'

Elements matching tabIndexIgnoreList will never be affected by makeFocusable(). It is usually used to ignore elements that are already focusable.

navigableFilter

  • Type: 'null' or function(HTMLElement)
  • Default: null

A callback function that accepts a DOM element as the first argument.

SpatialNavigation calls this function every time when it tries to traverse every single candidate. You can ignore arbitrary elements by returning false.

Custom Attributes

SpatialNavigation supports HTML data-* attributes as follows:

  • data-sn-left
  • data-sn-right
  • data-sn-up
  • data-sn-down

They specifies which element should be focused next when a user presses the corresponding arrow key. This setting overrides any other settings in enterTo and leaveFor.

The value of each attribute should be a Selector and only accepts strings (the valid selector string and @ syntax described below) for now. Any of these attributes can be omitted, and SpatialNavigation will follow the original rule to navigate.

Note: Assigning an empty string to any of these attributes makes SpatialNavigation go nowhere at that direction.

Selector

The type "Selector" can be any of the following types.

  • a valid selector string for "querySelectorAll" or jQuery (if it exists)
  • a NodeList or an array containing DOM elements
  • a single DOM element
  • a jQuery object
  • a string '@<sectionId>' to indicate the specified section (e.g. '@test-section' indicates the section whose id is test-section.
  • a string '@' to indicate the default section

Note: Certain methods do not accept the @ syntax (including both @ and @<sectionId>).

Events

Following custom events are triggered by SpatialNavigation. You can bind them by addEventListener(). Some events are marked "cancelable", which means you can cancel them by Event.preventDefault(), as usual.

Focus-related events are also wrappers of the native focus/blur events, so they are triggered as well even SpatialNavigation is not involved. In this case, some properties in event.detail may be omitted. This kind of properties is marked "Navigation Only" below.

Note: If you bind events via jQuery's .on() API, you must change to event.originalEvent.detail to access the detail objects.

sn:willmove

  • bubbles: true
  • cancelable: true
  • detail:
    • cause: 'keydown' or 'api'
    • sectionId: <String>
    • direction: 'left', 'right', 'up' or 'down'

Fired when SpatialNavigation is about to move the focus.

cause indicates why this move happens. 'keydown' means triggered by key events while 'api' means triggered by calling move()) directly.

sectionId indicates the currently focused section.

direction indicates the direction given by arrow keys or move() method.

sn:willunfocus

  • bubbles: true
  • cancelable: true
  • detail:
    • nextElement: <HTMLElement> (Navigation Only)
    • nextSectionId: <String> (Navigation Only)
    • direction: 'left', 'right', 'up' or 'down' (Navigation Only)
    • native: <Boolean>

Fired when an element is about to lose the focus.

nextElement and nextSectionId indicate where the focus will be moved next.

direction is similar to sn:willmove but will be omitted here if this move is not caused by direction-related actions (e.g. by @ syntax or focus() directly).

native indicates whether this event is triggered by native focus-related events or not.

Note: If it is caused by native blur event, SpatialNavigation will try to focus back to the original element when you cancel it (but not guaranteed).

sn:unfocused

  • bubbles: true
  • cancelable: false
  • detail:
    • nextElement: <HTMLElement> (Navigation Only)
    • nextSectionId: <String> (Navigation Only)
    • direction: 'left', 'right', 'up' or 'down' (Navigation Only)
    • native: <Boolean>

Fired when an element just lost the focus.

Event details are the same as sn:willunfocus.

sn:willfocus

  • bubbles: true
  • cancelable: true
  • detail:
    • sectionId: <String>
    • previousElement: <HTMLElement> (Navigation Only)
    • direction: 'left', 'right', 'up' or 'down' (Navigation Only)
    • native: <Boolean>

Fired when an element is about to get the focus.

sectionId indicates the currently focused section.

previousElement indicates the last focused element before this move.

direction and native are the same as sn:willunfocus.

Note: If it is caused by native focus event, SpatialNavigation will try to blur it immediately when you cancel it (but not guaranteed).

sn:focused

  • bubbles: true
  • cancelable: false
  • detail:
    • sectionId: <String>
    • previousElement: <HTMLElement> (Navigation Only)
    • direction: 'left', 'right', 'up' or 'down' (Navigation Only)
    • native: <Boolean>

Fired when an element just got the focus.

Event details are the same as sn:willfocus.

sn:navigatefailed

  • bubbles: true
  • cancelable: false
    • direction: 'left', 'right', 'up' or 'down'

Fired when SpatialNavigation fails to find the next element to be focused.

direction is the same as sn:willunfocus.

sn:enter-down

  • bubbles: true
  • cancelable: true

Fired when ENTER key is pressed down.

sn:enter-up

  • bubbles: true
  • cancelable: true

Fired when ENTER key is released.

Browser Support

Chrome 5, Firefox 12, IE 9, Opera 11.5, Safari 5

License

Copyright (c) 2022 Luke Chang. Licensed under the MPL 2.0.