Bug 1828477 - Support input type=date in non-tabbrowser windows. r=Gijs,geckoview-reviewers,ohall

Differential Revision: https://phabricator.services.mozilla.com/D175771
This commit is contained in:
Emilio Cobos Álvarez 2023-04-20 21:14:07 +00:00
Родитель d4b63553f2
Коммит 1d61a94f1b
14 изменённых файлов: 243 добавлений и 155 удалений

Просмотреть файл

@ -136,21 +136,6 @@
noautofocus="true"
hidden="true" />
<html:template id="dateTimePickerTemplate">
<!-- for date/time picker. consumeoutsideclicks is set to never, so that
clicks on the anchored input box are never consumed. -->
<panel id="DateTimePickerPanel"
type="arrow"
orient="vertical"
ignorekeys="true"
norolluponanchor="true"
noautofocus="true"
consumeoutsideclicks="never"
level="parent"
tabspecific="true">
</panel>
</html:template>
<html:template id="printPreviewStackTemplate">
<stack class="previewStack" rendering="true" flex="1" previewtype="primary">
<vbox class="previewRendering" flex="1">

Просмотреть файл

@ -172,8 +172,6 @@
arrowKeysShouldWrap: AppConstants == "macosx",
_dateTimePicker: null,
_previewMode: false,
_lastFindValue: "",
@ -806,16 +804,6 @@
}
},
_getAndMaybeCreateDateTimePickerPanel() {
if (!this._dateTimePicker) {
let wrapper = document.getElementById("dateTimePickerTemplate");
wrapper.replaceWith(wrapper.content);
this._dateTimePicker = document.getElementById("DateTimePickerPanel");
}
return this._dateTimePicker;
},
syncThrobberAnimations(aTab) {
aTab.ownerGlobal.promiseDocumentFlushed(() => {
if (!aTab.container) {

Просмотреть файл

@ -723,7 +723,7 @@ partial interface Window {
[NewObject, Func="nsGlobalWindowInner::IsPrivilegedChromeWindow"]
Promise<any> promiseDocumentFlushed(PromiseDocumentFlushedCallback callback);
[ChromeOnly]
[Func="IsChromeOrUAWidget"]
readonly attribute boolean isChromeWindow;
[ChromeOnly]

Просмотреть файл

@ -226,23 +226,28 @@ class PromptFactory {
}
_handleDateTime(aElement) {
const prompt = new lazy.GeckoViewPrompter(aElement.ownerGlobal);
const win = aElement.ownerGlobal;
const prompt = new lazy.GeckoViewPrompter(win);
const chromeEventHandler = aElement.ownerGlobal.docShell.chromeEventHandler;
const dismissPrompt = () => prompt.dismiss();
// Some controls don't have UA widget (bug 888320)
if (
["month", "week"].includes(aElement.type) &&
!aElement.openOrClosedShadowRoot
) {
aElement.addEventListener("blur", dismissPrompt, {
mozSystemGroup: true,
});
} else {
chromeEventHandler.addEventListener(
"MozCloseDateTimePicker",
dismissPrompt
);
{
const dateTimeBoxElement = aElement.dateTimeBoxElement;
if (["month", "week"].includes(aElement.type) && !dateTimeBoxElement) {
aElement.addEventListener("blur", dismissPrompt, {
mozSystemGroup: true,
});
} else {
chromeEventHandler.addEventListener(
"MozCloseDateTimePicker",
dismissPrompt
);
dateTimeBoxElement.dispatchEvent(
new win.CustomEvent("MozSetDateTimePickerState", { detail: true })
);
}
}
prompt.asyncShowPrompt(
@ -256,10 +261,8 @@ class PromptFactory {
},
result => {
// Some controls don't have UA widget (bug 888320)
if (
["month", "week"].includes(aElement.type) &&
!aElement.openOrClosedShadowRoot
) {
const dateTimeBoxElement = aElement.dateTimeBoxElement;
if (["month", "week"].includes(aElement.type) && !dateTimeBoxElement) {
aElement.removeEventListener("blur", dismissPrompt, {
mozSystemGroup: true,
});
@ -268,6 +271,9 @@ class PromptFactory {
"MozCloseDateTimePicker",
dismissPrompt
);
dateTimeBoxElement.dispatchEvent(
new win.CustomEvent("MozSetDateTimePickerState", { detail: false })
);
}
// OK: result

Просмотреть файл

@ -1412,6 +1412,87 @@ export var BrowserTestUtils = {
return menulist.menupopup;
},
/**
* Waits for the datetime picker popup to be shown.
*
* @param {Window} win
* A window to expect the popup in.
*
* @return {Promise}
* Resolves when the popup has been fully opened. The resolution value
* is the select popup.
*/
async waitForDateTimePickerPanelShown(win) {
let getPanel = () => win.document.getElementById("DateTimePickerPanel");
let panel = getPanel();
let ensureReady = async () => {
let frame = panel.querySelector("#dateTimePopupFrame");
let isValidUrl = () => {
return (
frame.browsingContext?.currentURI?.spec ==
"chrome://global/content/datepicker.xhtml" ||
frame.browsingContext?.currentURI?.spec ==
"chrome://global/content/timepicker.xhtml"
);
};
// Ensure it's loaded.
if (!isValidUrl() || frame.contentDocument.readyState != "complete") {
await new Promise(resolve => {
frame.addEventListener(
"load",
function listener() {
if (isValidUrl()) {
frame.removeEventListener("load", listener, { capture: true });
resolve();
}
},
{ capture: true }
);
});
}
// Ensure it's ready.
if (!frame.contentWindow.PICKER_READY) {
await new Promise(resolve => {
frame.contentDocument.addEventListener("PickerReady", resolve, {
once: true,
});
});
}
// And that l10n mutations are flushed.
// FIXME(bug 1828721): We should ideally localize everything before
// showing the panel.
if (frame.contentDocument.hasPendingL10nMutations) {
await new Promise(resolve => {
frame.contentDocument.addEventListener(
"L10nMutationsFinished",
resolve,
{
once: true,
}
);
});
}
};
if (!panel) {
await this.waitForMutationCondition(
win.document,
{ childList: true, subtree: true },
getPanel
);
panel = getPanel();
if (panel.state == "open") {
await ensureReady();
return panel;
}
}
await this.waitForEvent(panel, "popupshown");
await ensureReady();
return panel;
},
/**
* Adds a content event listener on the given browser
* element. Similar to waitForContentEvent, but the listener will

Просмотреть файл

@ -33,14 +33,12 @@ export class DateTimePickerChild extends JSWindowActorChild {
return;
}
if (this._inputElement.openOrClosedShadowRoot) {
// dateTimeBoxElement is within UA Widget Shadow DOM.
// An event dispatch to it can't be accessed by document.
let win = this._inputElement.ownerGlobal;
dateTimeBoxElement.dispatchEvent(
new win.CustomEvent("MozSetDateTimePickerState", { detail: false })
);
}
// dateTimeBoxElement is within UA Widget Shadow DOM.
// An event dispatch to it can't be accessed by document.
let win = this._inputElement.ownerGlobal;
dateTimeBoxElement.dispatchEvent(
new win.CustomEvent("MozSetDateTimePickerState", { detail: false })
);
this._inputElement = null;
}
@ -148,20 +146,16 @@ export class DateTimePickerChild extends JSWindowActorChild {
let dateTimeBoxElement = this._inputElement.dateTimeBoxElement;
if (!dateTimeBoxElement) {
throw new Error(
"How do we get this event without a UA Widget or XBL binding?"
);
throw new Error("How do we get this event without a UA Widget?");
}
if (this._inputElement.openOrClosedShadowRoot) {
// 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 })
);
}
// 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 })
);
this.addListeners(this._inputElement);

Просмотреть файл

@ -25,16 +25,13 @@ export class DateTimePickerParent extends JSWindowActorParent {
debug("receiveMessage: " + aMessage.name);
switch (aMessage.name) {
case "FormDateTime:OpenPicker": {
let topBrowsingContext = this.manager.browsingContext.top;
let browser = topBrowsingContext.embedderElement;
this.showPicker(browser, aMessage.data);
this.showPicker(aMessage.data);
break;
}
case "FormDateTime:ClosePicker": {
if (!this._picker) {
return;
}
this._picker.closePicker();
this.close();
break;
}
@ -63,7 +60,6 @@ export class DateTimePickerParent extends JSWindowActorParent {
}
case "popuphidden": {
this.sendAsyncMessage("FormDateTime:PickerClosed", {});
this._picker.closePicker();
this.close();
break;
}
@ -73,45 +69,62 @@ export class DateTimePickerParent extends JSWindowActorParent {
}
// Get picker from browser and show it anchored to the input box.
showPicker(aBrowser, aData) {
showPicker(aData) {
let rect = aData.rect;
let type = aData.type;
let detail = aData.detail;
debug("Opening picker with details: " + JSON.stringify(detail));
let window = aBrowser.ownerGlobal;
let tabbrowser = window.gBrowser;
if (!tabbrowser) {
// TODO(bug 1828477): Support non-<tabbrowser> windows
debug("no tabbrowser, exiting now.");
let topBC = this.browsingContext.top;
let window = topBC.topChromeWindow;
if (Services.focus.activeWindow != window) {
debug("Not in the active window");
return;
}
if (
Services.focus.activeWindow != window ||
tabbrowser.selectedBrowser != aBrowser
) {
// We were sent a message from a window or tab that went into the
// background, so we'll ignore it for now.
return;
{
let browser = topBC.embedderElement;
if (
browser &&
browser.ownerGlobal.gBrowser &&
browser.ownerGlobal.gBrowser.selectedBrowser != browser
) {
debug("In background tab");
return;
}
}
let panel = tabbrowser._getAndMaybeCreateDateTimePickerPanel();
this.oldFocus = window.document.activeElement;
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.addPickerListeners();
}
// Picker is closed, do some cleanup.
// Close the picker and do some cleanup.
close() {
if (this.oldFocus) {
// Restore focus to where it was before the picker opened.
this.oldFocus.focus();
this.oldFocus = null;
}
this._picker.closePicker();
// Restore focus to where it was before the picker opened.
this._oldFocus?.focus();
this._oldFocus = null;
this.removePickerListeners();
this._picker = null;
}

Просмотреть файл

@ -61,6 +61,7 @@ skip-if =
os == "linux" && fission && socketprocess_networking && !debug # high frequency intermittent, Bug 1673140
[browser_datetime_showPicker.js]
# do not skip
[browser_datetime_toplevel.js]
[browser_spinner.js]
skip-if =
tsan # Frequently times out on TSan

Просмотреть файл

@ -0,0 +1,27 @@
/* Any copyright is dedicated to the Public Domain.
* https://creativecommons.org/publicdomain/zero/1.0/ */
add_task(async function() {
let input = document.createElement("input");
input.type = "date";
registerCleanupFunction(() => input.remove());
document.body.appendChild(input);
let shown = BrowserTestUtils.waitForDateTimePickerPanelShown(window);
const shadowRoot = SpecialPowers.wrap(input).openOrClosedShadowRoot;
EventUtils.synthesizeMouseAtCenter(
shadowRoot.getElementById("calendar-button"),
{}
);
let popup = await shown;
ok(!!popup, "Should've shown the popup");
let hidden = BrowserTestUtils.waitForPopupEvent(popup, "hidden");
popup.hidePopup();
await hidden;
popup.remove();
});

Просмотреть файл

@ -9,8 +9,7 @@
*/
class DateTimeTestHelper {
constructor() {
this.panel = gBrowser._getAndMaybeCreateDateTimePickerPanel();
this.panel.setAttribute("animate", false);
this.panel = null;
this.tab = null;
this.frame = null;
}
@ -40,6 +39,8 @@ class DateTimeTestHelper {
await SpecialPowers.contentTransformsReceived(content);
});
let shown = this.waitForPickerReady();
if (openMethod === "click") {
await SpecialPowers.spawn(bc, [], () => {
const input = content.document.querySelector("input");
@ -52,8 +53,8 @@ class DateTimeTestHelper {
content.document.querySelector("input").showPicker();
});
}
this.panel = await shown;
this.frame = this.panel.querySelector("#dateTimePopupFrame");
await this.waitForPickerReady();
}
promisePickerClosed() {
@ -79,35 +80,8 @@ class DateTimeTestHelper {
);
}
async waitForPickerReady() {
let readyPromise;
let loadPromise = new Promise(resolve => {
let listener = () => {
if (
this.frame.browsingContext.currentURI.spec !=
"chrome://global/content/datepicker.xhtml" &&
this.frame.browsingContext.currentURI.spec !=
"chrome://global/content/timepicker.xhtml"
) {
return;
}
this.frame.removeEventListener("load", listener, { capture: true });
// Add the PickerReady event listener directly inside the load event
// listener to avoid missing the event.
readyPromise = BrowserTestUtils.waitForEvent(
this.frame.contentDocument,
"PickerReady"
);
resolve();
};
this.frame.addEventListener("load", listener, { capture: true });
});
await loadPromise;
// Wait for picker elements to be ready
await readyPromise;
waitForPickerReady() {
return BrowserTestUtils.waitForDateTimePickerPanelShown(window);
}
/**
@ -156,9 +130,8 @@ class DateTimeTestHelper {
* Clean up after tests. Remove the frame to prevent leak.
*/
cleanup() {
this.frame.remove();
this.frame?.remove();
this.frame = null;
this.panel.removeAttribute("animate");
this.panel = null;
}
}

Просмотреть файл

@ -41,6 +41,8 @@ function DatePicker(context) {
this._createComponents();
this._update();
this.components.calendar.focusDay();
// TODO(bug 1828721): This is a bit sad.
window.PICKER_READY = true;
document.dispatchEvent(new CustomEvent("PickerReady"));
},

Просмотреть файл

@ -611,19 +611,34 @@ this.DateTimeBoxWidget = class {
" target: " +
aEvent.target +
" rt: " +
aEvent.relatedTarget
aEvent.relatedTarget +
" open: " +
this.mIsPickerOpen
);
let target = aEvent.originalTarget;
target.setAttribute("typeBuffer", "");
this.setInputValueFromFields();
// No need to set and unset the focus state if the focus is staying within
// our input. Same about closing the picker.
if (aEvent.relatedTarget != this.mInputElement) {
this.mInputElement.setFocusState(false);
if (this.mIsPickerOpen) {
this.closeDateTimePicker();
}
// No need to set and unset the focus state (or closing the picker) if the
// focus is staying within our input.
if (aEvent.relatedTarget == this.mInputElement) {
return;
}
// If we're in chrome and the focus moves to a separate document
// (relatedTarget is null) we also don't want to close it, since it
// could've moved to the datetime popup itself.
if (
!aEvent.relatedTarget &&
this.window.isChromeWindow &&
this.window == this.window.top
) {
return;
}
this.mInputElement.setFocusState(false);
if (this.mIsPickerOpen) {
this.closeDateTimePicker();
}
}

Просмотреть файл

@ -35,6 +35,8 @@ function TimePicker(context) {
this._setDefaultState();
this._createComponents();
this._setComponentStates();
// TODO(bug 1828721): This is a bit sad.
window.PICKER_READY = true;
document.dispatchEvent(new CustomEvent("PickerReady"));
},

Просмотреть файл

@ -236,23 +236,6 @@ let JSWINDOWACTORS = {
enablePreference: "cookiebanners.bannerClicking.enabled",
},
DateTimePicker: {
parent: {
esModuleURI: "resource://gre/actors/DateTimePickerParent.sys.mjs",
},
child: {
esModuleURI: "resource://gre/actors/DateTimePickerChild.sys.mjs",
events: {
MozOpenDateTimePicker: {},
MozUpdateDateTimePicker: {},
MozCloseDateTimePicker: {},
},
},
allFrames: true,
},
ExtFind: {
child: {
esModuleURI: "resource://gre/actors/ExtFindChild.sys.mjs",
@ -524,6 +507,7 @@ let JSWINDOWACTORS = {
},
},
includeChrome: true,
allFrames: true,
},
@ -582,9 +566,7 @@ if (AppConstants.platform != "android") {
allFrames: true,
};
/**
* Note that GeckoView has another implementation in mobile/android/actors.
*/
// Note that GeckoView has another implementation in mobile/android/actors.
JSWINDOWACTORS.Select = {
parent: {
esModuleURI: "resource://gre/actors/SelectParent.sys.mjs",
@ -602,6 +584,25 @@ if (AppConstants.platform != "android") {
includeChrome: true,
allFrames: true,
};
// Note that GeckoView handles MozOpenDateTimePicker in GeckoViewPrompt.
JSWINDOWACTORS.DateTimePicker = {
parent: {
esModuleURI: "resource://gre/actors/DateTimePickerParent.sys.mjs",
},
child: {
esModuleURI: "resource://gre/actors/DateTimePickerChild.sys.mjs",
events: {
MozOpenDateTimePicker: {},
MozUpdateDateTimePicker: {},
MozCloseDateTimePicker: {},
},
},
includeChrome: true,
allFrames: true,
};
}
export var ActorManagerParent = {