Accessibility/SoftFocus

From MozillaWiki
Jump to: navigation, search

Background

It is useful to have a focus-like state that is exclusive to accessibility and is independant of the DOM state. From the user's perspective this allows reviewing a document or web application without directly affecting its state, like moving the caret or cycling through focusable elements. This is useful in mobile, for example a user could navigate to a text entry without having it capture input. Only if the user chooses to enter text they would "activate" the text entry which would bring up an on screen keyboard, etc. This feature would also allow easier navigation of content.

Interfaces

nsIAccessiblePivot

typedef short nsAccessiblePivotBoundary;

interface nsIAccessiblePivot : nsISupports
{
  const nsAccessiblePivotBoundary BOUNDARY_CHAR            = 0;
  const nsAccessiblePivotBoundary BOUNDARY_WORD            = 1;
  const nsAccessiblePivotBoundary BOUNDARY_LINE            = 2;
  const nsAccessiblePivotBoundary BOUNDARY_ATTRIBUTE_RANGE = 3;

  /*
   * The accessible the pivot is currently pointed at.
   */
  readonly attribute nsIAccessible accessible;

  /*
   * The document that owns this pivot.
   */
  readonly attribute nsIAccessibleDocument rootAccessible;

  /*
   * The start offset in the accessible's text. Only supported when the accessible has
   * the nsIAccessibleText interface. If no explicit offset was set or if it is not
   * supported in the pivot's accessible this is -1.
   */
  readonly attribute long startOffset;

  /*
   * The end offset in the accessible's text. Only supported when the accessible has
   * the nsIAccessibleText interface. If no explicit offset was set or if it is not
   * supported in the pivot's accessible this is -1.
   */
  readonly attribute long endOffset;

  /*
   * Set the accessible this pivot should point to.
   *
   * @param aAccessible the new accessible to point to.
   * @throws NS_ERROR_INVALID_ARG when given accessible is not in the pivot's document.
   */ 
  void setAccessible(in nsIAccessible aAccessible);

  /**
   * Set the pivot's text range in a text accessible.
   *
   * @param aStartOffset the start offset to set.
   * @param aEndOffset the end offset to set.
   * @throws NS_ERROR_NO_INTERFACE when the pivot's accessible does not have the
   *   nsIAccessibleText inteface.
   * @throws NS_ERROR_FAILURE when the offset exceeds the accessible's
   *   character count.
   */
  void setTextOffset(in long aStartOffset, in long aEndOffset);

  /**
   * Move pivot to next object via given traversal rule, which defines a filter
   *  and a search order.
   *
   * @param aRule traversal rule to use.
   * @param aLast traverse to the last occurance in the document.
   * @return true on success, false if there are no new nodes to traverse to.
   */
  boolean nexObject(in nsIAccessibleTraversalRule aRule, in boolean aLast);

  /**
   * Move pivot to previous object via given traversal rule, which defines a
   *  filter and a search order.
   *
   * @param aRule traversal rule to use.
   * @param aFirst traverse to the first occurance in the document.
   * @return true on success, false if there are no new nodes to traverse to.
   */
  boolean previousObject(in nsIAccessibleTraversalRule aRule, in boolean aFirst);

  /**
   * Move pivot to next text range.
   *
   * @param aBoundary type of boundary for next text range, character, word, etc.
   * @param aLast traverse to the last occurance in the document.
   * @return true on success, false if there are is no more text.
   */
  boolean nextText(in nsAccessiblePivotBoundary aBoundary, in boolean aLast);

  /**
   * Move pivot to previous text range.
   *
   * @param aBoundary type of boundary for previous text range, character, word,
   *   etc.
   * @param aFirst traverse to the first occurance in the document.
   * @return true on success, false if there are is no more text.
   */
  boolean previousText(in nsAccessiblePivotBoundary aBoundary, in boolean aFirst);

  /*
   * Add an observer for pivot changes.
   *
   * @param aObserver the observer object to be notified of pivot changes.
   */
  void addObserver(in nsIAccessiblePivotObserver aObserver);

  /*
   * Remove an observer for pivot changes.
   *
   * @param aObserver the observer object to remove from being notified.
   */
  void removeObserver(in nsIAccessiblePivotObserver aObserver);
};

nsIAccessiblePivotObserver

An observer class to get pivot changes.

interface nsIAccessiblePivotObserver : nsISupports
{
  /**
   * Called when observed pivot changes.
   */
  void onPivotChanged(in nsIAccessiblePivot aPivot,
                      in nsIAccessible aOldAccessible,
                      in long aOldStart, in long aOldEnd);
}

nsIAccessibleVirtualCursor

A top-level content document or a chrome document will typically support this interface.

interface nsIAccessibleVirtualCursor : nsISupports
{
  /**
   * The virtual cursor pivot this object manages.
   */
  readonly attribute nsIAccessiblePivot virtualCursor;
}

nsIAccessibleVirtualCursorChangeEvent

An accessible event of type EVENT_VIRTUALCURSOR_CHANGE. Fired when the virtual cursor's pivot is moved.

interface nsIAccessibleVirtualCursorChangeEvent: nsISupports
{
  /**
   * Document's virtual cursor pivot.
   */
  readonly attribute nsIAccessiblePivot pivot;

  /**
   * Previous object pointed at by virtual cursor. null if none.
   */
  readonly attribute nsIAccessible oldAccessible;

  /**
   * Previous start offset of pivot. -1 if none.
   */
  readonly attribute long oldStartOffset;

  /**
   * Previous end offset of pivot. -1 if none.
   */
  readonly attribute long oldEndOffset;
};

Example Usage

Setting Virtual Cursor

A traversal rule that goes to headers.

var headerTraversalRule = {
  QueryInterface: XPCOMUtils.generateQI([Ci.nsIAccessibleTraversalRule]),
  acceptRoles: [Ci.nsIAccessibleRole.ROLE_HEADER],
  acceptStates: 0xffffffff,
  acceptExtStates: 0xffffffff,
  skipStates: Ci.nsIAccessibleStates.STATE_INVISIBLE,
  skipExtStates: Ci.nsIAccessibleStates.EXT_STATE_DEFUNCT,
  traversalOrder: Ci.nsIAccessibleTraversalRule.ORDER_DEPTH_FIRST,
  filterAccessible: function (aAccessible) {return Ci.nsIAccessibleTraversalRule.FILTER_ACCEPT;}
}

A simple object traversal rule that goes to all leaves or focusable items.

var objectTraversalRule = {
  QueryInterface: XPCOMUtils.generateQI([Ci.nsIAccessibleTraversalRule]),
  acceptRoles: [],
  acceptStates: 0xffffffff,
  acceptExtStates: 0xffffffff,
  skipStates: Ci.nsIAccessibleStates.STATE_INVISIBLE,
  skipExtStates: Ci.nsIAccessibleStates.EXT_STATE_DEFUNCT,
  traversalOrder: Ci.nsIAccessibleTraversalRule.ORDER_DEPTH_FIRST,
  filterAccessible: function (aAccessible) {
     if (this._isFocusable(aAccessible))
       return Ci.nsIAccessibleTraversalRule.FILTER_ACCEPT;
     if (this._isFocusable(aAccessible.parent))
       return Ci.nsIAccessibleTraversalRule.FILTER_REJECT;
     if (aAccessible.childCount == 0)
       return Ci.nsIAccessibleTraversalRule.FILTER_ACCEPT;
  },
  _isFocusable: function (aAccessible) {
    let state = {};
    aAccessible.getState(state, null);
    return state.value & Ci.nsIAccessibleStates.EXT_STATE_FOCUSABLE != 0;
  }
}

An input event handler that changes the virtual cursor.

function inputEventHandler(event) {
  switch(event.keyCode) {
  case 40:
    docAcc.virtualCursor.nextObject(objectTraversalRule, event.shiftKey);
    break;
  case 38:
    docAcc.virtualCursor.previousObject(objectTraversalRule, event.shiftKey);
    break;
  case 39:
    docAcc.virtualCursor.nextObject(headerTraversalRule, event.shiftKey);
    break;
  case 37:
    docAcc.virtualCursor.previousObject(headerTraversalRule, event.shiftKey);
    break;
  default:
    break;
}

Presenting Virtual Cursor

An event listener that draws a ring for the virtual cursor. drawRect() is left to your imagination.

var VirtualCursorView = {
  handleEvent: function (aEvent) {
    if (aEvent.type != Cu.nsIAccessibleEvent.EVENT_VIRTUALCURSOR_CHANGED)
      return;

    let event = aEvent.QueryInterface(Ci.nsIAccessibleVirtualCursorChangeEvent);
    this.showRing(event.pivot.accessible, event.pivot.startOffset, event.pivot.endOffset);
  },

  showRing: function showRing (aAccessible, aStartOffset, aEndOffset) {
    let x = {};
    let y = {};
    let w = {};
    let h = {};

    if (aStartOffset >= 0 && aEndOffset >= 0) {
      try {
        let textAcc = aAccessible.QueryInterface(nsIAccessibleText);
        let start = {};
        let end = {};
        textAcc.getRangeExtents(aStartOffset, aEndOffset, x, y, w, h,
                                COORDTYPE_SCREEN_RELATIVE);
        drawRect(x.value, y.value, w.value, h.value);
        return;
      } catch (e) {
      }
    }

    aAccessible.getBounds(x, y, w, h);
    drawRect(x.value, y.value, w.value, h.value);
  }
}