chromium/chrome/browser/resources/chromeos/accessibility/chromevox/background/output/output_rules.js

// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

/**
 * @fileoverview Contains the rules for output based on type information.
 */
import {TestImportManager} from '/common/testing/test_import_manager.js';

import {AbstractRole, ChromeVoxRole, CustomRole} from '../../common/role_type.js';

import {OutputRoleInfo} from './output_role_info.js';
import {OutputCustomEvent, OutputFormatType, OutputNavigationType} from './output_types.js';

const EventType = chrome.automation.EventType;
const RoleType = chrome.automation.RoleType;

/**
 * @typedef {{
 *   event: string,
 *   role: string,
 *   navigation: (string|undefined),
 *   output: (string|undefined)}}
 */
export let OutputRuleSpecifier;

export class OutputRule {
  /** @param {!OutputEventType} event */
  constructor(event) {
    /** @protected {!OutputEventType} */
    this.event_ = this.getEvent_(event);
    /** @protected {!ChromeVoxRole} */
    this.role_ = CustomRole.DEFAULT;
    /** @protected {!OutputNavigationType|undefined} */
    this.navigation_;
    /** @protected {!OutputFormatType|undefined} */
    this.output_;
  }

  /**
   * @param {!OutputEventType} event
   * @return {!OutputEventType}
   * @private
   */
  getEvent_(event) {
    if (OutputRule.RULES[event]) {
      return event;
    }
    return OutputCustomEvent.NAVIGATE;
  }

  /** @return {!OutputRuleSpecifier} */
  get specifier() {
    return /** @type {!OutputRuleSpecifier} */ ({
      event: this.event_,
      role: this.role_,
      navigation: this.navigation_,
      output: this.output_,
    });
  }

  /** @return {string} */
  get formatString() {
    return OutputRule.RULES[this.event_][this.role_][this.output_];
  }

  /**
   * @param {ChromeVoxRole|undefined} role
   * @param {!OutputFormatType|!OutputNavigationType|undefined} formatName
   * @return {boolean} true if the role was set, false otherwise.
   */
  populateRole(role, formatName) {
    if (this.hasRule_(role, formatName) && role) {
      this.role_ = role;
      return true;
    } else if (
        this.hasRule_(parent(role), formatName) &&
        parent(role) !== CustomRole.NO_ROLE) {
      this.role_ = parent(role);
      return true;
    }
    return false;
  }

  // The following setter functions are a temporary measure.
  // TODO(anastasi): move the logic for determining the below properties into
  // this class.

  /** @param {!ChromeVoxRole} role */
  set role(role) {
    this.role_ = role;
  }

  /** @param {!OutputFormatType|undefined} output */
  set output(output) {
    this.output_ = output;
  }

  /** @return {!OutputEventType} */
  get event() {
    return this.event_;
  }
  /** @return {!ChromeVoxRole} */
  get role() {
    return this.role_;
  }
  /** @return {!OutputNavigationType|undefined} */
  get navigation() {
    return this.navigation_;
  }
  /** @return {!OutputFormatType|undefined} */
  get output() {
    return this.output_;
  }

  // ========= Private methods =========

  /**
   * @param {ChromeVoxRole|undefined} role
   * @param {!OutputFormatType|!OutputNavigationType|undefined} format
   * @return {boolean} Whether there is a rule for this role/format combo.
   * @private
   */
  hasRule_(role, format) {
    const eventBlock = OutputRule.RULES[this.event_];
    return role && eventBlock[role] && eventBlock[role][format];
  }
}

export class AncestryOutputRule extends OutputRule {
  /**
   * @param {!OutputEventType} eventType
   * @param {ChromeVoxRole|undefined} role
   * @param {!OutputNavigationType|undefined} navigationType
   * @param {boolean} tryBraille
   */
  constructor(eventType, role, navigationType, tryBraille) {
    super(eventType);

    this.populateRole(role, navigationType);
    this.populateNavigation(navigationType);
    this.populateOutput(tryBraille);
  }

  /** @param {!OutputNavigationType|undefined} navigationType */
  populateNavigation(navigationType) {
    if (navigationType && OutputRule.RULES[this.event_][this.role_] &&
        OutputRule.RULES[this.event_][this.role_][navigationType]) {
      this.navigation_ = navigationType;
    }
  }

  /** @param {boolean} tryBraille */
  populateOutput(tryBraille) {
    if (!OutputRule.RULES[this.event_][this.role_]) {
      // Invalid rule case.
      return;
    }

    const rule = OutputRule.RULES[this.event_][this.role_][this.navigation_];
    if (rule && rule.speak) {
      this.output_ = OutputFormatType.SPEAK;
    }
    if (rule && tryBraille && rule.braille) {
      this.output_ = OutputFormatType.BRAILLE;
    }
  }

  /** @return {boolean} */
  get defined() {
    return Boolean(
        OutputRule.RULES[this.event_][this.role_] &&
        OutputRule.RULES[this.event_][this.role_][this.navigation_]);
  }

  /** @return {string} */
  get enterFormat() {
    const rule = OutputRule.RULES[this.event_][this.role_][this.navigation_];
    if (this.output_) {
      return rule[this.output_];
    }
    return rule || '';
  }
}

/**
 * @param {ChromeVoxRole|undefined} role
 * @return {!ChromeVoxRole}
 */
function parent(role) {
  return OutputRoleInfo[role]?.inherits ?? CustomRole.NO_ROLE;
}

/**
 * An object that specifies the rules for outputting a certain role on a
 * specific event, based on the type of output.
 *
 * speak: The speech rule for when ChromeVox range lands exactly on the node.
 * braille: The braille rule for when ChromeVox range lands exactly on the node.
 * enter: The rule for when ChromeVox range enters the node's subtree.
 *    Can contain speak and braille properties.
 * leave: The rule for when ChromeVox range exits the node's subtree.
 * startOf: The rule applied for each ancestor diff of a range and its previous
 * leaf range.
 * endOf: The rule applied for each ancestor diff of a range and its
 * next leaf range.
 *
 * @typedef {{
 *     speak: (string|undefined),
 *     braille: (string|undefined),
 *     enter: (string|undefined|{
 *                speak: (string|undefined),
 *                braille: (string|undefined)
 *            }),
 *     leave: (string|undefined),
 *     startOf: (string|undefined),
 *     endOf: (string|undefined)
 * }}
 */
let OutputRuleDefinition;

/**
 * Rules specifying format of AutomationNodes for output.
 * @type {Object<OutputEventType, Object<ChromeVoxRole, !OutputRuleDefinition>>}
 * Please see above for more information on properties.
 */
OutputRule.RULES = {
  navigate: {
    [CustomRole.DEFAULT]: {
      speak: `$name $node(activeDescendant) $value $state $restriction $role
          $description`,
      braille: ``,
    },
    [AbstractRole.CONTAINER]: {
      startOf: `$nameFromNode $role $state $description`,
      endOf: `@end_of_container($role)`,
    },
    [AbstractRole.FORM_FIELD_CONTAINER]: {
      enter: `$nameFromNode $role $state $description`,
      leave: `@exited_container($role)`,
    },
    [AbstractRole.ITEM]: {
      // Note that ChromeVox generally does not output position/count. Only for
      // some roles (see sub-output rules) or when explicitly provided by an
      // author (via posInSet), do we include them in the output.
      enter: `$nameFromNode $role $state $restriction $description
          $if($posInSet, @describe_index($posInSet, $setSize))`,
      speak: `$state $nameOrTextContent= $role
          $if($posInSet, @describe_index($posInSet, $setSize))
          $description $restriction`,
    },
    [AbstractRole.LIST]: {
      startOf: `$nameFromNode $role $if($setSize, @@list_with_items($setSize))
          $restriction $description`,
      endOf: `@end_of_container($role) @@list_nested_level($listNestedLevel)`,
    },
    [AbstractRole.NAME_FROM_CONTENTS]: {
      speak: `$nameOrDescendants $node(activeDescendant) $value $state
          $restriction $role $description`,
    },
    [AbstractRole.RANGE]: {
      speak: `$name $node(activeDescendant) $description $role
          $if($value, $value, $if($valueForRange, $valueForRange))
          $state $restriction
          $if($minValueForRange, @aria_value_min($minValueForRange))
          $if($maxValueForRange, @aria_value_max($maxValueForRange))`,
    },
    [AbstractRole.SPAN]: {
      startOf: `$nameFromNode $role $state $description`,
      endOf: `@end_of_container($role)`,
    },
    [RoleType.ALERT]: {
      enter: `$name $role $state`,
      speak: `$earcon(ALERT_NONMODAL) $role $nameOrTextContent $description
          $state`,
    },
    [RoleType.ALERT_DIALOG]: {
      enter: `$earcon(ALERT_MODAL) $name $state $description $roleDescription
          $textContent`,
      speak: `$earcon(ALERT_MODAL) $name $nameOrTextContent $description $state
          $role`,
    },
    [RoleType.BUTTON]: {
      speak: `$name $node(activeDescendant) $state $restriction $role
          $description`,
    },
    [RoleType.CELL]: {
      enter: {
        speak: `$cellIndexText $node(tableCellColumnHeaders) $nameFromNode
            $roleDescription $state`,
        braille: `$state $cellIndexText $node(tableCellColumnHeaders)
            $nameFromNode $roleDescription`,
      },
      speak: `$nameFromNode $descendants $cellIndexText
          $node(tableCellColumnHeaders) $roleDescription $state $description`,
      braille: `$state
          $name $cellIndexText $node(tableCellColumnHeaders) $roleDescription
          $description`,
    },
    [RoleType.CHECK_BOX]: {
      speak: `$if($checked, $earcon(CHECK_ON), $earcon(CHECK_OFF))
          $name $role $if($checkedStateDescription, $checkedStateDescription, $checked)
          $description $state $restriction`,
    },
    [RoleType.CLIENT]: {speak: `$name`},
    [RoleType.COMBO_BOX_MENU_BUTTON]: {
      speak: `$name $value $role @aria_has_popup
          $if($setSize, @@list_with_items($setSize))
          $state $restriction $description`,
    },
    [RoleType.DATE]:
        {enter: `$nameFromNode $role $state $restriction $description`},
    [RoleType.DIALOG]: {enter: `$nameFromNode $role $description`},
    [RoleType.GENERIC_CONTAINER]: {
      enter: `$nameFromNode $description $state`,
      speak: `$nameOrTextContent $description $state`,
    },
    [RoleType.EMBEDDED_OBJECT]: {speak: `$name`},
    [RoleType.GRID]: {
      speak: `$name $node(activeDescendant) $role $state $restriction
          $description`,
    },
    [RoleType.GRID_CELL]: {
      enter: {
        speak: `$cellIndexText $node(tableCellColumnHeaders) $nameFromNode
            $roleDescription $state`,
        braille: `$state $cellIndexText $node(tableCellColumnHeaders)
            $nameFromNode $roleDescription`,
      },
      speak: `$nameFromNode $descendants $cellIndexText
          $node(tableCellColumnHeaders) $roleDescription $state $description`,
      braille: `$state
          $name $cellIndexText $node(tableCellColumnHeaders) $roleDescription
          $description
          $if($selected, @aria_selected_true)`,
    },
    [RoleType.GROUP]: {
      enter: `$nameFromNode $roleDescription $state $restriction $description`,
      speak: `$nameOrDescendants $value $state $restriction $roleDescription
          $description`,
      leave: ``,
    },
    [RoleType.HEADING]: {
      enter: `!relativePitch(hierarchicalLevel)
          $nameFromNode=
          $if($hierarchicalLevel, @tag_h+$hierarchicalLevel, $role) $state
          $description`,
      speak: `!relativePitch(hierarchicalLevel)
          $nameOrDescendants=
          $if($hierarchicalLevel, @tag_h+$hierarchicalLevel, $role) $state
          $restriction $description`,
    },
    [RoleType.IMAGE]: {
      speak: `$if($name, $name,
          $if($imageAnnotation, $imageAnnotation, $urlFilename))
          $value $state $role $description`,
    },
    [RoleType.IME_CANDIDATE]:
        {speak: `$name $phoneticReading @describe_index($posInSet, $setSize)`},
    [RoleType.INLINE_TEXT_BOX]: {speak: `$precedingBullet $name=`},
    [RoleType.INPUT_TIME]:
        {enter: `$nameFromNode $role $state $restriction $description`},
    [RoleType.LABEL_TEXT]: {
      speak: `$name $value $state $restriction $roleDescription $description`,
    },
    [RoleType.LINE_BREAK]: {speak: `$name=`},
    [RoleType.LINK]: {
      enter: `$nameFromNode= $role $state $restriction`,
      speak: `$name $value $state $restriction
          $if($inPageLinkTarget, @internal_link, $role) $description`,
    },
    [RoleType.LIST]: {
      speak: `$nameFromNode $descendants $role
          @@list_with_items($setSize) $description $state`,
    },
    [RoleType.LIST_BOX]: {
      enter: `$nameFromNode $role @@list_with_items($setSize)
          $restriction $description`,
    },
    [RoleType.LIST_BOX_OPTION]: {
      speak: `$state $name $role @describe_index($posInSet, $setSize)
          $description $restriction
          $nif($selected, @aria_selected_false)`,
      braille: `$state $name $role @describe_index($posInSet, $setSize)
          $description $restriction
          $if($selected, @aria_selected_true, @aria_selected_false)`,
    },
    [RoleType.LIST_MARKER]: {speak: `$name`},
    [RoleType.MENU]: {
      enter: `$name $role `,
      speak: `$name $node(activeDescendant)
          $role @@list_with_items($setSize) $description $state $restriction`,
    },
    [RoleType.MENU_ITEM]: {
      speak: `$name $role $if($hasPopup, @has_submenu)
          @describe_index($posInSet, $setSize) $description $state $restriction`,
    },
    [RoleType.MENU_ITEM_CHECK_BOX]: {
      speak: `$if($checked, $earcon(CHECK_ON), $earcon(CHECK_OFF))
          $name $role $checked $state $restriction $description
          @describe_index($posInSet, $setSize)`,
    },
    [RoleType.MENU_ITEM_RADIO]: {
      speak: `$if($checked, $earcon(CHECK_ON), $earcon(CHECK_OFF))
          $if($checked, @describe_menu_item_radio_selected($name),
          @describe_menu_item_radio_unselected($name)) $state $roleDescription
          $restriction $description
          @describe_index($posInSet, $setSize)`,
    },
    [RoleType.MENU_LIST_OPTION]: {
      speak: `$state $name $role @describe_index($posInSet, $setSize)
          $description $restriction
          $nif($selected, @aria_selected_false)`,
      braille: `$state $name $role @describe_index($posInSet, $setSize)
          $description $restriction
          $if($selected, @aria_selected_true, @aria_selected_false)`,
    },
    [RoleType.PARAGRAPH]: {speak: `$nameOrDescendants $roleDescription`},
    [RoleType.RADIO_BUTTON]: {
      speak: `$if($checked, $earcon(CHECK_ON), $earcon(CHECK_OFF))
          $if($checked, @describe_radio_selected($name),
          @describe_radio_unselected($name))
          @describe_index($posInSet, $setSize)
          $roleDescription $description $state $restriction`,
    },
    [RoleType.ROOT_WEB_AREA]:
        {enter: `$name`, speak: `$if($name, $name, @web_content)`},
    [RoleType.REGION]:
        {speak: `$state $nameOrTextContent $description $roleDescription`},
    [RoleType.ROW]: {
      startOf: `$node(tableRowHeader) $roleDescription
          $if($hierarchicalLevel, @describe_depth($hierarchicalLevel))`,
      speak: ` $if($hierarchicalLevel, @describe_depth($hierarchicalLevel))
          $name $node(activeDescendant) $value $state $restriction $role
          $if($selected, @aria_selected_true) $description`,
    },
    [RoleType.STATIC_TEXT]: {speak: `$precedingBullet $name= $description`},
    [RoleType.SWITCH]: {
      speak: `$if($checked, $earcon(CHECK_ON), $earcon(CHECK_OFF))
          $if($checked, @describe_switch_on($name),
          @describe_switch_off($name)) $roleDescription
          $description $state $restriction`,
    },
    [RoleType.TAB]: {
      speak: `@describe_tab($name) $roleDescription $description
          @describe_index($posInSet, $setSize) $state $restriction
          $if($selected, @aria_selected_true)`,
    },
    [RoleType.TABLE]: {
      enter: `$roleDescription @table_summary($name,
          $if($ariaRowCount, $ariaRowCount, $tableRowCount),
          $if($ariaColumnCount, $ariaColumnCount, $tableColumnCount))
          $node(tableHeader)`,
    },
    [RoleType.TAB_LIST]: {
      speak: `$name $node(activeDescendant) $state $restriction $role
          $description`,
    },
    [RoleType.TEXT_FIELD]: {
      speak: `$name $value
          $if($roleDescription, $roleDescription,
              $if($multiline, @tag_textarea,
                  $if($inputType, $inputType, $role)))
          $description $state $restriction`,
    },
    [RoleType.TIMER]: {
      speak: `$nameFromNode $descendants $value $state $role
        $description`,
    },
    [RoleType.TOGGLE_BUTTON]: {
      speak: `$if($checked, $earcon(CHECK_ON), $earcon(CHECK_OFF))
          $name $role $pressed $description $state $restriction`,
    },
    [RoleType.TOOLBAR]: {enter: `$name $role $description $restriction`},
    [RoleType.TREE]:
        {enter: `$name $role @@list_with_items($setSize) $restriction`},
    [RoleType.TREE_ITEM]: {
      enter: `$role $expanded $collapsed $restriction
          @describe_index($posInSet, $setSize)
          @describe_depth($hierarchicalLevel)`,
      speak: `$name
          $role $description $state $restriction
          $nif($selected, @aria_selected_false)
          @describe_index($posInSet, $setSize)
          @describe_depth($hierarchicalLevel)`,
    },
    [RoleType.UNKNOWN]: {speak: ``},
    [RoleType.WINDOW]: {
      enter: `@describe_window($name) $description`,
      speak: `@describe_window($name) $description $earcon(OBJECT_OPEN)`,
    },
  },
  [EventType.CONTROLS_CHANGED]: {
    [RoleType.TAB]: {
      speak: `@describe_tab($name) @describe_index($posInSet, $setSize)
          @aria_selected_true`,
    },
  },
  [EventType.MENU_START]: {
    [CustomRole.DEFAULT]:
        {speak: `@chrome_menu_opened($name)  $earcon(OBJECT_OPEN)`},
  },
  [EventType.MENU_END]: {
    [CustomRole.DEFAULT]: {speak: `@chrome_menu_closed $earcon(OBJECT_CLOSE)`},
  },
  [EventType.ALERT]: {
    [CustomRole.DEFAULT]:
        {speak: `$earcon(ALERT_NONMODAL) $nameOrTextContent $description`},
  },
};

TestImportManager.exportForTesting(OutputRule);