summaryrefslogtreecommitdiffstats
path: root/browser/components/touchbar
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/touchbar')
-rw-r--r--browser/components/touchbar/MacTouchBar.sys.mjs674
-rw-r--r--browser/components/touchbar/components.conf20
-rw-r--r--browser/components/touchbar/docs/index.rst364
-rw-r--r--browser/components/touchbar/moz.build18
-rw-r--r--browser/components/touchbar/tests/browser/browser.ini11
-rw-r--r--browser/components/touchbar/tests/browser/browser_touchbar_searchrestrictions.js154
-rw-r--r--browser/components/touchbar/tests/browser/browser_touchbar_tests.js159
-rw-r--r--browser/components/touchbar/tests/browser/readerModeArticle.html29
-rw-r--r--browser/components/touchbar/tests/browser/test-video.mp4bin0 -> 36502 bytes
-rw-r--r--browser/components/touchbar/tests/browser/video_test.html32
10 files changed, 1461 insertions, 0 deletions
diff --git a/browser/components/touchbar/MacTouchBar.sys.mjs b/browser/components/touchbar/MacTouchBar.sys.mjs
new file mode 100644
index 0000000000..315de69b8c
--- /dev/null
+++ b/browser/components/touchbar/MacTouchBar.sys.mjs
@@ -0,0 +1,674 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ UrlbarTokenizer: "resource:///modules/UrlbarTokenizer.sys.mjs",
+});
+
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.jsm",
+});
+
+XPCOMUtils.defineLazyServiceGetter(
+ lazy,
+ "touchBarUpdater",
+ "@mozilla.org/widget/touchbarupdater;1",
+ "nsITouchBarUpdater"
+);
+
+// For accessing TouchBarHelper methods from static contexts in this file.
+XPCOMUtils.defineLazyServiceGetter(
+ lazy,
+ "touchBarHelper",
+ "@mozilla.org/widget/touchbarhelper;1",
+ "nsITouchBarHelper"
+);
+
+/**
+ * Executes a XUL command on the top window. Called by the callbacks in each
+ * TouchBarInput.
+ *
+ * @param {string} commandName
+ * A XUL command.
+ */
+function execCommand(commandName) {
+ if (!TouchBarHelper.window) {
+ return;
+ }
+ let command = TouchBarHelper.window.document.getElementById(commandName);
+ if (command) {
+ command.doCommand();
+ }
+}
+
+/**
+ * Static helper function to convert a hexadecimal string to its integer
+ * value. Used to convert colours to a format accepted by Apple's NSColor code.
+ *
+ * @param {string} hexString
+ * A hexadecimal string, optionally beginning with '#'.
+ */
+function hexToInt(hexString) {
+ if (!hexString) {
+ return null;
+ }
+ if (hexString.charAt(0) == "#") {
+ hexString = hexString.slice(1);
+ }
+ let val = parseInt(hexString, 16);
+ return isNaN(val) ? null : val;
+}
+
+const kInputTypes = {
+ BUTTON: "button",
+ LABEL: "label",
+ MAIN_BUTTON: "mainButton",
+ POPOVER: "popover",
+ SCROLLVIEW: "scrollView",
+ SCRUBBER: "scrubber",
+};
+
+/**
+ * An object containing all implemented TouchBarInput objects.
+ */
+var gBuiltInInputs = {
+ Back: {
+ title: "back",
+ image: "chrome://browser/skin/back.svg",
+ type: kInputTypes.BUTTON,
+ callback: () => {
+ lazy.touchBarHelper.unfocusUrlbar();
+ execCommand("Browser:Back");
+ },
+ },
+ Forward: {
+ title: "forward",
+ image: "chrome://browser/skin/forward.svg",
+ type: kInputTypes.BUTTON,
+ callback: () => {
+ lazy.touchBarHelper.unfocusUrlbar();
+ execCommand("Browser:Forward");
+ },
+ },
+ Reload: {
+ title: "reload",
+ image: "chrome://global/skin/icons/reload.svg",
+ type: kInputTypes.BUTTON,
+ callback: () => {
+ lazy.touchBarHelper.unfocusUrlbar();
+ execCommand("Browser:Reload");
+ },
+ },
+ Home: {
+ title: "home",
+ image: "chrome://browser/skin/home.svg",
+ type: kInputTypes.BUTTON,
+ callback: () => {
+ let win = lazy.BrowserWindowTracker.getTopWindow();
+ win.BrowserHome();
+ },
+ },
+ Fullscreen: {
+ title: "fullscreen",
+ image: "chrome://browser/skin/fullscreen.svg",
+ type: kInputTypes.BUTTON,
+ callback: () => execCommand("View:FullScreen"),
+ },
+ Find: {
+ title: "find",
+ image: "chrome://global/skin/icons/search-glass.svg",
+ type: kInputTypes.BUTTON,
+ callback: () => execCommand("cmd_find"),
+ },
+ NewTab: {
+ title: "new-tab",
+ image: "chrome://global/skin/icons/plus.svg",
+ type: kInputTypes.BUTTON,
+ callback: () => execCommand("cmd_newNavigatorTabNoEvent"),
+ },
+ Sidebar: {
+ title: "open-sidebar",
+ image: "chrome://browser/skin/sidebars.svg",
+ type: kInputTypes.BUTTON,
+ callback: () => {
+ let win = lazy.BrowserWindowTracker.getTopWindow();
+ win.SidebarUI.toggle();
+ },
+ },
+ AddBookmark: {
+ title: "add-bookmark",
+ image: "chrome://browser/skin/bookmark-hollow.svg",
+ type: kInputTypes.BUTTON,
+ callback: () => execCommand("Browser:AddBookmarkAs"),
+ },
+ ReaderView: {
+ title: "reader-view",
+ image: "chrome://browser/skin/reader-mode.svg",
+ type: kInputTypes.BUTTON,
+ callback: () => execCommand("View:ReaderView"),
+ disabled: true, // Updated when the page is found to be Reader View-able.
+ },
+ OpenLocation: {
+ key: "open-location",
+ title: "open-location",
+ image: "chrome://global/skin/icons/search-glass.svg",
+ type: kInputTypes.MAIN_BUTTON,
+ callback: () => lazy.touchBarHelper.toggleFocusUrlbar(),
+ },
+ // This is a special-case `type: kInputTypes.SCRUBBER` element.
+ // Scrubbers are not yet generally implemented.
+ // See follow-up bug 1502539.
+ Share: {
+ title: "share",
+ image: "chrome://browser/skin/share.svg",
+ type: kInputTypes.SCRUBBER,
+ callback: () => execCommand("cmd_share"),
+ },
+ SearchPopover: {
+ title: "search-popover",
+ image: "chrome://global/skin/icons/search-glass.svg",
+ type: kInputTypes.POPOVER,
+ children: {
+ SearchScrollViewLabel: {
+ title: "search-search-in",
+ type: kInputTypes.LABEL,
+ },
+ SearchScrollView: {
+ key: "search-scrollview",
+ type: kInputTypes.SCROLLVIEW,
+ children: {
+ Bookmarks: {
+ title: "search-bookmarks",
+ type: kInputTypes.BUTTON,
+ callback: () =>
+ lazy.touchBarHelper.insertRestrictionInUrlbar(
+ lazy.UrlbarTokenizer.RESTRICT.BOOKMARK
+ ),
+ },
+ OpenTabs: {
+ title: "search-opentabs",
+ type: kInputTypes.BUTTON,
+ callback: () =>
+ lazy.touchBarHelper.insertRestrictionInUrlbar(
+ lazy.UrlbarTokenizer.RESTRICT.OPENPAGE
+ ),
+ },
+ History: {
+ title: "search-history",
+ type: kInputTypes.BUTTON,
+ callback: () =>
+ lazy.touchBarHelper.insertRestrictionInUrlbar(
+ lazy.UrlbarTokenizer.RESTRICT.HISTORY
+ ),
+ },
+ Tags: {
+ title: "search-tags",
+ type: kInputTypes.BUTTON,
+ callback: () =>
+ lazy.touchBarHelper.insertRestrictionInUrlbar(
+ lazy.UrlbarTokenizer.RESTRICT.TAG
+ ),
+ },
+ },
+ },
+ },
+ },
+};
+
+// We create a new flat object to cache strings. Since gBuiltInInputs is a
+// tree, caching/retrieval of localized strings would otherwise require tree
+// traversal.
+var localizedStrings = {};
+
+const kHelperObservers = new Set([
+ "bookmark-icon-updated",
+ "fullscreen-painted",
+ "reader-mode-available",
+ "touchbar-location-change",
+ "quit-application",
+ "intl:app-locales-changed",
+ "urlbar-focus",
+ "urlbar-blur",
+]);
+
+/**
+ * JS-implemented TouchBarHelper class.
+ * Provides services to the Mac Touch Bar.
+ */
+export class TouchBarHelper {
+ constructor() {
+ for (let topic of kHelperObservers) {
+ Services.obs.addObserver(this, topic);
+ }
+ // We cache our search popover since otherwise it is frequently
+ // created/destroyed for the urlbar-focus/blur events.
+ this._searchPopover = this.getTouchBarInput("SearchPopover");
+
+ this._inputsNotUpdated = new Set();
+ }
+
+ destructor() {
+ this._searchPopover = null;
+ for (let topic of kHelperObservers) {
+ Services.obs.removeObserver(this, topic);
+ }
+ }
+
+ get activeTitle() {
+ if (!TouchBarHelper.window) {
+ return "";
+ }
+ let tabbrowser = TouchBarHelper.window.ownerGlobal.gBrowser;
+ let activeTitle;
+ if (tabbrowser) {
+ activeTitle = tabbrowser.selectedBrowser.contentTitle;
+ }
+ return activeTitle;
+ }
+
+ get allItems() {
+ let layoutItems = Cc["@mozilla.org/array;1"].createInstance(
+ Ci.nsIMutableArray
+ );
+
+ let window = TouchBarHelper.window;
+ if (
+ !window ||
+ !window.isChromeWindow ||
+ window.document.documentElement.getAttribute("windowtype") !=
+ "navigator:browser"
+ ) {
+ return layoutItems;
+ }
+
+ // Every input must be updated at least once so that all assets (titles,
+ // icons) are loaded. We keep track of which inputs haven't updated and
+ // run an update on them ASAP.
+ this._inputsNotUpdated.clear();
+
+ for (let inputName of Object.keys(gBuiltInInputs)) {
+ let input = this.getTouchBarInput(inputName);
+ if (!input) {
+ continue;
+ }
+ this._inputsNotUpdated.add(inputName);
+ layoutItems.appendElement(input);
+ }
+
+ return layoutItems;
+ }
+
+ static get window() {
+ return lazy.BrowserWindowTracker.getTopWindow();
+ }
+
+ get document() {
+ if (!TouchBarHelper.window) {
+ return null;
+ }
+ return TouchBarHelper.window.document;
+ }
+
+ get isUrlbarFocused() {
+ if (!TouchBarHelper.window || !TouchBarHelper.window.gURLBar) {
+ return false;
+ }
+ return TouchBarHelper.window.gURLBar.focused;
+ }
+
+ toggleFocusUrlbar() {
+ if (this.isUrlbarFocused) {
+ this.unfocusUrlbar();
+ } else {
+ execCommand("Browser:OpenLocation");
+ }
+ }
+
+ unfocusUrlbar() {
+ if (!this.isUrlbarFocused) {
+ return;
+ }
+ TouchBarHelper.window.gURLBar.blur();
+ }
+
+ static get baseWindow() {
+ return TouchBarHelper.window
+ ? TouchBarHelper.window.docShell.treeOwner.QueryInterface(
+ Ci.nsIBaseWindow
+ )
+ : null;
+ }
+
+ getTouchBarInput(inputName) {
+ if (inputName == "SearchPopover" && this._searchPopover) {
+ return this._searchPopover;
+ }
+
+ if (!inputName || !gBuiltInInputs.hasOwnProperty(inputName)) {
+ return null;
+ }
+
+ let inputData = gBuiltInInputs[inputName];
+
+ let item = new TouchBarInput(inputData);
+
+ // Skip localization if there is already a cached localized title or if
+ // no title is needed.
+ if (
+ !inputData.hasOwnProperty("title") ||
+ localizedStrings[inputData.title]
+ ) {
+ return item;
+ }
+
+ // Async l10n fills in the localized input labels after the initial load.
+ this._l10n.formatValue(inputData.title).then(result => {
+ item.title = result;
+ localizedStrings[inputData.title] = result; // Cache result.
+ // Checking TouchBarHelper.window since this callback can fire after all windows are closed.
+ if (TouchBarHelper.window) {
+ if (this._inputsNotUpdated) {
+ this._inputsNotUpdated.delete(inputName);
+ }
+ lazy.touchBarUpdater.updateTouchBarInputs(TouchBarHelper.baseWindow, [
+ item,
+ ]);
+ }
+ });
+
+ return item;
+ }
+
+ /**
+ * Fetches a specific Touch Bar Input by name and updates it on the Touch Bar.
+ *
+ * @param {...*} inputNames
+ * A key/keys to a value/values in the gBuiltInInputs object in this file.
+ */
+ _updateTouchBarInputs(...inputNames) {
+ if (!TouchBarHelper.window || !inputNames.length) {
+ return;
+ }
+
+ let inputs = [];
+ for (let inputName of new Set([...inputNames, ...this._inputsNotUpdated])) {
+ let input = this.getTouchBarInput(inputName);
+ if (!input) {
+ continue;
+ }
+
+ this._inputsNotUpdated.delete(inputName);
+ inputs.push(input);
+ }
+
+ lazy.touchBarUpdater.updateTouchBarInputs(
+ TouchBarHelper.baseWindow,
+ inputs
+ );
+ }
+
+ /**
+ * Inserts a restriction token into the Urlbar ahead of the current typed
+ * search term.
+ *
+ * @param {string} restrictionToken
+ * The restriction token to be inserted into the Urlbar. Preferably
+ * sourced from UrlbarTokenizer.RESTRICT.
+ */
+ insertRestrictionInUrlbar(restrictionToken) {
+ if (!TouchBarHelper.window) {
+ return;
+ }
+ let searchString = "";
+ if (
+ TouchBarHelper.window.gURLBar.getAttribute("pageproxystate") != "valid"
+ ) {
+ searchString = TouchBarHelper.window.gURLBar.lastSearchString.trimStart();
+ if (
+ Object.values(lazy.UrlbarTokenizer.RESTRICT).includes(searchString[0])
+ ) {
+ searchString = searchString.substring(1).trimStart();
+ }
+ }
+
+ TouchBarHelper.window.gURLBar.search(
+ `${restrictionToken} ${searchString}`,
+ { searchModeEntry: "touchbar" }
+ );
+ }
+
+ observe(subject, topic, data) {
+ switch (topic) {
+ case "touchbar-location-change":
+ let updatedInputs = ["Back", "Forward"];
+ gBuiltInInputs.Back.disabled =
+ !TouchBarHelper.window.gBrowser.canGoBack;
+ gBuiltInInputs.Forward.disabled =
+ !TouchBarHelper.window.gBrowser.canGoForward;
+ if (subject.QueryInterface(Ci.nsIWebProgress)?.isTopLevel) {
+ this.activeUrl = data;
+ // ReaderView button is disabled on every toplevel location change
+ // since Reader View must determine if the new page can be Reader
+ // Viewed.
+ updatedInputs.push("ReaderView");
+ gBuiltInInputs.ReaderView.disabled = !data.startsWith("about:reader");
+ }
+ this._updateTouchBarInputs(...updatedInputs);
+ break;
+ case "fullscreen-painted":
+ if (TouchBarHelper.window.document.fullscreenElement) {
+ gBuiltInInputs.OpenLocation.title = "touchbar-fullscreen-exit";
+ gBuiltInInputs.OpenLocation.image =
+ "chrome://browser/skin/fullscreen-exit.svg";
+ gBuiltInInputs.OpenLocation.callback = () => {
+ TouchBarHelper.window.windowUtils.exitFullscreen();
+ };
+ } else {
+ gBuiltInInputs.OpenLocation.title = "open-location";
+ gBuiltInInputs.OpenLocation.image =
+ "chrome://global/skin/icons/search-glass.svg";
+ gBuiltInInputs.OpenLocation.callback = () =>
+ execCommand("Browser:OpenLocation", "OpenLocation");
+ }
+ this._updateTouchBarInputs("OpenLocation");
+ break;
+ case "bookmark-icon-updated":
+ gBuiltInInputs.AddBookmark.image =
+ data == "starred"
+ ? "chrome://browser/skin/bookmark.svg"
+ : "chrome://browser/skin/bookmark-hollow.svg";
+ this._updateTouchBarInputs("AddBookmark");
+ break;
+ case "reader-mode-available":
+ gBuiltInInputs.ReaderView.disabled = false;
+ this._updateTouchBarInputs("ReaderView");
+ break;
+ case "urlbar-focus":
+ if (!this._searchPopover) {
+ this._searchPopover = this.getTouchBarInput("SearchPopover");
+ }
+ lazy.touchBarUpdater.showPopover(
+ TouchBarHelper.baseWindow,
+ this._searchPopover,
+ true
+ );
+ break;
+ case "urlbar-blur":
+ if (!this._searchPopover) {
+ this._searchPopover = this.getTouchBarInput("SearchPopover");
+ }
+ lazy.touchBarUpdater.showPopover(
+ TouchBarHelper.baseWindow,
+ this._searchPopover,
+ false
+ );
+ break;
+ case "intl:app-locales-changed":
+ this._searchPopover = null;
+ localizedStrings = {};
+
+ // This event can fire before this._l10n updates to switch languages,
+ // so all the new translations are in the old language. To avoid this,
+ // we need to reinitialize this._l10n.
+ this._l10n = new Localization(["browser/touchbar/touchbar.ftl"]);
+ helperProto._l10n = this._l10n;
+
+ this._updateTouchBarInputs(...Object.keys(gBuiltInInputs));
+ break;
+ case "quit-application":
+ this.destructor();
+ break;
+ }
+ }
+}
+
+const helperProto = TouchBarHelper.prototype;
+helperProto.QueryInterface = ChromeUtils.generateQI(["nsITouchBarHelper"]);
+helperProto._l10n = new Localization(["browser/touchbar/touchbar.ftl"]);
+
+/**
+ * A representation of a Touch Bar input.
+ *
+ * @param {object} input
+ * An object representing a Touch Bar Input.
+ * Contains listed properties.
+ * @param {string} input.title
+ * The lookup key for the button's localized text title.
+ * @param {string} input.image
+ * A URL pointing to an SVG internal to Firefox.
+ * @param {string} input.type
+ * The type of Touch Bar input represented by the object.
+ * Must be a value from kInputTypes.
+ * @param {Function} input.callback
+ * A callback invoked when a touchbar item is touched.
+ * @param {string} [input.color]
+ * A string in hex format specifying the button's background color.
+ * If omitted, the default background color is used.
+ * @param {bool} [input.disabled]
+ * If `true`, the Touch Bar input is greyed out and inoperable.
+ * @param {Array} [input.children]
+ * An array of input objects that will be displayed as children of
+ * this input. Available only for types KInputTypes.POPOVER and
+ * kInputTypes.SCROLLVIEW.
+ */
+export class TouchBarInput {
+ constructor(input) {
+ this._key = input.key || input.title;
+ this._title = localizedStrings[input.title] || "";
+ this._image = input.image;
+ this._type = input.type;
+ this._callback = input.callback;
+ this._color = hexToInt(input.color);
+ this._disabled = input.hasOwnProperty("disabled") ? input.disabled : false;
+ if (input.children) {
+ this._children = [];
+ let toLocalize = [];
+ for (let childData of Object.values(input.children)) {
+ let initializedChild = new TouchBarInput(childData);
+ if (!initializedChild) {
+ continue;
+ }
+ // Children's types are prepended by the parent's type. This is so we
+ // can uniquely identify a child input from a standalone input with
+ // the same name. (e.g. a button called "back" in a popover would be a
+ // "popover-button.back" vs. a "button.back").
+ initializedChild.type = input.type + "-" + initializedChild.type;
+ this._children.push(initializedChild);
+ // Skip l10n for inputs without a title or those already localized.
+ if (childData.title && !localizedStrings[childData.title]) {
+ toLocalize.push(initializedChild);
+ }
+ }
+ this._localizeChildren(toLocalize);
+ }
+ }
+
+ get key() {
+ return this._key;
+ }
+ get title() {
+ return this._title;
+ }
+ set title(title) {
+ this._title = title;
+ }
+ get image() {
+ return this._image ? Services.io.newURI(this._image) : null;
+ }
+ set image(image) {
+ this._image = image;
+ }
+ get type() {
+ return this._type == "" ? "button" : this._type;
+ }
+ set type(type) {
+ this._type = type;
+ }
+ get callback() {
+ return this._callback;
+ }
+ set callback(callback) {
+ this._callback = callback;
+ }
+ get color() {
+ return this._color;
+ }
+ set color(color) {
+ this._color = this.hexToInt(color);
+ }
+ get disabled() {
+ return this._disabled || false;
+ }
+ set disabled(disabled) {
+ this._disabled = disabled;
+ }
+ get children() {
+ if (!this._children) {
+ return null;
+ }
+ let children = Cc["@mozilla.org/array;1"].createInstance(
+ Ci.nsIMutableArray
+ );
+ for (let child of this._children) {
+ children.appendElement(child);
+ }
+ return children;
+ }
+
+ /**
+ * Apply Fluent l10n to child inputs.
+ *
+ * @param {Array} children
+ * An array of initialized TouchBarInputs.
+ */
+ async _localizeChildren(children) {
+ if (!children || !children.length) {
+ return;
+ }
+
+ let titles = await helperProto._l10n.formatValues(
+ children.map(child => ({ id: child.key }))
+ );
+ // In the TouchBarInput constuctor, we filtered so children contains only
+ // those inputs with titles to be localized. We can be confident that the
+ // results in titles match up with the inputs to be localized.
+ children.forEach(function (child, index) {
+ child.title = titles[index];
+ localizedStrings[child.key] = child.title;
+ });
+
+ lazy.touchBarUpdater.updateTouchBarInputs(
+ TouchBarHelper.baseWindow,
+ children
+ );
+ }
+}
+
+TouchBarInput.prototype.QueryInterface = ChromeUtils.generateQI([
+ "nsITouchBarInput",
+]);
diff --git a/browser/components/touchbar/components.conf b/browser/components/touchbar/components.conf
new file mode 100644
index 0000000000..409bf092d6
--- /dev/null
+++ b/browser/components/touchbar/components.conf
@@ -0,0 +1,20 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+Classes = [
+ {
+ 'cid': '{ea109912-3acc-48de-b679-c23b6a122da5}',
+ 'contract_ids': ['@mozilla.org/widget/touchbarhelper;1'],
+ 'esModule': 'resource:///modules/MacTouchBar.sys.mjs',
+ 'constructor': 'TouchBarHelper',
+ },
+ {
+ 'cid': '{77441d17-f29c-49d7-982f-f20a5ab5a900}',
+ 'contract_ids': ['@mozilla.org/widget/touchbarinput;1'],
+ 'esModule': 'resource:///modules/MacTouchBar.sys.mjs',
+ 'constructor': 'TouchBarInput',
+ },
+]
diff --git a/browser/components/touchbar/docs/index.rst b/browser/components/touchbar/docs/index.rst
new file mode 100644
index 0000000000..6f11988900
--- /dev/null
+++ b/browser/components/touchbar/docs/index.rst
@@ -0,0 +1,364 @@
+Touch Bar
+=========
+
+The Touch Bar is a hardware component on some MacBook Pros released from 2016.
+It is a display above the keyboard that allows more flexible types of
+input than is otherwise possible with a normal keyboard. Apple offers Touch Bar
+APIs so developers can extend the Touch Bar to display inputs specific to their
+application. Firefox consumes these APIs to offer a customizable row of inputs
+in the Touch Bar.
+
+In Apple's documentation, the term "the Touch Bar" refers to the hardware.
+The term "a Touch Bar" refers not to the hardware but to a collection of inputs
+shown on the Touch Bar. This means that there can be multiple "Touch Bars" that
+switch out as the user switches contexts. The same naming convention is used in
+this document.
+
+In this document and in the code, the word "input" is used to refer to
+an interactive element in the Touch Bar. It is often interchangeable with
+"button", but "input" can also refer to any element displayed in the Touch Bar.
+
+The Touch Bar should never offer functionality unavailable to Firefox users
+without the Touch Bar. Most macOS Firefox users do not have the Touch Bar and
+some choose to disable it. Apple's own `Human Interface Guidelines`_ (HIG)
+forbids this kind of Touch Bar functionality. Please read the HIG for more
+design considerations before you plan on implementing a new Touch Bar feature.
+
+If you have questions about the Touch Bar that are not answered in this
+document, feel free to reach out to `Harry Twyford`_ (:harry on Slack).
+He wrote this document and Firefox's initial Touch Bar implementation.
+
+.. _Human Interface Guidelines: https://developer.apple.com/design/human-interface-guidelines/macos/touch-bar/touch-bar-overview/
+
+.. _Harry Twyford: mailto:harry@mozilla.com
+
+.. contents:: Table of Contents
+
+Overview
+~~~~~~~~
+
+Firefox's Touch Bar implementation is equal parts JavaScript and Cocoa
+(Objective-C++). The JavaScript code lives in ``browser/components/touchbar``
+and the Cocoa code lives in ``widget/cocoa``, mostly in ``nsTouchBar.mm``. The
+Cocoa code is a consumer of Apple's Touch Bar APIs and defines what types of
+Touch Bar inputs are available to its own consumers. The JS code in
+``browser/components/touchbar`` provides services to ``nsTouchBar.mm`` and
+defines what inputs the user actually sees in the Touch Bar. There is two-way
+communication between the JS and the Cocoa: the Cocoa code asks the JS what
+inputs it should display, and the JS asks the Cocoa code to update those inputs
+when needed.
+
+JavaScript API
+~~~~~~~~~~~~~~
+
+``browser/components/touchbar/MacTouchBar.sys.mjs`` defines what specific inputs are
+available to the user, what icon they will have, what action they will perform,
+and so on. Inputs are defined in the ``gBuiltInInputs`` object `in that file`_.
+When creating a new object in ``gBuiltInInputs``, the available properties are
+documented in the JSDoc for ``TouchBarInput``:
+
+.. highlight:: JavaScript
+.. code::
+
+ /**
+ * A representation of a Touch Bar input.
+ * @param {string} input.title
+ * The lookup key for the button's localized text title.
+ * @param {string} input.image
+ * A URL pointing to an SVG internal to Firefox.
+ * @param {string} input.type
+ * The type of Touch Bar input represented by the object.
+ * Must be a value from kInputTypes.
+ * @param {Function} input.callback
+ * A callback invoked when a touchbar item is touched.
+ * @param {string} [input.color]
+ * A string in hex format specifying the button's background color.
+ * If omitted, the default background color is used.
+ * @param {bool} [input.disabled]
+ * If `true`, the Touch Bar input is greyed out and inoperable.
+ * @param {Array} [input.children]
+ * An array of input objects that will be displayed as children of
+ * this input. Available only for types KInputTypes.POPOVER and
+ * kInputTypes.SCROLLVIEW.
+ */
+
+Clarification on some of these properties is warranted.
+
+* ``title`` is the key to a Fluent translation defined in ``browser/locales/<LOCALE>/browser/touchbar/touchbar.ftl``.
+* ``type`` must be a value from the ``kInputTypes`` enum in ``MacTouchBar.sys.mjs``.
+ For example, ``kInputTypes.BUTTON``. More information on input types follows
+ below.
+* ``callback`` points to a JavaScript function. Any chrome-level JavaScript can
+ be executed. ``execCommand`` is a convenience method in ``MacTouchBar.sys.mjs``
+ that takes a XUL command as a string and executes that command. For instance,
+ one input sets ``callback`` to ``execCommand("Browser:Back")``.
+* ``children`` is an array of objects with the same properties as members of
+ ``gBuiltInInputs``. When used with an input of type
+ ``kInputTypes.SCROLLVIEW``, ``children`` can only contain inputs of type
+ ``kInputTypes.BUTTON``. When used with an input of type
+ ``kInputTypes.POPOVER``, any input type except another ``kInputTypes.POPOVER``
+ can be used.
+
+.. _in that file: https://searchfox.org/mozilla-central/rev/ebe492edacc75bb122a2b380e4cafcca3470864c/browser/components/touchbar/MacTouchBar.jsm#82
+
+Input types
+-----------
+
+Button
+ A simple button. If ``image`` is not specified, the buttons displays the text
+ label from ``title``. If both ``image`` and ``title`` are specified, only the
+ ``image`` is shown. The action specified in ``callback`` is executed when the
+ button is pressed.
+
+ .. caution::
+
+ Even if the ``title`` will not be shown in the Touch Bar, you must still
+ define a ``title`` property.
+
+Main Button
+ Similar to a button, but displayed at double the width. A main button
+ displays both the string in ``title`` and the icon in ``image``. Only one
+ main button should be shown in the Touch Bar at any time, although this is
+ not enforced.
+
+Label
+ A non-interactive text label. This input takes only the attributes ``title``
+ and ``type``.
+
+Popover
+ Initially represented in the Touch Bar as a button, a popover will display an
+ entirely different set of inputs when pressed. These different inputs should
+ be defined in the ``children`` property of the parent. Popovers can also be
+ shown and hidden programmatically, by calling
+
+ .. highlight:: JavaScript
+ .. code::
+
+ gTouchBarUpdater.showPopover(
+ TouchBarHelper.baseWindow,
+ [POPOVER],
+ {true | false}
+ );
+
+ where the second argument is a reference to a popover TouchBarInput and
+ the third argument is whether the popover should be shown or hidden.
+
+Scroll View
+ A Scroll View is a scrolling list of buttons. The buttons should be defined
+ in the Scroll View's ``children`` array.
+
+ .. note::
+
+ In Firefox, a list of search shortcuts appears in the Touch Bar when the
+ address bar is focused. This is an example of a ScrollView contained within
+ a popover. The popover is opened programmatically with
+ ``gTouchBarUpdater.showPopover`` when the address bar is focused and it is
+ hidden when the address bar is blurred.
+
+Examples
+--------
+Some examples of ``gBuiltInInputs`` objects follow.
+
+A simple button
+ .. highlight:: JavaScript
+ .. code::
+
+ Back: {
+ title: "back",
+ image: "chrome://browser/skin/back.svg",
+ type: kInputTypes.BUTTON,
+ callback: () => execCommand("Browser:Back", "Back"),
+ },
+
+ A button is defined with a title, icon, type, and a callback. The callback
+ simply calls the XUL command to go back.
+
+The search popover
+ This is the input that occupies the Touch Bar when the address bar is focused.
+
+ .. highlight:: JavaScript
+ .. code::
+
+ SearchPopover: {
+ title: "search-popover",
+ image: "chrome://global/skin/icons/search-glass.svg",
+ type: kInputTypes.POPOVER,
+ children: {
+ SearchScrollViewLabel: {
+ title: "search-search-in",
+ type: kInputTypes.LABEL,
+ },
+ SearchScrollView: {
+ key: "search-scrollview",
+ type: kInputTypes.SCROLLVIEW,
+ children: {
+ Bookmarks: {
+ title: "search-bookmarks",
+ type: kInputTypes.BUTTON,
+ callback: () =>
+ gTouchBarHelper.insertRestrictionInUrlbar(
+ UrlbarTokenizer.RESTRICT.BOOKMARK
+ ),
+ },
+ History: {
+ title: "search-history",
+ type: kInputTypes.BUTTON,
+ callback: () =>
+ gTouchBarHelper.insertRestrictionInUrlbar(
+ UrlbarTokenizer.RESTRICT.HISTORY
+ ),
+ },
+ OpenTabs: {
+ title: "search-opentabs",
+ type: kInputTypes.BUTTON,
+ callback: () =>
+ gTouchBarHelper.insertRestrictionInUrlbar(
+ UrlbarTokenizer.RESTRICT.OPENPAGE
+ ),
+ },
+ Tags: {
+ title: "search-tags",
+ type: kInputTypes.BUTTON,
+ callback: () =>
+ gTouchBarHelper.insertRestrictionInUrlbar(
+ UrlbarTokenizer.RESTRICT.TAG
+ ),
+ },
+ Titles: {
+ title: "search-titles",
+ type: kInputTypes.BUTTON,
+ callback: () =>
+ gTouchBarHelper.insertRestrictionInUrlbar(
+ UrlbarTokenizer.RESTRICT.TITLE
+ ),
+ },
+ },
+ },
+ },
+ },
+
+ At the top level, a Popover is defined. This allows a collection of children
+ to be shown in a separate Touch Bar. The Popover has two children: a Label,
+ and a Scroll View. The Scroll View displays five similar buttons that call a
+ helper method to insert search shortcut symbols into the address bar.
+
+Adding a new input
+------------------
+Adding a new input is easy: just add a new object to ``gBuiltInInputs``. This
+will make the input available in the Touch Bar customization window (accessible
+from the Firefox menu bar item).
+
+If you want to to add your new input to the default set, add its identifier
+here_, where ``type`` is a value from ``kAllowedInputTypes`` in that
+file and ``key`` is the value you set for ``title`` in ``gBuiltInInputs``.
+You should request approval from UX before changing the default set of inputs.
+
+.. _here: https://searchfox.org/mozilla-central/rev/ebe492edacc75bb122a2b380e4cafcca3470864c/widget/cocoa/nsTouchBar.mm#100
+
+If you are interested in adding new features to Firefox's implementation of the
+Touch Bar API, read on!
+
+
+Cocoa API
+~~~~~~~~~
+Firefox implements Apple's Touch Bar API in its Widget: Cocoa code with an
+``nsTouchBar`` class. ``nsTouchBar`` interfaces between Apple's Touch Bar API
+and the ``TouchBarHelper`` JavaScript API.
+
+The best resource to understand the Touch Bar API is Apple's
+`official documentation`_. This documentation will cover how Firefox implements
+these APIs and how one might extend ``nsTouchBar`` to enable new Touch Bar
+features.
+
+Every new Firefox window initializes ``nsTouchBar`` (link_). The function
+``makeTouchBar`` is looked for automatically on every new instance of an
+``NSWindow*``. If ``makeTouchBar`` is defined, that window will own a new
+instance of ``nsTouchBar``.
+
+At the time of this writing, every window initializes ``nsTouchBar`` with a
+default set of inputs. In the future, Firefox windows other than the main
+browser window (such as the Library window or DevTools) may initialize
+``nsTouchBar`` with a different set of inputs.
+
+``nsTouchBar`` has two different initialization methods: ``init`` and
+``initWithInputs``. The former is a convenience method for the latter, calling
+``initWithInputs`` with a nil argument. When that happens, a Touch Bar is
+created containing a default set of inputs. ``initWithInputs`` can also take an
+``NSArray<TouchBarInput*>*``. In that case, a non-customizable Touch Bar will be
+initialized with only those inputs available.
+
+.. _official documentation: https://developer.apple.com/documentation/appkit/nstouchbar?language=objc
+.. _link: https://searchfox.org/mozilla-central/rev/ebe492edacc75bb122a2b380e4cafcca3470864c/widget/cocoa/nsCocoaWindow.mm#2877
+
+NSTouchBarItemIdentifiers
+-------------------------
+The architecture of the Touch Bar is based largely around an ``NSString*``
+wrapper class called ``NSTouchBarItemIdentifier``. Every input in the Touch Bar
+has a unique ``NSTouchBarItemIdentifier``. They are structured in reverse-URI
+format like so:
+
+``com.mozilla.firefox.touchbar.[TYPE].[KEY]``
+
+[TYPE] is a string indicating the type of the input, e.g. "button". If an
+input is a child of another input, the parent's type is prepended to the child's
+type, e.g. "scrubber.button" indicates a button contained in a scrubber.
+
+[KEY] is the ``title`` attribute defined for that input on the JS side.
+
+If you need to generate an identifier, use the convenience method
+``[TouchBarInput nativeIdentifierWithType:withKey:]``.
+
+.. caution::
+
+ Do not create a new input that would have the same identifier as any other
+ input. All identifiers must be unique.
+
+.. warning::
+
+ ``NSTouchBarItemIdentifier`` `is used in one other place`_: setting
+ ``customizationIdentifier``. Do not ever change this string. If it is changed,
+ any customizations users have made to the layout of their Touch Bar in Firefox
+ will be erased.
+
+Each identifier is tied to a ``TouchBarInput``. ``TouchBarInput`` is a class
+that holds the properties specified for each input in ``gBuiltInInputs``.
+``nsTouchBar`` uses them to create instances of ``NSTouchBarItem``
+which are the actual objects used by Apple's Touch Bar API and displayed in the
+Touch Bar. It is important to understand the difference between
+``TouchBarInput`` and ``NSTouchBarItem``!
+
+.. _is used in one other place: https://searchfox.org/mozilla-central/rev/ebe492edacc75bb122a2b380e4cafcca3470864c/widget/cocoa/nsTouchBar.mm#71
+
+TouchBarInput creation flow
+---------------------------
+Creating a Touch Bar and its ``TouchBarInputs`` flows as follows:
+
+#. ``[nsTouchBar init]`` is called from ``[NSWindow makeTouchBar]``.
+
+#. ``init`` populates two NSArrays: ``customizationAllowedItemIdentifiers`` and
+ ``defaultItemIdentifiers``. It also initializes a ``TouchBarInput`` object
+ for every element in the union of the two arrays and stores them in
+ ``NSMutableDictionary<NSTouchBarItemIdentifier, TouchBarInput*>* mappedLayoutItems``.
+
+#. ``touchBar:makeItemForIdentifier:`` is called for every element in the union
+ of the two arrays of identifiers. This method retrieves the ``TouchBarInput``
+ for the given identifier and uses it to initialize a ``NSTouchBarItem``.
+ ``touchBar:makeItemForIdentifier:`` reads the ``type`` attribute from the
+ ``TouchBarInput`` to determine what ``NSTouchBarItem`` subclass should be
+ initialized. Our Touch Bar code currently supports ``NSCustomTouchBarItem``
+ (buttons, main buttons); ``NSPopoverTouchBarItem`` (popovers);
+ ``NSTextField`` (labels); and ``NSScrollView`` (ScrollViews).
+
+#. Once the ``NSTouchBarItem`` is initialized, its properties are populated with
+ an assortment of "update" methods. These include ``updateButton``,
+ ``updateMainButton``, ``updateLabel``, ``updatePopover``, and
+ ``updateScrollView``.
+
+#. Since the localization of ``TouchBarInput`` titles happens asynchronously in
+ JavaScript code, the l10n callback executes
+ ``[nsTouchBarUpdater updateTouchBarInputs:]``. This method reads the
+ identifier of the input(s) that need to be updated and calls their respective
+ "update" methods. This method is most often used to update ``title`` after
+ l10n is complete. It can also be used to update any property of a
+ ``TouchBarInput``; for instance, one might wish to change ``color``
+ when a specific event occurs in the browser.
diff --git a/browser/components/touchbar/moz.build b/browser/components/touchbar/moz.build
new file mode 100644
index 0000000000..ef3852d349
--- /dev/null
+++ b/browser/components/touchbar/moz.build
@@ -0,0 +1,18 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+with Files("**"):
+ BUG_COMPONENT = ("Core", "Widget: Cocoa")
+
+BROWSER_CHROME_MANIFESTS += ["tests/browser/browser.ini"]
+
+EXTRA_JS_MODULES += [
+ "MacTouchBar.sys.mjs",
+]
+
+XPCOM_MANIFESTS += [
+ "components.conf",
+]
+
+SPHINX_TREES["/browser/touchbar"] = "docs"
diff --git a/browser/components/touchbar/tests/browser/browser.ini b/browser/components/touchbar/tests/browser/browser.ini
new file mode 100644
index 0000000000..4356d936f5
--- /dev/null
+++ b/browser/components/touchbar/tests/browser/browser.ini
@@ -0,0 +1,11 @@
+[DEFAULT]
+
+[browser_touchbar_searchrestrictions.js]
+https_first_disabled = true
+skip-if = os != "mac"
+[browser_touchbar_tests.js]
+skip-if = os != "mac" || verify # Bug 1611300
+support-files =
+ readerModeArticle.html
+ test-video.mp4
+ video_test.html
diff --git a/browser/components/touchbar/tests/browser/browser_touchbar_searchrestrictions.js b/browser/components/touchbar/tests/browser/browser_touchbar_searchrestrictions.js
new file mode 100644
index 0000000000..3a78e9a4b1
--- /dev/null
+++ b/browser/components/touchbar/tests/browser/browser_touchbar_searchrestrictions.js
@@ -0,0 +1,154 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+ChromeUtils.defineESModuleGetters(this, {
+ UrlbarUtils: "resource:///modules/UrlbarUtils.sys.mjs",
+ UrlbarTokenizer: "resource:///modules/UrlbarTokenizer.sys.mjs",
+});
+
+ChromeUtils.defineLazyGetter(this, "UrlbarTestUtils", () => {
+ const { UrlbarTestUtils: module } = ChromeUtils.importESModule(
+ "resource://testing-common/UrlbarTestUtils.sys.mjs"
+ );
+ module.init(this);
+ return module;
+});
+
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "TouchBarHelper",
+ "@mozilla.org/widget/touchbarhelper;1",
+ "nsITouchBarHelper"
+);
+
+/**
+ * Tests the search restriction buttons in the Touch Bar.
+ */
+
+/**
+ * @param {string} input
+ * The value to be inserted in the Urlbar.
+ * @param {UrlbarTokenizer.RESTRICT} token
+ * A restriction token corresponding to a Touch Bar button.
+ */
+async function searchAndCheckState({ input, token }) {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: input,
+ });
+ input = input.trimStart();
+ if (Object.values(UrlbarTokenizer.RESTRICT).includes(input[0])) {
+ input = input.slice(1).trimStart();
+ }
+ let searchMode = UrlbarUtils.searchModeForToken(token);
+ let expectedValue = searchMode ? input : `${token} ${input}`;
+ TouchBarHelper.insertRestrictionInUrlbar(token);
+
+ if (searchMode) {
+ searchMode.entry = "touchbar";
+ await UrlbarTestUtils.assertSearchMode(window, searchMode);
+ }
+ Assert.equal(
+ gURLBar.value,
+ expectedValue,
+ "The search restriction token should have been entered."
+ );
+
+ await UrlbarTestUtils.promisePopupClose(window);
+}
+
+add_task(async function insertTokens() {
+ const tests = [
+ {
+ input: "mozilla",
+ token: UrlbarTokenizer.RESTRICT.HISTORY,
+ },
+ {
+ input: "mozilla",
+ token: UrlbarTokenizer.RESTRICT.BOOKMARK,
+ },
+ {
+ input: "mozilla",
+ token: UrlbarTokenizer.RESTRICT.TAG,
+ },
+ {
+ input: "mozilla",
+ token: UrlbarTokenizer.RESTRICT.OPENPAGE,
+ },
+ ];
+ for (let test of tests) {
+ await searchAndCheckState(test);
+ }
+});
+
+add_task(async function existingTokens() {
+ const tests = [
+ {
+ input: "* mozilla",
+ token: UrlbarTokenizer.RESTRICT.HISTORY,
+ },
+ {
+ input: "+ mozilla",
+ token: UrlbarTokenizer.RESTRICT.BOOKMARK,
+ },
+ {
+ input: "( $ ^ mozilla",
+ token: UrlbarTokenizer.RESTRICT.TAG,
+ },
+ {
+ input: "^*+%?#$ mozilla",
+ token: UrlbarTokenizer.RESTRICT.TAG,
+ },
+ ];
+ for (let test of tests) {
+ await searchAndCheckState(test);
+ }
+});
+
+add_task(async function stripSpaces() {
+ const tests = [
+ {
+ input: " ^ mozilla",
+ token: UrlbarTokenizer.RESTRICT.HISTORY,
+ },
+ {
+ input: " + mozilla ",
+ token: UrlbarTokenizer.RESTRICT.BOOKMARK,
+ },
+ {
+ input: " moz illa ",
+ token: UrlbarTokenizer.RESTRICT.TAG,
+ },
+ ];
+ for (let test of tests) {
+ await searchAndCheckState(test);
+ }
+});
+
+add_task(async function clearURLs() {
+ const tests = [
+ {
+ loadUrl: "http://example.com/",
+ token: UrlbarTokenizer.RESTRICT.HISTORY,
+ },
+ {
+ loadUrl: "about:mozilla",
+ token: UrlbarTokenizer.RESTRICT.BOOKMARK,
+ },
+ ];
+ let win = BrowserWindowTracker.getTopWindow();
+ await UrlbarTestUtils.promisePopupClose(win);
+ for (let { loadUrl, token } of tests) {
+ let browser = win.gBrowser.selectedBrowser;
+ let loadedPromise = BrowserTestUtils.browserLoaded(browser, false, loadUrl);
+ BrowserTestUtils.loadURIString(browser, loadUrl);
+ await loadedPromise;
+ if (win.gURLBar.getAttribute("pageproxystate") != "valid") {
+ await TestUtils.waitForCondition(
+ () => win.gURLBar.getAttribute("pageproxystate") == "valid"
+ );
+ }
+ await searchAndCheckState({ input: "", token });
+ }
+});
diff --git a/browser/components/touchbar/tests/browser/browser_touchbar_tests.js b/browser/components/touchbar/tests/browser/browser_touchbar_tests.js
new file mode 100644
index 0000000000..f9cacd55b2
--- /dev/null
+++ b/browser/components/touchbar/tests/browser/browser_touchbar_tests.js
@@ -0,0 +1,159 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "TouchBarHelper",
+ "@mozilla.org/widget/touchbarhelper;1",
+ "nsITouchBarHelper"
+);
+
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "TouchBarInput",
+ "@mozilla.org/widget/touchbarinput;1",
+ "nsITouchBarInput"
+);
+
+const TEST_PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "http://example.com"
+);
+
+function is_element_visible(aElement, aMsg) {
+ isnot(aElement, null, "Element should not be null when checking visibility");
+ ok(!BrowserTestUtils.is_hidden(aElement), aMsg);
+}
+
+function is_element_hidden(aElement, aMsg) {
+ isnot(aElement, null, "Element should not be null when checking visibility");
+ ok(BrowserTestUtils.is_hidden(aElement), aMsg);
+}
+
+/**
+ * Tests if our bookmark button updates with our event.
+ */
+add_task(async function updateBookmarkButton() {
+ // We first check the default state. This also serves the purpose of forcing
+ // nsITouchBarHelper to load on Macs without Touch Bars so that it will be
+ // listening for "bookmark-icon-updated".
+ Assert.equal(
+ TouchBarHelper.getTouchBarInput("AddBookmark").image.spec,
+ "chrome://browser/skin/bookmark-hollow.svg",
+ "AddBookmark image should be unfilled bookmark after event."
+ );
+
+ Services.obs.notifyObservers(null, "bookmark-icon-updated", "starred");
+ Assert.equal(
+ TouchBarHelper.getTouchBarInput("AddBookmark").image.spec,
+ "chrome://browser/skin/bookmark.svg",
+ "AddBookmark image should be filled bookmark after event."
+ );
+
+ Services.obs.notifyObservers(null, "bookmark-icon-updated", "unstarred");
+ Assert.equal(
+ TouchBarHelper.getTouchBarInput("AddBookmark").image.spec,
+ "chrome://browser/skin/bookmark-hollow.svg",
+ "AddBookmark image should be unfilled bookmark after event."
+ );
+});
+
+/**
+ * Tests if our Reader View button updates when a page can be reader viewed.
+ */
+add_task(async function updateReaderView() {
+ const PREF_READERMODE = "reader.parse-on-load.enabled";
+ await SpecialPowers.pushPrefEnv({ set: [[PREF_READERMODE, true]] });
+
+ // The page actions reader mode button
+ var readerButton = document.getElementById("reader-mode-button");
+ is_element_hidden(readerButton, "Reader Mode button should be hidden.");
+
+ Assert.equal(
+ TouchBarHelper.getTouchBarInput("ReaderView").disabled,
+ true,
+ "ReaderView Touch Bar button should be disabled by default."
+ );
+
+ let url = TEST_PATH + "readerModeArticle.html";
+ await BrowserTestUtils.withNewTab(url, async function () {
+ await BrowserTestUtils.waitForCondition(() => !readerButton.hidden);
+
+ Assert.equal(
+ TouchBarHelper.getTouchBarInput("ReaderView").disabled,
+ false,
+ "ReaderView Touch Bar button should be enabled on reader-able pages."
+ );
+ });
+});
+
+add_task(async function updateMainButtonInFullscreen() {
+ Assert.equal(
+ TouchBarHelper.getTouchBarInput("OpenLocation").image.spec,
+ "chrome://global/skin/icons/search-glass.svg",
+ "OpenLocation should be displaying the search glass icon."
+ );
+ BrowserTestUtils.loadURIString(
+ gBrowser.selectedBrowser,
+ TEST_PATH + "video_test.html"
+ );
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ let entered = waitForFullScreenState(gBrowser.selectedBrowser, true);
+ // Fullscreen video must be triggered from a user input handler so the video
+ // page contains a script to enter fullscreen on Enter instead of us calling
+ // requestFullscreen directly here.
+ EventUtils.synthesizeKey("KEY_Enter");
+ await entered;
+ Assert.equal(
+ TouchBarHelper.getTouchBarInput("OpenLocation").image.spec,
+ "chrome://browser/skin/fullscreen-exit.svg",
+ "OpenLocation should be displaying the exit fullscreen icon."
+ );
+ let exited = waitForFullScreenState(gBrowser.selectedBrowser, false);
+ EventUtils.synthesizeKey("KEY_Enter");
+ await exited;
+ Assert.equal(
+ TouchBarHelper.getTouchBarInput("OpenLocation").image.spec,
+ "chrome://global/skin/icons/search-glass.svg",
+ "OpenLocation should be displaying the search glass icon."
+ );
+});
+
+add_task(async function toggleUrlbarFocusOnOpenLocation() {
+ Assert.equal(TouchBarHelper.isUrlbarFocused, false, "Urlbar is unfocused.");
+ TouchBarHelper.toggleFocusUrlbar();
+ Assert.equal(TouchBarHelper.isUrlbarFocused, true, "Urlbar is unfocused.");
+ TouchBarHelper.toggleFocusUrlbar();
+});
+
+add_task(async function unfocusUrlbar() {
+ window.gURLBar.focus();
+ Assert.equal(TouchBarHelper.isUrlbarFocused, true, "Urlbar is unfocused.");
+ TouchBarHelper.unfocusUrlbar();
+ Assert.equal(TouchBarHelper.isUrlbarFocused, false, "Urlbar is unfocused.");
+});
+
+function waitForFullScreenState(browser, state) {
+ info("inside waitforfullscreenstate");
+ return new Promise(resolve => {
+ let eventReceived = false;
+
+ let observe = (subject, topic, data) => {
+ if (!eventReceived) {
+ return;
+ }
+ Services.obs.removeObserver(observe, "fullscreen-painted");
+ resolve();
+ };
+ Services.obs.addObserver(observe, "fullscreen-painted");
+
+ window.addEventListener(
+ `MozDOMFullscreen:${state ? "Entered" : "Exited"}`,
+ () => {
+ eventReceived = true;
+ },
+ { once: true }
+ );
+ });
+}
diff --git a/browser/components/touchbar/tests/browser/readerModeArticle.html b/browser/components/touchbar/tests/browser/readerModeArticle.html
new file mode 100644
index 0000000000..7cb014d2c3
--- /dev/null
+++ b/browser/components/touchbar/tests/browser/readerModeArticle.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8" />
+<title>Article title</title>
+<meta name="description" content="This is the article description." />
+</head>
+<body>
+<header>Site header</header>
+<div>
+<h1>Article title</h1>
+<ul>
+ <li><a id="foo-anchor" href="#foo">by John Doe</a></li>
+</ul>
+<h2 class="author">by Jane Doe</h2>
+<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec a diam lectus. Sed sit amet ipsum mauris. Maecenas congue ligula ac quam viverra nec consectetur ante hendrerit. Donec et mollis dolor. Praesent et diam eget libero egestas mattis sit amet vitae augue. Nam tincidunt congue enim, ut porta lorem lacinia consectetur. Donec ut libero sed arcu vehicula ultricies a non tortor. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean ut gravida lorem. Ut turpis felis, pulvinar a semper sed, adipiscing id dolor. Pellentesque auctor nisi id magna consequat sagittis. Curabitur dapibus enim sit amet elit pharetra tincidunt feugiat nisl imperdiet. Ut convallis libero in urna ultrices accumsan. Donec sed odio eros. Donec viverra mi quis quam pulvinar at malesuada arcu rhoncus. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. In rutrum accumsan ultricies. Mauris vitae nisi at sem facilisis semper ac in est.</p>
+<p>Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.</p>
+<p>Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.</p>
+<p>Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.</p>
+<p>Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.</p>
+<p id="foo">by John Doe</p>
+<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec a diam lectus. Sed sit amet ipsum mauris. Maecenas congue ligula ac quam viverra nec consectetur ante hendrerit. Donec et mollis dolor. Praesent et diam eget libero egestas mattis sit amet vitae augue. Nam tincidunt congue enim, ut porta lorem lacinia consectetur. Donec ut libero sed arcu vehicula ultricies a non tortor. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean ut gravida lorem. Ut turpis felis, pulvinar a semper sed, adipiscing id dolor. Pellentesque auctor nisi id magna consequat sagittis. Curabitur dapibus enim sit amet elit pharetra tincidunt feugiat nisl imperdiet. Ut convallis libero in urna ultrices accumsan. Donec sed odio eros. Donec viverra mi quis quam pulvinar at malesuada arcu rhoncus. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. In rutrum accumsan ultricies. Mauris vitae nisi at sem facilisis semper ac in est.</p>
+<p>Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.</p>
+<p>Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.</p>
+<p>Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.</p>
+<p>Vivamus fermentum semper porta. Nunc diam velit, adipiscing ut tristique vitae, sagittis vel odio. Maecenas convallis ullamcorper ultricies. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, id fringilla sem nunc vel mi. Nam dictum, odio nec pretium volutpat, arcu ante placerat erat, non tristique elit urna et turpis. Quisque mi metus, ornare sit amet fermentum et, tincidunt et orci. Fusce eget orci a orci congue vestibulum. Ut dolor diam, elementum et vestibulum eu, porttitor vel elit. Curabitur venenatis pulvinar tellus gravida ornare. Sed et erat faucibus nunc euismod ultricies ut id justo. Nullam cursus suscipit nisi, et ultrices justo sodales nec. Fusce venenatis facilisis lectus ac semper. Aliquam at massa ipsum. Quisque bibendum purus convallis nulla ultrices ultricies. Nullam aliquam, mi eu aliquam tincidunt, purus velit laoreet tortor, viverra pretium nisi quam vitae mi. Fusce vel volutpat elit. Nam sagittis nisi dui.</p>
+</div>
+</body>
+</html>
diff --git a/browser/components/touchbar/tests/browser/test-video.mp4 b/browser/components/touchbar/tests/browser/test-video.mp4
new file mode 100644
index 0000000000..6ea66eb1fc
--- /dev/null
+++ b/browser/components/touchbar/tests/browser/test-video.mp4
Binary files differ
diff --git a/browser/components/touchbar/tests/browser/video_test.html b/browser/components/touchbar/tests/browser/video_test.html
new file mode 100644
index 0000000000..173de35f0e
--- /dev/null
+++ b/browser/components/touchbar/tests/browser/video_test.html
@@ -0,0 +1,32 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Touch Bar Fullscreen Video Test</title>
+</head>
+<style>
+ video {
+ display: block;
+ border: 1px solid black;
+ }
+</style>
+<body>
+ <video id="test-video" src="test-video.mp4" controls loop="true"></video>
+</body>
+<script>
+ let video = document.getElementById("test-video");
+
+ function toggleFullScreen() {
+ if (!document.fullscreenElement) {
+ document.documentElement.requestFullscreen();
+ } else if (document.exitFullscreen) {
+ document.exitFullscreen();
+ }
+ }
+ document.addEventListener("keydown", function(e) {
+ if (e.keyCode == 13) {
+ toggleFullScreen();
+ }
+ });
+</script>
+</html>