功能提示

功能提示指向并描述内容页面或浏览器 chrome 中的功能。它们可以包含单个消息或一系列消息。提示与聚光灯或其他对话框的不同之处在于,它们不会阻止与浏览器的其他交互。功能提示目前仅适用于浏览器 chrome 中的实验。例如,可以轻松配置提示指向浏览器 chrome 中的工具栏按钮。

提示可以使用以下内容元素进行配置(每个元素都是可选的)

  • 标题

  • 副标题

  • 内联标题图标

  • 标题上方的大插图

  • 主要操作按钮

  • 次要操作按钮

  • 其他操作按钮

  • 关闭按钮

  • 复选框和/或单选按钮

提示的箭头(指向锚点的三角形插入符号)可以位于提示任何边缘的中间或角落,并且可以锚定到其锚元素上的相同位置。箭头位置和锚点位置都支持所有 8 个基点和序数方向。箭头也可以完全隐藏。还有一个可选效果可以突出显示提示锚定的按钮。此突出显示仅在锚元素是按钮时才有效。

示例

Feature Callout

测试功能提示

通过开发者工具:

  1. 转到 about:config,将首选项 browser.newtabpage.activity-stream.asrouter.devtoolsEnabled 设置为 true

  2. 打开一个新标签页,并在 urlbar 中转到 about:asrouter

  3. 在开发者工具的消息部分,使用查找栏搜索 feature_callout

  4. 您应该会看到一个名为 TEST_FEATURE_TOUR 的示例 JSON 消息。点击它旁边的 Show 应该会显示提示

  5. 您可以使用更改直接修改文本区域中的消息,或通过粘贴自定义消息 JSON 来修改。点击 Modify 显示更新的消息。确保它是有效的 JSON,并注意不要在数组中的最后一个成员或对象中的最后一个属性后添加不必要的逗号,因为它们会使消息无效。

  6. 出于这些测试目的,目标和触发器将被忽略,因为消息将通过按下“修改”按钮触发。因此,您将无法通过此方法测试触发器和目标。

  7. 确保根据以下架构涵盖所有必需的属性

  8. 点击 Share 将链接复制到剪贴板,该链接可以粘贴到 urlbar 中以预览消息,并且可以共享以获取团队的反馈

  • 注意:一次只能显示一个功能提示。您必须先关闭现有的提示,然后才能显示新的提示。

通过本地提供程序:

您还可以通过将功能提示添加到 本地提供程序 来测试它们。虽然比使用开发者工具慢,但这在您想要测试触发器或目标时很有用,或者当您的提示的锚点是在 about:asrouter 上不可见的元素(例如 urlbar 按钮)时很有用。

通过实验:

您可以通过在树中创建实验或登陆消息来测试功能提示。 消息传递之旅 捕获了通过 Nimbus 创建和测试实验的过程。这是最耗时的方法,但如果您的提示将作为实验启动,那么它也提供了最准确的预览。

架构

interface FeatureCallout {
  // Unique id for the message. Used to store impressions, recorded in telemetry
  id: string;
  template: "feature_callout";
  // Targeting expression string. JEXL is used for evaluation. See the Targeting
  // section below for details.
  targeting: string;
  // Trigger object. See the Triggers section below for details.
  trigger: {
    // The trigger's unique identifier, e.g. "nthTabClosed".
    id: string;
    // A set of parameters for the triggers. Usage depends on the trigger id.
    params?: any;
    // A set of URL match patterns (like globs) used by some triggers.
    // See https://mdn.org.cn/en-US/docs/Mozilla/Add-ons/WebExtensions/Match_patterns
    patterns?: string[];
  };
  // An optional object specifying frequency caps for the message.
  frequency?: {
    // A basic limit on the number of times the message can be shown to a user
    // across the entire lifetime of the user profile.
    lifetime?: number;
    // An array specifying any number of limits on the number of times the
    // message can be shown to a user within a specific time period. This can be
    // specified in addition to or instead of a lifetime limit.
    custom?: Array<{
      // The number of times the message can be shown within the period.
      cap: number;
      // The period of time in milliseconds. For example, 24 hours is 86400000.
      period: number;
    }>;
  };
  // An array of message groups, which are used for frequency capping. Typically
  // this should be ["cfr"], unless you have a specific reason to do otherwise.
  groups?: string[];
  // Messages can optionally have a priority or weight, influencing the order in
  // which they're shown. Higher priority messages are shown first. Messages can
  // also be selected randomly based on their weight. However, weight is rarely
  // used. We recommend using neither weight nor priority, unless you are adding
  // multiple messages with the same trigger and similar targeting.
  weight?: number; // e.g. weight: 200 is more likely than weight: 100
  priority?: number; // e.g. priority: 2 beats priority: 1
  // Whether the message should be skipped in automated tests. If omitted, the
  // message can be shown in tests. A truthy value will skip the message in
  // tests. The value should be a string explaining why the message needs to be
  // skipped. You can still test messages with this property in automation by
  // stubbing `ASRouter.messagesEnabledInAutomation` (adding the message id to
  // the array). This way, you can avoid showing the message in all tests except
  // the specific test where you want to test it. This has no effect for
  // messages in Nimbus experiments. It's for local messages only.
  skip_in_tests?: string;
  content: {
    // The same as the id above
    id: string;
    template: "multistage";
    backdrop: "transparent";
    transitions: false;
    disableHistoryUpdates: true;
    // The name of a preference that will be used to store screen progress. Only
    // relevant if your callout has multiple screens and serves as a tour. This
    // allows tour progress to persist across sessions and even devices, if the
    // pref is synced via FxA. In most cases, this will not be needed.
    tour_pref_name?: string;
    // A default value for the pref. Can be used if the pref is not set in
    // Firefox's default prefs. This is the default value that will be used
    // until the pref is set by the user interacting with the callout. It will
    // be used to determine the starting screen. Values are JSON objects like
    // this: { "screen": "SCREEN_1", "complete": false }
    // The "screen" property is the id of the screen to start on, and the
    // "complete" property is a boolean indicating whether the tour has been
    // completed (it should always be false here). As with tour_pref_name, this
    // should usually be omitted.
    tour_pref_default_value?: string;
    // Set to "block" to block all telemetry. Recommended to omit this.
    metrics?: string;
    // An array of screens that should be shown in sequence. The first screen
    // will be shown immediately. If the screen includes actions (such as
    // `primary_button.action`) with `navigate: true`, the user can advance to
    // the next screen, causing the first screen to fade out and the next screen
    // to fade in.
    screens: [
      {
        id: string;
        // Feature callouts with multiple screens show a series of dots at the
        // bottom, indicating which screen the user is on. This property allows
        // you to hide those dots. The steps indicator is already hidden if
        // there's only one screen, since it's unnecessary. Defaults to false.
        force_hide_steps_indicator?: boolean;
        // An array of anchor objects. Each anchor object represents a single
        // element on the page that the callout should be anchored to. The
        // callout will be anchored to the first visible element in the array.
        anchors: [
          {
            // A CSS selector for the element to anchor to. The callout will be
            // anchored to the first visible element that matches this selector.
            // This supports a special token %triggerTab% that functions as a
            // selector for the tab that triggered the callout, usually (but not
            // always) the selected tab. It can be placed at any position in the
            // selector, like other tokens. For example:
            // "#tabbrowser-tabs %triggerTab%[visuallyselected] .tab-icon-image"
            selector: string;
            // An object representing how the callout should be positioned
            // relative to the anchor element.
            panel_position: {
              // The point on the anchor that the callout should be tied to. See
              // PopupAttachmentPoint below for the possible values. These are
              // the same values used by XULPopupElements.
              anchor_attachment: PopupAttachmentPoint;
              // The point on the callout that should be tied to the anchor.
              callout_attachment: PopupAttachmentPoint;
              // Offsets in pixels to apply to the callout position in the
              // horizontal and vertical directions. Generally not needed.
              offset_x?: number;
              offset_y?: number;
            };
            // Hide the arrow that points from the callout to the anchor?
            hide_arrow?: boolean;
            // Whether to set the [open] style on the anchor element while the
            // callout is showing. False to set it, true to not set it. Not all
            // elements have an [open] style. Buttons do, for example. It's
            // usually similar to :active.
            no_open_on_anchor?: boolean;
            // The desired width of the arrow in a number of pixels. 33.94113 by
            // default (this corresponds to a triangle with 24px edges). This
            // also affects the height of the arrow.
            arrow_width?: number;
          }
        ];
        content: {
          position: "callout";
          // By default, callouts don't hide if the user clicks outside of them.
          // Set this to true to make the callout hide on outside clicks.
          autohide?: boolean;
          // Callout card width as a CSS value, e.g. "400px" or "min-content".
          // Defaults to "400px".
          width?: string;
          // Callout card padding in pixels. Defaults to 24.
          padding?: number;
          // Callouts normally have a vertical layout, with rows of content. If
          // you want a single row with a more inline layout, you can use this
          // property, which works well in tandem with title_logo.
          layout?: "inline";
          // An optional object representing a large illustration to show above
          // other content. See Logo below for the possible properties.
          logo?: Logo;
          // The callout's headline. This is optional but commonly used. Can be
          // a raw string or a LocalizableThing (see interface below).
          title?: Label;
          // An optional object representing an icon to show next to the title.
          // See TitleLogo below for the possible properties.
          title_logo?: TitleLogo;
          // A subtitle to show below the title. Typically a longer paragraph.
          subtitle?: Label;
          primary_button?: {
            // Text to show inside the button.
            label: Label;
            // Buttons can optionally show an arrow icon, indicating that
            // clicking the button will advance to the next screen.
            has_arrow_icon?: boolean;
            // Buttons can be disabled. The boolean option isn't really useful,
            // since there's no logic to enable the button. However, if your
            // screen uses the "multiselect" tile (see tiles), you can use
            // "hasActiveMultiSelect" to disable the button until the user
            // selects something.
            disabled?: boolean | "hasActiveMultiSelect";
            // Primary buttons can have a "primary" or "secondary" style. This
            // is useful because you can't change the order of the buttons, but
            // you can swap the primary and secondary buttons' styles.
            style?: "primary" | "secondary";
            // The action to take when the button is clicked. See Action below.
            action: Action;
          };
          secondary_button?: {
            label: Label;
            // Extra text to show before the button.
            text: Label;
            has_arrow_icon?: boolean;
            disabled?: boolean | "hasActiveMultiSelect";
            style?: "primary" | "secondary";
            action: Action;
          };
          additional_button?: {
            label: Label;
            // If you have several buttons, you can use this property to control
            // the orientation of the buttons. By default, buttons are laid out
            // in a complex way. Use row or column to override this.
            flow?: "row" | "column";
            disabled?: boolean;
            // The additional button can also be styled as a link.
            style?: "primary" | "secondary" | "link";
            action: Action;
            // Justification/alignment of the buttons row/column. Defaults to
            // "end" (right-justified buttons). You can use space-between if,
            // for example, you have 2 buttons and you want one on the left and
            // one on the right.
            alignment?: "start" | "end" | "space-between";
          };
          dismiss_button?: {
            // This can be used to control the ARIA attributes and tooltip.
            // Usually it's omitted, since it has a correct default value.
            label?: Label;
            // The button can be 32px or 24px. Defaults to 32px.
            size?: "small" | "large";
            action: Action;
            // CSS overrides.
            marginBlock?: string;
            marginInline?: string;
          };
          // A split button is an additional_button or secondary_button split
          // into 2 buttons: one that performs the main action, and one with an
          // arrow that opens a dropdown submenu (which this property controls).
          submenu_button?: {
            // This defines the dropdown menu that appears when the user clicks
            // the split button.
            submenu: SubmenuItem[];
            // The submenu button can only be a split button, so a secondary or
            // additional button needs to exist for it to attach to.
            attached_to: "secondary_button" | "additional_button";
            // Used mainly to control the ARIA label and tooltip (tooltips are
            // currently broken), but can also be used to override CSS styles.
            label?: Label;
            // Whether the split button should follow the primary or secondary
            // button style. Set this to the same style you specified for the
            // button it's attached to. Defaults to "secondary".
            style?: "primary" | "secondary";
          };
          // Predefined content modules. The only one currently supported in
          // feature callout is "multiselect", which allows you to show a series
          // of checkboxes and/or radio buttons.
          tiles?: {
            type: "multiselect";
            // Depends on the type, but we only support "multiselect" currently.
            data: MultiSelectItem[];
            // Allows CSS overrides of the multiselect container.
            style?: {
              color?: string;
              fontSize?: string;
              fontWeight?: string;
              letterSpacing?: string;
              lineHeight?: string;
              marginBlock?: string;
              marginInline?: string;
              paddingBlock?: string;
              paddingInline?: string;
              whiteSpace?: string;
              flexDirection?: string;
              flexWrap?: string;
              flexFlow?: string;
              flexGrow?: string;
              flexShrink?: string;
              justifyContent?: string;
              alignItems?: string;
              gap?: string;
              // Any CSS properties starting with "--" are also allowed, to
              // override CSS variables used in _feature-callout.scss.
              "--some-variable"?: string;
            };
          };
          // The dots in the corner that show what screen you're on and how many
          // screens there are in total. This property is only used to override
          // the ARIA attributes or tooltip. Not recommended.
          steps_indicator?: {
            string_id: string;
          };
          // An extra block of configurable content below the title/subtitle but
          // above the optional `tiles` section and the main buttons. Styles not
          // yet implemented; not recommended.
          above_button_content?: LinkParagraphOrImage[];
          // An optional array of event listeners to add to the page where the
          // feature callout is shown. This can be used to perform actions in
          // response to interactions and other events outside of the feature
          // callout itself. The prototypical use case is dismissing the feature
          // callout when the user clicks the button the callout is anchored to.
          // It also supports performing actions on a timeout/interval.
          page_event_listeners?: Array<{
            params: {
              // Event type string, e.g. "click". This supports:
              // 1. Any DOM event type
              // 2. "timeout" and "interval" for timers
              // 3. Internal feature callout events: "touradvance" and
              //    "tourend". This can be used to perform actions when the user
              //    advances to the next screen or finishes the callout tour.
              type: string;
              // Target selector, e.g. `tag.class, #id[attr]` - Not needed for
              // all types.
              selectors?: string;
              // addEventListener options
              options: {
                // Handle events in capturing phase?
                capture?: boolean;
                // Remove listener after first event?
                once?: boolean;
                // Prevent default action in event handler?
                preventDefault?: boolean;
                // Used only for `timeout` and `interval` event types. These
                // don't set up real event listeners, but instead invoke the
                // action on a timer.
                interval?: number;
                // Extend addEventListener to all windows? Not compatible with
                // `interval`.
                every_window: boolean;
              };
            };
            action: {
              // One of the special message action ids.
              type?: "string";
              // Data to pass to the action. Depends on the action.
              data?: any;
              // Dismiss screen after performing action? If there's no type, the
              // action will *only_ dismiss the callout.
              dismiss?: boolean;
            };
          }>;
        };
      }
    ];
    // Specify the index of the screen to start on. Generally unused.
    startScreen?: number;
  };
}

type PopupAttachmentPoint =
  | "topleft"
  | "topright"
  | "bottomleft"
  | "bottomright"
  | "leftcenter"
  | "rightcenter"
  | "topcenter"
  | "bottomcenter";

type Label = string | LocalizableThing;

interface LocalizableThing {
  // A raw, untranslated string, typically used for EN-only experiments.
  raw?: string;
  // A Fluent string id from a .ftl file.
  string_id?: string;
  // Arguments to pass to Fluent. Used for Fluent strings that have variables.
  args?: {
    [key: string]: string;
  };
  // A string to use as the element's aria-label attribute value.
  aria_label?: string;
  // CSS overrides.
  color?: string;
  fontSize?: string;
  fontWeight?: string;
  letterSpacing?: string;
  lineHeight?: string;
  marginBlock?: string;
  marginInline?: string;
  paddingBlock?: string;
  paddingInline?: string;
  whiteSpace?: string;
}

interface Logo {
  imageURL: "chrome://branding/content/about-logo.svg";
  darkModeImageURL: string;
  reducedMotionImageURL: string;
  darkModeReducedMotionImageURL: string;
  // <img> alt text. Defaults to ""
  alt: string;
  // CSS style overrides for the icon.
  width: string;
  height: string;
  marginBlock: string;
  marginInline: string;
}

interface TitleLogo extends Logo {
  // Logo alignment relative to the title. Use "top" if you have multiple rows
  // of text and you want the logo aligned to the top row. Defaults to "center".
  alignment: "top" | "bottom" | "center";
}

// Click the Special Message Actions link at the bottom of the page.
interface Action {
  // One of the special message action ids.
  type?: "string";
  // Data to pass to the action. Depends on the action.
  data?: any;
  // Set to true if you want the action to advance to the next screen or hide
  // the callout if it's the last screen. Can be used in lieu of "type" and
  // "data" to create a button that just advances the screen.
  navigate?: boolean;
  // Same as "navigate" but dismisses the callout instead of advancing to the
  // next screen.
  dismiss?: boolean;
  // Set to true if this action is for the primary button and you're using the
  // "multiselect" tile. This is what allows the primary button to perform the
  // actions specified by the user's checkbox/radio selections. It will combine
  // all the actions for all the selected checkboxes/radios into this action's
  // data.actions array, and perform them in series.
  collectSelect?: boolean;
}

// Either an image or a paragraph that supports inline links. Currently requires
// Fluent strings. Raw strings are not supported.
interface LinkParagraphOrImage extends Logo {
  // Which type of content this is.
  type: "image" | "text";

  // Each of these is only used if `type` is "text".
  // The `text` object contains the Fluent string id. Doesn't support raw text.
  text: LocalizableThing;
  // An array of key names. Each link key must exist in screen.content. For
  // example, if link_keys is ["learn_more"], then there must be a key named
  // "learn_more" in screen.content. The value of that key must be an object
  // with an `action` property (which is an Action). Moreover, the string_id in
  // the `text` object (see the property above) must refer to a Fluent string
  // that contains an anchor element with `data-l10n-name="learn_more"`, e.g.:
  //   my-string = Do the thing! <a data-l10n-name="learn_more">Learn more</a>
  link_keys: string[];
}

interface MultiSelectItem {
  // A unique id for this item, distinguishing it from other items.
  id: string;
  type: "checkbox" | "radio";
  // Radios need to be members of radio groups to work properly. Set the same
  // group for your radios to make sure only one can be selected at a time.
  group?: string;
  // Set to true to make it selected/checked by default.
  defaultValue: false;
  label?: Label;
  // By default, multiselect items appear in the order they're listed
  // in the data array. Set this to true to randomize the order. This
  // is most commonly used to randomize the order of answer choices
  // for a survey question, to avoid the first-choice bias.
  // Instead of randomizing the entire set, we randomize specific
  // items. Any adjacent items with randomize will be randomized in-place. So
  // if there are 4 items with randomize, followed by 1 nonrandom item, the 4
  // will be randomized but the 5th will stay at the bottom.
  randomize?: boolean;
  // CSS overrides for the div box containing the item and its optional label.
  style?: {
    color?: string;
    fontSize?: string;
    fontWeight?: string;
    letterSpacing?: string;
    lineHeight?: string;
    marginBlock?: string;
    marginInline?: string;
    paddingBlock?: string;
    paddingInline?: string;
    whiteSpace?: string;
    flexDirection?: string;
    flexWrap?: string;
    flexFlow?: string;
    flexGrow?: string;
    flexShrink?: string;
    justifyContent?: string;
    alignItems?: string;
    gap?: string;
  };
  // You can replace the checkbox check/radio circle with an icon by using a
  // bunch of CSS overrides.
  icon?: {
    style: {
      color?: string;
      fontSize?: string;
      fontWeight?: string;
      letterSpacing?: string;
      lineHeight?: string;
      marginBlock?: string;
      marginInline?: string;
      paddingBlock?: string;
      paddingInline?: string;
      whiteSpace?: string;
      width?: string;
      height?: string;
      background?: string;
      backgroundColor?: string;
      backgroundImage?: string;
      backgroundSize?: string;
      backgroundPosition?: string;
      backgroundRepeat?: string;
      backgroundOrigin?: string;
      backgroundClip?: string;
      border?: string;
      borderRadius?: string;
      appearance?: string;
      fill?: string;
      stroke?: string;
      outline?: string;
      outlineOffset?: string;
      boxShadow?: string;
    };
  };
  // The action is not performed until the user clicks the primary button.
  action: Action;
}

interface SubmenuItem {
  // Submenus can have 3 types of items, just like normal menupopups
  // in Firefox.
  type: "action" | "menu" | "separator";
  // The id is used to identify the submenu item in telemetry.
  id?: string;
  label: Label;
  // Used only for type "action". The action to perform when the
  // submenu item is clicked.
  action?: Action;
  // Used only for type "menu". The submenu items to show when the
  // user hovers over this item. This is a recursive structure.
  submenu: SubmenuItem[];
  // An optional URL specifying an icon to show next to the label.
  icon?: string;
}

示例 JSON

{
  "id": "TEST_FEATURE_TOUR",
  "template": "feature_callout",
  "groups": [],
  "targeting": "true",
  "content": {
    "id": "TEST_FEATURE_TOUR",
    "template": "multistage",
    "backdrop": "transparent",
    "transitions": false,
    "disableHistoryUpdates": true,
    "screens": [
      {
        "id": "FEATURE_CALLOUT_1",
        "anchors": [
          {
            "selector": "#PanelUI-menu-button",
            "panel_position": {
              "anchor_attachment": "bottomcenter",
              "callout_attachment": "topright"
            }
          }
        ],
        "content": {
          "position": "callout",
          "title": {
            "raw": "Panel Feature Callout"
          },
          "subtitle": {
            "raw": "Hello!"
          },
          "primary_button": {
            "label": {
              "raw": "Advance"
            },
            "action": {
              "navigate": true
            }
          },
          "dismiss_button": {
            "action": {
              "dismiss": true
            }
          }
        }
      }
    ]
  },
}

目标

消息使用 JEXL 目标表达式来确定用户是否有资格查看消息。有关详细信息,请参阅 使用 JEXL 进行目标定位指南目标属性

触发器

触发器用于确定何时应显示消息。有关详细信息,请参阅 触发器监听器

特殊消息操作

按钮、链接和其他操作调用可以使用一组预定义操作中的一个或多个。 点击此处 以获取有效操作的完整列表。