tor-browser

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

commit 7e7ba259147af401f6bd149884c81d790501df0c
parent e987524d34436cacf823d319d71bab33f162ba1c
Author: Kagami Sascha Rosylight <krosylight@proton.me>
Date:   Fri, 19 Dec 2025 09:25:43 +0000

Bug 2004409 - Split window opener logic from DateTimePicker classes r=emilio

Differential Revision: https://phabricator.services.mozilla.com/D275250

Diffstat:
Mtesting/mochitest/BrowserTestUtils/BrowserTestUtils.sys.mjs | 2+-
Mtoolkit/actors/DateTimePickerChild.sys.mjs | 214+++++++++++++++++++++----------------------------------------------------------
Mtoolkit/actors/DateTimePickerParent.sys.mjs | 142++++++++-----------------------------------------------------------------------
Atoolkit/actors/InputPickerChildCommon.sys.mjs | 188+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atoolkit/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++++++++++++++++++++++++-------------------------------------------------------
Atoolkit/modules/InputPickerPanelCommon.sys.mjs | 163+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtoolkit/modules/moz.build | 6+++++-
16 files changed, 681 insertions(+), 456 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("#dateTimePopupFrame"); + let frame = panel.querySelector("#inputPickerPopupFrame"); let isValidUrl = () => { return ( frame.browsingContext?.currentURI?.spec == diff --git a/toolkit/actors/DateTimePickerChild.sys.mjs b/toolkit/actors/DateTimePickerChild.sys.mjs @@ -2,77 +2,30 @@ * 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", -}); +import { InputPickerChildCommon } from "./InputPickerChildCommon.sys.mjs"; -/** - * 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. - */ +export class DateTimePickerChild extends InputPickerChildCommon { constructor() { - super(); - - this._inputElement = null; + super("DateTimePicker"); } /** * Cleanup function called when picker is closed. + * + * @param {HTMLInputElement} inputElement */ - close() { - this.removeListeners(this._inputElement); - let dateTimeBoxElement = this._inputElement.dateTimeBoxElement; + closeImpl(inputElement) { + let dateTimeBoxElement = 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 = this._inputElement.ownerGlobal; + let win = 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() { @@ -80,119 +33,62 @@ export class DateTimePickerChild extends JSWindowActorChild { } /** - * nsIMessageListener. + * Element updater function called when the picker value is changed. + * + * @param {ReceiveMessageArgument} aMessage + * @param {HTMLInputElement} inputElement */ - receiveMessage(aMessage) { - switch (aMessage.name) { - case "FormDateTime:PickerClosed": { - if (!this._inputElement) { - return; - } - - this.close(); - break; - } - case "FormDateTime:PickerValueChanged": { - if (!this._inputElement) { - return; - } - - let dateTimeBoxElement = this._inputElement.dateTimeBoxElement; - if (!dateTimeBoxElement) { - return; - } + pickerValueChangedImpl(aMessage, inputElement) { + let dateTimeBoxElement = inputElement.dateTimeBoxElement; + if (!dateTimeBoxElement) { + return; + } - let win = this._inputElement.ownerGlobal; + let win = 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; - } + // 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), + }) + ); } /** - * nsIDOMEventListener, for chrome events sent by the input element and other - * DOM events. + * 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. */ - 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; - } - - 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; - } - - this._inputElement = aEvent.originalTarget; - - let dateTimeBoxElement = this._inputElement.dateTimeBoxElement; - if (!dateTimeBoxElement) { - throw new Error("How do we get this event without a UA Widget?"); - } + openPickerImpl(inputElement) { + // Time picker is disabled when preffed off + if (inputElement.type == "time" && !this.getTimePickerPref()) { + return undefined; + } - // 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 dateTimeBoxElement = inputElement.dateTimeBoxElement; + if (!dateTimeBoxElement) { + throw new Error("How do we get this event without a UA Widget?"); + } - this.addListeners(this._inputElement); + // 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 }) + ); - 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; - } + 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(), + }; } } diff --git a/toolkit/actors/DateTimePickerParent.sys.mjs b/toolkit/actors/DateTimePickerParent.sys.mjs @@ -2,141 +2,25 @@ * 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("-*- DateTimePickerParent: " + aStr + "\n"); - } -} +import { InputPickerParentCommon } from "./InputPickerParentCommon.sys.mjs"; const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { - DateTimePickerPanel: "resource://gre/modules/DateTimePickerPanel.sys.mjs", + DateTimePickerPanel: "moz-src:///toolkit/modules/DateTimePickerPanel.sys.mjs", }); -/* - * 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; +export class DateTimePickerParent extends InputPickerParentCommon { + constructor() { + super("DateTimePicker"); } - // 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; + /** + * 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); } } diff --git a/toolkit/actors/InputPickerChildCommon.sys.mjs b/toolkit/actors/InputPickerChildCommon.sys.mjs @@ -0,0 +1,188 @@ +/* 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 @@ -0,0 +1,170 @@ +/* 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,8 +48,6 @@ 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", @@ -83,3 +81,10 @@ 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("#dateTimePopupFrame") + .querySelector("#inputPickerPopupFrame") .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("#dateTimePopupFrame").contentDocument + helper.panel.querySelector("#inputPickerPopupFrame").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( - "#dateTimePopupFrame" + "#inputPickerPopupFrame" ).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( - "#dateTimePopupFrame" + "#inputPickerPopupFrame" ).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( - "#dateTimePopupFrame" + "#inputPickerPopupFrame" ).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( - "#dateTimePopupFrame" + "#inputPickerPopupFrame" ).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( - "#dateTimePopupFrame" + "#inputPickerPopupFrame" ).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( - "#dateTimePopupFrame" + "#inputPickerPopupFrame" ).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( - "#dateTimePopupFrame" + "#inputPickerPopupFrame" ).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("#dateTimePopupFrame").contentDocument + helper.panel.querySelector("#inputPickerPopupFrame").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( - "#dateTimePopupFrame" + "#inputPickerPopupFrame" ).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( - "#dateTimePopupFrame" + "#inputPickerPopupFrame" ).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("#dateTimePopupFrame"); + this.frame = this.panel.querySelector("#inputPickerPopupFrame"); } 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 #dateTimePopupFrame + * of the #inputPickerPopupFrame * @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: "resource://gre/actors/DateTimePickerParent.sys.mjs", + esModuleURI: "moz-src:///toolkit/actors/DateTimePickerParent.sys.mjs", }, child: { - esModuleURI: "resource://gre/actors/DateTimePickerChild.sys.mjs", + esModuleURI: "moz-src:///toolkit/actors/DateTimePickerChild.sys.mjs", events: { MozOpenDateTimePicker: {}, MozCloseDateTimePicker: {}, diff --git a/toolkit/modules/DateTimePickerPanel.sys.mjs b/toolkit/modules/DateTimePickerPanel.sys.mjs @@ -2,86 +2,69 @@ * 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 DateTimePickerPanel { - constructor(element) { - this.element = element; +import { InputPickerPanelCommon } from "./InputPickerPanelCommon.sys.mjs"; - 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"; - } +/** @import {OpenPickerInfo} from "./InputPickerPanelCommon.sys.mjs" */ - 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; +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"; + +export class DateTimePickerPanel extends InputPickerPanelCommon { + constructor(element) { + super(element, "chrome://global/content/datetimepicker.xhtml"); } - openPicker(type, rect, detail) { + /** + * Picker window initialization function called when opening the picker + * + * @param {string} type The input element type + * @returns {OpenPickerInfo} + */ + openPickerImpl(type) { 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": { - this.dateTimePopupFrame.style.width = this.TIME_PICKER_WIDTH; - this.dateTimePopupFrame.style.height = this.TIME_PICKER_HEIGHT; - break; + return { + type, + width: TIME_PICKER_WIDTH, + height: TIME_PICKER_HEIGHT, + }; } case "date": { - this.dateTimePopupFrame.style.width = this.DATE_PICKER_WIDTH; - this.dateTimePopupFrame.style.height = this.DATE_PICKER_HEIGHT; - break; + return { + type, + width: DATE_PICKER_WIDTH, + height: DATE_PICKER_HEIGHT, + }; } case "datetime-local": { - this.dateTimePopupFrame.style.width = this.DATETIME_PICKER_WIDTH; - this.dateTimePopupFrame.style.height = this.DATETIME_PICKER_HEIGHT; - break; + return { + type, + width: DATETIME_PICKER_WIDTH, + height: DATETIME_PICKER_HEIGHT, + }; } } - 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(); + throw new Error(`Unexpected type ${type}`); } - initPicker(detail) { + /** + * 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) { let locale = new Services.intl.Locale( Services.locale.webExposedLocales[0], { @@ -99,7 +82,7 @@ export class DateTimePickerPanel { const { year, month, day, hour, minute } = detail.value; const flattenDetail = { - type: this.type, + type, year, // Month value from input box starts from 1 instead of 0 month: month == undefined ? undefined : month - 1, @@ -115,7 +98,7 @@ export class DateTimePickerPanel { stepBase: detail.stepBase, }; - if (this.type !== "time") { + if (type !== "time") { const { firstDayOfWeek, weekends } = this.getCalendarInfo(locale); const monthDisplayNames = new Services.intl.DisplayNames(locale, { @@ -143,59 +126,33 @@ export class DateTimePickerPanel { weekdayStrings, }); } - this.postMessageToPicker({ - name: "PickerInit", - detail: flattenDetail, - }); + return flattenDetail; } - 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) { + /** + * 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) { case "time": { - detail = { - hour: value.hour, - minute: value.minute, - }; - break; + return { hour, minute }; } case "date": { - 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; + return { year, month, day }; } case "datetime-local": { - 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; + return { year, month, day, hour, minute }; } } - this.element.dispatchEvent( - new CustomEvent("DateTimePickerValueChanged", { - detail, - }) - ); + throw new Error(`Unexpected type ${type}`); } getCalendarInfo(locale) { @@ -218,46 +175,4 @@ export class DateTimePickerPanel { 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 @@ -0,0 +1,163 @@ +/* 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,7 +162,6 @@ EXTRA_JS_MODULES += [ "Console.sys.mjs", "ContentDOMReference.sys.mjs", "CreditCard.sys.mjs", - "DateTimePickerPanel.sys.mjs", "DeferredTask.sys.mjs", "E10SUtils.sys.mjs", "EventEmitter.sys.mjs", @@ -215,6 +214,11 @@ 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",