tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit 9cfe831685694267ced2344018359b0f042d3cb8
parent bb5b4322e10ab27810d333e7a152e75ec4c47a97
Author: Sandor Molnar <smolnar@mozilla.com>
Date:   Fri, 19 Dec 2025 12:21:23 +0200

Revert "Bug 2004409 - Split window opener logic from DateTimePicker classes r=emilio" for causing mochitest failures

This reverts commit 7e7ba259147af401f6bd149884c81d790501df0c.

Diffstat:
Mtesting/mochitest/BrowserTestUtils/BrowserTestUtils.sys.mjs | 2+-
Mtoolkit/actors/DateTimePickerChild.sys.mjs | 214++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------
Mtoolkit/actors/DateTimePickerParent.sys.mjs | 142+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
Dtoolkit/actors/InputPickerChildCommon.sys.mjs | 188-------------------------------------------------------------------------------
Dtoolkit/actors/InputPickerParentCommon.sys.mjs | 170-------------------------------------------------------------------------------
Mtoolkit/actors/moz.build | 9++-------
Mtoolkit/content/tests/browser/datetime/browser_datetime_blur.js | 2+-
Mtoolkit/content/tests/browser/datetime/browser_datetime_datepicker_keynav.js | 8++++----
Mtoolkit/content/tests/browser/datetime/browser_datetime_datepicker_monthyear.js | 8++++----
Mtoolkit/content/tests/browser/datetime/browser_datetime_datepicker_prev_next_month.js | 2+-
Mtoolkit/content/tests/browser/datetime/browser_spinner_keynav.js | 4++--
Mtoolkit/content/tests/browser/datetime/head.js | 4++--
Mtoolkit/modules/ActorManagerParent.sys.mjs | 4++--
Mtoolkit/modules/DateTimePickerPanel.sys.mjs | 211+++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------
Dtoolkit/modules/InputPickerPanelCommon.sys.mjs | 163-------------------------------------------------------------------------------
Mtoolkit/modules/moz.build | 6+-----
16 files changed, 456 insertions(+), 681 deletions(-)

diff --git a/testing/mochitest/BrowserTestUtils/BrowserTestUtils.sys.mjs b/testing/mochitest/BrowserTestUtils/BrowserTestUtils.sys.mjs @@ -1467,7 +1467,7 @@ export var BrowserTestUtils = { let getPanel = () => win.document.getElementById("DateTimePickerPanel"); let panel = getPanel(); let ensureReady = async () => { - let frame = panel.querySelector("#inputPickerPopupFrame"); + let frame = panel.querySelector("#dateTimePopupFrame"); let isValidUrl = () => { return ( frame.browsingContext?.currentURI?.spec == diff --git a/toolkit/actors/DateTimePickerChild.sys.mjs b/toolkit/actors/DateTimePickerChild.sys.mjs @@ -2,30 +2,77 @@ * 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 { InputPickerChildCommon } from "./InputPickerChildCommon.sys.mjs"; +const lazy = {}; +ChromeUtils.defineESModuleGetters(lazy, { + LayoutUtils: "resource://gre/modules/LayoutUtils.sys.mjs", +}); -export class DateTimePickerChild extends InputPickerChildCommon { +/** + * DateTimePickerChild is the communication channel between the input box + * (content) for date/time input types and its picker (chrome). + */ +export class DateTimePickerChild extends JSWindowActorChild { + /** + * On init, just listen for the event to open the picker, once the picker is + * opened, we'll listen for update and close events. + */ constructor() { - super("DateTimePicker"); + super(); + + this._inputElement = null; } /** * Cleanup function called when picker is closed. - * - * @param {HTMLInputElement} inputElement */ - closeImpl(inputElement) { - let dateTimeBoxElement = inputElement.dateTimeBoxElement; + close() { + this.removeListeners(this._inputElement); + let dateTimeBoxElement = this._inputElement.dateTimeBoxElement; if (!dateTimeBoxElement) { + this._inputElement = null; return; } // dateTimeBoxElement is within UA Widget Shadow DOM. // An event dispatch to it can't be accessed by document. - let win = inputElement.ownerGlobal; + let win = this._inputElement.ownerGlobal; dateTimeBoxElement.dispatchEvent( new win.CustomEvent("MozSetDateTimePickerState", { detail: false }) ); + + this._inputElement = null; + } + + /** + * Called after picker is opened to start listening for input box update + * events. + */ + addListeners(aElement) { + aElement.ownerGlobal.addEventListener("pagehide", this); + } + + /** + * Stop listeneing for events when picker is closed. + */ + removeListeners(aElement) { + aElement.ownerGlobal.removeEventListener("pagehide", this); + } + + /** + * Helper function that returns the CSS direction property of the element. + */ + getComputedDirection(aElement) { + return aElement.ownerGlobal + .getComputedStyle(aElement) + .getPropertyValue("direction"); + } + + /** + * Helper function that returns the rect of the element, which is the position + * relative to the left/top of the content area. + */ + getBoundingContentRect(aElement) { + return lazy.LayoutUtils.getElementBoundingScreenRect(aElement); } getTimePickerPref() { @@ -33,62 +80,119 @@ export class DateTimePickerChild extends InputPickerChildCommon { } /** - * Element updater function called when the picker value is changed. - * - * @param {ReceiveMessageArgument} aMessage - * @param {HTMLInputElement} inputElement + * nsIMessageListener. */ - pickerValueChangedImpl(aMessage, inputElement) { - let dateTimeBoxElement = inputElement.dateTimeBoxElement; - if (!dateTimeBoxElement) { - return; - } + receiveMessage(aMessage) { + switch (aMessage.name) { + case "FormDateTime:PickerClosed": { + if (!this._inputElement) { + return; + } - let win = inputElement.ownerGlobal; + this.close(); + break; + } + case "FormDateTime:PickerValueChanged": { + if (!this._inputElement) { + return; + } - // dateTimeBoxElement is within UA Widget Shadow DOM. - // An event dispatch to it can't be accessed by document. - dateTimeBoxElement.dispatchEvent( - new win.CustomEvent("MozPickerValueChanged", { - detail: Cu.cloneInto(aMessage.data, win), - }) - ); + let dateTimeBoxElement = this._inputElement.dateTimeBoxElement; + if (!dateTimeBoxElement) { + return; + } + + let win = this._inputElement.ownerGlobal; + + // dateTimeBoxElement is within UA Widget Shadow DOM. + // An event dispatch to it can't be accessed by document. + dateTimeBoxElement.dispatchEvent( + new win.CustomEvent("MozPickerValueChanged", { + detail: Cu.cloneInto(aMessage.data, win), + }) + ); + break; + } + default: + break; + } } /** - * Picker initialization function called when opening the picker - * - * @param {HTMLInputElement} inputElement - * @returns An argument object to pass to the picker panel, or undefined to stop. + * nsIDOMEventListener, for chrome events sent by the input element and other + * DOM events. */ - openPickerImpl(inputElement) { - // Time picker is disabled when preffed off - if (inputElement.type == "time" && !this.getTimePickerPref()) { - return undefined; - } + handleEvent(aEvent) { + switch (aEvent.type) { + case "MozOpenDateTimePicker": { + // Time picker is disabled when preffed off + if ( + !aEvent.originalTarget.ownerGlobal.HTMLInputElement.isInstance( + aEvent.originalTarget + ) || + (aEvent.originalTarget.type == "time" && !this.getTimePickerPref()) + ) { + return; + } - let dateTimeBoxElement = inputElement.dateTimeBoxElement; - if (!dateTimeBoxElement) { - throw new Error("How do we get this event without a UA Widget?"); - } + if (this._inputElement) { + // This happens when we're trying to open a picker when another picker + // is still open. We ignore this request to let the first picker + // close gracefully. + return; + } - // dateTimeBoxElement is within UA Widget Shadow DOM. - // An event dispatch to it can't be accessed by document, because - // the event is not composed. - let win = inputElement.ownerGlobal; - dateTimeBoxElement.dispatchEvent( - new win.CustomEvent("MozSetDateTimePickerState", { detail: true }) - ); + this._inputElement = aEvent.originalTarget; + + let dateTimeBoxElement = this._inputElement.dateTimeBoxElement; + if (!dateTimeBoxElement) { + throw new Error("How do we get this event without a UA Widget?"); + } + + // dateTimeBoxElement is within UA Widget Shadow DOM. + // An event dispatch to it can't be accessed by document, because + // the event is not composed. + let win = this._inputElement.ownerGlobal; + dateTimeBoxElement.dispatchEvent( + new win.CustomEvent("MozSetDateTimePickerState", { detail: true }) + ); - let value = inputElement.getDateTimeInputBoxValue(); - return { - // Pass partial value if it's available, otherwise pass input - // element's value. - value: Object.keys(value).length ? value : inputElement.value, - min: inputElement.getMinimum(), - max: inputElement.getMaximum(), - step: inputElement.getStep(), - stepBase: inputElement.getStepBase(), - }; + this.addListeners(this._inputElement); + + let value = this._inputElement.getDateTimeInputBoxValue(); + this.sendAsyncMessage("FormDateTime:OpenPicker", { + rect: this.getBoundingContentRect(this._inputElement), + dir: this.getComputedDirection(this._inputElement), + type: this._inputElement.type, + detail: { + // Pass partial value if it's available, otherwise pass input + // element's value. + value: Object.keys(value).length ? value : this._inputElement.value, + min: this._inputElement.getMinimum(), + max: this._inputElement.getMaximum(), + step: this._inputElement.getStep(), + stepBase: this._inputElement.getStepBase(), + }, + }); + break; + } + case "MozCloseDateTimePicker": { + this.sendAsyncMessage("FormDateTime:ClosePicker", {}); + this.close(); + break; + } + case "pagehide": { + if ( + this._inputElement && + this._inputElement.ownerDocument == aEvent.target + ) { + this.sendAsyncMessage("FormDateTime:ClosePicker", {}); + this.close(); + } + break; + } + default: + break; + } } } diff --git a/toolkit/actors/DateTimePickerParent.sys.mjs b/toolkit/actors/DateTimePickerParent.sys.mjs @@ -2,25 +2,141 @@ * 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 { InputPickerParentCommon } from "./InputPickerParentCommon.sys.mjs"; +const DEBUG = false; +function debug(aStr) { + if (DEBUG) { + dump("-*- DateTimePickerParent: " + aStr + "\n"); + } +} const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { - DateTimePickerPanel: "moz-src:///toolkit/modules/DateTimePickerPanel.sys.mjs", + DateTimePickerPanel: "resource://gre/modules/DateTimePickerPanel.sys.mjs", }); -export class DateTimePickerParent extends InputPickerParentCommon { - constructor() { - super("DateTimePicker"); +/* + * DateTimePickerParent receives message from content side (input box) and + * is reposible for opening, closing and updating the picker. Similarly, + * DateTimePickerParent listens for picker's events and notifies the content + * side (input box) about them. + */ +export class DateTimePickerParent extends JSWindowActorParent { + receiveMessage(aMessage) { + debug("receiveMessage: " + aMessage.name); + switch (aMessage.name) { + case "FormDateTime:OpenPicker": { + this.showPicker(aMessage.data); + break; + } + case "FormDateTime:ClosePicker": { + if (!this._picker) { + return; + } + this.close(); + break; + } + default: + break; + } + } + + handleEvent(aEvent) { + debug("handleEvent: " + aEvent.type); + switch (aEvent.type) { + case "DateTimePickerValueCleared": { + this.sendAsyncMessage("FormDateTime:PickerValueChanged", null); + break; + } + case "DateTimePickerValueChanged": { + this.sendAsyncMessage("FormDateTime:PickerValueChanged", aEvent.detail); + break; + } + case "popuphidden": { + this.sendAsyncMessage("FormDateTime:PickerClosed", {}); + this.close(); + break; + } + default: + break; + } + } + + // Get picker from browser and show it anchored to the input box. + showPicker(aData) { + let rect = aData.rect; + let type = aData.type; + let detail = aData.detail; + + debug("Opening picker with details: " + JSON.stringify(detail)); + let topBC = this.browsingContext.top; + let window = topBC.topChromeWindow; + if (Services.focus.activeWindow != window) { + debug("Not in the active window"); + return; + } + + { + let browser = topBC.embedderElement; + if ( + browser && + browser.ownerGlobal.gBrowser && + browser.ownerGlobal.gBrowser.selectedBrowser != browser + ) { + debug("In background tab"); + return; + } + } + + this._cleanupPicker(); + let doc = window.document; + let panel = doc.getElementById("DateTimePickerPanel"); + if (!panel) { + panel = doc.createXULElement("panel"); + panel.id = "DateTimePickerPanel"; + panel.setAttribute("type", "arrow"); + panel.setAttribute("orient", "vertical"); + panel.setAttribute("ignorekeys", "true"); + panel.setAttribute("noautofocus", "true"); + // This ensures that clicks on the anchored input box are never consumed. + panel.setAttribute("consumeoutsideclicks", "never"); + panel.setAttribute("level", "parent"); + panel.setAttribute("tabspecific", "true"); + let container = + doc.getElementById("mainPopupSet") || + doc.querySelector("popupset") || + doc.documentElement.appendChild(doc.createXULElement("popupset")); + container.appendChild(panel); + } + this._oldFocus = doc.activeElement; + this._picker = new lazy.DateTimePickerPanel(panel); + this._picker.openPicker(type, rect, detail); + this._picker.element.addEventListener("popuphidden", this); + this._picker.element.addEventListener("DateTimePickerValueChanged", this); + this._picker.element.addEventListener("DateTimePickerValueCleared", this); + } + + _cleanupPicker() { + if (!this._picker) { + return; + } + this._picker.closePicker(); + this._picker.element.removeEventListener("popuphidden", this); + this._picker.element.removeEventListener( + "DateTimePickerValueChanged", + this + ); + this._picker.element.removeEventListener( + "DateTimePickerValueCleared", + this + ); + this._picker = null; } - /** - * A picker creator function called when showing a picker - * - * @param {XULElement} panel A panel element - * @returns A panel object that manages the element - */ - createPickerImpl(panel) { - return new lazy.DateTimePickerPanel(panel); + // Close the picker and do some cleanup. + close() { + this._cleanupPicker(); + // Restore focus to where it was before the picker opened. + this._oldFocus?.focus(); + this._oldFocus = null; } } diff --git a/toolkit/actors/InputPickerChildCommon.sys.mjs b/toolkit/actors/InputPickerChildCommon.sys.mjs @@ -1,188 +0,0 @@ -/* 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/. */ - -const lazy = {}; -ChromeUtils.defineESModuleGetters(lazy, { - LayoutUtils: "resource://gre/modules/LayoutUtils.sys.mjs", -}); - -/** - * InputPickerChildCommon is the communication channel between the input box - * (content) for each input types and its picker (chrome). - */ -export class InputPickerChildCommon extends JSWindowActorChild { - /** @type {HTMLInputElement} */ - #inputElement = null; - #inputType = ""; - #namespace; - /** @type {AbortController} */ - #abortController; - - /** - * On init, just listen for the event to open the picker, once the picker is - * opened, we'll listen for update and close events. - * - * @param {string} namespace Affects the event names, e.g. Foo makes it - * accept FooValueChanged event. - * Align it with ActorManagerParent declaration. - */ - constructor(namespace) { - super(); - this.#namespace = namespace; - } - - /** - * Cleanup function called when picker is closed. - */ - close() { - this.#abortController.abort(); - this.closeImpl(this.#inputElement); - this.#inputElement = null; - this.#inputType = ""; - } - - /** - * @param {HTMLInputElement} _inputElement - */ - closeImpl(_inputElement) { - throw new Error("Not implemented"); - } - - /** - * Called after picker is opened to start listening for input box update - * events. - */ - addListeners(aElement) { - this.#abortController = new AbortController(); - aElement.ownerGlobal.addEventListener("pagehide", this, { - signal: this.#abortController.signal, - }); - } - - /** - * Helper function that returns the CSS direction property of the element. - */ - getComputedDirection(aElement) { - return aElement.ownerGlobal - .getComputedStyle(aElement) - .getPropertyValue("direction"); - } - - /** - * Helper function that returns the rect of the element, which is the position - * relative to the left/top of the content area. - */ - getBoundingContentRect(aElement) { - return lazy.LayoutUtils.getElementBoundingScreenRect(aElement); - } - - /** - * MessageListener - */ - receiveMessage(aMessage) { - if (!this.#inputElement || this.#inputElement.type !== this.#inputType) { - // Either we are already closed by content or the input type is changed - return; - } - switch (aMessage.name) { - case "InputPicker:Closed": { - this.close(); - break; - } - case "InputPicker:ValueChanged": { - this.pickerValueChangedImpl(aMessage, this.#inputElement); - break; - } - } - } - - /** - * Element updater function called when the picker value is changed. - * - * @param {ReceiveMessageArgument} _aMessage - * @param {HTMLInputElement} _inputElement - */ - pickerValueChangedImpl(_aMessage, _inputElement) { - throw new Error("Not implemented"); - } - - /** - * nsIDOMEventListener, for chrome events sent by the input element and other - * DOM events. - */ - handleEvent(aEvent) { - switch (aEvent.type) { - case `MozOpen${this.#namespace}`: { - if ( - !aEvent.originalTarget.ownerGlobal.HTMLInputElement.isInstance( - aEvent.originalTarget - ) - ) { - return; - } - - if (this.#inputElement) { - // This happens when we're trying to open a picker when another picker - // is still open. We ignore this request to let the first picker - // close gracefully. - return; - } - - /** @type {HTMLInputElement} */ - const inputElement = aEvent.originalTarget; - const openPickerDetail = this.openPickerImpl(inputElement); - if (!openPickerDetail) { - // The impl doesn't want to proceed in this case - return; - } - - this.#inputElement = inputElement; - this.#inputType = inputElement.type; - this.addListeners(inputElement); - - this.sendAsyncMessage(`InputPicker:Open`, { - rect: this.getBoundingContentRect(inputElement), - dir: this.getComputedDirection(inputElement), - type: inputElement.type, - detail: openPickerDetail, - }); - break; - } - case `MozClose${this.#namespace}`: { - this.sendAsyncMessage(`InputPicker:Close`, {}); - this.close(); - break; - } - case "pagehide": { - if (this.#inputElement?.ownerDocument == aEvent.target) { - this.sendAsyncMessage(`InputPicker:Close`, {}); - this.close(); - } - break; - } - default: - break; - } - } - - /** - * Picker initialization function called when opening the picker - * - * @param {HTMLInputElement} _inputElement - * @returns An argument object to pass to the picker, or undefined to stop opening one. - */ - openPickerImpl(_inputElement) { - throw new Error("Not implemented"); - } - - /** - * Picker updater function when the input value is updated - * - * @param {HTMLInputElement} _inputElement - * @returns An argument object to pass to the picker - */ - updatePickerImpl(_inputElement) { - throw new Error("Not implemented"); - } -} diff --git a/toolkit/actors/InputPickerParentCommon.sys.mjs b/toolkit/actors/InputPickerParentCommon.sys.mjs @@ -1,170 +0,0 @@ -/* 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/. */ - -const DEBUG = false; -function debug(aStr) { - if (DEBUG) { - dump("-*- InputPickerParent: " + aStr + "\n"); - } -} - -/* - * InputPickerParentCommon receives message from content side (input box) and - * is reposible for opening, closing and updating the picker. Similarly, - * InputPickerParentCommon listens for picker's events and notifies the content - * side (input box) about them. - */ -export class InputPickerParentCommon extends JSWindowActorParent { - #namespace; - #picker; - /** @type {Element | undefined} */ - #oldFocus; - /** @type {AbortController} */ - #abortController; - - /** - * @param {string} namespace Affects the input panel id, mostly to prevent - * accidental mis-pairing of wrong panel and actor. - */ - constructor(namespace) { - super(); - this.#namespace = namespace; - } - - receiveMessage(aMessage) { - debug("receiveMessage: " + aMessage.name); - switch (aMessage.name) { - case `InputPicker:Open`: { - this.showPicker(aMessage.data); - break; - } - case `InputPicker:Close`: { - if (!this.#picker) { - return; - } - this.close(); - break; - } - default: - break; - } - } - - handleEvent(aEvent) { - debug("handleEvent: " + aEvent.type); - switch (aEvent.type) { - case "InputPickerValueCleared": { - this.sendAsyncMessage("InputPicker:ValueChanged", null); - break; - } - case "InputPickerValueChanged": { - this.sendAsyncMessage("InputPicker:ValueChanged", aEvent.detail); - break; - } - case "popuphidden": { - this.sendAsyncMessage(`InputPicker:Closed`, {}); - this.close(); - break; - } - default: - break; - } - } - - /** - * A panel creator function called when showing a picker - * - * @param {XULElement} _panel A panel element - * @returns A panel object that manages the element - */ - createPickerImpl(_panel) { - throw new Error("Not implemented"); - } - - // Get picker from browser and show it anchored to the input box. - showPicker(aData) { - let rect = aData.rect; - let type = aData.type; - let detail = aData.detail; - - debug("Opening picker with details: " + JSON.stringify(detail)); - let topBC = this.browsingContext.top; - let window = topBC.topChromeWindow; - if (Services.focus.activeWindow != window) { - debug("Not in the active window"); - return; - } - - { - let browser = topBC.embedderElement; - if ( - browser && - browser.ownerGlobal.gBrowser && - browser.ownerGlobal.gBrowser.selectedBrowser != browser - ) { - debug("In background tab"); - return; - } - } - - this.#cleanupPicker(); - let doc = window.document; - const id = `${this.#namespace}Panel`; - let panel = doc.getElementById(id); - if (!panel) { - panel = doc.createXULElement("panel"); - panel.id = id; - panel.setAttribute("type", "arrow"); - panel.setAttribute("orient", "vertical"); - panel.setAttribute("ignorekeys", "true"); - panel.setAttribute("noautofocus", "true"); - // This ensures that clicks on the anchored input box are never consumed. - panel.setAttribute("consumeoutsideclicks", "never"); - panel.setAttribute("level", "parent"); - panel.setAttribute("tabspecific", "true"); - let container = - doc.getElementById("mainPopupSet") || - doc.querySelector("popupset") || - doc.documentElement.appendChild(doc.createXULElement("popupset")); - container.appendChild(panel); - } - this.#oldFocus = doc.activeElement; - this.#picker = this.createPickerImpl(panel); - this.#picker.openPicker(type, rect, detail); - this.addPickerListeners(panel); - } - - #cleanupPicker() { - if (!this.#picker) { - return; - } - this.#picker.closePicker(); - this.#abortController.abort(); - this.#picker = null; - } - - // Close the picker and do some cleanup. - close() { - this.#cleanupPicker(); - // Restore focus to where it was before the picker opened. - this.#oldFocus?.focus(); - this.#oldFocus = null; - } - - // Listen to picker's event. - addPickerListeners(panel) { - if (!this.#picker) { - return; - } - this.#abortController = new AbortController(); - const { signal } = this.#abortController; - panel.addEventListener("popuphidden", this, { signal }); - panel.addEventListener("InputPickerValueChanged", this, { - signal, - }); - panel.addEventListener("InputPickerValueCleared", this, { - signal, - }); - } -} diff --git a/toolkit/actors/moz.build b/toolkit/actors/moz.build @@ -48,6 +48,8 @@ FINAL_TARGET_FILES.actors += [ "ContentMetaParent.sys.mjs", "ControllersChild.sys.mjs", "ControllersParent.sys.mjs", + "DateTimePickerChild.sys.mjs", + "DateTimePickerParent.sys.mjs", "ExtFindChild.sys.mjs", "FindBarChild.sys.mjs", "FindBarParent.sys.mjs", @@ -81,10 +83,3 @@ FINAL_TARGET_FILES.actors += [ "WebChannelChild.sys.mjs", "WebChannelParent.sys.mjs", ] - -MOZ_SRC_FILES += [ - "DateTimePickerChild.sys.mjs", - "DateTimePickerParent.sys.mjs", - "InputPickerChildCommon.sys.mjs", - "InputPickerParentCommon.sys.mjs", -] diff --git a/toolkit/content/tests/browser/datetime/browser_datetime_blur.js b/toolkit/content/tests/browser/datetime/browser_datetime_blur.js @@ -116,7 +116,7 @@ add_task(async function test_parent_blur() { ); Assert.equal( helper.panel - .querySelector("#inputPickerPopupFrame") + .querySelector("#dateTimePopupFrame") .contentDocument.activeElement.getAttribute("role"), "gridcell", "The picker is opened and a calendar day is focused" diff --git a/toolkit/content/tests/browser/datetime/browser_datetime_datepicker_keynav.js b/toolkit/content/tests/browser/datetime/browser_datetime_datepicker_keynav.js @@ -66,7 +66,7 @@ add_task(async function test_datepicker_keyboard_nav() { ); Assert.equal( - helper.panel.querySelector("#inputPickerPopupFrame").contentDocument + helper.panel.querySelector("#dateTimePopupFrame").contentDocument .activeElement.textContent, "15", "Picker is opened with a focus set to the currently selected date" @@ -286,7 +286,7 @@ add_task(async function test_datepicker_keyboard_arrows() { `data:text/html,<input id=date type=date value=${inputValue}>` ); let pickerDoc = helper.panel.querySelector( - "#inputPickerPopupFrame" + "#dateTimePopupFrame" ).contentDocument; Assert.equal(helper.panel.state, "open", "Panel should be opened"); @@ -368,7 +368,7 @@ add_task(async function test_datepicker_keyboard_home_end() { `data:text/html,<input id=date type=date value=${inputValue}>` ); let pickerDoc = helper.panel.querySelector( - "#inputPickerPopupFrame" + "#dateTimePopupFrame" ).contentDocument; Assert.equal(helper.panel.state, "open", "Panel should be opened"); @@ -454,7 +454,7 @@ add_task(async function test_datepicker_keyboard_pgup_pgdown() { `data:text/html,<input id=date type=date value=${inputValue}>` ); let pickerDoc = helper.panel.querySelector( - "#inputPickerPopupFrame" + "#dateTimePopupFrame" ).contentDocument; Assert.equal(helper.panel.state, "open", "Panel should be opened"); diff --git a/toolkit/content/tests/browser/datetime/browser_datetime_datepicker_monthyear.js b/toolkit/content/tests/browser/datetime/browser_datetime_datepicker_monthyear.js @@ -18,7 +18,7 @@ add_task(async function test_monthyear_close_date() { `data:text/html, <input type="date" value=${inputValue}>` ); let pickerDoc = helper.panel.querySelector( - "#inputPickerPopupFrame" + "#dateTimePopupFrame" ).contentDocument; // Move focus from the selected date to the month-year toggle button: @@ -49,7 +49,7 @@ add_task(async function test_monthyear_close_datetime() { `data:text/html, <input type="datetime-local" value=${inputValue}>` ); let pickerDoc = helper.panel.querySelector( - "#inputPickerPopupFrame" + "#dateTimePopupFrame" ).contentDocument; // Move focus from the selected date to the month-year toggle button: @@ -78,7 +78,7 @@ add_task(async function test_monthyear_escape_date() { `data:text/html, <input type="date" value=${inputValue}>` ); let pickerDoc = helper.panel.querySelector( - "#inputPickerPopupFrame" + "#dateTimePopupFrame" ).contentDocument; // Move focus from the today's date to the month-year toggle button: @@ -150,7 +150,7 @@ add_task(async function test_monthyear_escape_datetime() { `data:text/html, <input type="datetime-local" value=${inputValue}>` ); let pickerDoc = helper.panel.querySelector( - "#inputPickerPopupFrame" + "#dateTimePopupFrame" ).contentDocument; // Move focus from the today's date to the month-year toggle button: diff --git a/toolkit/content/tests/browser/datetime/browser_datetime_datepicker_prev_next_month.js b/toolkit/content/tests/browser/datetime/browser_datetime_datepicker_prev_next_month.js @@ -371,7 +371,7 @@ add_task(async function test_datepicker_reopened_prev_next_month_btn() { await ready; Assert.equal( - helper.panel.querySelector("#inputPickerPopupFrame").contentDocument + helper.panel.querySelector("#dateTimePopupFrame").contentDocument .activeElement.textContent, "2", "Picker is opened with a focus set to the currently selected date" diff --git a/toolkit/content/tests/browser/datetime/browser_spinner_keynav.js b/toolkit/content/tests/browser/datetime/browser_spinner_keynav.js @@ -29,7 +29,7 @@ add_task(async function test_spinner_month_keyboard_arrows() { `data:text/html, <input type="date" value="${inputValue}">` ); let pickerDoc = helper.panel.querySelector( - "#inputPickerPopupFrame" + "#dateTimePopupFrame" ).contentDocument; info("Testing general keyboard navigation"); @@ -341,7 +341,7 @@ add_task(async function test_spinner_year_keyboard_arrows() { `data:text/html, <input type="date" value="${inputValue}">` ); let pickerDoc = helper.panel.querySelector( - "#inputPickerPopupFrame" + "#dateTimePopupFrame" ).contentDocument; info("Testing general keyboard navigation"); diff --git a/toolkit/content/tests/browser/datetime/head.js b/toolkit/content/tests/browser/datetime/head.js @@ -58,7 +58,7 @@ class DateTimeTestHelper { }); } this.panel = await shown; - this.frame = this.panel.querySelector("#inputPickerPopupFrame"); + this.frame = this.panel.querySelector("#dateTimePopupFrame"); } promisePickerClosed() { @@ -296,7 +296,7 @@ async function testCalendarBtnAttribute(attr, val, presenceOnly = false) { * * @param {string} key: A keyboard Event.key that will be synthesized * @param {object} document: Reference to the content document - * of the #inputPickerPopupFrame + * of the #dateTimePopupFrame * @param {number} tabs: How many times "Tab" key should be pressed * to move a keyboard focus to a needed spinner * (1 for month/default and 2 for year) diff --git a/toolkit/modules/ActorManagerParent.sys.mjs b/toolkit/modules/ActorManagerParent.sys.mjs @@ -694,11 +694,11 @@ if (AppConstants.platform != "android") { // Note that GeckoView handles MozOpenDateTimePicker in GeckoViewPrompt. JSWINDOWACTORS.DateTimePicker = { parent: { - esModuleURI: "moz-src:///toolkit/actors/DateTimePickerParent.sys.mjs", + esModuleURI: "resource://gre/actors/DateTimePickerParent.sys.mjs", }, child: { - esModuleURI: "moz-src:///toolkit/actors/DateTimePickerChild.sys.mjs", + esModuleURI: "resource://gre/actors/DateTimePickerChild.sys.mjs", events: { MozOpenDateTimePicker: {}, MozCloseDateTimePicker: {}, diff --git a/toolkit/modules/DateTimePickerPanel.sys.mjs b/toolkit/modules/DateTimePickerPanel.sys.mjs @@ -2,69 +2,86 @@ * 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 { InputPickerPanelCommon } from "./InputPickerPanelCommon.sys.mjs"; - -/** @import {OpenPickerInfo} from "./InputPickerPanelCommon.sys.mjs" */ +export class DateTimePickerPanel { + constructor(element) { + this.element = element; -const TIME_PICKER_WIDTH = "13em"; -const TIME_PICKER_HEIGHT = "22em"; -const DATE_PICKER_WIDTH = "24em"; -const DATE_PICKER_HEIGHT = "27em"; -const DATETIME_PICKER_WIDTH = "40em"; -const DATETIME_PICKER_HEIGHT = "27em"; + this.TIME_PICKER_WIDTH = "13em"; + this.TIME_PICKER_HEIGHT = "22em"; + this.DATE_PICKER_WIDTH = "24em"; + this.DATE_PICKER_HEIGHT = "27em"; + this.DATETIME_PICKER_WIDTH = "40em"; + this.DATETIME_PICKER_HEIGHT = "27em"; + } -export class DateTimePickerPanel extends InputPickerPanelCommon { - constructor(element) { - super(element, "chrome://global/content/datetimepicker.xhtml"); + get dateTimePopupFrame() { + let frame = this.element.querySelector("#dateTimePopupFrame"); + if (!frame) { + frame = this.element.ownerDocument.createXULElement("iframe"); + frame.id = "dateTimePopupFrame"; + this.element.appendChild(frame); + } + return frame; } - /** - * Picker window initialization function called when opening the picker - * - * @param {string} type The input element type - * @returns {OpenPickerInfo} - */ - openPickerImpl(type) { + openPicker(type, rect, detail) { if ( type == "datetime-local" && !Services.prefs.getBoolPref("dom.forms.datetime.timepicker") ) { type = "date"; } + this.pickerState = {}; + // TODO: Resize picker according to content zoom level + this.element.style.fontSize = "10px"; + this.type = type; + this.detail = detail; + this.dateTimePopupFrame.addEventListener("load", this, true); + this.dateTimePopupFrame.setAttribute( + "src", + "chrome://global/content/datetimepicker.xhtml" + ); switch (type) { case "time": { - return { - type, - width: TIME_PICKER_WIDTH, - height: TIME_PICKER_HEIGHT, - }; + this.dateTimePopupFrame.style.width = this.TIME_PICKER_WIDTH; + this.dateTimePopupFrame.style.height = this.TIME_PICKER_HEIGHT; + break; } case "date": { - return { - type, - width: DATE_PICKER_WIDTH, - height: DATE_PICKER_HEIGHT, - }; + this.dateTimePopupFrame.style.width = this.DATE_PICKER_WIDTH; + this.dateTimePopupFrame.style.height = this.DATE_PICKER_HEIGHT; + break; } case "datetime-local": { - return { - type, - width: DATETIME_PICKER_WIDTH, - height: DATETIME_PICKER_HEIGHT, - }; + this.dateTimePopupFrame.style.width = this.DATETIME_PICKER_WIDTH; + this.dateTimePopupFrame.style.height = this.DATETIME_PICKER_HEIGHT; + break; } } - throw new Error(`Unexpected type ${type}`); + this.element.openPopupAtScreenRect( + "after_start", + rect.left, + rect.top, + rect.width, + rect.height, + false, + false + ); + } + + closePicker(clear) { + if (clear) { + this.element.dispatchEvent(new CustomEvent("DateTimePickerValueCleared")); + } + this.pickerState = {}; + this.type = undefined; + this.dateTimePopupFrame.removeEventListener("load", this, true); + this.dateTimePopupFrame.contentWindow.removeEventListener("message", this); + this.dateTimePopupFrame.setAttribute("src", ""); + this.element.hidePopup(); } - /** - * Popup frame initialization function called when the picker window is loaded - * - * @param {string} type The picker type - * @param {object} detail The argument from the child actor's openPickerImpl - * @returns An argument object to pass to the popup frame - */ - initPickerImpl(type, detail) { + initPicker(detail) { let locale = new Services.intl.Locale( Services.locale.webExposedLocales[0], { @@ -82,7 +99,7 @@ export class DateTimePickerPanel extends InputPickerPanelCommon { const { year, month, day, hour, minute } = detail.value; const flattenDetail = { - type, + type: this.type, year, // Month value from input box starts from 1 instead of 0 month: month == undefined ? undefined : month - 1, @@ -98,7 +115,7 @@ export class DateTimePickerPanel extends InputPickerPanelCommon { stepBase: detail.stepBase, }; - if (type !== "time") { + if (this.type !== "time") { const { firstDayOfWeek, weekends } = this.getCalendarInfo(locale); const monthDisplayNames = new Services.intl.DisplayNames(locale, { @@ -126,33 +143,59 @@ export class DateTimePickerPanel extends InputPickerPanelCommon { weekdayStrings, }); } - return flattenDetail; + this.postMessageToPicker({ + name: "PickerInit", + detail: flattenDetail, + }); } - /** - * Input element state updater function called when the picker value is changed - * - * @param {string} type - * @param {object} pickerState - */ - sendPickerValueChangedImpl(type, pickerState) { - let { year, month, day, hour, minute } = pickerState; - if (month !== undefined) { - // Month value from input box starts from 1 instead of 0 - month += 1; - } - switch (type) { + setInputBoxValue() { + const value = { + year: this.pickerState.year, + month: this.pickerState.month, + day: this.pickerState.day, + hour: this.pickerState.hour, + minute: this.pickerState.minute, + }; + this.sendPickerValueChanged(value); + } + + sendPickerValueChanged(value) { + let detail = {}; + switch (this.type) { case "time": { - return { hour, minute }; + detail = { + hour: value.hour, + minute: value.minute, + }; + break; } case "date": { - return { year, month, day }; + detail = { + year: value.year, + // Month value from input box starts from 1 instead of 0 + month: value.month == undefined ? undefined : value.month + 1, + day: value.day, + }; + break; } case "datetime-local": { - return { year, month, day, hour, minute }; + detail = { + year: value.year, + // Month value from input box starts from 1 instead of 0 + month: value.month == undefined ? undefined : value.month + 1, + day: value.day, + hour: value.hour, + minute: value.minute, + }; + break; } } - throw new Error(`Unexpected type ${type}`); + this.element.dispatchEvent( + new CustomEvent("DateTimePickerValueChanged", { + detail, + }) + ); } getCalendarInfo(locale) { @@ -175,4 +218,46 @@ export class DateTimePickerPanel extends InputPickerPanelCommon { weekends, }; } + + handleEvent(aEvent) { + switch (aEvent.type) { + case "load": { + this.initPicker(this.detail); + this.dateTimePopupFrame.contentWindow.addEventListener("message", this); + break; + } + case "message": { + this.handleMessage(aEvent); + break; + } + } + } + + handleMessage(aEvent) { + if ( + !this.dateTimePopupFrame.contentDocument.nodePrincipal.isSystemPrincipal + ) { + return; + } + + switch (aEvent.data.name) { + case "PickerPopupChanged": { + this.pickerState = aEvent.data.detail; + this.setInputBoxValue(); + break; + } + case "ClosePopup": { + this.closePicker(aEvent.data.detail); + break; + } + } + } + + postMessageToPicker(data) { + if ( + this.dateTimePopupFrame.contentDocument.nodePrincipal.isSystemPrincipal + ) { + this.dateTimePopupFrame.contentWindow.postMessage(data, "*"); + } + } } diff --git a/toolkit/modules/InputPickerPanelCommon.sys.mjs b/toolkit/modules/InputPickerPanelCommon.sys.mjs @@ -1,163 +0,0 @@ -/* 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/. */ - -export class InputPickerPanelCommon { - #element; - #filename; - #pickerState; - #type; - #detail; - /** @type {AbortController} */ - #abortController; - - /** - * @param {XULElement} element - * @param {string} filename - */ - constructor(element, filename) { - this.#element = element; - this.#filename = filename; - } - - get #popupFrame() { - const id = `${this.#element.id}PopupFrame`; - let frame = this.#element.ownerDocument.getElementById(id); - if (!frame) { - frame = this.#element.ownerDocument.createXULElement("iframe"); - frame.id = id; - this.#element.appendChild(frame); - } - return frame; - } - - openPicker(type, rect, detail) { - const impl = this.openPickerImpl(type); - this.#pickerState = {}; - // TODO: Resize picker according to content zoom level - this.#element.style.fontSize = "10px"; - this.#type = impl.type; - this.#detail = detail; - this.#abortController = new AbortController(); - this.#popupFrame.addEventListener("load", this, { - capture: true, - signal: this.#abortController.signal, - }); - this.#popupFrame.setAttribute("src", this.#filename); - this.#popupFrame.style.width = impl.width; - this.#popupFrame.style.height = impl.height; - this.#element.openPopupAtScreenRect( - "after_start", - rect.left, - rect.top, - rect.width, - rect.height, - false, - false - ); - } - - /** - * @typedef {object} OpenPickerInfo - * @property {string} type The picker type - * @property {string} width The picker width in CSS value - * @property {string} height The picker height in CSS value - * - * Picker window initialization function called when opening the picker - * - * @param {string} _type The input element type - * @returns {OpenPickerInfo} - */ - openPickerImpl(_type) { - throw new Error("Not implemented"); - } - - closePicker(clear) { - if (clear) { - this.#element.dispatchEvent(new CustomEvent(`InputPickerValueCleared`)); - } - this.#pickerState = {}; - this.#type = undefined; - this.#abortController.abort(); - this.#popupFrame.setAttribute("src", ""); - this.#element.hidePopup(); - } - - initPicker(detail) { - const implDetail = this.initPickerImpl(this.#type, detail); - this.postMessageToPicker({ - name: "PickerInit", - detail: implDetail, - }); - } - - /** - * Popup frame initialization function called when the picker window is loaded - * - * @param {string} _type The picker type - * @param {object} _detail The argument from the child actor's openPickerImpl - * @returns An argument object to pass to the popup frame - */ - initPickerImpl(_type, _detail) { - throw new Error("Not implemented"); - } - - sendPickerValueChanged() { - let detail = this.sendPickerValueChangedImpl(this.#type, this.#pickerState); - this.#element.dispatchEvent( - new CustomEvent(`InputPickerValueChanged`, { - detail, - }) - ); - } - - /** - * Input element state updater function called when the picker value is changed - * - * @param {string} _type - * @param {object} _pickerState - */ - sendPickerValueChangedImpl(_type, _pickerState) { - throw new Error("Not implemented"); - } - - handleEvent(aEvent) { - switch (aEvent.type) { - case "load": { - this.initPicker(this.#detail); - this.#popupFrame.contentWindow.addEventListener("message", this, { - signal: this.#abortController.signal, - }); - break; - } - case "message": { - this.handleMessage(aEvent); - break; - } - } - } - - handleMessage(aEvent) { - if (!this.#popupFrame.contentDocument.nodePrincipal.isSystemPrincipal) { - return; - } - - switch (aEvent.data.name) { - case "PickerPopupChanged": { - this.#pickerState = aEvent.data.detail; - this.sendPickerValueChanged(); - break; - } - case "ClosePopup": { - this.closePicker(aEvent.data.detail); - break; - } - } - } - - postMessageToPicker(data) { - if (this.#popupFrame.contentDocument.nodePrincipal.isSystemPrincipal) { - this.#popupFrame.contentWindow.postMessage(data, "*"); - } - } -} diff --git a/toolkit/modules/moz.build b/toolkit/modules/moz.build @@ -162,6 +162,7 @@ EXTRA_JS_MODULES += [ "Console.sys.mjs", "ContentDOMReference.sys.mjs", "CreditCard.sys.mjs", + "DateTimePickerPanel.sys.mjs", "DeferredTask.sys.mjs", "E10SUtils.sys.mjs", "EventEmitter.sys.mjs", @@ -214,11 +215,6 @@ EXTRA_JS_MODULES += [ "WebChannel.sys.mjs", ] -MOZ_SRC_FILES += [ - "DateTimePickerPanel.sys.mjs", - "InputPickerPanelCommon.sys.mjs", -] - if CONFIG["MOZ_ASAN_REPORTER"]: EXTRA_JS_MODULES += [ "AsanReporter.sys.mjs",