Bug 1163374 - Basic MathML Accessibility support in AccessFu. r=yzen
authorFrédéric Wang <fred.wang@free.fr>
Mon, 13 Jul 2015 11:53:00 +0200
changeset 285530 95a508b38259a73be5620e43751366f73ea47fe5
parent 285529 608bad61a61910c17c058383f1c7fd5ebf6f2dbf
child 285531 bba8855ab99e7be48afe12755cff8a08bf99727b
push id5067
push userraliiev@mozilla.com
push dateMon, 21 Sep 2015 14:04:52 +0000
treeherdermozilla-beta@14221ffe5b2f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersyzen
bugs1163374
milestone42.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1163374 - Basic MathML Accessibility support in AccessFu. r=yzen
accessible/jsat/OutputGenerator.jsm
accessible/jsat/TraversalRules.jsm
accessible/jsat/Utils.jsm
accessible/tests/mochitest/jsat/a11y.ini
accessible/tests/mochitest/jsat/doc_traversal.html
accessible/tests/mochitest/jsat/test_output_mathml.html
accessible/tests/mochitest/jsat/test_traversal.html
dom/locales/en-US/chrome/accessibility/AccessFu.properties
--- a/accessible/jsat/OutputGenerator.jsm
+++ b/accessible/jsat/OutputGenerator.jsm
@@ -187,16 +187,92 @@ let OutputGenerator = {
       return;
     }
     aOutput[this.outputOrder === OUTPUT_DESC_FIRST ? 'unshift' : 'push']({
       string: landmarkName
     });
   },
 
   /**
+   * Adds math roles to the output, for a MathML accessible.
+   * @param {Array} aOutput Output array.
+   * @param {nsIAccessible} aAccessible current accessible object.
+   * @param {String} aRoleStr aAccessible's role string.
+   */
+  _addMathRoles: function _addMathRoles(aOutput, aAccessible, aRoleStr) {
+    // First, determine the actual role to use (e.g. mathmlfraction).
+    let roleStr = aRoleStr;
+    switch(aAccessible.role) {
+      case Roles.MATHML_CELL:
+      case Roles.MATHML_ENCLOSED:
+      case Roles.MATHML_LABELED_ROW:
+      case Roles.MATHML_ROOT:
+      case Roles.MATHML_SQUARE_ROOT:
+      case Roles.MATHML_TABLE:
+      case Roles.MATHML_TABLE_ROW:
+        // Use the default role string.
+        break;
+      case Roles.MATHML_MULTISCRIPTS:
+      case Roles.MATHML_OVER:
+      case Roles.MATHML_SUB:
+      case Roles.MATHML_SUB_SUP:
+      case Roles.MATHML_SUP:
+      case Roles.MATHML_UNDER:
+      case Roles.MATHML_UNDER_OVER:
+        // For scripted accessibles, use the string 'mathmlscripted'.
+        roleStr = 'mathmlscripted';
+        break;
+      case Roles.MATHML_FRACTION:
+        // From a semantic point of view, the only important point is to
+        // distinguish between fractions that have a bar and those that do not.
+        // Per the MathML 3 spec, the latter happens iff the linethickness
+        // attribute is of the form [zero-float][optional-unit]. In that case,
+        // we use the string 'mathmlfractionwithoutbar'.
+        let linethickness = Utils.getAttributes(aAccessible).linethickness;
+        if (linethickness) {
+            let numberMatch = linethickness.match(/^(?:\d|\.)+/);
+            if (numberMatch && !parseFloat(numberMatch[0])) {
+                roleStr += 'withoutbar';
+            }
+        }
+        break;
+      default:
+        // Otherwise, do not output the actual role.
+        roleStr = null;
+        break;
+    }
+
+    // Get the math role based on the position in the parent accessible
+    // (e.g. numerator for the first child of a mathmlfraction).
+    let mathRole = Utils.getMathRole(aAccessible);
+    if (mathRole) {
+      aOutput[this.outputOrder === OUTPUT_DESC_FIRST ? 'push' : 'unshift']
+        ({string: this._getOutputName(mathRole)});
+    }
+    if (roleStr) {
+      aOutput[this.outputOrder === OUTPUT_DESC_FIRST ? 'push' : 'unshift']
+        ({string: this._getOutputName(roleStr)});
+    }
+  },
+
+  /**
+   * Adds MathML menclose notations to the output.
+   * @param {Array} aOutput Output array.
+   * @param {nsIAccessible} aAccessible current accessible object.
+   */
+  _addMencloseNotations: function _addMencloseNotations(aOutput, aAccessible) {
+    let notations = Utils.getAttributes(aAccessible).notation || 'longdiv';
+    aOutput[this.outputOrder === OUTPUT_DESC_FIRST ? 'push' : 'unshift'].apply(
+      aOutput, [for (notation of notations.split(' '))
+        {string: this._getOutputName('notation-' + notation)}
+      ]
+    );
+  },
+
+  /**
    * Adds an entry type attribute to the description if available.
    * @param {Array} aOutput Output array.
    * @param {nsIAccessible} aAccessible current accessible object.
    * @param {String} aRoleStr aAccessible's role string.
    */
   _addType: function _addType(aOutput, aAccessible, aRoleStr) {
     if (aRoleStr !== 'entry') {
       return;
@@ -207,17 +283,17 @@ let OutputGenerator = {
     if (!typeName || typeName === 'text') {
       return;
     }
     aOutput.push({string: 'textInputType_' + typeName});
   },
 
   _addState: function _addState(aOutput, aState, aRoleStr) {}, // jshint ignore:line
 
-  _addRole: function _addRole(aOutput, aRoleStr) {}, // jshint ignore:line
+  _addRole: function _addRole(aOutput, aAccessible, aRoleStr) {}, // jshint ignore:line
 
   get outputOrder() {
     if (!this._utteranceOrder) {
       this._utteranceOrder = new PrefCache('accessibility.accessfu.utterance');
     }
     return typeof this._utteranceOrder.value === 'number' ?
       this._utteranceOrder.value : this.defaultOutputOrder;
   },
@@ -296,27 +372,87 @@ let OutputGenerator = {
     'key': NAME_FROM_SUBTREE_RULE,
     'image map': INCLUDE_DESC,
     'option': INCLUDE_DESC,
     'listbox': INCLUDE_DESC,
     'definitionlist': INCLUDE_DESC | INCLUDE_NAME,
     'dialog': INCLUDE_DESC | INCLUDE_NAME,
     'chrome window': IGNORE_EXPLICIT_NAME,
     'app root': IGNORE_EXPLICIT_NAME,
-    'statusbar': NAME_FROM_SUBTREE_RULE },
+    'statusbar': NAME_FROM_SUBTREE_RULE,
+    'mathml table': INCLUDE_DESC | INCLUDE_NAME,
+    'mathml labeled row': NAME_FROM_SUBTREE_RULE,
+    'mathml table row': NAME_FROM_SUBTREE_RULE,
+    'mathml cell': INCLUDE_DESC | INCLUDE_NAME,
+    'mathml fraction': INCLUDE_DESC,
+    'mathml square root': INCLUDE_DESC,
+    'mathml root': INCLUDE_DESC,
+    'mathml enclosed': INCLUDE_DESC,
+    'mathml sub': INCLUDE_DESC,
+    'mathml sup': INCLUDE_DESC,
+    'mathml sub sup': INCLUDE_DESC,
+    'mathml under': INCLUDE_DESC,
+    'mathml over': INCLUDE_DESC,
+    'mathml under over': INCLUDE_DESC,
+    'mathml multiscripts': INCLUDE_DESC,
+    'mathml identifier': INCLUDE_DESC,
+    'mathml number': INCLUDE_DESC,
+    'mathml operator': INCLUDE_DESC,
+    'mathml text': INCLUDE_DESC,
+    'mathml string literal': INCLUDE_DESC,
+    'mathml row': INCLUDE_DESC,
+    'mathml style': INCLUDE_DESC,
+    'mathml error': INCLUDE_DESC },
+
+  mathmlRolesSet: new Set([
+    Roles.MATHML_MATH,
+    Roles.MATHML_IDENTIFIER,
+    Roles.MATHML_NUMBER,
+    Roles.MATHML_OPERATOR,
+    Roles.MATHML_TEXT,
+    Roles.MATHML_STRING_LITERAL,
+    Roles.MATHML_GLYPH,
+    Roles.MATHML_ROW,
+    Roles.MATHML_FRACTION,
+    Roles.MATHML_SQUARE_ROOT,
+    Roles.MATHML_ROOT,
+    Roles.MATHML_FENCED,
+    Roles.MATHML_ENCLOSED,
+    Roles.MATHML_STYLE,
+    Roles.MATHML_SUB,
+    Roles.MATHML_SUP,
+    Roles.MATHML_SUB_SUP,
+    Roles.MATHML_UNDER,
+    Roles.MATHML_OVER,
+    Roles.MATHML_UNDER_OVER,
+    Roles.MATHML_MULTISCRIPTS,
+    Roles.MATHML_TABLE,
+    Roles.LABELED_ROW,
+    Roles.MATHML_TABLE_ROW,
+    Roles.MATHML_CELL,
+    Roles.MATHML_ACTION,
+    Roles.MATHML_ERROR,
+    Roles.MATHML_STACK,
+    Roles.MATHML_LONG_DIVISION,
+    Roles.MATHML_STACK_GROUP,
+    Roles.MATHML_STACK_ROW,
+    Roles.MATHML_STACK_CARRIES,
+    Roles.MATHML_STACK_CARRY,
+    Roles.MATHML_STACK_LINE
+  ]),
 
   objectOutputFunctions: {
     _generateBaseOutput:
       function _generateBaseOutput(aAccessible, aRoleStr, aState, aFlags) {
         let output = [];
 
         if (aFlags & INCLUDE_DESC) {
           this._addState(output, aState, aRoleStr);
           this._addType(output, aAccessible, aRoleStr);
-          this._addRole(output, aRoleStr);
+          this._addRole(output, aAccessible, aRoleStr);
         }
 
         if (aFlags & INCLUDE_VALUE && aAccessible.value.trim()) {
           output[this.outputOrder === OUTPUT_DESC_FIRST ? 'push' : 'unshift'](
             aAccessible.value);
         }
 
         this._addName(output, aAccessible, aFlags);
@@ -343,17 +479,17 @@ let OutputGenerator = {
     },
 
     pagetab: function pagetab(aAccessible, aRoleStr, aState, aFlags) {
       let itemno = {};
       let itemof = {};
       aAccessible.groupPosition({}, itemof, itemno);
       let output = [];
       this._addState(output, aState);
-      this._addRole(output, aRoleStr);
+      this._addRole(output, aAccessible, aRoleStr);
       output.push({
         string: 'objItemOfN',
         args: [itemno.value, itemof.value]
       });
 
       this._addName(output, aAccessible, aFlags);
       this._addLandmark(output, aAccessible);
 
@@ -369,17 +505,17 @@ let OutputGenerator = {
         Logger.logException(x);
         return output;
       } finally {
         // Check if it's a layout table, and bail out if true.
         // We don't want to speak any table information for layout tables.
         if (table.isProbablyForLayout()) {
           return output;
         }
-        this._addRole(output, aRoleStr);
+        this._addRole(output, aAccessible, aRoleStr);
         output.push.call(output, {
           string: this._getOutputName('tblColumnInfo'),
           count: table.columnCount
         }, {
           string: this._getOutputName('tblRowInfo'),
           count: table.rowCount
         });
         this._addName(output, aAccessible, aFlags);
@@ -389,16 +525,33 @@ let OutputGenerator = {
     },
 
     gridcell: function gridcell(aAccessible, aRoleStr, aState, aFlags) {
       let output = [];
       this._addState(output, aState);
       this._addName(output, aAccessible, aFlags);
       this._addLandmark(output, aAccessible);
       return output;
+    },
+
+    // Use the table output functions for MathML tabular elements.
+    mathmltable: function mathmltable() {
+      return this.objectOutputFunctions.table.apply(this, arguments);
+    },
+
+    mathmlcell: function mathmlcell() {
+      return this.objectOutputFunctions.cell.apply(this, arguments);
+    },
+
+    mathmlenclosed: function mathmlenclosed(aAccessible, aRoleStr, aState,
+                                            aFlags, aContext) {
+      let output = this.objectOutputFunctions.defaultFunc.
+        apply(this, [aAccessible, aRoleStr, aState, aFlags, aContext]);
+      this._addMencloseNotations(output, aAccessible);
+      return output;
     }
   }
 };
 
 /**
  * Generates speech utterances from objects, actions and state changes.
  * An utterance is an array of speech data.
  *
@@ -592,18 +745,22 @@ this.UtteranceGenerator = {  // jshint i
       return this.objectOutputFunctions.defaultFunc.apply(this, arguments);
     }
   },
 
   _getContextStart: function _getContextStart(aContext) {
     return aContext.newAncestry;
   },
 
-  _addRole: function _addRole(aOutput, aRoleStr) {
-    aOutput.push({string: this._getOutputName(aRoleStr)});
+  _addRole: function _addRole(aOutput, aAccessible, aRoleStr) {
+    if (this.mathmlRolesSet.has(aAccessible.role)) {
+      this._addMathRoles(aOutput, aAccessible, aRoleStr);
+    } else {
+      aOutput.push({string: this._getOutputName(aRoleStr)});
+    }
   },
 
   _addState: function _addState(aOutput, aState, aRoleStr) {
 
     if (aState.contains(States.UNAVAILABLE)) {
       aOutput.push({string: 'stateUnavailable'});
     }
 
@@ -652,17 +809,17 @@ this.UtteranceGenerator = {  // jshint i
     if (aState.contains(States.SELECTED)) {
       aOutput.push({string: 'stateSelected'});
     }
   },
 
   _getListUtterance:
     function _getListUtterance(aAccessible, aRoleStr, aFlags, aItemCount) {
       let utterance = [];
-      this._addRole(utterance, aRoleStr);
+      this._addRole(utterance, aAccessible, aRoleStr);
       utterance.push({
         string: this._getOutputName('listItemsCount'),
         count: aItemCount
       });
 
       this._addName(utterance, aAccessible, aFlags);
       this._addLandmark(utterance, aAccessible);
 
@@ -800,18 +957,22 @@ this.BrailleGenerator = {  // jshint ign
 
     return [];
   },
 
   _getOutputName: function _getOutputName(aName) {
     return OutputGenerator._getOutputName(aName) + 'Abbr';
   },
 
-  _addRole: function _addRole(aBraille, aRoleStr) {
-    aBraille.push({string: this._getOutputName(aRoleStr)});
+  _addRole: function _addRole(aBraille, aAccessible, aRoleStr) {
+    if (this.mathmlRolesSet.has(aAccessible.role)) {
+      this._addMathRoles(aBraille, aAccessible, aRoleStr);
+    } else {
+      aBraille.push({string: this._getOutputName(aRoleStr)});
+    }
   },
 
   _addState: function _addState(aBraille, aState, aRoleStr) {
     if (aState.contains(States.CHECKABLE)) {
       aBraille.push({
         string: aState.contains(States.CHECKED) ?
           this._getOutputName('stateChecked') :
           this._getOutputName('stateUnchecked')
--- a/accessible/jsat/TraversalRules.jsm
+++ b/accessible/jsat/TraversalRules.jsm
@@ -97,17 +97,18 @@ var gSimpleTraversalRoles =
    Roles.SLIDER,
    Roles.SPINBUTTON,
    Roles.OPTION,
    Roles.LISTITEM,
    Roles.GRID_CELL,
    Roles.COLUMNHEADER,
    Roles.ROWHEADER,
    Roles.STATUSBAR,
-   Roles.SWITCH];
+   Roles.SWITCH,
+   Roles.MATHML_MATH];
 
 var gSimpleMatchFunc = function gSimpleMatchFunc(aAccessible) {
   // An object is simple, if it either has a single child lineage,
   // or has a flat subtree.
   function isSingleLineage(acc) {
     for (let child = acc; child; child = child.firstChild) {
       if (Utils.visibleChildCount(child) > 1) {
         return false;
--- a/accessible/jsat/Utils.jsm
+++ b/accessible/jsat/Utils.jsm
@@ -410,31 +410,51 @@ this.Utils = { // jshint ignore:line
     for (let value of values) {
       if (attrSet.has(value)) {
         return value;
       }
     }
   },
 
   getLandmarkName: function getLandmarkName(aAccessible) {
-    const landmarks = [
+    return this.matchRoles(aAccessible, [
       'banner',
       'complementary',
       'contentinfo',
       'main',
       'navigation',
       'search'
-    ];
+    ]);
+  },
+
+  getMathRole: function getMathRole(aAccessible) {
+    return this.matchRoles(aAccessible, [
+      'base',
+      'close-fence',
+      'denominator',
+      'numerator',
+      'open-fence',
+      'overscript',
+      'presubscript',
+      'presuperscript',
+      'root-index',
+      'subscript',
+      'superscript',
+      'underscript'
+    ]);
+  },
+
+  matchRoles: function matchRoles(aAccessible, aRoles) {
     let roles = this.getAttributes(aAccessible)['xml-roles'];
     if (!roles) {
       return;
     }
 
-    // Looking up a role that would match a landmark.
-    return this.matchAttributeValue(roles, landmarks);
+    // Looking up a role that would match any in the provided roles.
+    return this.matchAttributeValue(roles, aRoles);
   },
 
   getEmbeddedControl: function getEmbeddedControl(aLabel) {
     if (aLabel) {
       let relation = aLabel.getRelationByType(Relations.LABEL_FOR);
       for (let i = 0; i < relation.targetsCount; i++) {
         let target = relation.getTarget(i);
         if (target.parent === aLabel) {
@@ -879,18 +899,22 @@ PivotContext.prototype = {
       return this._cells.get(domNode);
     }
 
     let cellInfo = {};
     let getAccessibleCell = function getAccessibleCell(aAccessible) {
       if (!aAccessible) {
         return null;
       }
-      if ([Roles.CELL, Roles.COLUMNHEADER, Roles.ROWHEADER].indexOf(
-        aAccessible.role) < 0) {
+      if ([
+            Roles.CELL,
+            Roles.COLUMNHEADER,
+            Roles.ROWHEADER,
+            Roles.MATHML_CELL
+          ].indexOf(aAccessible.role) < 0) {
           return null;
       }
       try {
         return aAccessible.QueryInterface(Ci.nsIAccessibleTableCell);
       } catch (x) {
         Logger.logException(x);
         return null;
       }
@@ -945,17 +969,19 @@ PivotContext.prototype = {
 
     cellInfo.columnHeaders = [];
     if (cellInfo.columnChanged && cellInfo.current.role !==
       Roles.COLUMNHEADER) {
       cellInfo.columnHeaders = [headers for (headers of getHeaders( // jshint ignore:line
         cellInfo.current.columnHeaderCells))];
     }
     cellInfo.rowHeaders = [];
-    if (cellInfo.rowChanged && cellInfo.current.role === Roles.CELL) {
+    if (cellInfo.rowChanged &&
+        (cellInfo.current.role === Roles.CELL ||
+         cellInfo.current.role === Roles.MATHML_CELL)) {
       cellInfo.rowHeaders = [headers for (headers of getHeaders( // jshint ignore:line
         cellInfo.current.rowHeaderCells))];
     }
 
     this._cells.set(domNode, cellInfo);
     return cellInfo;
   },
 
--- a/accessible/tests/mochitest/jsat/a11y.ini
+++ b/accessible/tests/mochitest/jsat/a11y.ini
@@ -13,13 +13,14 @@ support-files =
 skip-if = buildapp == 'mulet'
 [test_content_text.html]
 skip-if = buildapp == 'mulet'
 [test_explicit_names.html]
 [test_gesture_tracker.html]
 [test_hints.html]
 [test_landmarks.html]
 [test_live_regions.html]
+[test_output_mathml.html]
 [test_output.html]
 [test_quicknav_modes.html]
 [test_tables.html]
 [test_pointer_relay.html]
 [test_traversal.html]
--- a/accessible/tests/mochitest/jsat/doc_traversal.html
+++ b/accessible/tests/mochitest/jsat/doc_traversal.html
@@ -140,10 +140,16 @@
         <td>Messy Stuff</td>
       </tr>
     </tbody>
   </table>
   <div id="statusbar-1" role="status">Last sync:<span>2 days ago</span></div>
   <div aria-label="Last sync: 30min ago" id="statusbar-2" role="status"></div>
 
   <span id="switch-1" role="switch" aria-checked="false" aria-label="Light switch"></span>
+  <p>This is a MathML formula <math id="math-1" display="block">
+    <mfrac>
+      <mrow><mi>x</mi><mo>+</mo><mn>1</mn></mrow>
+      <msqrt><mn>3</mn><mo>/</mo><mn>4</mn></msqrt>
+    </mfrac>
+  </math> with some text after.</p>
 </body>
 </html>
new file mode 100644
--- /dev/null
+++ b/accessible/tests/mochitest/jsat/test_output_mathml.html
@@ -0,0 +1,305 @@
+<html>
+<head>
+  <title>[AccessFu] MathML Accessibility Support</title>
+
+  <link rel="stylesheet" type="text/css"
+        href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+  <script type="application/javascript"
+          src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="application/javascript"
+          src="../common.js"></script>
+  <script type="application/javascript"
+          src="output.js"></script>
+  <script type="application/javascript">
+
+    function doTest() {
+      // Test the following accOrElmOrID.
+      var tests = [{
+          accOrElmOrID: "math-1",
+          expectedUtterance: [
+            [{"string":"open-fence"},"(","x",",","y",{"string":"close-fence"},")"],
+            ["(",{"string":"open-fence"},"x",",","y",")",{"string":"close-fence"}]
+          ],
+          expectedBraille: [
+            [{"string":"open-fenceAbbr"},"(","x",",","y",{"string":"close-fenceAbbr"},")"],
+            ["(",{"string":"open-fenceAbbr"},"x",",","y",")",{"string":"close-fenceAbbr"}]
+          ]
+        }, {
+          accOrElmOrID: "mfrac-1",
+          expectedUtterance: [
+            [{"string":"mathmlfraction"},{"string":"numerator"},"a",{"string":"denominator"},"b"],
+            ["a",{"string":"numerator"},"b",{"string":"denominator"},{"string":"mathmlfraction"}]
+          ],
+          expectedBraille: [
+            [{"string":"mathmlfractionAbbr"},{"string":"numeratorAbbr"},"a",{"string":"denominatorAbbr"},"b"],
+            ["a",{"string":"numeratorAbbr"},"b",{"string":"denominatorAbbr"},{"string":"mathmlfractionAbbr"}]
+          ]
+        }, {
+          accOrElmOrID: "mfrac-2",
+          expectedUtterance: [
+            [{"string":"mathmlfractionwithoutbar"},{"string":"numerator"},"a",{"string":"denominator"},"b"],
+            ["a",{"string":"numerator"},"b",{"string":"denominator"},{"string":"mathmlfractionwithoutbar"}]
+          ],
+          expectedBraille: [
+            [{"string":"mathmlfractionwithoutbarAbbr"},{"string":"numeratorAbbr"},"a",{"string":"denominatorAbbr"},"b"],
+            ["a",{"string":"numeratorAbbr"},"b",{"string":"denominatorAbbr"},{"string":"mathmlfractionwithoutbarAbbr"}]
+          ]
+        }, {
+          accOrElmOrID: "msub-1",
+          expectedUtterance: [
+            [{"string":"mathmlscripted"},{"string":"base"},"a",{"string":"subscript"},"b"],
+            ["a",{"string":"base"},"b",{"string":"subscript"},{"string":"mathmlscripted"}]
+          ],
+          expectedBraille: [
+            [{"string":"mathmlscriptedAbbr"},{"string":"baseAbbr"},"a",{"string":"subscriptAbbr"},"b"],
+            ["a",{"string":"baseAbbr"},"b",{"string":"subscriptAbbr"},{"string":"mathmlscriptedAbbr"}]
+          ]
+        }, {
+          accOrElmOrID: "msup-1",
+          expectedUtterance: [
+            [{"string":"mathmlscripted"},{"string":"base"},"a",{"string":"superscript"},"b"],
+            ["a",{"string":"base"},"b",{"string":"superscript"},{"string":"mathmlscripted"}]
+          ],
+          expectedBraille: [
+            [{"string":"mathmlscriptedAbbr"},{"string":"baseAbbr"},"a",{"string":"superscriptAbbr"},"b"],
+            ["a",{"string":"baseAbbr"},"b",{"string":"superscriptAbbr"},{"string":"mathmlscriptedAbbr"}]
+          ]
+        }, {
+          accOrElmOrID: "msubsup-1",
+          expectedUtterance: [
+            [{"string":"mathmlscripted"},{"string":"base"},"a",{"string":"subscript"},"b",{"string":"superscript"},"c"],
+            ["a",{"string":"base"},"b",{"string":"subscript"},"c",{"string":"superscript"},{"string":"mathmlscripted"}]
+          ],
+          expectedBraille: [
+            [{"string":"mathmlscriptedAbbr"},{"string":"baseAbbr"},"a",{"string":"subscriptAbbr"},"b",{"string":"superscriptAbbr"},"c"],
+            ["a",{"string":"baseAbbr"},"b",{"string":"subscriptAbbr"},"c",{"string":"superscriptAbbr"},{"string":"mathmlscriptedAbbr"}]
+          ]
+        }, {
+          accOrElmOrID: "mmultiscripts-1",
+          expectedUtterance: [
+            [{"string":"mathmlscripted"},{"string":"base"},"a",{"string":"subscript"},"b",{"string":"superscript"},"c",{"string":"superscript"},"d",{"string":"presubscript"},"e",{"string":"presubscript"},"f",{"string":"presuperscript"},"g"],
+            ["a",{"string":"base"},"b",{"string":"subscript"},"c",{"string":"superscript"},"d",{"string":"superscript"},"e",{"string":"presubscript"},"f",{"string":"presubscript"},"g",{"string":"presuperscript"},{"string":"mathmlscripted"}]
+          ],
+          expectedBraille: [
+            [{"string":"mathmlscriptedAbbr"},{"string":"baseAbbr"},"a",{"string":"subscriptAbbr"},"b",{"string":"superscriptAbbr"},"c",{"string":"superscriptAbbr"},"d",{"string":"presubscriptAbbr"},"e",{"string":"presubscriptAbbr"},"f",{"string":"presuperscriptAbbr"},"g"],
+            ["a",{"string":"baseAbbr"},"b",{"string":"subscriptAbbr"},"c",{"string":"superscriptAbbr"},"d",{"string":"superscriptAbbr"},"e",{"string":"presubscriptAbbr"},"f",{"string":"presubscriptAbbr"},"g",{"string":"presuperscriptAbbr"},{"string":"mathmlscriptedAbbr"}]
+          ]
+        }, {
+          accOrElmOrID: "munder-1",
+          expectedUtterance: [
+            [{"string":"mathmlscripted"},{"string":"base"},"a",{"string":"underscript"},"b"],
+            ["a",{"string":"base"},"b",{"string":"underscript"},{"string":"mathmlscripted"}]
+          ],
+          expectedBraille: [
+            [{"string":"mathmlscriptedAbbr"},{"string":"baseAbbr"},"a",{"string":"underscriptAbbr"},"b"],
+            ["a",{"string":"baseAbbr"},"b",{"string":"underscriptAbbr"},{"string":"mathmlscriptedAbbr"}]
+          ]
+        }, {
+          accOrElmOrID: "mover-1",
+          expectedUtterance: [
+            [{"string":"mathmlscripted"},{"string":"base"},"a",{"string":"overscript"},"b"],
+            ["a",{"string":"base"},"b",{"string":"overscript"},{"string":"mathmlscripted"}]
+          ],
+          expectedBraille: [
+            [{"string":"mathmlscriptedAbbr"},{"string":"baseAbbr"},"a",{"string":"overscriptAbbr"},"b"],
+            ["a",{"string":"baseAbbr"},"b",{"string":"overscriptAbbr"},{"string":"mathmlscriptedAbbr"}]
+          ]
+        }, {
+          accOrElmOrID: "munderover-1",
+          expectedUtterance: [
+            [{"string":"mathmlscripted"},{"string":"base"},"a",{"string":"underscript"},"b",{"string":"overscript"},"c"],
+            ["a",{"string":"base"},"b",{"string":"underscript"},"c",{"string":"overscript"},{"string":"mathmlscripted"}]
+          ],
+          expectedBraille: [
+            [{"string":"mathmlscriptedAbbr"},{"string":"baseAbbr"},"a",{"string":"underscriptAbbr"},"b",{"string":"overscriptAbbr"},"c"],
+            ["a",{"string":"baseAbbr"},"b",{"string":"underscriptAbbr"},"c",{"string":"overscriptAbbr"},{"string":"mathmlscriptedAbbr"}]
+          ]
+        }, {
+          accOrElmOrID: "mroot-1",
+          expectedUtterance: [
+            [{"string":"mathmlroot"},{"string":"base"},"a",{"string":"root-index"},"b"],
+            ["a",{"string":"base"},"b",{"string":"root-index"},{"string":"mathmlroot"}]
+          ],
+          expectedBraille: [
+            [{"string":"mathmlrootAbbr"},{"string":"baseAbbr"},"a",{"string":"root-indexAbbr"},"b"],
+            ["a",{"string":"baseAbbr"},"b",{"string":"root-indexAbbr"},{"string":"mathmlrootAbbr"}]
+          ]
+        }, {
+          accOrElmOrID: "mtable-1",
+          expectedUtterance: [
+            [{"string":"mathmltable"},{"string":"tblColumnInfo","count":3},{"string":"tblRowInfo","count":2},{"string":"columnInfo","args":[1]},{"string":"rowInfo","args":[1]},"a",{"string":"columnInfo","args":[2]},{"string":"rowInfo","args":[1]},"b",{"string":"columnInfo","args":[3]},{"string":"rowInfo","args":[1]},"c",{"string":"columnInfo","args":[1]},{"string":"rowInfo","args":[2]},"d",{"string":"columnInfo","args":[2]},{"string":"rowInfo","args":[2]},"e",{"string":"columnInfo","args":[3]},{"string":"rowInfo","args":[2]},"f"],
+            ["a",{"string":"columnInfo","args":[1]},{"string":"rowInfo","args":[1]},"b",{"string":"columnInfo","args":[2]},{"string":"rowInfo","args":[1]},"c",{"string":"columnInfo","args":[3]},{"string":"rowInfo","args":[1]},"d",{"string":"columnInfo","args":[1]},{"string":"rowInfo","args":[2]},"e",{"string":"columnInfo","args":[2]},{"string":"rowInfo","args":[2]},"f",{"string":"columnInfo","args":[3]},{"string":"rowInfo","args":[2]},{"string":"mathmltable"},{"string":"tblColumnInfo","count":3},{"string":"tblRowInfo","count":2}]
+          ],
+          expectedBraille: [
+            [{"string":"mathmltableAbbr"},{"string":"tblColumnInfoAbbr","count":3},{"string":"tblRowInfoAbbr","count":2},{"string":"cellInfoAbbr","args":[1,1]},"a",{"string":"cellInfoAbbr","args":[2,1]},"b",{"string":"cellInfoAbbr","args":[3,1]},"c",{"string":"cellInfoAbbr","args":[1,2]},"d",{"string":"cellInfoAbbr","args":[2,2]},"e",{"string":"cellInfoAbbr","args":[3,2]},"f"],
+            ["a",{"string":"cellInfoAbbr","args":[1,1]},"b",{"string":"cellInfoAbbr","args":[2,1]},"c",{"string":"cellInfoAbbr","args":[3,1]},"d",{"string":"cellInfoAbbr","args":[1,2]},"e",{"string":"cellInfoAbbr","args":[2,2]},"f",{"string":"cellInfoAbbr","args":[3,2]},{"string":"mathmltableAbbr"},{"string":"tblColumnInfoAbbr","count":3},{"string":"tblRowInfoAbbr","count":2}]
+          ]
+      }, {
+          accOrElmOrID: "menclose-1",
+          expectedUtterance: [
+            [{"string":"mathmlenclosed"},{"string":"notation-longdiv"},"a"],
+            ["a",{"string":"notation-longdiv"},{"string":"mathmlenclosed"}]
+          ],
+          expectedBraille: [
+            [{"string":"mathmlenclosedAbbr"},{"string":"notation-longdivAbbr"},"a"],
+            ["a",{"string":"notation-longdivAbbr"},{"string":"mathmlenclosedAbbr"}]
+          ]
+        }, {
+          accOrElmOrID: "menclose-2",
+          expectedUtterance: [
+            [{"string":"mathmlenclosed"},{"string":"notation-circle"},"a"],
+            ["a",{"string":"notation-circle"},{"string":"mathmlenclosed"}]
+          ],
+          expectedBraille: [
+            [{"string":"mathmlenclosedAbbr"},{"string":"notation-circleAbbr"},"a"],
+            ["a",{"string":"notation-circleAbbr"},{"string":"mathmlenclosedAbbr"}]
+          ]
+        }, {
+          accOrElmOrID: "menclose-3",
+          expectedUtterance: [
+            [{"string":"mathmlenclosed"},{"string":"notation-left"},{"string":"notation-top"},{"string":"notation-bottom"},"a"],
+            ["a",{"string":"notation-left"},{"string":"notation-top"},{"string":"notation-bottom"},{"string":"mathmlenclosed"}]
+          ],
+          expectedBraille: [
+            [{"string":"mathmlenclosedAbbr"},{"string":"notation-leftAbbr"},{"string":"notation-topAbbr"},{"string":"notation-bottomAbbr"},"a"],
+            ["a",{"string":"notation-leftAbbr"},{"string":"notation-topAbbr"},{"string":"notation-bottomAbbr"},{"string":"mathmlenclosedAbbr"}]
+          ]
+        }];
+
+      // Test all possible utterance order preference values.
+      tests.forEach(function run(test) {
+        var outputOrderValues = [0, 1];
+        outputOrderValues.forEach(function testOutputOrder(outputOrder) {
+          SpecialPowers.setIntPref(PREF_UTTERANCE_ORDER, outputOrder);
+          testOutput(test.expectedUtterance[outputOrder], test.accOrElmOrID,
+            test.oldAccOrElmOrID, 1);
+          testOutput(test.expectedBraille[outputOrder], test.accOrElmOrID,
+            test.oldAccOrElmOrID, 0);
+        });
+      });
+
+      // If there was an original utterance order preference, revert to it.
+      SpecialPowers.clearUserPref(PREF_UTTERANCE_ORDER);
+      SimpleTest.finish();
+    }
+
+    SimpleTest.waitForExplicitFinish();
+    addA11yLoadEvent(doTest);
+  </script>
+</head>
+<body>
+  <div id="root">
+    <a target="_blank"
+       href="https://bugzilla.mozilla.org/show_bug.cgi?id=1163374"
+       title="[AccessFu] MathML Accessibility Support">
+      Mozilla Bug 1163374
+    </a>
+    <p id="display"></p>
+    <div id="content" style="display: none"></div>
+    <pre id="test"></pre>
+
+    <math id="math-1"><mo>(</mo><mi>x</mi><mo>,</mo><mi>y</mi><mo>)</mo></math>
+
+    <math>
+      <mfrac id="mfrac-1">
+        <mi>a</mi>
+        <mi>b</mi>
+      </mfrac>
+    </math>
+
+    <math>
+      <mfrac id="mfrac-2" linethickness="0px">
+        <mi>a</mi>
+        <mi>b</mi>
+      </mfrac>
+    </math>
+
+    <math>
+      <msub id="msub-1">
+        <mi>a</mi>
+        <mi>b</mi>
+      </msub>
+    </math>
+    <math>
+      <msup id="msup-1">
+        <mi>a</mi>
+        <mi>b</mi>
+      </msup>
+    </math>
+    <math>
+      <msubsup id="msubsup-1">
+        <mi>a</mi>
+        <mi>b</mi>
+        <mi>c</mi>
+      </msubsup>
+    </math>
+    <math>
+      <mmultiscripts id="mmultiscripts-1">
+        <mi>a</mi>
+        <mi>b</mi>
+        <mi>c</mi>
+        <none/>
+        <mi>d</mi>
+        <mprescripts/>
+        <mi>e</mi>
+        <none/>
+        <mi>f</mi>
+        <mi>g</mi>
+      </mmultiscripts>
+    </math>
+
+    <math>
+      <munder id="munder-1">
+        <mi>a</mi>
+        <mi>b</mi>
+      </munder>
+    </math>
+    <math>
+      <mover id="mover-1">
+        <mi>a</mi>
+        <mi>b</mi>
+      </mover>
+    </math>
+    <math>
+      <munderover id="munderover-1">
+        <mi>a</mi>
+        <mi>b</mi>
+        <mi>c</mi>
+      </munderover>
+    </math>
+
+    <math>
+      <mroot id="mroot-1">
+        <mi>a</mi>
+        <mi>b</mi>
+      </mroot>
+    </math>
+
+    <math>
+      <mtable id="mtable-1">
+        <mtr>
+          <mtd><mi>a</mi></mtd>
+          <mtd><mi>b</mi></mtd>
+          <mtd><mi>c</mi></mtd>
+        </mtr>
+        <mtr>
+          <mtd><mi>d</mi></mtd>
+          <mtd><mi>e</mi></mtd>
+          <mtd><mi>f</mi></mtd>
+        </mtr>
+      </mtable>
+    </math>
+
+    <math>
+      <menclose id="menclose-1"><mi>a</mi></menclose>
+    </math>
+    <math>
+      <menclose id="menclose-2" notation="circle"><mi>a</mi></menclose>
+    </math>
+    <math>
+      <menclose id="menclose-3" notation="left top bottom"><mi>a</mi></menclose>
+    </math>
+
+  </div>
+</body>
+</html>
--- a/accessible/tests/mochitest/jsat/test_traversal.html
+++ b/accessible/tests/mochitest/jsat/test_traversal.html
@@ -119,17 +119,18 @@
                               'checkbox-1-5', ' LeLisp', '• JavaScript',
                               'heading-5', 'image-2', 'image-3',
                               'Not actually an image', 'link-1', 'anchor-1',
                               'link-2', 'anchor-2', 'link-3', '3', '1', '4',
                               '1', 'Sunday', 'M', 'Week 1', '3', '4', '7', '2',
                               '5 8', 'gridcell4', 'Just an innocuous separator',
                               'Dirty Words', 'Meaning', 'Mud', 'Wet Dirt',
                               'Dirt', 'Messy Stuff', 'statusbar-1', 'statusbar-2',
-                              'switch-1']);
+                              'switch-1', 'This is a MathML formula ', 'math-1',
+                              'with some text after.']);
 
       gQueue.invoke();
     }
 
     SimpleTest.waitForExplicitFinish();
     addLoadEvent(function () {
       /* We open a new browser because we need to test with a top-level content
          document. */
--- a/dom/locales/en-US/chrome/accessibility/AccessFu.properties
+++ b/dom/locales/en-US/chrome/accessibility/AccessFu.properties
@@ -70,19 +70,41 @@ listbox        =       list box
 flatequation   =       flat equation
 gridcell       =       gridcell
 note           =       note
 figure         =       figure
 definitionlist =       definition list
 term           =       term
 definition     =       definition
 
+mathmltable              = math table
+mathmlcell               = cell
+mathmlenclosed           = enclosed
+mathmlfraction           = fraction
+mathmlfractionwithoutbar = fraction without bar
+mathmlroot               = root
+mathmlscripted           = scripted
+mathmlsquareroot         = square root
+
 # More sophisticated roles which are not actual numeric roles
 textarea       =       text area
 
+base           =       base
+close-fence    =       closing fence
+denominator    =       denominator
+numerator      =       numerator
+open-fence     =       opening fence
+overscript     =       overscript
+presubscript   =       presubscript
+presuperscript =       presuperscript
+root-index     =       root index
+subscript      =       subscript
+superscript    =       superscript
+underscript    =       underscript
+
 # Text input types
 textInputType_date   =       date
 textInputType_email  =       e-mail
 textInputType_search =       search
 textInputType_tel    =       telephone
 textInputType_url    =       URL
 
 # More sophisticated object descriptions
@@ -186,16 +208,36 @@ quicknav_ListItem    = List items
 quicknav_Link        = Links
 quicknav_List        = Lists
 quicknav_PageTab     = Page tabs
 quicknav_RadioButton = Radio buttons
 quicknav_Separator   = Separators
 quicknav_Table       = Tables
 quicknav_Checkbox    = Check boxes
 
+# MathML menclose notations.
+# See developer.mozilla.org/docs/Web/MathML/Element/menclose#attr-notation
+notation-longdiv            = long division
+notation-actuarial          = actuarial
+notation-phasorangle        = phasor angle
+notation-radical            = radical
+notation-box                = box
+notation-roundedbox         = rounded box
+notation-circle             = circle
+notation-left               = left
+notation-right              = right
+notation-top                = top
+notation-bottom             = bottom
+notation-updiagonalstrike   = up diagonal strike
+notation-downdiagonalstrike = down diagonal strike
+notation-verticalstrike     = vertical strike
+notation-horizontalstrike   = horizontal strike
+notation-updiagonalarrow    = up diagonal arrow
+notation-madruwb            = madruwb
+
 # Shortened role names for braille
 menubarAbbr        =       menu bar
 scrollbarAbbr      =       scroll bar
 gripAbbr           =       grip
 alertAbbr          =       alert
 menupopupAbbr      =       menu popup
 documentAbbr       =       document
 paneAbbr           =       pane
@@ -269,8 +311,48 @@ tblColumnInfoAbbr = #1c;#1c
 # See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
 tblRowInfoAbbr = #1r;#1r
 cellInfoAbbr = c%Sr%S
 
 stateCheckedAbbr = (x)
 stateUncheckedAbbr = ( )
 statePressedAbbr = (x)
 stateUnpressedAbbr = ( )
+
+mathmlenclosedAbbr           = enclosed
+mathmltableAbbr              = tbl
+mathmlcellAbbr               = cell
+mathmlfractionAbbr           = frac
+mathmlfractionwithoutbarAbbr = frac no bar
+mathmlrootAbbr               = root
+mathmlscriptedAbbr           = scripted
+mathmlsquarerootAbbr         = sqrt
+
+baseAbbr           = base
+close-fenceAbbr    = close
+denominatorAbbr    = den
+numeratorAbbr      = num
+open-fenceAbbr     = open
+overscriptAbbr     = over
+presubscriptAbbr   = presub
+presuperscriptAbbr = presup
+root-indexAbbr     = index
+subscriptAbbr      = sub
+superscriptAbbr    = sup
+underscriptAbbr    = under
+
+notation-longdivAbbr            = longdiv
+notation-actuarialAbbr          = act
+notation-phasorangleAbbr        = phasang
+notation-radicalAbbr            = rad
+notation-boxAbbr                = box
+notation-roundedboxAbbr         = rndbox
+notation-circleAbbr             = circ
+notation-leftAbbr               = lft
+notation-rightAbbr              = rght
+notation-topAbbr                = top
+notation-bottomAbbr             = bot
+notation-updiagonalstrikeAbbr   = updiagstrike
+notation-downdiagonalstrikeAbbr = dwndiagstrike
+notation-verticalstrikeAbbr     = vstrike
+notation-horizontalstrikeAbbr   = hstrike
+notation-updiagonalarrowAbbr    = updiagarrow
+notation-madruwbAbbr            = madruwb